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
@@ -0,0 +1,171 @@
1
+ // Qwen MTP numerical graph. Request state, sampling and checkpoint retention
2
+ // belong to its callers; the cache supplies positions and attention storage.
3
+ import type { MlxArray } from "../mlx/array";
4
+ import * as ops from "../mlx/ops";
5
+ import type { ModelConfig } from "../config";
6
+ import type { Weights } from "../weights";
7
+ import { disposing, QuantizedLinear, RMSNorm, type Cache, type Mask } from "../model/gemma4-base";
8
+ import { DenseLinear } from "../model/universal/modules";
9
+ import { compiledSwiglu } from "../model/qwen3_5";
10
+
11
+ type MtpLinear = DenseLinear | QuantizedLinear;
12
+
13
+ /** Projection tensors belong to Weights; dense transpose views belong to
14
+ * the provider's resource stack. Quantized heads keep no dense copy. */
15
+ function loadMtpLinear(
16
+ weights: Weights, path: string, config: ModelConfig, resources: DisposableStack,
17
+ ): MtpLinear {
18
+ if (weights.has(`${path}.scales`)) return QuantizedLinear.load(weights, path, config);
19
+ const layer = new DenseLinear(weights.tensor(`${path}.weight`), null);
20
+ resources.use(layer.wT);
21
+ return layer;
22
+ }
23
+
24
+ /** Qwen3Attention.forward with the companion's dense or quantized projections.
25
+ * Attention operations follow src/model/qwen3_5.ts verbatim. */
26
+ class MtpAttention {
27
+ readonly qProj: MtpLinear;
28
+ readonly kProj: MtpLinear;
29
+ readonly vProj: MtpLinear;
30
+ readonly oProj: MtpLinear;
31
+ readonly qNorm: RMSNorm;
32
+ readonly kNorm: RMSNorm;
33
+ readonly nHeads: number;
34
+ readonly nKvHeads: number;
35
+ readonly headDim: number;
36
+ readonly scale: number;
37
+ readonly ropeDims: number;
38
+ readonly ropeBase: number;
39
+
40
+ constructor(weights: Weights, config: ModelConfig, prefix: string, resources: DisposableStack) {
41
+ const t = config.text;
42
+ this.nHeads = t.numAttentionHeads;
43
+ this.nKvHeads = t.numKeyValueHeads;
44
+ this.headDim = t.headDim;
45
+ this.scale = Math.pow(this.headDim, -0.5);
46
+ this.ropeDims = Math.trunc(this.headDim * t.partialRotaryFactor);
47
+ this.ropeBase = t.ropeParameters.full_attention?.ropeTheta ?? 10000;
48
+ this.qProj = loadMtpLinear(weights, `${prefix}.q_proj`, config, resources);
49
+ this.kProj = loadMtpLinear(weights, `${prefix}.k_proj`, config, resources);
50
+ this.vProj = loadMtpLinear(weights, `${prefix}.v_proj`, config, resources);
51
+ this.oProj = loadMtpLinear(weights, `${prefix}.o_proj`, config, resources);
52
+ this.qNorm = new RMSNorm(weights.tensor(`${prefix}.q_norm.weight`), t.rmsNormEps);
53
+ this.kNorm = new RMSNorm(weights.tensor(`${prefix}.k_norm.weight`), t.rmsNormEps);
54
+ }
55
+
56
+ forward(x: MlxArray, mask: Mask, cache: Cache): MlxArray {
57
+ const [B, L] = x.shape as [number, number, number];
58
+ const qp = this.qProj.forward(x);
59
+ const qpr = disposing(qp, ops.reshape(qp, [B, L, this.nHeads, this.headDim * 2]));
60
+ const [qHeads, gateHeads] = ops.split(qpr, [this.headDim], -1) as [MlxArray, MlxArray];
61
+ qpr.dispose();
62
+ const gate = disposing(gateHeads, ops.reshape(gateHeads, [B, L, this.nHeads * this.headDim]));
63
+
64
+ let k = this.kProj.forward(x);
65
+ let v = this.vProj.forward(x);
66
+
67
+ let q = this.qNorm.forward(qHeads);
68
+ qHeads.dispose();
69
+ q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
70
+ k = disposing(k, ops.reshape(k, [B, L, this.nKvHeads, this.headDim]));
71
+ k = disposing(k, this.kNorm.forward(k));
72
+ k = disposing(k, ops.transposeAxes(k, [0, 2, 1, 3]));
73
+ v = disposing(v, ops.reshape(v, [B, L, this.nKvHeads, this.headDim]));
74
+ v = disposing(v, ops.transposeAxes(v, [0, 2, 1, 3]));
75
+
76
+ const offsets = cache.ropeOffsetArr;
77
+ q = disposing(q, offsets
78
+ ? ops.ropeDynamic(q, this.ropeDims, this.ropeBase, offsets, null)
79
+ : ops.rope(q, this.ropeDims, this.ropeBase, cache.offset, null));
80
+ k = disposing(k, offsets
81
+ ? ops.ropeDynamic(k, this.ropeDims, this.ropeBase, offsets, null)
82
+ : ops.rope(k, this.ropeDims, this.ropeBase, cache.offset, null));
83
+
84
+ const [keys, values] = cache.updateAndFetch(k, v);
85
+ k.dispose();
86
+ v.dispose();
87
+ const attn = ops.sdpa(q, keys, values, this.scale, mask.mode, mask.arr);
88
+ keys.dispose();
89
+ values.dispose();
90
+ q.dispose();
91
+
92
+ const attnT = ops.transposeAxes(attn, [0, 2, 1, 3]);
93
+ attn.dispose();
94
+ const merged = ops.reshape(attnT, [B, L, -1]);
95
+ attnT.dispose();
96
+ const sig = ops.sigmoid(gate);
97
+ gate.dispose();
98
+ const gated = ops.mul(merged, sig);
99
+ merged.dispose();
100
+ sig.dispose();
101
+ const out = this.oProj.forward(gated);
102
+ gated.dispose();
103
+ return out;
104
+ }
105
+ }
106
+
107
+ /** The one MTP decoder block: fc-merge → attention → swiglu MLP → norm. */
108
+ export class MtpModule {
109
+ readonly fc: MtpLinear;
110
+ readonly preFcNormEmbedding: RMSNorm;
111
+ readonly preFcNormHidden: RMSNorm;
112
+ readonly attn: MtpAttention;
113
+ readonly mlpGate: MtpLinear;
114
+ readonly mlpUp: MtpLinear;
115
+ readonly mlpDown: MtpLinear;
116
+ readonly inputNorm: RMSNorm;
117
+ readonly postAttnNorm: RMSNorm;
118
+ readonly finalNorm: RMSNorm;
119
+
120
+ constructor(weights: Weights, config: ModelConfig, resources: DisposableStack) {
121
+ const eps = config.text.rmsNormEps;
122
+ this.fc = loadMtpLinear(weights, "fc", config, resources);
123
+ this.preFcNormEmbedding = new RMSNorm(weights.tensor("pre_fc_norm_embedding.weight"), eps);
124
+ this.preFcNormHidden = new RMSNorm(weights.tensor("pre_fc_norm_hidden.weight"), eps);
125
+ this.attn = new MtpAttention(weights, config, "layers.0.self_attn", resources);
126
+ this.mlpGate = loadMtpLinear(weights, "layers.0.mlp.gate_proj", config, resources);
127
+ this.mlpUp = loadMtpLinear(weights, "layers.0.mlp.up_proj", config, resources);
128
+ this.mlpDown = loadMtpLinear(weights, "layers.0.mlp.down_proj", config, resources);
129
+ this.inputNorm = new RMSNorm(weights.tensor("layers.0.input_layernorm.weight"), eps);
130
+ this.postAttnNorm = new RMSNorm(weights.tensor("layers.0.post_attention_layernorm.weight"), eps);
131
+ this.finalNorm = new RMSNorm(weights.tensor("norm.weight"), eps);
132
+ }
133
+
134
+ /** One block forward over [B,S,·]: token embeddings ([B,S,H], target
135
+ * embed_tokens output) paired with hiddens ([B,S,H], target pre-final-norm
136
+ * or the module's own chained output). Appends S rows to `cache`; returns
137
+ * the module output [B,S,H] (post final norm — what the target lm_head
138
+ * consumes AND what chains into the next step's `hidden`). */
139
+ forward(tokenEmbeds: MlxArray, hiddens: MlxArray, cache: Cache): MlxArray {
140
+ const embNorm = this.preFcNormEmbedding.forward(tokenEmbeds);
141
+ const hidNorm = this.preFcNormHidden.forward(hiddens);
142
+ const joined = ops.concatAxis([embNorm, hidNorm], 2);
143
+ embNorm.dispose();
144
+ hidNorm.dispose();
145
+ const x = this.fc.forward(joined);
146
+ joined.dispose();
147
+
148
+ // Decoder layer (Qwen3Layer.forward shape).
149
+ const L = x.shape[1]!;
150
+ const mask = cache.makeMask(L, null);
151
+ const xn = this.inputNorm.forward(x);
152
+ const r = this.attn.forward(xn, mask, cache);
153
+ xn.dispose();
154
+ mask.arr?.dispose();
155
+ const h = ops.add(x, r);
156
+ x.dispose();
157
+ r.dispose();
158
+ const hn = this.postAttnNorm.forward(h);
159
+ const g = this.mlpGate.forward(hn);
160
+ const u = this.mlpUp.forward(hn);
161
+ hn.dispose();
162
+ const act = compiledSwiglu(g, u);
163
+ g.dispose(); u.dispose();
164
+ const m = this.mlpDown.forward(act);
165
+ act.dispose();
166
+ const out = ops.add(h, m);
167
+ h.dispose();
168
+ m.dispose();
169
+ return disposing(out, this.finalNorm.forward(out));
170
+ }
171
+ }
@@ -0,0 +1,230 @@
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
+ /** Align the companion to verified external continuations through the same
137
+ * true-hidden prefill graph. Different rows retain different prefix lengths. */
138
+ consume(tokens: MlxArray, context: MlxArray, lengths: readonly number[]): void {
139
+ this.#cache.specRoundBegin();
140
+ this.prefill(tokens, context);
141
+ this.#cache.specRoundRollback([...lengths]);
142
+ using positions = ops.fromInt32(lengths.map(length => length - 1), [lengths.length, 1, 1]);
143
+ const hidden = ops.takeAlongAxis(context, positions, 1);
144
+ this.#hidden!.dispose(); this.#hidden = hidden;
145
+ }
146
+
147
+ /** Nonnegative depth, with every request's first pending token already known.
148
+ * Build the complete device dependency chain before reading proposal IDs. */
149
+ draft(pending: readonly number[], depth: number, steps: readonly number[]): number[][] {
150
+ const B = pending.length;
151
+ const tokens: MlxArray[] = [];
152
+ let ids: MlxArray | null = ops.fromInt32([...pending], [B, 1]);
153
+ let chained: MlxArray | null = null;
154
+ this.#cache.specRoundBegin();
155
+ try {
156
+ // A zero-proposal round still processes the pending target token. Keep
157
+ // its companion KV aligned so a later round can resume speculation.
158
+ if (depth === 0) {
159
+ using embeds = this.target.embed(ids!);
160
+ this.module.forward(embeds, this.#hidden!, this.#cache).dispose();
161
+ this.#drafts = Array.from({ length: B }, () => []);
162
+ this.#depth = 0;
163
+ this.#hidden!.dispose(); this.#hidden = null;
164
+ return this.#drafts;
165
+ }
166
+ for (let position = 0; position < depth; position++) {
167
+ using embeds = this.target.embed(ids!);
168
+ const output = this.module.forward(embeds, chained ?? this.#hidden!, this.#cache);
169
+ chained?.dispose(); chained = output;
170
+ using logits = this.target.logitsFromHidden(output);
171
+ using flat = ops.reshape(logits, [B, logits.shape.at(-1)!]);
172
+ using logprobs = toLogprobs(flat);
173
+ const token = this.sampling!.sample(logprobs, steps.map(step => step + position));
174
+ tokens.push(token);
175
+ ids!.dispose(); ids = ops.reshape(token, [B, 1]);
176
+ }
177
+ using packed = ops.concatAxis(tokens, 0);
178
+ const read = packed.toIntTokens();
179
+ this.#drafts = Array.from({ length: B }, (_, row) =>
180
+ Array.from({ length: depth }, (_, position) => read[position * B + row]!));
181
+ this.#depth = depth;
182
+ this.#hidden!.dispose(); this.#hidden = null;
183
+ return this.#drafts;
184
+ } finally {
185
+ ids?.dispose(); chained?.dispose();
186
+ for (const token of tokens) token.dispose();
187
+ }
188
+ }
189
+
190
+ /** Resolve draft storage using each request's accepted prefix. The verify
191
+ * context is borrowed [B,depth+1,H], before the target's final norm. */
192
+ commit(accepted: readonly number[], context: MlxArray): void {
193
+ const B = accepted.length, depth = this.#depth;
194
+ const appended = Math.max(1, depth);
195
+ const kept = accepted.map(count => Math.min(count + 1, appended));
196
+ if (kept.every(count => count === appended)) this.#cache.specRoundCommit();
197
+ else this.#cache.specRoundRollback(kept);
198
+
199
+ // Full acceptance needs the last accepted token's missing draft row,
200
+ // using the TRUE target hidden at its preceding position. Other requests
201
+ // keep their current coverage; their temporary append is never retained.
202
+ const extra = accepted.map(count => Number(depth > 0 && count === depth));
203
+ if (extra.some(Boolean)) {
204
+ using ids = ops.fromInt32(this.#drafts.map(row => row[depth - 1]!), [B, 1]);
205
+ using embeds = this.target.embed(ids);
206
+ using hidden = context.slice([0, depth - 1, 0], [B, depth, context.shape[2]!]);
207
+ this.#cache.specRoundBegin();
208
+ this.module.forward(embeds, hidden, this.#cache).dispose();
209
+ this.#cache.specRoundRollback(extra);
210
+ }
211
+ using positions = ops.fromInt32([...accepted], [B, 1, 1]);
212
+ this.#hidden = ops.takeAlongAxis(context, positions, 1);
213
+ this.#drafts = []; this.#depth = 0;
214
+ }
215
+
216
+ /** Only between committed rounds. Returned state is independent of the
217
+ * group's storage and uses the existing single-request cache representation. */
218
+ extractRow(row: number): MtpRowState {
219
+ const cache = this.#cache.extractRow(row);
220
+ try {
221
+ using view = this.#hidden!.slice([row, 0, 0], [row + 1, 1, this.#hidden!.shape[2]!]);
222
+ return { cache, hidden: materializeCopy(view) };
223
+ } catch (error) { cache.dispose(); throw error; }
224
+ }
225
+
226
+ dispose(): void {
227
+ this.#cache.dispose(); this.#hidden?.dispose(); this.#hidden = null;
228
+ this.#drafts = []; this.#depth = 0; this.#prefilled = [];
229
+ }
230
+ }