mlx-bun 0.0.10 → 0.0.12

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 (216) hide show
  1. package/README.md +107 -32
  2. package/THIRD_PARTY_LICENSES.md +10 -0
  3. package/docs/archive/mlx-bun-lab-report.html +275 -0
  4. package/docs/archive/wwdc-mlx-bun.html +848 -0
  5. package/docs/design/audio-input-plan.md +356 -0
  6. package/docs/design/batching-perf-path.md +58 -35
  7. package/docs/design/batching-v2-plan.md +23 -9
  8. package/docs/design/bucketing-stage.md +2 -1
  9. package/docs/design/colibri-glm52-port.md +1479 -0
  10. package/docs/design/compat-cli-surface-design.md +4 -3
  11. package/docs/design/decode-speed-program.md +172 -0
  12. package/docs/design/docs-reorg-plan.md +1 -1
  13. package/docs/design/dspark-serving-program.md +371 -0
  14. package/docs/design/dspark-speculative-decoding.md +167 -5
  15. package/docs/design/faithful-l1-consolidation.md +310 -0
  16. package/docs/design/generic-model-support.md +8 -5
  17. package/docs/design/grammar-spec-batching-integration.md +301 -0
  18. package/docs/design/minicpm5-decode-megakernel.md +16 -11
  19. package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
  20. package/docs/design/omlx-adoption-map.md +14 -6
  21. package/docs/design/orpo-training.md +34 -30
  22. package/docs/design/paged-kv-cache.md +214 -0
  23. package/docs/design/parallel-slots.md +82 -49
  24. package/docs/design/parity-tier-dag.md +43 -33
  25. package/docs/design/pre-colibri-stabilization.md +329 -0
  26. package/docs/design/repo-cleanup-plan.md +189 -0
  27. package/docs/design/runtime-isolation.md +19 -3
  28. package/docs/design/segmented-backward-training.md +3 -2
  29. package/docs/design/ssd-kv-cold-tier.md +130 -0
  30. package/docs/design/structured-output.md +484 -0
  31. package/docs/design/the-dreaming-master-plan.md +2 -3
  32. package/docs/design/turboquant-kv.md +235 -0
  33. package/docs/design/unified-engine-frontier-plan.md +774 -0
  34. package/docs/design/web-chat-beat-matrix.md +315 -0
  35. package/docs/design/web-chat-redesign.md +1112 -0
  36. package/docs/investigations/colibri-oracle-pin.md +1284 -0
  37. package/docs/investigations/dspark-handoff.md +182 -16
  38. package/docs/investigations/indexshare-performance-spike.md +357 -0
  39. package/docs/investigations/lab-build-journal.md +2 -2
  40. package/docs/investigations/openwiki-evaluation.md +249 -0
  41. package/docs/planning/IDEAS.md +14 -0
  42. package/docs/planning/PRODUCT_ROADMAP.md +2 -3
  43. package/docs/planning/chat-ui-vision-memory-first.md +199 -0
  44. package/docs/planning/chat-ui-vision.md +50 -0
  45. package/docs/planning/memory-docs-and-dag-plan.md +15 -6
  46. package/docs/planning/release-notes-v0.0.11.md +239 -0
  47. package/docs/planning/release-notes-v0.0.12.md +80 -0
  48. package/docs/reference/cli.md +63 -18
  49. package/docs/reference/distribution.md +37 -6
  50. package/docs/reference/embedding.md +40 -16
  51. package/docs/reference/features-matrix.md +99 -0
  52. package/docs/reference/library-api.md +69 -20
  53. package/docs/reference/memory.md +26 -6
  54. package/docs/reference/models.md +60 -3
  55. package/docs/reference/orpo-quickstart.md +3 -4
  56. package/docs/reference/server-api.md +717 -24
  57. package/docs/reference/server-config.md +265 -129
  58. package/docs/reference/training.md +24 -16
  59. package/package.json +5 -3
  60. package/src/anthropic.ts +67 -44
  61. package/src/audio/conformer.ts +647 -0
  62. package/src/audio/decode.ts +133 -0
  63. package/src/audio/features.ts +202 -0
  64. package/src/audio/transcode.ts +67 -0
  65. package/src/chat-template.ts +155 -10
  66. package/src/cli.ts +483 -113
  67. package/src/config.ts +116 -24
  68. package/src/diffusion/diffusion-generate.ts +14 -8
  69. package/src/download.ts +143 -12
  70. package/src/eval/README.md +7 -6
  71. package/src/eval/ifeval.ts +63 -120
  72. package/src/eval/kl.ts +59 -3
  73. package/src/eval/quality-db.ts +2 -2
  74. package/src/eval/runner.ts +17 -8
  75. package/src/eval/tasks/ifeval.ts +167 -31
  76. package/src/expert-io.ts +391 -0
  77. package/src/expert-offload.ts +15 -2
  78. package/src/expert-residency.ts +1134 -0
  79. package/src/expert-usage.ts +415 -0
  80. package/src/flags.ts +13 -0
  81. package/src/generate.ts +438 -35
  82. package/src/grammar.ts +585 -0
  83. package/src/hf-push.ts +1 -1
  84. package/src/hub-rest.ts +242 -0
  85. package/src/index.ts +14 -2
  86. package/src/jobs/runner.ts +37 -18
  87. package/src/kv-store.ts +586 -93
  88. package/src/lora.ts +85 -14
  89. package/src/md-modules.d.ts +42 -0
  90. package/src/media-fetch.ts +426 -0
  91. package/src/memory/rest.ts +245 -0
  92. package/src/memory/vault.ts +74 -0
  93. package/src/mlx/array.ts +149 -19
  94. package/src/mlx/autograd.ts +53 -16
  95. package/src/mlx/checkpoint.ts +46 -13
  96. package/src/mlx/custom-vjp.ts +83 -35
  97. package/src/mlx/ffi.ts +27 -3
  98. package/src/mlx/metal-kernel.ts +7 -4
  99. package/src/mlx/ops.ts +153 -23
  100. package/src/mlx/turboquant-ops.ts +831 -0
  101. package/src/mlx/turboquant-tables.ts +237 -0
  102. package/src/model/batched-mask.ts +62 -1
  103. package/src/model/batched-quant.ts +206 -0
  104. package/src/model/batched-rotating-quant.ts +346 -0
  105. package/src/model/batched-rotating.ts +46 -1
  106. package/src/model/compiled-decode.ts +4 -15
  107. package/src/model/factory.ts +103 -6
  108. package/src/model/gemma4-base.ts +395 -39
  109. package/src/model/gemma4.ts +122 -33
  110. package/src/model/generated/gemma4-12b.ts +9 -13
  111. package/src/model/generated/gemma4-26b.ts +10 -5
  112. package/src/model/generated/gemma4-e4b.ts +14 -9
  113. package/src/model/glm52-atlas.ts +511 -0
  114. package/src/model/glm52-cache.ts +706 -0
  115. package/src/model/glm52-config.ts +262 -0
  116. package/src/model/glm52-container.ts +305 -0
  117. package/src/model/glm52-coupling.ts +388 -0
  118. package/src/model/glm52-dsa.ts +602 -0
  119. package/src/model/glm52-expert-layout.ts +253 -0
  120. package/src/model/glm52-layout.ts +152 -0
  121. package/src/model/glm52-memory.ts +473 -0
  122. package/src/model/glm52-mla.ts +786 -0
  123. package/src/model/glm52-moe.ts +223 -0
  124. package/src/model/glm52-pilot.ts +537 -0
  125. package/src/model/glm52-reference.ts +445 -0
  126. package/src/model/glm52-residency.ts +557 -0
  127. package/src/model/glm52-resident-weights.ts +135 -0
  128. package/src/model/glm52-streamed-experts.ts +453 -0
  129. package/src/model/glm52-streamed-metal.ts +610 -0
  130. package/src/model/glm52-weights.ts +503 -0
  131. package/src/model/glm52.ts +1153 -0
  132. package/src/model/minicpm5.ts +58 -102
  133. package/src/model/paged-kv.ts +253 -0
  134. package/src/model/qwen3-delta.ts +76 -15
  135. package/src/model/qwen3-moe.ts +404 -0
  136. package/src/model/qwen3.ts +9 -8
  137. package/src/model/qwen3_5.ts +88 -23
  138. package/src/model/support.ts +19 -1
  139. package/src/model/universal/dense.ts +40 -5
  140. package/src/model/universal/rope.ts +36 -21
  141. package/src/native/expert_io.c +523 -0
  142. package/src/native-pack.ts +6 -4
  143. package/src/pi-web.ts +1299 -82
  144. package/src/preflight.ts +27 -8
  145. package/src/prompt-cache.ts +293 -33
  146. package/src/quantize/quantizer.ts +16 -1
  147. package/src/registry.ts +57 -2
  148. package/src/responses.ts +62 -49
  149. package/src/serve/batch-scheduler.ts +1050 -78
  150. package/src/serve/generation-gateway.ts +252 -24
  151. package/src/serve/isolate.ts +417 -0
  152. package/src/serve/lane-registry.ts +44 -0
  153. package/src/serve/session-search.ts +326 -0
  154. package/src/server.ts +1701 -175
  155. package/src/spec/assistant-source.ts +150 -0
  156. package/src/spec/deepspec-source.ts +143 -0
  157. package/src/spec/dflash-source.ts +124 -0
  158. package/src/spec/dspark/ab-stats.ts +136 -0
  159. package/src/spec/dspark/calibration.ts +112 -0
  160. package/src/spec/dspark/deepspec-module.ts +801 -0
  161. package/src/spec/dspark/generate-dflash.ts +72 -20
  162. package/src/spec/dspark/loader.ts +23 -0
  163. package/src/spec/dspark/module-dflash.ts +278 -36
  164. package/src/spec/dspark/quantize-drafter.ts +91 -0
  165. package/src/spec/generate.ts +59 -25
  166. package/src/spec/glm52-mtp-source.ts +392 -0
  167. package/src/spec/ngram-source.ts +132 -0
  168. package/src/spec/serve-loop.ts +617 -0
  169. package/src/spec/source.ts +118 -0
  170. package/src/spec/two-model.ts +171 -0
  171. package/src/ssd-cache.ts +111 -35
  172. package/src/tokenizer.ts +288 -2
  173. package/src/tool-approvals.ts +131 -0
  174. package/src/tool-call.ts +438 -22
  175. package/src/train/flash-cce.ts +2 -2
  176. package/src/train/job.ts +0 -12
  177. package/src/train/lora-params.ts +17 -6
  178. package/src/train/trainer.ts +60 -30
  179. package/src/vision/prompt.ts +217 -48
  180. package/src/web/app.html +1197 -1791
  181. package/src/web/app.js +5694 -0
  182. package/src/web/icon.svg +13 -0
  183. package/src/web/manifest.webmanifest +24 -0
  184. package/src/web/src/adapters-panel.ts +288 -0
  185. package/src/web/src/api.ts +57 -0
  186. package/src/web/src/assistant.ts +485 -0
  187. package/src/web/src/chat.ts +1050 -0
  188. package/src/web/src/composer.ts +959 -0
  189. package/src/web/src/dataset.ts +132 -0
  190. package/src/web/src/finetune.ts +267 -0
  191. package/src/web/src/hub.ts +428 -0
  192. package/src/web/src/main.ts +98 -0
  193. package/src/web/src/markdown.ts +0 -0
  194. package/src/web/src/memory-panel.ts +572 -0
  195. package/src/web/src/model-picker.ts +159 -0
  196. package/src/web/src/palette.ts +346 -0
  197. package/src/web/src/protocol.ts +62 -0
  198. package/src/web/src/quantize.ts +167 -0
  199. package/src/web/src/rag.ts +274 -0
  200. package/src/web/src/sessions.ts +469 -0
  201. package/src/web/src/shell.ts +804 -0
  202. package/src/web/src/status.ts +300 -0
  203. package/src/web/src/ui-catalog.ts +92 -0
  204. package/src/web/sw.js +71 -0
  205. package/src/web/vendor/README.md +61 -0
  206. package/src/web/vendor/hljs-LICENSE +29 -0
  207. package/src/web/vendor/hljs-theme.css +59 -0
  208. package/src/web/vendor/hljs.js +26 -0
  209. package/src/web-tools.ts +54 -2
  210. package/src/weights.ts +82 -19
  211. package/src/model/fused-decode-kernel.ts +0 -312
  212. package/src/model/fused-geglu-kernel.ts +0 -198
  213. package/src/model/fused-mlp-kernel.ts +0 -169
  214. package/src/model/fused-swiglu-kernel.ts +0 -240
  215. package/src/model/steel-linear-kernel.ts +0 -96
  216. package/src/status-page.html +0 -497
