mlx-bun 0.3.0 → 0.5.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 (231) hide show
  1. package/README.md +57 -32
  2. package/docs/README.md +58 -42
  3. package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
  4. package/docs/archive/investigations/openjarvis-fit.md +198 -0
  5. package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
  6. package/docs/archive/planning/release-notes-v0.4.0.md +45 -0
  7. package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
  8. package/docs/design/batching.md +1535 -50
  9. package/docs/design/decode-speed-program.md +394 -1171
  10. package/docs/design/generic-model-support.md +195 -3
  11. package/docs/design/kv-cache.md +538 -225
  12. package/docs/design/speculative-decoding.md +552 -50
  13. package/docs/design/turboquant.md +140 -17
  14. package/docs/design/unified-engine-frontier-plan.md +259 -150
  15. package/docs/planning/PRODUCT_ROADMAP.md +21 -6
  16. package/docs/planning/public-readiness-handoff.md +244 -0
  17. package/docs/planning/public-readiness.md +149 -0
  18. package/docs/reference/benchmarks.md +3537 -1112
  19. package/docs/reference/cli.md +102 -2
  20. package/docs/reference/distribution.md +45 -13
  21. package/docs/reference/environment.md +159 -13
  22. package/docs/reference/library-api.md +18 -4
  23. package/docs/reference/memory.md +1 -1
  24. package/docs/reference/models.md +28 -7
  25. package/docs/reference/server-api.md +257 -33
  26. package/docs/reference/server-config.md +372 -151
  27. package/docs/reference/troubleshooting.md +13 -21
  28. package/package.json +2 -2
  29. package/src/audio/audiotoolbox.ts +112 -0
  30. package/src/audio/conformer.ts +8 -0
  31. package/src/audio/mic-capture.ts +98 -0
  32. package/src/audio/silero-vad.ts +379 -0
  33. package/src/audio/whisper-decode.ts +774 -0
  34. package/src/audio/whisper-fast.ts +732 -0
  35. package/src/audio/whisper-format.ts +73 -0
  36. package/src/audio/whisper-mel.ts +185 -0
  37. package/src/audio/whisper-timing.ts +274 -0
  38. package/src/audio/whisper-tokenizer.ts +239 -0
  39. package/src/audio/whisper-transcribe.ts +410 -0
  40. package/src/backends/mlx/assistant-target.ts +46 -0
  41. package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
  42. package/src/backends/mlx/autoregressive.ts +22 -1
  43. package/src/backends/mlx/batch-group.ts +464 -236
  44. package/src/backends/mlx/cache-attachment.ts +67 -0
  45. package/src/backends/mlx/cache-layout.ts +31 -0
  46. package/src/backends/mlx/checkpoint-state.ts +53 -0
  47. package/src/backends/mlx/continuation-persistence.ts +107 -0
  48. package/src/backends/mlx/continuation-request.ts +43 -0
  49. package/src/backends/mlx/continuation.ts +77 -0
  50. package/src/backends/mlx/draft-target.ts +7 -22
  51. package/src/backends/mlx/encoder-cache.ts +25 -0
  52. package/src/backends/mlx/fill-append.ts +21 -8
  53. package/src/backends/mlx/fill-group.ts +497 -0
  54. package/src/backends/mlx/full-kv-row-append.ts +32 -0
  55. package/src/backends/mlx/full-kv-row-donor.ts +54 -0
  56. package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
  57. package/src/backends/mlx/gateway-binding.ts +122 -19
  58. package/src/backends/mlx/kv-codec.ts +9 -0
  59. package/src/backends/mlx/kv-maintenance.ts +161 -0
  60. package/src/backends/mlx/kv-tensor-rows.ts +212 -0
  61. package/src/backends/mlx/mixed-iteration.ts +62 -0
  62. package/src/backends/mlx/model-prompt.ts +46 -32
  63. package/src/backends/mlx/model-serving.ts +5 -4
  64. package/src/backends/mlx/paged-cache-codec.ts +27 -0
  65. package/src/backends/mlx/prefill-cohort.ts +100 -0
  66. package/src/backends/mlx/prefill-policy.ts +26 -0
  67. package/src/backends/mlx/prefill-rows.ts +244 -0
  68. package/src/backends/mlx/prefill.ts +1 -1
  69. package/src/backends/mlx/projected-context-rows.ts +72 -0
  70. package/src/backends/mlx/prompt-input.ts +29 -0
  71. package/src/backends/mlx/qwen-prompt-input.ts +40 -0
  72. package/src/backends/mlx/request-state-policy.ts +54 -0
  73. package/src/backends/mlx/rollback.ts +45 -4
  74. package/src/backends/mlx/row-sampling.ts +49 -0
  75. package/src/backends/mlx/serial-executor.ts +51 -73
  76. package/src/backends/mlx/speculative-group.ts +438 -0
  77. package/src/backends/mlx/speculative-round.ts +161 -0
  78. package/src/backends/mlx/speculative.ts +24 -7
  79. package/src/backends/mlx/state-rows.ts +104 -0
  80. package/src/backends/mlx/state-views.ts +21 -1
  81. package/src/backends/mlx/target-layout-capability.ts +35 -0
  82. package/src/backends/mlx/transitioning-kv-rows.ts +173 -0
  83. package/src/cli.ts +333 -36
  84. package/src/contracts/execution.ts +21 -0
  85. package/src/contracts/object-cache.ts +8 -0
  86. package/src/contracts/pi-provider.ts +1 -1
  87. package/src/contracts/pi.ts +3 -1
  88. package/src/contracts/prefix-cache.ts +33 -0
  89. package/src/contracts/resources.ts +7 -0
  90. package/src/contracts/sampling.ts +23 -0
  91. package/src/contracts/scheduling.ts +27 -0
  92. package/src/engine/execution-plan.ts +38 -25
  93. package/src/engine/execution-tasks.ts +44 -0
  94. package/src/engine/namespaced-cache.ts +13 -0
  95. package/src/engine/resources.ts +12 -1
  96. package/src/engine/scheduler.ts +32 -7
  97. package/src/fill/fill-session.ts +14 -3
  98. package/src/fit.ts +29 -15
  99. package/src/generate.ts +105 -195
  100. package/src/grammar.ts +13 -0
  101. package/src/inference/draft-acceptance.ts +41 -0
  102. package/src/inference/draft-output.ts +49 -0
  103. package/src/inference/prefill.ts +6 -0
  104. package/src/inference/rollback.ts +2 -2
  105. package/src/kv-scheme.ts +92 -12
  106. package/src/kv-store.ts +183 -93
  107. package/src/lab/README.md +5 -1
  108. package/src/lab/paged-kv/paged-attention.ts +117 -0
  109. package/src/lab/paged-kv/paged-kv-rows.ts +111 -0
  110. package/src/lab/paged-kv/paged-kv.ts +126 -46
  111. package/src/md-modules.d.ts +5 -0
  112. package/src/mlx/array.ts +39 -1
  113. package/src/mlx/ffi.ts +19 -0
  114. package/src/mlx/normalized-argmax.ts +116 -0
  115. package/src/mlx/ops.ts +97 -10
  116. package/src/model/artifact-identity.ts +73 -0
  117. package/src/model/batched-kv.ts +235 -0
  118. package/src/model/batched-mask.ts +12 -4
  119. package/src/model/batched-quant.ts +8 -2
  120. package/src/model/batched-quantized-kv.ts +144 -0
  121. package/src/model/batched-rotating-quant.ts +76 -15
  122. package/src/model/batched-rotating-state.ts +76 -0
  123. package/src/model/batched-rotating.ts +64 -22
  124. package/src/model/batched-row-storage.ts +52 -0
  125. package/src/model/batched-ssm.ts +52 -0
  126. package/src/model/batched-turboquant-kv.ts +94 -0
  127. package/src/model/compiled-decode.ts +7 -1
  128. package/src/model/decoded-kv-donor.ts +12 -0
  129. package/src/model/delayed-quantized-kv.ts +72 -0
  130. package/src/model/delayed-rotating-quantized-kv.ts +93 -0
  131. package/src/model/delayed-turboquant-kv.ts +57 -0
  132. package/src/model/factory.ts +26 -0
  133. package/src/model/fingerprint.ts +12 -0
  134. package/src/model/full-prefill-padding.ts +56 -0
  135. package/src/model/full-prefill-row.ts +105 -0
  136. package/src/model/gemma4-base.ts +183 -82
  137. package/src/model/gemma4.ts +116 -28
  138. package/src/model/generated/gemma4-12b.ts +4 -1
  139. package/src/model/generated/gemma4-26b.ts +4 -1
  140. package/src/model/generated/gemma4-e4b.ts +4 -1
  141. package/src/model/glm52-cache.ts +80 -4
  142. package/src/model/glm52-mla.ts +12 -6
  143. package/src/model/glm52.ts +19 -40
  144. package/src/model/kv-attention-view.ts +70 -0
  145. package/src/model/minicpm5.ts +11 -7
  146. package/src/model/profile.ts +29 -4
  147. package/src/model/quantized-append-attention.ts +31 -0
  148. package/src/model/qwen3-delta-state.ts +74 -0
  149. package/src/model/qwen3-delta.ts +59 -36
  150. package/src/model/qwen3-mrope.ts +7 -9
  151. package/src/model/qwen3_5.ts +177 -75
  152. package/src/model/rotating-kv-donor.ts +35 -0
  153. package/src/model/rotating-kv-layout.ts +262 -0
  154. package/src/model/rotating-row-transaction.ts +35 -0
  155. package/src/model/speculative-rotating-kv.ts +101 -0
  156. package/src/model/ssm-prefill-padding.ts +39 -0
  157. package/src/model/support.ts +8 -0
  158. package/src/model/token-groups.ts +66 -0
  159. package/src/model/trellis-linear.ts +4 -4
  160. package/src/model/turboquant-codec.ts +89 -0
  161. package/src/model/universal/dense.ts +14 -13
  162. package/src/model/whisper.ts +435 -0
  163. package/src/native/mic_capture.swift +92 -0
  164. package/src/native-pack.ts +6 -5
  165. package/src/pi-web.ts +9 -1
  166. package/src/preflight.ts +31 -15
  167. package/src/prompt-cache.ts +306 -86
  168. package/src/runtime-config.ts +1 -5
  169. package/src/safetensors.ts +9 -2
  170. package/src/sampler.ts +37 -25
  171. package/src/serve/audio-routes.ts +323 -0
  172. package/src/serve/chat-request.ts +2 -0
  173. package/src/serve/chat-stage.ts +19 -14
  174. package/src/serve/completion-executor.ts +9 -1
  175. package/src/serve/discovery-routes.ts +17 -1
  176. package/src/serve/generated-token-history.ts +57 -0
  177. package/src/serve/generation-gateway.ts +88 -38
  178. package/src/serve/model-binding.ts +7 -1
  179. package/src/serve/model-host.ts +3 -4
  180. package/src/serve/preparation.ts +3 -3
  181. package/src/serve/prompt-response-trace.ts +14 -0
  182. package/src/serve/request-plan.ts +6 -3
  183. package/src/serve/request-prep.ts +40 -25
  184. package/src/serve/text-completion-stage.ts +2 -2
  185. package/src/serve/transcription-server.ts +64 -0
  186. package/src/serve/transcription-service.ts +502 -0
  187. package/src/server.ts +185 -261
  188. package/src/spec/assistant-rows.ts +93 -0
  189. package/src/spec/assistant-source.ts +28 -18
  190. package/src/spec/cache-identity.ts +22 -0
  191. package/src/spec/deepspec-rows.ts +29 -0
  192. package/src/spec/deepspec-source.ts +12 -10
  193. package/src/spec/dflash-source.ts +28 -4
  194. package/src/spec/drafter.ts +153 -142
  195. package/src/spec/dspark/deepspec-module.ts +95 -120
  196. package/src/spec/dspark/module-dflash.ts +126 -170
  197. package/src/spec/glm52-mtp-graph.ts +29 -0
  198. package/src/spec/glm52-mtp-rows.ts +132 -0
  199. package/src/spec/glm52-mtp-source.ts +54 -136
  200. package/src/spec/glm52-mtp-state.ts +33 -0
  201. package/src/spec/ngram-source.ts +115 -21
  202. package/src/spec/projected-draft-rows.ts +73 -0
  203. package/src/spec/qwen-mtp-module.ts +171 -0
  204. package/src/spec/qwen-mtp-rows.ts +230 -0
  205. package/src/spec/qwen-mtp-source.ts +84 -226
  206. package/src/spec/qwen-mtp-state.ts +30 -0
  207. package/src/spec/serve-loop.ts +73 -69
  208. package/src/spec/source.ts +93 -15
  209. package/src/spec/two-model-rows.ts +62 -0
  210. package/src/spec/two-model.ts +45 -103
  211. package/src/ssd-cache.ts +107 -36
  212. package/src/ssd-durability.ts +29 -27
  213. package/src/storage/host-buffer.ts +24 -0
  214. package/src/storage/kv-writer.ts +83 -0
  215. package/src/storage/kv-writer.worker.js +264 -0
  216. package/src/storage/retention-policy.ts +37 -0
  217. package/src/tiered-prompt-cache.ts +45 -0
  218. package/src/vision/embedder.ts +8 -0
  219. package/src/vision/prompt.ts +101 -65
  220. package/src/vision/qwen3vl-prompt.ts +101 -64
  221. package/src/vision/qwen3vl-tower.ts +11 -0
  222. package/src/vision/siglip.ts +8 -0
  223. package/src/web/app.html +12 -0
  224. package/src/web/app.js +217 -57
  225. package/src/web/src/chat.ts +4 -1
  226. package/src/web/src/composer.ts +3 -0
  227. package/src/web/src/voice.ts +141 -0
  228. package/src/web/sw.js +1 -1
  229. package/src/backends/mlx/memory-guard.ts +0 -40
  230. package/src/spec/prefix-state.ts +0 -45
  231. /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
