mlx-bun 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/docs/README.md +4 -1
  2. package/docs/archive/investigations/openjarvis-fit.md +198 -0
  3. package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
  4. package/docs/design/batching.md +1321 -49
  5. package/docs/design/decode-speed-program.md +331 -1163
  6. package/docs/design/generic-model-support.md +79 -3
  7. package/docs/design/kv-cache.md +198 -54
  8. package/docs/design/speculative-decoding.md +387 -24
  9. package/docs/design/turboquant.md +123 -13
  10. package/docs/design/unified-engine-frontier-plan.md +144 -142
  11. package/docs/planning/PRODUCT_ROADMAP.md +16 -0
  12. package/docs/planning/release-notes-v0.4.0.md +45 -0
  13. package/docs/reference/benchmarks.md +2457 -6
  14. package/docs/reference/cli.md +41 -2
  15. package/docs/reference/distribution.md +16 -9
  16. package/docs/reference/environment.md +55 -5
  17. package/docs/reference/models.md +13 -4
  18. package/docs/reference/server-api.md +79 -25
  19. package/docs/reference/server-config.md +178 -84
  20. package/docs/reference/troubleshooting.md +13 -21
  21. package/package.json +1 -1
  22. package/src/backends/mlx/assistant-target.ts +46 -0
  23. package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
  24. package/src/backends/mlx/batch-group.ts +368 -224
  25. package/src/backends/mlx/cache-attachment.ts +67 -0
  26. package/src/backends/mlx/cache-layout.ts +31 -0
  27. package/src/backends/mlx/checkpoint-state.ts +53 -0
  28. package/src/backends/mlx/continuation-persistence.ts +106 -0
  29. package/src/backends/mlx/continuation-request.ts +43 -0
  30. package/src/backends/mlx/continuation.ts +77 -0
  31. package/src/backends/mlx/draft-target.ts +7 -22
  32. package/src/backends/mlx/full-kv-row-append.ts +32 -0
  33. package/src/backends/mlx/full-kv-row-donor.ts +54 -0
  34. package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
  35. package/src/backends/mlx/gateway-binding.ts +77 -18
  36. package/src/backends/mlx/kv-codec.ts +9 -0
  37. package/src/backends/mlx/kv-maintenance.ts +133 -0
  38. package/src/backends/mlx/kv-tensor-rows.ts +212 -0
  39. package/src/backends/mlx/model-serving.ts +1 -1
  40. package/src/backends/mlx/prefill-cohort.ts +89 -0
  41. package/src/backends/mlx/prefill-rows.ts +200 -0
  42. package/src/backends/mlx/prefill.ts +1 -1
  43. package/src/backends/mlx/projected-context-rows.ts +72 -0
  44. package/src/backends/mlx/request-state-policy.ts +41 -0
  45. package/src/backends/mlx/rollback.ts +41 -3
  46. package/src/backends/mlx/row-sampling.ts +49 -0
  47. package/src/backends/mlx/serial-executor.ts +35 -66
  48. package/src/backends/mlx/speculative-group.ts +359 -0
  49. package/src/backends/mlx/speculative-round.ts +161 -0
  50. package/src/backends/mlx/state-rows.ts +104 -0
  51. package/src/backends/mlx/state-views.ts +21 -1
  52. package/src/backends/mlx/target-layout-capability.ts +35 -0
  53. package/src/backends/mlx/transitioning-kv-rows.ts +165 -0
  54. package/src/cli.ts +11 -36
  55. package/src/contracts/execution.ts +11 -0
  56. package/src/contracts/prefix-cache.ts +23 -0
  57. package/src/contracts/resources.ts +7 -0
  58. package/src/contracts/sampling.ts +23 -0
  59. package/src/contracts/scheduling.ts +18 -0
  60. package/src/engine/execution-plan.ts +18 -16
  61. package/src/engine/resources.ts +12 -1
  62. package/src/engine/scheduler.ts +10 -5
  63. package/src/fit.ts +29 -15
  64. package/src/generate.ts +77 -173
  65. package/src/inference/draft-acceptance.ts +41 -0
  66. package/src/inference/draft-output.ts +49 -0
  67. package/src/inference/rollback.ts +2 -2
  68. package/src/kv-scheme.ts +70 -12
  69. package/src/kv-store.ts +70 -16
  70. package/src/lab/paged-kv/paged-kv-rows.ts +83 -0
  71. package/src/lab/paged-kv/paged-kv.ts +25 -11
  72. package/src/mlx/array.ts +2 -1
  73. package/src/model/artifact-identity.ts +73 -0
  74. package/src/model/batched-kv.ts +235 -0
  75. package/src/model/batched-mask.ts +12 -4
  76. package/src/model/batched-quant.ts +8 -2
  77. package/src/model/batched-quantized-kv.ts +144 -0
  78. package/src/model/batched-rotating-quant.ts +76 -15
  79. package/src/model/batched-rotating-state.ts +76 -0
  80. package/src/model/batched-rotating.ts +64 -22
  81. package/src/model/batched-row-storage.ts +52 -0
  82. package/src/model/batched-ssm.ts +52 -0
  83. package/src/model/batched-turboquant-kv.ts +93 -0
  84. package/src/model/compiled-decode.ts +7 -1
  85. package/src/model/decoded-kv-donor.ts +12 -0
  86. package/src/model/delayed-quantized-kv.ts +72 -0
  87. package/src/model/delayed-rotating-quantized-kv.ts +93 -0
  88. package/src/model/delayed-turboquant-kv.ts +55 -0
  89. package/src/model/full-prefill-padding.ts +56 -0
  90. package/src/model/full-prefill-row.ts +105 -0
  91. package/src/model/gemma4-base.ts +162 -80
  92. package/src/model/gemma4.ts +34 -26
  93. package/src/model/generated/gemma4-12b.ts +4 -1
  94. package/src/model/generated/gemma4-26b.ts +4 -1
  95. package/src/model/generated/gemma4-e4b.ts +4 -1
  96. package/src/model/glm52-cache.ts +80 -4
  97. package/src/model/glm52-mla.ts +12 -6
  98. package/src/model/glm52.ts +19 -40
  99. package/src/model/kv-attention-view.ts +67 -0
  100. package/src/model/minicpm5.ts +11 -7
  101. package/src/model/qwen3-delta-state.ts +74 -0
  102. package/src/model/qwen3-delta.ts +59 -36
  103. package/src/model/qwen3_5.ts +82 -42
  104. package/src/model/rotating-kv-donor.ts +35 -0
  105. package/src/model/rotating-kv-layout.ts +262 -0
  106. package/src/model/rotating-row-transaction.ts +35 -0
  107. package/src/model/speculative-rotating-kv.ts +101 -0
  108. package/src/model/ssm-prefill-padding.ts +39 -0
  109. package/src/model/trellis-linear.ts +2 -2
  110. package/src/model/turboquant-codec.ts +83 -0
  111. package/src/model/universal/dense.ts +14 -13
  112. package/src/preflight.ts +31 -15
  113. package/src/prompt-cache.ts +42 -44
  114. package/src/sampler.ts +36 -25
  115. package/src/serve/chat-stage.ts +2 -2
  116. package/src/serve/generation-gateway.ts +39 -30
  117. package/src/serve/model-host.ts +3 -4
  118. package/src/serve/request-plan.ts +4 -3
  119. package/src/serve/request-prep.ts +2 -4
  120. package/src/serve/text-completion-stage.ts +2 -2
  121. package/src/server.ts +64 -108
  122. package/src/spec/assistant-rows.ts +92 -0
  123. package/src/spec/assistant-source.ts +28 -18
  124. package/src/spec/cache-identity.ts +22 -0
  125. package/src/spec/deepspec-rows.ts +29 -0
  126. package/src/spec/deepspec-source.ts +12 -10
  127. package/src/spec/dflash-source.ts +28 -4
  128. package/src/spec/drafter.ts +153 -142
  129. package/src/spec/dspark/deepspec-module.ts +95 -120
  130. package/src/spec/dspark/module-dflash.ts +126 -170
  131. package/src/spec/glm52-mtp-graph.ts +29 -0
  132. package/src/spec/glm52-mtp-rows.ts +132 -0
  133. package/src/spec/glm52-mtp-source.ts +53 -136
  134. package/src/spec/glm52-mtp-state.ts +33 -0
  135. package/src/spec/ngram-source.ts +92 -20
  136. package/src/spec/projected-draft-rows.ts +73 -0
  137. package/src/spec/qwen-mtp-module.ts +171 -0
  138. package/src/spec/qwen-mtp-rows.ts +219 -0
  139. package/src/spec/qwen-mtp-source.ts +81 -226
  140. package/src/spec/qwen-mtp-state.ts +30 -0
  141. package/src/spec/serve-loop.ts +70 -68
  142. package/src/spec/source.ts +80 -15
  143. package/src/spec/two-model-rows.ts +61 -0
  144. package/src/spec/two-model.ts +43 -101
  145. package/src/ssd-cache.ts +33 -21
  146. package/src/ssd-durability.ts +11 -7
  147. package/src/web/app.js +57 -57
  148. package/src/backends/mlx/memory-guard.ts +0 -40
  149. package/src/spec/prefix-state.ts +0 -45
  150. /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