@@ -0,0 +1,91 @@
1
+ // Quantize a DeepSpec-format drafter checkpoint (Phase 1a of the DSpark
2
+ // serving program, docs/design/dspark-serving-program.md).
3
+ //
4
+ // Thin policy layer over the house quantizer (src/quantize): the generic
5
+ // shape-eligibility rule already captures every 2-D matmul weight — the
6
+ // layer projections, fc, lm_head, markov_w2, and the two gather tables
7
+ // (embed_tokens, markov_w1: quantized-embedding gather-dequant at load,
8
+ // mlx QuantizedEmbedding semantics). The drafter-specific overlay:
9
+ //
10
+ // KEEP bf16: confidence_head.* — threshold-sensitive (the truncation
11
+ // comparison runs on its sigmoid; borderline flips change proposal
12
+ // lengths) and tiny (~4k params), so quantizing it buys nothing.
13
+ // Norms / layer_scalar / biases are 1-D and never shape-eligible.
14
+ //
15
+ // Drafter numerics only move ACCEPTANCE, never correctness (the target
16
+ // verifies every draft) — the quality gate for any scheme swept through
17
+ // here is the Phase-1c acceptance A/B, not a KL battery.
18
+
19
+ import { quantizeModelDir, type QuantizeResult } from "../../quantize";
20
+ import { DeepspecDrafter } from "./deepspec-module";
21
+ import { MlxArray } from "../../mlx/array";
22
+ import { Dtype } from "../../mlx/ffi";
23
+
24
+ /** Module bases kept full-precision regardless of shape eligibility. */
25
+ const KEEP_BF16 = [/^confidence_head\./];
26
+
27
+ /** The drafter per-tensor policy: quantize everything shape-eligible
28
+ * except the KEEP_BF16 set. */
29
+ export function drafterQuantizePredicate(base: string): boolean {
30
+ return !KEEP_BF16.some((re) => re.test(base));
31
+ }
32
+
33
+ export interface QuantizeDrafterOptions {
34
+ bits: 4 | 8;
35
+ groupSize: 32 | 64;
36
+ /** Skip the post-write load smoke (tests exercise loading separately). */
37
+ skipLoadSmoke?: boolean;
38
+ onProgress?: (stage: string, message: string, progress: number) => void;
39
+ }
40
+
41
+ /**
42
+ * Quantize the DeepSpec drafter at `srcDir` into `outDir` and smoke the
43
+ * result: reload through DeepspecDrafter (quantized detection + graph
44
+ * build) and eval one projectContext row through the quantized fc path.
45
+ */
46
+ export async function quantizeDrafterDir(
47
+ srcDir: string,
48
+ outDir: string,
49
+ opts: QuantizeDrafterOptions,
50
+ ): Promise<QuantizeResult> {
51
+ const raw = (await Bun.file(`${srcDir}/config.json`).json()) as Record<string, any>;
52
+ if (raw.architectures?.[0] !== "Gemma4DSparkModel")
53
+ throw new Error(
54
+ `quantizeDrafterDir: expected a DeepSpec checkpoint (architectures[0]==="Gemma4DSparkModel"), got ${JSON.stringify(raw.architectures)} — for regular models use \`mlx-bun quantize\``,
55
+ );
56
+ if (raw.quantization ?? raw.quantization_config)
57
+ throw new Error("quantizeDrafterDir: source is already quantized — start from the bf16 checkpoint");
58
+
59
+ const result = await quantizeModelDir(
60
+ srcDir,
61
+ outDir,
62
+ {
63
+ bits: opts.bits,
64
+ groupSize: opts.groupSize,
65
+ quantizePredicate: drafterQuantizePredicate,
66
+ },
67
+ opts.onProgress ? (e) => opts.onProgress!(e.stage, e.message, e.progress) : undefined,
68
+ );
69
+
70
+ if (!opts.skipLoadSmoke) {
71
+ // Load smoke: quantized detection + one real quantized matmul (fc) so a
72
+ // wrong spec/layout fails HERE, not at serve time.
73
+ const d = await DeepspecDrafter.load(outDir);
74
+ try {
75
+ const width = d.tapLayers.length * d.hidden;
76
+ const zeros = MlxArray.fromFloat32(new Float32Array(width), [1, 1, width]);
77
+ const x = zeros.astype(Dtype.bfloat16);
78
+ zeros.dispose();
79
+ const proj = d.projectContext(x);
80
+ x.dispose();
81
+ const got = proj.shape;
82
+ proj.dispose();
83
+ if (got.length !== 3 || got[2] !== d.hidden)
84
+ throw new Error(`load smoke: projectContext shape ${JSON.stringify(got)}, expected [1,1,${d.hidden}]`);
85
+ } finally {
86
+ d.dispose();
87
+ }
88
+ }
89
+
90
+ return result;
91
+ }
@@ -128,18 +128,36 @@ export function specGenerate(
128
128
  full: (caches[donors.full] as never as { temporalView(): [MlxArray, MlxArray] }).temporalView(),
129
129
  });