@@ -30,206 +30,42 @@ 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
+ supportsExternalTokens: true,
51
+ checkpointNamespace: () => this.#checkpointNamespace,
52
+ open: options => this.#openRows(options.target, options.sampling, options.checkpoints),
53
+ openPrefill: options => this.#openRows(options.target, null, options.checkpoints),
54
+ };
220
55
  readonly id: string;
221
56
  readonly weightsBytes: number;
222
57
  readonly #module: MtpModule;
223
58
  readonly #config: ModelConfig;
224
59
  readonly #resources: DisposableStack;
225
- readonly #prefixStore = new SpeculativePrefixStore<QwenMtpPrefixState>();
60
+ readonly #checkpointNamespace: string;
226
61
 
227
- private constructor(id: string, config: ModelConfig, weightsBytes: number, module: MtpModule, resources: DisposableStack) {
62
+ private constructor(id: string, config: ModelConfig, weightsBytes: number, module: MtpModule, resources: DisposableStack, checkpointNamespace: string) {
228
63
  this.id = id;
229
64
  this.#config = config;
230
65
  this.#module = module;
231
66
  this.weightsBytes = weightsBytes;
232
67
  this.#resources = resources;
68
+ this.#checkpointNamespace = checkpointNamespace;
233
69
  }
234
70
 
235
71
  static async load(dir: string): Promise<QwenMtpProvider> {
@@ -242,16 +78,24 @@ export class QwenMtpProvider implements DraftProvider {
242
78
  const weightsBytes = [...weights.shards.files.values()]
243
79
  .reduce((a, f) => a + f.mmap.size, 0);
244
80
  const module = new MtpModule(weights, config, resources);
81
+ // Stable across restarts and distinct for differently folded/quantized
82
+ // companions. Hash once at provider load, outside inference execution.
83
+ const identity = await artifactIdentity(configFingerprint(config),
84
+ [...weights.shards.files].map(([name, shard]) => ({ name, path: shard.path })));
245
85
  return new QwenMtpProvider(
246
86
  dir.split("/").filter(Boolean).at(-1) ?? "qwen-mtp",
247
- config, weightsBytes, module, resources.move(),
87
+ config, weightsBytes, module, resources.move(), `qwen-mtp-v1:${identity}`,
248
88
  );
249
89
  }
250
90
 
251
91
  open(opts: Parameters<DraftProvider["open"]>[0]): DraftSource {
92
+ return new QwenMtpSource(this.#target(opts.target), this.#module, opts.sampler, this.#checkpointNamespace);
93
+ }
94
+
95
+ #target(view: TargetView): QwenMtpTarget {
252
96
  if (this.#resources.disposed)
253
97
  throw new Error("qwen MTP provider is disposed");
254
- const target = opts.target.qwenMtp;
98
+ const target = view.qwenMtp;
255
99
  if (!target)
256
100
  throw new Error("qwen MTP drafting requires a qwen3_5-family target");
257
101
  if (target.hiddenSize !== this.#config.text.hiddenSize) {
@@ -260,14 +104,51 @@ export class QwenMtpProvider implements DraftProvider {
260
104
  `${target.hiddenSize} — split from a different checkpoint?`,
261
105
  );
262
106
  }
263
- return new QwenMtpSource(target, this.#module, opts.sampler,
264
- { store: this.#prefixStore, identity: opts.target.identity });
107
+ return target;
108
+ }
109
+
110
+ #openRows(view: TargetView, sampling: DraftRowSampling | null,
111
+ checkpoints: readonly (DraftRowCheckpoint | null)[]): DraftRowGroup & DraftPrefillGroup {
112
+ const target = this.#target(view);
113
+ const rows = new QwenMtpRows(target, this.#module, sampling, []);
114
+ const prepareAppend = (checkpoints: readonly (DraftRowCheckpoint | null)[]) => {
115
+ const states: Array<MtpRowState | null> = [];
116
+ let change: PreparedStateChange | undefined;
117
+ try {
118
+ for (const checkpoint of checkpoints) states.push(checkpoint ? restoreQwenMtpState(checkpoint) : null);
119
+ change = rows.prepareAppend(states);
120
+ disposeResources(states.splice(0).flatMap(state => state ? [state.cache, state.hidden] : []));
121
+ return change;
122
+ } catch (error) {
123
+ return cleanupFailure(error, () => disposeResources([
124
+ ...states.flatMap(state => state ? [state.cache, state.hidden] : []), ...(change ? [change] : []),
125
+ ]));
126
+ }
127
+ };
128
+ const append = (checkpoints: readonly (DraftRowCheckpoint | null)[]) => applyStateChanges([() => prepareAppend(checkpoints)]);
129
+ try { append(checkpoints); }
130
+ catch (error) { return cleanupFailure(error, () => rows.dispose()); }
131
+ return {
132
+ namespace: this.#checkpointNamespace, prefillMode: "full", tapLayers: [target.layerCount - 1],
133
+ get rowCount() { return rows.rowCount; },
134
+ append, prepareAppend, prefill: (tokens, context) => rows.prefill(tokens, context!), materialize: rows.materialize.bind(rows),
135
+ filterRows: rows.filterRows.bind(rows),
136
+ draft: rows.draft.bind(rows), commit: rows.commit.bind(rows),
137
+ consume: rows.consume.bind(rows),
138
+ capture(row) {
139
+ const state = rows.extractRow(row);
140
+ try { return captureQwenMtpState(state); }
141
+ finally { disposeResources([state.cache, state.hidden]); }
142
+ },
143
+ dispose: rows.dispose.bind(rows),
144
+ };
265
145
  }
266
146
 
147
+
267
148
  dispose(): void {
268
149
  // Release cached transpose views before their native weight maps. MLX
269
150
  // retains buffers needed by outstanding GPU commands until completion.
270
- disposeResources([this.#prefixStore, { dispose: () => this.#resources.dispose() }]);
151
+ this.#resources.dispose();
271
152
  }
272
153
  }
273
154
 
@@ -284,7 +165,7 @@ export class QwenMtpSource implements DraftSource {
284
165
  readonly #module: MtpModule;
285
166
  readonly #sampler: Sampler;
286
167
  #cache = new KVCache();
287
- readonly prefix: DraftSource["prefix"];
168
+ readonly checkpoint: NonNullable<DraftSource["checkpoint"]>;
288
169
  #prefilledTokens = 0;
289
170
  #hasDrafted = false;
290
171
  /** Target pre-norm hidden at the position preceding the next pending
@@ -295,52 +176,29 @@ export class QwenMtpSource implements DraftSource {
295
176
  #closed = false;
296
177
 
297
178
  constructor(target: QwenMtpTarget, module: MtpModule, sampler: Sampler,
298
- prefix?: { store: SpeculativePrefixStore<QwenMtpPrefixState>; identity: object }) {
179
+ namespace = "qwen-mtp-v1") {
299
180
  this.#target = target;
300
181
  this.#module = module;
301
182
  this.#sampler = sampler;
302
183
  this.tapLayers = [target.layerCount - 1];
303
- if (prefix) this.prefix = {
304
- restore: (prompt, caches, namespace, maxBytes) => {
184
+ this.checkpoint = {
185
+ namespace,
186
+ restore: (tokens, attachment) => {
305
187
  this.#checkOpen();
306
188
  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(); }
189
+ throw new Error("Qwen MTP checkpoint restore requires a fresh source");
190
+ const state = restoreQwenMtpState({ processedTokens: tokens, attachment });
191
+ try { disposeResources([this.#cache, ...(this.#pendingTrueHidden ? [this.#pendingTrueHidden] : [])]); }
192
+ catch (error) { return cleanupFailure(error, () => disposeResources([state.cache, state.hidden])); }
193
+ this.#cache = state.cache;
194
+ this.#pendingTrueHidden = state.hidden;
195
+ this.#prefilledTokens = tokens;
324
196
  },
325
- capture: (tokens, caches, namespace, maxBytes) => {
197
+ capture: (tokens) => {
326
198
  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)); }
199
+ if (!this.#pendingTrueHidden || this.#cache.offset !== tokens - 1 || this.#roundAppended !== 0)
200
+ throw new Error("Qwen MTP snapshot requires an aligned committed boundary");
201
+ return captureQwenMtpState({ cache: this.#cache, hidden: this.#pendingTrueHidden }).attachment;
344
202
  },
345
203
  };
346
204
  }
@@ -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
+ }