@@ -0,0 +1,219 @@
1
+ import type { MlxArray } from "../mlx/array";
2
+ import * as ops from "../mlx/ops";
3
+ import { materializeCopy } from "../mlx/materialize";
4
+ import { toLogprobs } from "../sampler";
5
+ import { BatchedKVCache } from "../model/batched-kv";
6
+ import { KVCache } from "../model/gemma4-base";
7
+ import type { DraftRowSampling, QwenMtpTarget } from "./source";
8
+ import type { MtpModule } from "./qwen-mtp-module";
9
+ import type { PreparedStateChange } from "../contracts/resources";
10
+ import { applyStateChanges, cleanupFailure, disposeResources } from "../engine/resources";
11
+
12
+ /** A committed request's draft KV and the preceding true target hidden row.
13
+ * Construction borrows these arrays; extraction returns owned state. */
14
+ export interface MtpRowState {
15
+ readonly cache: KVCache;
16
+ readonly hidden: MlxArray;
17
+ }
18
+
19
+ /** Sampling owns each request's RNG policy. Scores are [B,V] log-probabilities;
20
+ * the returned owned [B] token array stays on device through the draft chain. */
21
+ export type MtpRowSampling = DraftRowSampling;
22
+
23
+ /** The same draft graph and state lifecycle for one or several requests.
24
+ * Admission, target verification, output publication and persistence belong to
25
+ * the caller. Rows remain fixed within a draft/commit round. */
26
+ export class QwenMtpRows {
27
+ #cache = new BatchedKVCache();
28
+ #hidden: MlxArray | null = null;
29
+ #prefilled: boolean[] = [];
30
+ #drafts: number[][] = [];
31
+ #depth = 0;
32
+
33
+ constructor(
34
+ readonly target: QwenMtpTarget,
35
+ readonly module: MtpModule,
36
+ readonly sampling: MtpRowSampling | null,
37
+ states: readonly (MtpRowState | null)[],
38
+ ) {
39
+ this.append(states);
40
+ }
41
+
42
+ get rowCount(): number { return this.#cache.rowOffsets.length; }
43
+
44
+ /** Add committed state at a method boundary. Inputs remain caller-owned. */
45
+ append(states: readonly (MtpRowState | null)[]): void {
46
+ if (!states.length) return;
47
+ applyStateChanges([() => this.prepareAppend(states)]);
48
+ }
49
+
50
+ prepareAppend(states: readonly (MtpRowState | null)[]): PreparedStateChange {
51
+ if (!states.length) return { commit() {}, dispose() {} };
52
+ const prefilled = [...this.#prefilled, ...states.map(state => state !== null)];
53
+ const example = this.#hidden ?? states.find(state => state !== null)?.hidden;
54
+ let hidden: MlxArray | null = null;
55
+ let cache: BatchedKVCache | null = new BatchedKVCache();
56
+ const held: { dispose(): void }[] = [];
57
+ try {
58
+ if (example) {
59
+ const zero = (rows: number) => {
60
+ const array = ops.zeros([rows, 1, this.target.hiddenSize], example.dtype);
61
+ held.push(array); return array;
62
+ };
63
+ hidden = ops.concatAxis([
64
+ ...(this.rowCount ? [this.#hidden ?? zero(this.rowCount)] : []),
65
+ ...states.map(state => state?.hidden ?? zero(1)),
66
+ ], 0);
67
+ }
68
+ cache.mergeRows([...(this.rowCount ? [this.#cache] : []), ...states.map(state => {
69
+ if (state) return state.cache;
70
+ const empty = new KVCache(); held.push(empty); return empty;
71
+ })]);
72
+ } catch (error) { return cleanupFailure(error, () => disposeResources([cache!, ...(hidden ? [hidden] : [])])); }
73
+ finally { disposeResources(held); }
74
+ return {
75
+ commit: () => {
76
+ const previousCache = this.#cache, previousHidden = this.#hidden;
77
+ this.#cache = cache!; this.#hidden = hidden;
78
+ this.#prefilled = prefilled;
79
+ cache = previousCache; hidden = previousHidden;
80
+ },
81
+ dispose: () => {
82
+ const held = [cache, hidden].filter(value => value !== null);
83
+ cache = null; hidden = null;
84
+ disposeResources(held);
85
+ },
86
+ };
87
+ }
88
+
89
+ /** Retire/reorder committed requests without involving their sampler policy. */
90
+ filterRows(keep: readonly number[]): void {
91
+ if (!keep.length) { this.dispose(); return; }
92
+ using indices = ops.fromInt32([...keep], [keep.length]);
93
+ const hidden = this.#hidden ? ops.takeAxis(this.#hidden, indices, 0) : null;
94
+ try { this.#cache.filterRows(keep); }
95
+ catch (error) { hidden?.dispose(); throw error; }
96
+ this.#hidden?.dispose(); this.#hidden = hidden;
97
+ this.#prefilled = keep.map(row => this.#prefilled[row]!);
98
+ }
99
+
100
+ /** Build companion KV from borrowed new target tokens/taps at the same B.
101
+ * The first token bridges to each restored row's preceding true hidden.
102
+ * Cold rows discard that bridge. All later rows pair token[p+1] with h[p].
103
+ * Only KV and the final true hidden are evaluated; unused attention/MLP
104
+ * outputs remain outside the evaluated prefill graph. */
105
+ prefill(tokens: MlxArray, context: MlxArray): void {
106
+ const [B, length] = tokens.shape as [number, number];
107
+ const H = context.shape[2]!;
108
+ if (this.#prefilled.some(Boolean)) {
109
+ using first = tokens.slice([0, 0], [B, 1]);
110
+ using embeds = this.target.embed(first);
111
+ const mixed = this.#prefilled.some(value => !value);
112
+ if (mixed) this.#cache.specRoundBegin();
113
+ this.module.forward(embeds, this.#hidden!, this.#cache).dispose();
114
+ if (mixed) this.#cache.specRoundRollback(this.#prefilled.map(Number));
115
+ }
116
+ for (let pos = 0; pos + 1 < length; pos += 2048) {
117
+ const end = Math.min(pos + 2048, length - 1);
118
+ using ids = tokens.slice([0, pos + 1], [B, end + 1]);
119
+ using embeds = this.target.embed(ids);
120
+ using hidden = context.slice([0, pos, 0], [B, end, H]);
121
+ this.module.forward(embeds, hidden, this.#cache).dispose();
122
+ ops.evalAll(this.#cache.state());
123
+ }
124
+ using tail = context.slice([0, length - 1, 0], [B, length, H]);
125
+ const hidden = materializeCopy(tail);
126
+ this.#hidden?.dispose(); this.#hidden = hidden;
127
+ ops.evalAll([hidden, ...this.#cache.state()]);
128
+ this.#prefilled.fill(true);
129
+ }
130
+
131
+ materialize(): void {
132
+ const state = [...this.#cache.state(), ...(this.#hidden ? [this.#hidden] : [])];
133
+ if (state.length) ops.evalAll(state);
134
+ }
135
+
136
+ /** Nonnegative depth, with every request's first pending token already known.
137
+ * Build the complete device dependency chain before reading proposal IDs. */
138
+ draft(pending: readonly number[], depth: number, steps: readonly number[]): number[][] {
139
+ const B = pending.length;
140
+ const tokens: MlxArray[] = [];
141
+ let ids: MlxArray | null = ops.fromInt32([...pending], [B, 1]);
142
+ let chained: MlxArray | null = null;
143
+ this.#cache.specRoundBegin();
144
+ try {
145
+ // A zero-proposal round still processes the pending target token. Keep
146
+ // its companion KV aligned so a later round can resume speculation.
147
+ if (depth === 0) {
148
+ using embeds = this.target.embed(ids!);
149
+ this.module.forward(embeds, this.#hidden!, this.#cache).dispose();
150
+ this.#drafts = Array.from({ length: B }, () => []);
151
+ this.#depth = 0;
152
+ this.#hidden!.dispose(); this.#hidden = null;
153
+ return this.#drafts;
154
+ }
155
+ for (let position = 0; position < depth; position++) {
156
+ using embeds = this.target.embed(ids!);
157
+ const output = this.module.forward(embeds, chained ?? this.#hidden!, this.#cache);
158
+ chained?.dispose(); chained = output;
159
+ using logits = this.target.logitsFromHidden(output);
160
+ using flat = ops.reshape(logits, [B, logits.shape.at(-1)!]);
161
+ using logprobs = toLogprobs(flat);
162
+ const token = this.sampling!.sample(logprobs, steps.map(step => step + position));
163
+ tokens.push(token);
164
+ ids!.dispose(); ids = ops.reshape(token, [B, 1]);
165
+ }
166
+ using packed = ops.concatAxis(tokens, 0);
167
+ const read = packed.toIntTokens();
168
+ this.#drafts = Array.from({ length: B }, (_, row) =>
169
+ Array.from({ length: depth }, (_, position) => read[position * B + row]!));
170
+ this.#depth = depth;
171
+ this.#hidden!.dispose(); this.#hidden = null;
172
+ return this.#drafts;
173
+ } finally {
174
+ ids?.dispose(); chained?.dispose();
175
+ for (const token of tokens) token.dispose();
176
+ }
177
+ }
178
+
179
+ /** Resolve draft storage using each request's accepted prefix. The verify
180
+ * context is borrowed [B,depth+1,H], before the target's final norm. */
181
+ commit(accepted: readonly number[], context: MlxArray): void {
182
+ const B = accepted.length, depth = this.#depth;
183
+ const appended = Math.max(1, depth);
184
+ const kept = accepted.map(count => Math.min(count + 1, appended));
185
+ if (kept.every(count => count === appended)) this.#cache.specRoundCommit();
186
+ else this.#cache.specRoundRollback(kept);
187
+
188
+ // Full acceptance needs the last accepted token's missing draft row,
189
+ // using the TRUE target hidden at its preceding position. Other requests
190
+ // keep their current coverage; their temporary append is never retained.
191
+ const extra = accepted.map(count => Number(depth > 0 && count === depth));
192
+ if (extra.some(Boolean)) {
193
+ using ids = ops.fromInt32(this.#drafts.map(row => row[depth - 1]!), [B, 1]);
194
+ using embeds = this.target.embed(ids);
195
+ using hidden = context.slice([0, depth - 1, 0], [B, depth, context.shape[2]!]);
196
+ this.#cache.specRoundBegin();
197
+ this.module.forward(embeds, hidden, this.#cache).dispose();
198
+ this.#cache.specRoundRollback(extra);
199
+ }
200
+ using positions = ops.fromInt32([...accepted], [B, 1, 1]);
201
+ this.#hidden = ops.takeAlongAxis(context, positions, 1);
202
+ this.#drafts = []; this.#depth = 0;
203
+ }
204
+
205
+ /** Only between committed rounds. Returned state is independent of the
206
+ * group's storage and uses the existing single-request cache representation. */
207
+ extractRow(row: number): MtpRowState {
208
+ const cache = this.#cache.extractRow(row);
209
+ try {
210
+ using view = this.#hidden!.slice([row, 0, 0], [row + 1, 1, this.#hidden!.shape[2]!]);
211
+ return { cache, hidden: materializeCopy(view) };
212
+ } catch (error) { cache.dispose(); throw error; }
213
+ }
214
+
215
+ dispose(): void {
216
+ this.#cache.dispose(); this.#hidden?.dispose(); this.#hidden = null;
217
+ this.#drafts = []; this.#depth = 0; this.#prefilled = [];
218
+ }
219
+ }
@@ -30,206 +30,40 @@ import { materializeCopy } from "../mlx/materialize";
30
30
  import { toLogprobs } from "../sampler";
31
31
  import { loadModelConfig, type ModelConfig } from "../config";
32
32
  import { Weights } from "../weights";
33
- import { disposing, KVCache, QuantizedLinear, RMSNorm, type Mask } from "../model/gemma4-base";
34
- import { DenseLinear } from "../model/universal/modules";
35
- import type { DraftProvider, DraftSource, QwenMtpTarget } from "./source";
33
+ import { KVCache } from "../model/gemma4-base";
34
+ import { MtpModule } from "./qwen-mtp-module";
35
+ import type { DraftProvider, DraftSource, DraftRowCheckpoint, DraftRowGroup, DraftPrefillGroup, DraftRowSampling, GroupedDraftProvider, QwenMtpTarget, TargetView } from "./source";
36
+ import { QwenMtpRows, type MtpRowState } from "./qwen-mtp-rows";
37
+ import { captureQwenMtpState, restoreQwenMtpState } from "./qwen-mtp-state";
36
38
  import type { Cache } from "../model/gemma4";
37
- import { cloneKvCaches } from "../kv-store";
38
- import { cacheBytes } from "../prompt-cache";
39
- import { disposeResources } from "../engine/resources";
40
- import { SpeculativePrefixStore, type SpeculativePrefixState } from "./prefix-state";
39
+ import { applyStateChanges, cleanupFailure, disposeResources } from "../engine/resources";
40
+ import { artifactIdentity } from "../model/artifact-identity";
41
+ import { configFingerprint } from "../model/fingerprint";
42
+ import type { PreparedStateChange } from "../contracts/resources";
41
43
 
42
44
  type Sampler = (logprobs: MlxArray, step: number) => MlxArray;
43
45
 
44
46
  const DRAFT_PREFILL_CHUNK = 2048;
45
47
 
46
- type MtpLinear = DenseLinear | QuantizedLinear;
47
-
48
- /** Projection tensors belong to Weights; dense transpose views belong to
49
- * the provider's resource stack. Quantized heads keep no dense copy. */
50
- function loadMtpLinear(
51
- weights: Weights, path: string, config: ModelConfig, resources: DisposableStack,
52
- ): MtpLinear {
53
- if (weights.has(`${path}.scales`)) return QuantizedLinear.load(weights, path, config);
54
- const layer = new DenseLinear(weights.tensor(`${path}.weight`), null);
55
- resources.use(layer.wT);
56
- return layer;
57
- }
58
-
59
- /** Qwen3Attention.forward with the companion's dense or quantized projections.
60
- * Attention operations follow src/model/qwen3_5.ts verbatim. */
61
- class MtpAttention {
62
- readonly qProj: MtpLinear;
63
- readonly kProj: MtpLinear;
64
- readonly vProj: MtpLinear;
65
- readonly oProj: MtpLinear;
66
- readonly qNorm: RMSNorm;
67
- readonly kNorm: RMSNorm;
68
- readonly nHeads: number;
69
- readonly nKvHeads: number;
70
- readonly headDim: number;
71
- readonly scale: number;
72
- readonly ropeDims: number;
73
- readonly ropeBase: number;
74
-
75
- constructor(weights: Weights, config: ModelConfig, prefix: string, resources: DisposableStack) {
76
- const t = config.text;
77
- this.nHeads = t.numAttentionHeads;
78
- this.nKvHeads = t.numKeyValueHeads;
79
- this.headDim = t.headDim;
80
- this.scale = Math.pow(this.headDim, -0.5);
81
- this.ropeDims = Math.trunc(this.headDim * t.partialRotaryFactor);
82
- this.ropeBase = t.ropeParameters.full_attention?.ropeTheta ?? 10000;
83
- this.qProj = loadMtpLinear(weights, `${prefix}.q_proj`, config, resources);
84
- this.kProj = loadMtpLinear(weights, `${prefix}.k_proj`, config, resources);
85
- this.vProj = loadMtpLinear(weights, `${prefix}.v_proj`, config, resources);
86
- this.oProj = loadMtpLinear(weights, `${prefix}.o_proj`, config, resources);
87
- this.qNorm = new RMSNorm(weights.tensor(`${prefix}.q_norm.weight`), t.rmsNormEps);
88
- this.kNorm = new RMSNorm(weights.tensor(`${prefix}.k_norm.weight`), t.rmsNormEps);
89
- }
90
-
91
- forward(x: MlxArray, mask: Mask, cache: KVCache): MlxArray {
92
- const [B, L] = x.shape as [number, number, number];
93
- const qp = this.qProj.forward(x);
94
- const qpr = disposing(qp, ops.reshape(qp, [B, L, this.nHeads, this.headDim * 2]));
95
- const [qHeads, gateHeads] = ops.split(qpr, [this.headDim], -1) as [MlxArray, MlxArray];
96
- qpr.dispose();
97
- const gate = disposing(gateHeads, ops.reshape(gateHeads, [B, L, this.nHeads * this.headDim]));
98
-
99
- let k = this.kProj.forward(x);
100
- let v = this.vProj.forward(x);
101
-
102
- let q = this.qNorm.forward(qHeads);
103
- qHeads.dispose();
104
- q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
105
- k = disposing(k, ops.reshape(k, [B, L, this.nKvHeads, this.headDim]));
106
- k = disposing(k, this.kNorm.forward(k));
107
- k = disposing(k, ops.transposeAxes(k, [0, 2, 1, 3]));
108
- v = disposing(v, ops.reshape(v, [B, L, this.nKvHeads, this.headDim]));
109
- v = disposing(v, ops.transposeAxes(v, [0, 2, 1, 3]));
110
-
111
- q = disposing(q, ops.rope(q, this.ropeDims, this.ropeBase, cache.offset, null));
112
- k = disposing(k, ops.rope(k, this.ropeDims, this.ropeBase, cache.offset, null));
113
-
114
- const [keys, values] = cache.updateAndFetch(k, v);
115
- k.dispose();
116
- v.dispose();
117
- const attn = ops.sdpa(q, keys, values, this.scale, mask.mode, mask.arr);
118
- keys.dispose();
119
- values.dispose();
120
- q.dispose();
121
-
122
- const attnT = ops.transposeAxes(attn, [0, 2, 1, 3]);
123
- attn.dispose();
124
- const merged = ops.reshape(attnT, [B, L, -1]);
125
- attnT.dispose();
126
- const sig = ops.sigmoid(gate);
127
- gate.dispose();
128
- const gated = ops.mul(merged, sig);
129
- merged.dispose();
130
- sig.dispose();
131
- const out = this.oProj.forward(gated);
132
- gated.dispose();
133
- return out;
134
- }
135
- }
136
-
137
- /** The one MTP decoder block: fc-merge → attention → swiglu MLP → norm. */
138
- class MtpModule {
139
- readonly fc: MtpLinear;
140
- readonly preFcNormEmbedding: RMSNorm;
141
- readonly preFcNormHidden: RMSNorm;
142
- readonly attn: MtpAttention;
143
- readonly mlpGate: MtpLinear;
144
- readonly mlpUp: MtpLinear;
145
- readonly mlpDown: MtpLinear;
146
- readonly inputNorm: RMSNorm;
147
- readonly postAttnNorm: RMSNorm;
148
- readonly finalNorm: RMSNorm;
149
-
150
- constructor(weights: Weights, config: ModelConfig, resources: DisposableStack) {
151
- const eps = config.text.rmsNormEps;
152
- this.fc = loadMtpLinear(weights, "fc", config, resources);
153
- this.preFcNormEmbedding = new RMSNorm(weights.tensor("pre_fc_norm_embedding.weight"), eps);
154
- this.preFcNormHidden = new RMSNorm(weights.tensor("pre_fc_norm_hidden.weight"), eps);
155
- this.attn = new MtpAttention(weights, config, "layers.0.self_attn", resources);
156
- this.mlpGate = loadMtpLinear(weights, "layers.0.mlp.gate_proj", config, resources);
157
- this.mlpUp = loadMtpLinear(weights, "layers.0.mlp.up_proj", config, resources);
158
- this.mlpDown = loadMtpLinear(weights, "layers.0.mlp.down_proj", config, resources);
159
- this.inputNorm = new RMSNorm(weights.tensor("layers.0.input_layernorm.weight"), eps);
160
- this.postAttnNorm = new RMSNorm(weights.tensor("layers.0.post_attention_layernorm.weight"), eps);
161
- this.finalNorm = new RMSNorm(weights.tensor("norm.weight"), eps);
162
- }
163
-
164
- /** One block forward over [1,S,·]: token embeddings ([1,S,H], target
165
- * embed_tokens output) paired with hiddens ([1,S,H], target pre-final-norm
166
- * or the module's own chained output). Appends S rows to `cache`; returns
167
- * the module output [1,S,H] (post final norm — what the target lm_head
168
- * consumes AND what chains into the next step's `hidden`). */
169
- forward(tokenEmbeds: MlxArray, hiddens: MlxArray, cache: KVCache): MlxArray {
170
- const embNorm = this.preFcNormEmbedding.forward(tokenEmbeds);
171
- const hidNorm = this.preFcNormHidden.forward(hiddens);
172
- const joined = ops.concatAxis([embNorm, hidNorm], 2);
173
- embNorm.dispose();
174
- hidNorm.dispose();
175
- const x = this.fc.forward(joined);
176
- joined.dispose();
177
-
178
- // Decoder layer (Qwen3Layer.forward shape).
179
- const L = x.shape[1]!;
180
- const mask = cache.makeMask(L, null);
181
- const xn = this.inputNorm.forward(x);
182
- const r = this.attn.forward(xn, mask, cache);
183
- xn.dispose();
184
- mask.arr?.dispose();
185
- const h = ops.add(x, r);
186
- x.dispose();
187
- r.dispose();
188
- const hn = this.postAttnNorm.forward(h);
189
- const g = this.mlpGate.forward(hn);
190
- const u = this.mlpUp.forward(hn);
191
- hn.dispose();
192
- const silu = ops.silu(g);
193
- g.dispose();
194
- const act = ops.mul(silu, u);
195
- silu.dispose();
196
- u.dispose();
197
- const m = this.mlpDown.forward(act);
198
- act.dispose();
199
- const out = ops.add(h, m);
200
- h.dispose();
201
- m.dispose();
202
- return disposing(out, this.finalNorm.forward(out));
203
- }
204
- }
205
-
206
- class QwenMtpPrefixState implements SpeculativePrefixState {
207
- constructor(readonly tokens: number[], readonly targetIdentity: object,
208
- readonly namespace: string, readonly bytes: number, readonly target: Cache[],
209
- public draft: KVCache | null, public hidden: MlxArray | null) {}
210
-
211
- dispose(): void {
212
- const resources = [...this.target.splice(0), this.draft, this.hidden];
213
- this.draft = null;
214
- this.hidden = null;
215
- disposeResources(resources.filter((r) => r != null));
216
- }
217
- }
218
-
219
48
  export class QwenMtpProvider implements DraftProvider {
49
+ readonly grouped: GroupedDraftProvider = {
50
+ open: options => this.#openRows(options.target, options.sampling, options.checkpoints),
51
+ openPrefill: options => this.#openRows(options.target, null, options.checkpoints),
52
+ };
220
53
  readonly id: string;
221
54
  readonly weightsBytes: number;
222
55
  readonly #module: MtpModule;
223
56
  readonly #config: ModelConfig;
224
57
  readonly #resources: DisposableStack;
225
- readonly #prefixStore = new SpeculativePrefixStore<QwenMtpPrefixState>();
58
+ readonly #checkpointNamespace: string;
226
59
 
227
- private constructor(id: string, config: ModelConfig, weightsBytes: number, module: MtpModule, resources: DisposableStack) {
60
+ private constructor(id: string, config: ModelConfig, weightsBytes: number, module: MtpModule, resources: DisposableStack, checkpointNamespace: string) {
228
61
  this.id = id;
229
62
  this.#config = config;
230
63
  this.#module = module;
231
64
  this.weightsBytes = weightsBytes;
232
65
  this.#resources = resources;
66
+ this.#checkpointNamespace = checkpointNamespace;
233
67
  }
234
68
 
235
69
  static async load(dir: string): Promise<QwenMtpProvider> {
@@ -242,16 +76,24 @@ export class QwenMtpProvider implements DraftProvider {
242
76
  const weightsBytes = [...weights.shards.files.values()]
243
77
  .reduce((a, f) => a + f.mmap.size, 0);
244
78
  const module = new MtpModule(weights, config, resources);
79
+ // Stable across restarts and distinct for differently folded/quantized
80
+ // companions. Hash once at provider load, outside inference execution.
81
+ const identity = await artifactIdentity(configFingerprint(config),
82
+ [...weights.shards.files].map(([name, shard]) => ({ name, path: shard.path })));
245
83
  return new QwenMtpProvider(
246
84
  dir.split("/").filter(Boolean).at(-1) ?? "qwen-mtp",
247
- config, weightsBytes, module, resources.move(),
85
+ config, weightsBytes, module, resources.move(), `qwen-mtp-v1:${identity}`,
248
86
  );
249
87
  }
250
88
 
251
89
  open(opts: Parameters<DraftProvider["open"]>[0]): DraftSource {
90
+ return new QwenMtpSource(this.#target(opts.target), this.#module, opts.sampler, this.#checkpointNamespace);
91
+ }
92
+
93
+ #target(view: TargetView): QwenMtpTarget {
252
94
  if (this.#resources.disposed)
253
95
  throw new Error("qwen MTP provider is disposed");
254
- const target = opts.target.qwenMtp;
96
+ const target = view.qwenMtp;
255
97
  if (!target)
256
98
  throw new Error("qwen MTP drafting requires a qwen3_5-family target");
257
99
  if (target.hiddenSize !== this.#config.text.hiddenSize) {
@@ -260,14 +102,50 @@ export class QwenMtpProvider implements DraftProvider {
260
102
  `${target.hiddenSize} — split from a different checkpoint?`,
261
103
  );
262
104
  }
263
- return new QwenMtpSource(target, this.#module, opts.sampler,
264
- { store: this.#prefixStore, identity: opts.target.identity });
105
+ return target;
106
+ }
107
+
108
+ #openRows(view: TargetView, sampling: DraftRowSampling | null,
109
+ checkpoints: readonly (DraftRowCheckpoint | null)[]): DraftRowGroup & DraftPrefillGroup {
110
+ const target = this.#target(view);
111
+ const rows = new QwenMtpRows(target, this.#module, sampling, []);
112
+ const prepareAppend = (checkpoints: readonly (DraftRowCheckpoint | null)[]) => {
113
+ const states: Array<MtpRowState | null> = [];
114
+ let change: PreparedStateChange | undefined;
115
+ try {
116
+ for (const checkpoint of checkpoints) states.push(checkpoint ? restoreQwenMtpState(checkpoint) : null);
117
+ change = rows.prepareAppend(states);
118
+ disposeResources(states.splice(0).flatMap(state => state ? [state.cache, state.hidden] : []));
119
+ return change;
120
+ } catch (error) {
121
+ return cleanupFailure(error, () => disposeResources([
122
+ ...states.flatMap(state => state ? [state.cache, state.hidden] : []), ...(change ? [change] : []),
123
+ ]));
124
+ }
125
+ };
126
+ const append = (checkpoints: readonly (DraftRowCheckpoint | null)[]) => applyStateChanges([() => prepareAppend(checkpoints)]);
127
+ try { append(checkpoints); }
128
+ catch (error) { return cleanupFailure(error, () => rows.dispose()); }
129
+ return {
130
+ namespace: this.#checkpointNamespace, prefillMode: "full", tapLayers: [target.layerCount - 1],
131
+ get rowCount() { return rows.rowCount; },
132
+ append, prepareAppend, prefill: (tokens, context) => rows.prefill(tokens, context!), materialize: rows.materialize.bind(rows),
133
+ filterRows: rows.filterRows.bind(rows),
134
+ draft: rows.draft.bind(rows), commit: rows.commit.bind(rows),
135
+ capture(row) {
136
+ const state = rows.extractRow(row);
137
+ try { return captureQwenMtpState(state); }
138
+ finally { disposeResources([state.cache, state.hidden]); }
139
+ },
140
+ dispose: rows.dispose.bind(rows),
141
+ };
265
142
  }
266
143
 
144
+
267
145
  dispose(): void {
268
146
  // Release cached transpose views before their native weight maps. MLX
269
147
  // retains buffers needed by outstanding GPU commands until completion.
270
- disposeResources([this.#prefixStore, { dispose: () => this.#resources.dispose() }]);
148
+ this.#resources.dispose();
271
149
  }
272
150
  }
273
151
 
@@ -284,7 +162,7 @@ export class QwenMtpSource implements DraftSource {
284
162
  readonly #module: MtpModule;
285
163
  readonly #sampler: Sampler;
286
164
  #cache = new KVCache();
287
- readonly prefix: DraftSource["prefix"];
165
+ readonly checkpoint: NonNullable<DraftSource["checkpoint"]>;
288
166
  #prefilledTokens = 0;
289
167
  #hasDrafted = false;
290
168
  /** Target pre-norm hidden at the position preceding the next pending
@@ -295,52 +173,29 @@ export class QwenMtpSource implements DraftSource {
295
173
  #closed = false;
296
174
 
297
175
  constructor(target: QwenMtpTarget, module: MtpModule, sampler: Sampler,
298
- prefix?: { store: SpeculativePrefixStore<QwenMtpPrefixState>; identity: object }) {
176
+ namespace = "qwen-mtp-v1") {
299
177
  this.#target = target;
300
178
  this.#module = module;
301
179
  this.#sampler = sampler;
302
180
  this.tapLayers = [target.layerCount - 1];
303
- if (prefix) this.prefix = {
304
- restore: (prompt, caches, namespace, maxBytes) => {
181
+ this.checkpoint = {
182
+ namespace,
183
+ restore: (tokens, attachment) => {
305
184
  this.#checkOpen();
306
185
  if (this.#prefilledTokens !== 0 || this.#hasDrafted)
307
- throw new Error("Qwen MTP prefix restore requires a fresh source");
308
- const entry = prefix.store.take(prompt, prefix.identity, namespace, maxBytes);
309
- if (!entry) return 0;
310
- try {
311
- if (entry.target.length !== caches.length || !entry.draft || !entry.hidden ||
312
- entry.draft.offset !== entry.tokens.length - 1)
313
- throw new Error("invalid paired Qwen MTP prefix state");
314
- disposeResources(caches);
315
- caches.splice(0, caches.length, ...entry.target.splice(0));
316
- this.#cache.dispose();
317
- this.#cache = entry.draft;
318
- entry.draft = null;
319
- this.#pendingTrueHidden?.dispose();
320
- this.#pendingTrueHidden = entry.hidden;
321
- entry.hidden = null;
322
- return this.#prefilledTokens = entry.tokens.length;
323
- } finally { entry.dispose(); }
186
+ throw new Error("Qwen MTP checkpoint restore requires a fresh source");
187
+ const state = restoreQwenMtpState({ processedTokens: tokens, attachment });
188
+ try { disposeResources([this.#cache, ...(this.#pendingTrueHidden ? [this.#pendingTrueHidden] : [])]); }
189
+ catch (error) { return cleanupFailure(error, () => disposeResources([state.cache, state.hidden])); }
190
+ this.#cache = state.cache;
191
+ this.#pendingTrueHidden = state.hidden;
192
+ this.#prefilledTokens = tokens;
324
193
  },
325
- capture: (tokens, caches, namespace, maxBytes) => {
194
+ capture: (tokens) => {
326
195
  this.#checkOpen();
327
- if (tokens.length !== this.#prefilledTokens || !this.#pendingTrueHidden ||
328
- this.#cache.offset !== tokens.length - 1 || this.#roundAppended !== 0)
329
- throw new Error("Qwen MTP snapshot requires an aligned prefill boundary");
330
- const bytes = cacheBytes([...caches, this.#cache]) + this.#pendingTrueHidden.nbytes;
331
- if (bytes > maxBytes) { prefix.store.dispose(); return; }
332
- const retained: Cache[] = [];
333
- let draft: KVCache | null = null, hidden: MlxArray | null = null;
334
- try {
335
- retained.push(...cloneKvCaches(caches));
336
- draft = cloneKvCaches([this.#cache])[0] as KVCache;
337
- hidden = this.#pendingTrueHidden.slice([0, 0, 0], this.#pendingTrueHidden.shape);
338
- const entry = new QwenMtpPrefixState([...tokens], prefix.identity, namespace,
339
- bytes, retained.splice(0), draft, hidden);
340
- draft = null;
341
- hidden = null;
342
- prefix.store.put(entry, maxBytes);
343
- } finally { disposeResources([...retained, draft, hidden].filter((r) => r != null)); }
196
+ if (!this.#pendingTrueHidden || this.#cache.offset !== tokens - 1 || this.#roundAppended !== 0)
197
+ throw new Error("Qwen MTP snapshot requires an aligned committed boundary");
198
+ return captureQwenMtpState({ cache: this.#cache, hidden: this.#pendingTrueHidden }).attachment;
344
199
  },
345
200
  };
346
201
  }
@@ -0,0 +1,30 @@
1
+ import type { MlxArray } from "../mlx/array";
2
+ import { KVCache } from "../model/gemma4-base";
3
+ import { cloneAttachments } from "../backends/mlx/checkpoint-state";
4
+ import { disposeResources } from "../engine/resources";
5
+ import type { MtpRowState } from "./qwen-mtp-rows";
6
+ import type { DraftRowCheckpoint } from "./source";
7
+
8
+ /** The existing Qwen companion attachment is also the state interchange
9
+ * between prepared requests and a group. Storage-tier policy stays outside. */
10
+ export function restoreQwenMtpState(checkpoint: DraftRowCheckpoint): MtpRowState {
11
+ const { processedTokens: tokens, attachment } = checkpoint;
12
+ if (attachment.schema !== "qwen-mtp-v1" || attachment.metadata.draftOffset !== tokens - 1)
13
+ throw new Error("invalid paired Qwen MTP checkpoint alignment");
14
+ const held = cloneAttachments([attachment])[0]!.tensors;
15
+ let cache: KVCache | null = new KVCache();
16
+ try {
17
+ if (tokens > 1) { cache.restoreState(held[0]!, held[1]!, tokens - 1); held.splice(0, 2); }
18
+ const state = { cache, hidden: held.pop()! }; cache = null; return state;
19
+ } finally { disposeResources([...held, ...(cache ? [cache] : [])]); }
20
+ }
21
+
22
+ export function captureQwenMtpState(state: MtpRowState): DraftRowCheckpoint {
23
+ const held: MlxArray[] = [];
24
+ try {
25
+ if (state.cache.offset > 0) held.push(...state.cache.temporalView());
26
+ held.push(state.hidden.slice([0, 0, 0], [...state.hidden.shape]));
27
+ return { processedTokens: state.cache.offset + 1,
28
+ attachment: { schema: "qwen-mtp-v1", metadata: { draftOffset: state.cache.offset }, tensors: held.splice(0) } };
29
+ } finally { disposeResources(held); }
30
+ }