130
130
 
131
+ // Round/prefill scratch tensors, hoisted so the finally frees them on a
132
+ // mid-round throw (the 2f not-trimmable throw, or any mlx op) — try-body
133
+ // locals are invisible to the finally. Each is nulled at its normal
134
+ // disposal, so the finally never double-frees. Same discipline as
135
+ // src/spec/serve-loop.ts / generate-dflash.ts (2026-07-06 review fix);
136
+ // see memory [[mlx-inline-slice-leak-pattern]].
137
+ let prefillH: MlxArray | null = null;
138
+ let lastHidden: MlxArray | null = null;
139
+ let vHidden: MlxArray | null = null;
140
+
131
141
  try {
132
- // 1. prefill
142
+ // 1. prefill — FULL-prompt single forward, deliberately. Per-oracle
143
+ // convention (2026-07-07 re-anchor): mlx-lm's speculative path drains to
144
+ // len-1 with no step-0 (the SERVE loop, whose oracle it is, now mirrors
145
+ // that — src/spec/serve-loop.ts); THIS loop's oracle is optiq
146
+ // spec_generate, which prefills the whole prompt in one forward and
147
+ // samples token0 from it (runtime/spec/runtime.py:162-170, read from the
148
+ // installed oracle venv) — the 2026-06-14 bit-exact gate was established
149
+ // against exactly that shape. Do not tail-split here.
133
150
  const t0 = performance.now();
134
151
  const ids = ops.fromInt32(promptTokens, [1, promptTokens.length]);
135
- const hidden = model.forwardHidden(ids, caches);
152
+ prefillH = model.forwardHidden(ids, caches);
136
153
  ids.dispose();
137
154
  stats.targetCalls++;
138
- const H = hidden.shape[2]!;
139
- const Lp = hidden.shape[1]!;
140
- let next = pickFromHidden(model, hidden, Lp - 1);
141
- let lastHidden = hidden.slice([0, Lp - 1, 0], [1, Lp, H]);
142
- hidden.dispose();
155
+ const H = prefillH.shape[2]!;
156
+ const Lp = prefillH.shape[1]!;
157
+ let next = pickFromHidden(model, prefillH, Lp - 1);
158
+ lastHidden = prefillH.slice([0, Lp - 1, 0], [1, Lp, H]);
159
+ prefillH.dispose();
160
+ prefillH = null;
143
161
  stats.prefillMs = performance.now() - t0;
144
162
 
145
163
  // EOS convention: an EOS id stops generation and counts toward
@@ -158,30 +176,38 @@ export function specGenerate(
158
176
  const position = caches[0]!.offset - 1;
159
177
  const shared = readDonors();
160
178
 
161
- // 2a. draft γ tokens against the frozen donor views
179
+ // 2a. draft γ tokens against the frozen donor views. try/finally so a
180
+ // mid-draft throw still frees the chained hiddens + donor views + the
181
+ // in-flight emb (assistant-source.ts draft() discipline).
162
182
  const drafts: number[] = [];
163
183
  let dTok = next;
164
- let dHid = lastHidden; // borrowed for k=0
184
+ let dHid = lastHidden!; // borrowed for k=0
165
185
  const ownedHiddens: MlxArray[] = [];
166
- for (let k = 0; k < gamma; k++) {
167
- const emb = embedScaled(dTok);
168
- const step = drafter.forward(emb, dHid, shared, position + k);
169
- emb.dispose();
170
- drafts.push(step.token);
171
- stats.drafted++;
172
- ownedHiddens.push(step.nextHidden);
173
- dTok = step.token;
174
- dHid = step.nextHidden;
175
- }
176
- for (const a of ownedHiddens) a.dispose();
177
- for (const [k, v] of [shared.sliding, shared.full]) {
178
- k.dispose();
179
- v.dispose();
186
+ let emb: MlxArray | null = null;
187
+ try {
188
+ for (let k = 0; k < gamma; k++) {
189
+ emb = embedScaled(dTok);
190
+ const step = drafter.forward(emb, dHid, shared, position + k);
191
+ emb.dispose();
192
+ emb = null;
193
+ drafts.push(step.token);
194
+ stats.drafted++;
195
+ ownedHiddens.push(step.nextHidden);
196
+ dTok = step.token;
197
+ dHid = step.nextHidden;
198
+ }
199
+ } finally {
200
+ emb?.dispose();
201
+ for (const a of ownedHiddens) a.dispose();
202
+ for (const [k, v] of [shared.sliding, shared.full]) {
203
+ k.dispose();
204
+ v.dispose();
205
+ }
180
206
  }
181
207
 
182
208
  // 2b. verify all γ drafts (+ the pending token) in one forward
183
209
  const vIds = ops.fromInt32([next, ...drafts], [1, gamma + 1]);
184
- const vHidden = model.forwardHidden(vIds, caches);
210
+ vHidden = model.forwardHidden(vIds, caches);
185
211
  vIds.dispose();
186
212
  stats.targetCalls++;
187
213
 
@@ -199,6 +225,7 @@ export function specGenerate(
199
225
  stats.emitted++;
200
226
  if (isEos || stats.emitted >= maxTokens) {
201
227
  vHidden.dispose();
228
+ vHidden = null;
202
229
  break outer;
203
230
  }
204
231
  }
@@ -221,18 +248,25 @@ export function specGenerate(
221
248
 
222
249
  if (emitIsEos || stats.emitted >= maxTokens) {
223
250
  vHidden.dispose();
251
+ vHidden = null;
224
252
  break;
225
253
  }
226
254
 
227
255
  // 2g. chain state
228
256
  next = emit;
229
- lastHidden.dispose();
257
+ lastHidden!.dispose();
230
258
  lastHidden = vHidden.slice([0, kAccept, 0], [1, kAccept + 1, H]);
231
259
  vHidden.dispose();
260
+ vHidden = null;
232
261
  }
233
262
  stats.decodeMs = performance.now() - tDecode;
234
263
  lastHidden.dispose();
264
+ lastHidden = null;
235
265
  } finally {
266
+ // Survivors of a mid-round throw (each nulled at its normal disposal).
267
+ prefillH?.dispose();
268
+ lastHidden?.dispose();
269
+ vHidden?.dispose();
236
270
  for (const c of caches) c.dispose();
237
271
  }
238
272
 
@@ -0,0 +1,392 @@
1
+ // Native GLM-5.2 MTP draft source.
2
+ //
3
+ // The target and MTP row share embeddings, the output head, and the resident
4
+ // dense target weights. Only the MTP layer's own dense tensors and int8 routed
5
+ // experts add model weight residency. The generic speculative serve loop owns
6
+ // verification, grammar composition, target-cache rollback, sampling, and
7
+ // telemetry. Prompt lookup remains the model-free alternative DraftProvider;
8
+ // selecting one provider per request prevents two independent draft histories
9
+ // from advancing the target or grammar state twice.
10
+
11
+ import { MlxArray } from "../mlx/array";
12
+ import * as ops from "../mlx/ops";
13
+ import { toLogprobs } from "../sampler";
14
+ import { Glm52DecoderLayer, Glm52Model } from "../model/glm52";
15
+ import { MLACache } from "../model/glm52-cache";
16
+ import { rmsNormF32Mlx } from "../model/glm52-mla";
17
+ import type { DraftProvider, DraftSource } from "./source";
18
+
19
+ type Sampler = (logprobs: MlxArray, step: number) => MlxArray;
20
+
21
+ function lastToken(feed: readonly number[]): number {
22
+ const token = feed.at(-1);
23
+ if (!Number.isSafeInteger(token) || token! < 0)
24
+ throw new Error("native MTP requires a non-empty non-negative token feed");
25
+ return token!;
26
+ }
27
+
28
+ export class Glm52NativeMtpProvider implements DraftProvider {
29
+ readonly id = "glm52-native-mtp";
30
+ readonly weightsBytes = 0;
31
+ readonly #layer: Glm52DecoderLayer;
32
+
33
+ constructor(readonly model: Glm52Model) {
34
+ if (!model.capabilities.mtpMetadata)
35
+ throw new Error("GLM-5.2 artifact does not contain a complete MTP row");
36
+ if (model.capabilities.mtpEnabled === false)
37
+ throw new Error("native GLM-5.2 MTP is disabled for this model instance");
38
+ if (model.glmConfig.numNextnPredictLayers !== 1) {
39
+ throw new Error(
40
+ `native GLM-5.2 MTP requires exactly one next-token layer; got ` +
41
+ `${model.glmConfig.numNextnPredictLayers}`,
42
+ );
43
+ }
44
+ const mtpBackend = model.expertRuntime?.mtpExecutor ?? null;
45
+ if (model.expertBackend && !mtpBackend) {
46
+ throw new Error(
47
+ "streamed GLM-5.2 MTP requires the bounded int8 MTP expert tier",
48
+ );
49
+ }
50
+ this.#layer = new Glm52DecoderLayer(
51
+ model.glmConfig,
52
+ model.weights,
53
+ model.glmConfig.numHiddenLayers,
54
+ false,
55
+ mtpBackend,
56
+ );
57
+ }
58
+
59
+ open(opts: Parameters<DraftProvider["open"]>[0]): DraftSource {
60
+ if (opts.target.model !== this.model)
61
+ throw new Error("native MTP provider was opened for a different target");
62
+ return new Glm52NativeMtpSource(this.model, this.#layer, opts.sampler);
63
+ }
64
+
65
+ dispose(): void {
66
+ // The target model owns every shared/MTP weight and the expert tier.
67
+ }
68
+ }
69
+
70
+ export class Glm52NativeMtpSource implements DraftSource {
71
+ readonly prefillMode = "full" as const;
72
+ readonly pinTargetKernelFamily = true;
73
+ readonly weightsBytes = 0;
74
+ #cache: MLACache;
75
+ #roundStart = 0;
76
+ #lastDraftCount = 0;
77
+ #closed = false;
78
+
79
+ constructor(
80
+ readonly model: Glm52Model,
81
+ readonly layer: Glm52DecoderLayer,
82
+ readonly sampler: Sampler,
83
+ ) {
84
+ this.#cache = new MLACache({
85
+ kvLoraRank: model.glmConfig.kvLoraRank,
86
+ ropeHeadDim: model.glmConfig.qkRopeHeadDim,
87
+ maxTokens: model.glmConfig.maxPositionEmbeddings,
88
+ role: "mtp",
89
+ });
90
+ }
91
+
92
+ get cacheOffset(): number {
93
+ return this.#cache.offset;
94
+ }
95
+
96
+ get cacheBytes(): number {
97
+ return this.#cache.byteLength;
98
+ }
99
+
100
+ /** Caller-owned zero-copy snapshot for the v3 KV persistence writer. */
101
+ clonePersistentCache(): MLACache {
102
+ this.#checkOpen();
103
+ if (this.#lastDraftCount !== 0)
104
+ throw new Error("native MTP cache cannot snapshot an uncommitted round");
105
+ if (this.#cache.offset === 0)
106
+ throw new Error("native MTP cache cannot snapshot empty state");
107
+ const state = this.#cache.fetch();
108
+ const clone = new MLACache({
109
+ kvLoraRank: this.#cache.kvLoraRank,
110
+ ropeHeadDim: this.#cache.ropeHeadDim,
111
+ maxTokens: this.#cache.maxTokens,
112
+ role: "mtp",
113
+ });
114
+ try {
115
+ clone.restoreCompressedState(state.latent, state.rope, null, this.#cache.offset);
116
+ return clone;
117
+ } catch (error) {
118
+ state.latent.dispose();
119
+ state.rope.dispose();
120
+ clone.dispose();
121
+ throw error;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Adopt one restored `mtp-mla` cache. Ownership transfers only after every
127
+ * role/geometry/offset check passes; the source disposes it thereafter.
128
+ */
129
+ restorePersistentCache(cache: MLACache): void {
130
+ this.#checkOpen();
131
+ if (this.#lastDraftCount !== 0)
132
+ throw new Error("native MTP cache cannot restore during a draft round");
133
+ if (this.#cache.offset !== 0)
134
+ throw new Error("native MTP cache restore requires an empty source");
135
+ if (cache.role !== "mtp" || cache.dsa)
136
+ throw new Error("native MTP restore requires one mtp-mla cache");
137
+ if (
138
+ cache.kvLoraRank !== this.#cache.kvLoraRank ||
139
+ cache.ropeHeadDim !== this.#cache.ropeHeadDim ||
140
+ cache.maxTokens !== this.#cache.maxTokens
141
+ ) {
142
+ throw new Error("native MTP restored cache geometry does not match model");
143
+ }
144
+ if (cache.offset <= 0 || cache.batchSize !== 1)
145
+ throw new Error("native MTP restored cache must contain one non-empty row");
146
+ this.#cache.dispose();
147
+ this.#cache = cache;
148
+ this.#roundStart = cache.offset;
149
+ }
150
+
151
+ prefill(_promptIds: number[]): void {
152
+ this.#checkOpen();
153
+ if (this.#cache.offset !== 0)
154
+ throw new Error("native MTP source cannot be prefilled twice");
155
+ // Direct Colibri intentionally leaves MTP KV empty here ("niente
156
+ // prefill"): its first draft opens a decode-only window from the target
157
+ // anchor hidden and pending token. Seeding this cache with the prompt
158
+ // would change both the oracle state and the first speculative round.
159
+ }
160
+
161
+ async draft(
162
+ feed: number[],
163
+ n: number,
164
+ stepBase: number,
165
+ anchorHidden?: MlxArray,
166
+ ): Promise<number[]> {
167
+ this.#checkOpen();
168
+ if (!anchorHidden)
169
+ throw new Error("native MTP drafting requires the target anchor hidden");
170
+ if (!Number.isSafeInteger(n) || n < 0)
171
+ throw new Error(`native MTP draft length must be non-negative; got ${n}`);
172
+ if (this.#lastDraftCount !== 0)
173
+ throw new Error("native MTP draft called before the prior round committed");
174
+ const [batch, tokens, hidden] = anchorHidden.shape;
175
+ if (batch !== 1 || tokens !== 1 || hidden !== this.model.glmConfig.hiddenSize) {
176
+ throw new Error(
177
+ `native MTP anchor must be [1,1,${this.model.glmConfig.hiddenSize}], ` +
178
+ `got [${anchorHidden.shape.join(",")}]`,
179
+ );
180
+ }
181
+
182
+ this.#roundStart = this.#cache.offset;
183
+ let token = lastToken(feed);
184
+ let state: MlxArray | null = ops.contiguous(anchorHidden);
185
+ const drafts: number[] = [];
186
+ try {
187
+ for (let position = 0; position < n; position++) {
188
+ const nextState = await this.#step(token, state, stepBase + position);
189
+ state.dispose();
190
+ state = nextState.hidden;
191
+ token = nextState.token;
192
+ drafts.push(token);
193
+ }
194
+ this.#lastDraftCount = drafts.length;
195
+ return drafts;
196
+ } catch (error) {
197
+ const appended = this.#cache.offset - this.#roundStart;
198
+ if (appended > 0) this.#cache.trim(appended);
199
+ throw error;
200
+ } finally {
201
+ state?.dispose();
202
+ }
203
+ }
204
+
205
+ async commit(
206
+ d: number,
207
+ kAccept: number,
208
+ vCtxML?: MlxArray,
209
+ verifiedHidden?: MlxArray,
210
+ acceptedTokens: readonly number[] = [],
211
+ ): Promise<void> {
212
+ this.#checkOpen();
213
+ vCtxML?.dispose();
214
+ if (d !== this.#lastDraftCount)
215
+ throw new Error(`native MTP commit d=${d}, expected ${this.#lastDraftCount}`);
216
+ if (!Number.isSafeInteger(kAccept) || kAccept < 0 || kAccept > d)
217
+ throw new Error(`native MTP accepted ${kAccept} outside [0,${d}]`);
218
+ if (acceptedTokens.length !== kAccept)
219
+ throw new Error(
220
+ `native MTP received ${acceptedTokens.length} accepted tokens for k=${kAccept}`,
221
+ );
222
+ if (kAccept > 0 && !verifiedHidden)
223
+ throw new Error("native MTP absorption requires verified target hidden rows");
224
+
225
+ try {
226
+ // Only the first speculative MTP row is conditioned on a true target
227
+ // hidden. Later rows use earlier MTP hiddens, so accepted positions are
228
+ // rebuilt below from the target's verified hidden window.
229
+ const speculativeTail = Math.max(d - 1, 0);
230
+ if (speculativeTail > 0) this.#cache.trim(speculativeTail);
231
+ if (kAccept > 0)
232
+ await this.#absorb(acceptedTokens, verifiedHidden!);
233
+ const expected = this.#roundStart + (d > 0 ? 1 : 0) + kAccept;
234
+ if (this.#cache.offset !== expected) {
235
+ throw new Error(
236
+ `native MTP cache committed to ${this.#cache.offset}, expected ${expected}`,
237
+ );
238
+ }
239
+ } catch (error) {
240
+ const appended = this.#cache.offset - this.#roundStart;
241
+ if (appended > 0) this.#cache.trim(appended);
242
+ throw error;
243
+ } finally {
244
+ this.#lastDraftCount = 0;
245
+ }
246
+ }
247
+
248
+ dispose(): void {
249
+ if (this.#closed) return;
250
+ this.#closed = true;
251
+ this.#cache.dispose();
252
+ }
253
+
254
+ async #step(
255
+ token: number,
256
+ hidden: MlxArray,
257
+ sampleStep: number,
258
+ ): Promise<{ hidden: MlxArray; token: number }> {
259
+ const config = this.model.glmConfig;
260
+ const ids = ops.fromInt32([token], [1, 1]);
261
+ let embedded: MlxArray | null = null;
262
+ let embeddedNorm: MlxArray | null = null;
263
+ let hiddenNorm: MlxArray | null = null;
264
+ let joined: MlxArray | null = null;
265
+ let projected: MlxArray | null = null;
266
+ let output: MlxArray | null = null;
267
+ let headInput: MlxArray | null = null;
268
+ let logits: MlxArray | null = null;
269
+ let logprobs: MlxArray | null = null;
270
+ let sampled: MlxArray | null = null;
271
+ try {
272
+ embedded = this.model.weights.embedding(
273
+ ids,
274
+ "model.embed_tokens.weight",
275
+ config.vocabSize,
276
+ config.hiddenSize,
277
+ );
278
+ embeddedNorm = rmsNormF32Mlx(
279
+ embedded,
280
+ this.model.weights.tensor(
281
+ `model.layers.${config.numHiddenLayers}.enorm.weight`,
282
+ ),
283
+ config.rmsNormEps,
284
+ );
285
+ hiddenNorm = rmsNormF32Mlx(
286
+ hidden,
287
+ this.model.weights.tensor(
288
+ `model.layers.${config.numHiddenLayers}.hnorm.weight`,
289
+ ),
290
+ config.rmsNormEps,
291
+ );
292
+ joined = ops.concatAxis([embeddedNorm, hiddenNorm], 2);
293
+ projected = this.model.weights.linear(
294
+ joined,
295
+ `model.layers.${config.numHiddenLayers}.eh_proj.weight`,
296
+ config.hiddenSize,
297
+ 2 * config.hiddenSize,
298
+ );
299
+ output = await this.layer.forwardAsync(projected, this.#cache, null);
300
+ headInput = rmsNormF32Mlx(
301
+ output,
302
+ this.model.weights.tensor(
303
+ `model.layers.${config.numHiddenLayers}.shared_head.norm.weight`,
304
+ ),
305
+ config.rmsNormEps,
306
+ );
307
+ logits = this.model.logitsFromHidden(headInput);
308
+ logprobs = toLogprobs(logits);
309
+ sampled = this.sampler(logprobs, sampleStep);
310
+ return {
311
+ hidden: ops.contiguous(output),
312
+ token: ops.itemUint32(sampled),
313
+ };
314
+ } finally {
315
+ ids.dispose();
316
+ embedded?.dispose();
317
+ embeddedNorm?.dispose();
318
+ hiddenNorm?.dispose();
319
+ joined?.dispose();
320
+ projected?.dispose();
321
+ output?.dispose();
322
+ headInput?.dispose();
323
+ logits?.dispose();
324
+ logprobs?.dispose();
325
+ sampled?.dispose();
326
+ }
327
+ }
328
+
329
+ async #absorb(
330
+ acceptedTokens: readonly number[],
331
+ verifiedHidden: MlxArray,
332
+ ): Promise<void> {
333
+ const config = this.model.glmConfig;
334
+ const k = acceptedTokens.length;
335
+ const ids = ops.fromInt32([...acceptedTokens], [1, k]);
336
+ let embedded: MlxArray | null = null;
337
+ let embeddedNorm: MlxArray | null = null;
338
+ let trueRows: MlxArray | null = null;
339
+ let hiddenNorm: MlxArray | null = null;
340
+ let joined: MlxArray | null = null;
341
+ let projected: MlxArray | null = null;
342
+ let output: MlxArray | null = null;
343
+ try {
344
+ embedded = this.model.weights.embedding(
345
+ ids,
346
+ "model.embed_tokens.weight",
347
+ config.vocabSize,
348
+ config.hiddenSize,
349
+ );
350
+ embeddedNorm = rmsNormF32Mlx(
351
+ embedded,
352
+ this.model.weights.tensor(
353
+ `model.layers.${config.numHiddenLayers}.enorm.weight`,
354
+ ),
355
+ config.rmsNormEps,
356
+ );
357
+ trueRows = verifiedHidden.slice(
358
+ [0, 0, 0],
359
+ [1, k, config.hiddenSize],
360
+ );
361
+ hiddenNorm = rmsNormF32Mlx(
362
+ trueRows,
363
+ this.model.weights.tensor(
364
+ `model.layers.${config.numHiddenLayers}.hnorm.weight`,
365
+ ),
366
+ config.rmsNormEps,
367
+ );
368
+ joined = ops.concatAxis([embeddedNorm, hiddenNorm], 2);
369
+ projected = this.model.weights.linear(
370
+ joined,
371
+ `model.layers.${config.numHiddenLayers}.eh_proj.weight`,
372
+ config.hiddenSize,
373
+ 2 * config.hiddenSize,
374
+ );
375
+ output = await this.layer.forwardAsync(projected, this.#cache, null);
376
+ output.eval();
377
+ } finally {
378
+ ids.dispose();
379
+ embedded?.dispose();
380
+ embeddedNorm?.dispose();
381
+ trueRows?.dispose();
382
+ hiddenNorm?.dispose();
383
+ joined?.dispose();
384
+ projected?.dispose();
385
+ output?.dispose();
386
+ }
387
+ }
388
+
389
+ #checkOpen(): void {
390
+ if (this.#closed) throw new Error("native MTP source is closed");
391
+ }
392
+ }