mlx-bun 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/docs/README.md +4 -1
  2. package/docs/archive/investigations/openjarvis-fit.md +198 -0
  3. package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
  4. package/docs/design/batching.md +1321 -49
  5. package/docs/design/decode-speed-program.md +331 -1163
  6. package/docs/design/generic-model-support.md +79 -3
  7. package/docs/design/kv-cache.md +198 -54
  8. package/docs/design/speculative-decoding.md +387 -24
  9. package/docs/design/turboquant.md +123 -13
  10. package/docs/design/unified-engine-frontier-plan.md +144 -142
  11. package/docs/planning/PRODUCT_ROADMAP.md +16 -0
  12. package/docs/planning/release-notes-v0.4.0.md +45 -0
  13. package/docs/reference/benchmarks.md +2457 -6
  14. package/docs/reference/cli.md +41 -2
  15. package/docs/reference/distribution.md +16 -9
  16. package/docs/reference/environment.md +55 -5
  17. package/docs/reference/models.md +13 -4
  18. package/docs/reference/server-api.md +79 -25
  19. package/docs/reference/server-config.md +178 -84
  20. package/docs/reference/troubleshooting.md +13 -21
  21. package/package.json +1 -1
  22. package/src/backends/mlx/assistant-target.ts +46 -0
  23. package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
  24. package/src/backends/mlx/batch-group.ts +368 -224
  25. package/src/backends/mlx/cache-attachment.ts +67 -0
  26. package/src/backends/mlx/cache-layout.ts +31 -0
  27. package/src/backends/mlx/checkpoint-state.ts +53 -0
  28. package/src/backends/mlx/continuation-persistence.ts +106 -0
  29. package/src/backends/mlx/continuation-request.ts +43 -0
  30. package/src/backends/mlx/continuation.ts +77 -0
  31. package/src/backends/mlx/draft-target.ts +7 -22
  32. package/src/backends/mlx/full-kv-row-append.ts +32 -0
  33. package/src/backends/mlx/full-kv-row-donor.ts +54 -0
  34. package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
  35. package/src/backends/mlx/gateway-binding.ts +77 -18
  36. package/src/backends/mlx/kv-codec.ts +9 -0
  37. package/src/backends/mlx/kv-maintenance.ts +133 -0
  38. package/src/backends/mlx/kv-tensor-rows.ts +212 -0
  39. package/src/backends/mlx/model-serving.ts +1 -1
  40. package/src/backends/mlx/prefill-cohort.ts +89 -0
  41. package/src/backends/mlx/prefill-rows.ts +200 -0
  42. package/src/backends/mlx/prefill.ts +1 -1
  43. package/src/backends/mlx/projected-context-rows.ts +72 -0
  44. package/src/backends/mlx/request-state-policy.ts +41 -0
  45. package/src/backends/mlx/rollback.ts +41 -3
  46. package/src/backends/mlx/row-sampling.ts +49 -0
  47. package/src/backends/mlx/serial-executor.ts +35 -66
  48. package/src/backends/mlx/speculative-group.ts +359 -0
  49. package/src/backends/mlx/speculative-round.ts +161 -0
  50. package/src/backends/mlx/state-rows.ts +104 -0
  51. package/src/backends/mlx/state-views.ts +21 -1
  52. package/src/backends/mlx/target-layout-capability.ts +35 -0
  53. package/src/backends/mlx/transitioning-kv-rows.ts +165 -0
  54. package/src/cli.ts +11 -36
  55. package/src/contracts/execution.ts +11 -0
  56. package/src/contracts/prefix-cache.ts +23 -0
  57. package/src/contracts/resources.ts +7 -0
  58. package/src/contracts/sampling.ts +23 -0
  59. package/src/contracts/scheduling.ts +18 -0
  60. package/src/engine/execution-plan.ts +18 -16
  61. package/src/engine/resources.ts +12 -1
  62. package/src/engine/scheduler.ts +10 -5
  63. package/src/fit.ts +29 -15
  64. package/src/generate.ts +77 -173
  65. package/src/inference/draft-acceptance.ts +41 -0
  66. package/src/inference/draft-output.ts +49 -0
  67. package/src/inference/rollback.ts +2 -2
  68. package/src/kv-scheme.ts +70 -12
  69. package/src/kv-store.ts +70 -16
  70. package/src/lab/paged-kv/paged-kv-rows.ts +83 -0
  71. package/src/lab/paged-kv/paged-kv.ts +25 -11
  72. package/src/mlx/array.ts +2 -1
  73. package/src/model/artifact-identity.ts +73 -0
  74. package/src/model/batched-kv.ts +235 -0
  75. package/src/model/batched-mask.ts +12 -4
  76. package/src/model/batched-quant.ts +8 -2
  77. package/src/model/batched-quantized-kv.ts +144 -0
  78. package/src/model/batched-rotating-quant.ts +76 -15
  79. package/src/model/batched-rotating-state.ts +76 -0
  80. package/src/model/batched-rotating.ts +64 -22
  81. package/src/model/batched-row-storage.ts +52 -0
  82. package/src/model/batched-ssm.ts +52 -0
  83. package/src/model/batched-turboquant-kv.ts +93 -0
  84. package/src/model/compiled-decode.ts +7 -1
  85. package/src/model/decoded-kv-donor.ts +12 -0
  86. package/src/model/delayed-quantized-kv.ts +72 -0
  87. package/src/model/delayed-rotating-quantized-kv.ts +93 -0
  88. package/src/model/delayed-turboquant-kv.ts +55 -0
  89. package/src/model/full-prefill-padding.ts +56 -0
  90. package/src/model/full-prefill-row.ts +105 -0
  91. package/src/model/gemma4-base.ts +162 -80
  92. package/src/model/gemma4.ts +34 -26
  93. package/src/model/generated/gemma4-12b.ts +4 -1
  94. package/src/model/generated/gemma4-26b.ts +4 -1
  95. package/src/model/generated/gemma4-e4b.ts +4 -1
  96. package/src/model/glm52-cache.ts +80 -4
  97. package/src/model/glm52-mla.ts +12 -6
  98. package/src/model/glm52.ts +19 -40
  99. package/src/model/kv-attention-view.ts +67 -0
  100. package/src/model/minicpm5.ts +11 -7
  101. package/src/model/qwen3-delta-state.ts +74 -0
  102. package/src/model/qwen3-delta.ts +59 -36
  103. package/src/model/qwen3_5.ts +82 -42
  104. package/src/model/rotating-kv-donor.ts +35 -0
  105. package/src/model/rotating-kv-layout.ts +262 -0
  106. package/src/model/rotating-row-transaction.ts +35 -0
  107. package/src/model/speculative-rotating-kv.ts +101 -0
  108. package/src/model/ssm-prefill-padding.ts +39 -0
  109. package/src/model/trellis-linear.ts +2 -2
  110. package/src/model/turboquant-codec.ts +83 -0
  111. package/src/model/universal/dense.ts +14 -13
  112. package/src/preflight.ts +31 -15
  113. package/src/prompt-cache.ts +42 -44
  114. package/src/sampler.ts +36 -25
  115. package/src/serve/chat-stage.ts +2 -2
  116. package/src/serve/generation-gateway.ts +39 -30
  117. package/src/serve/model-host.ts +3 -4
  118. package/src/serve/request-plan.ts +4 -3
  119. package/src/serve/request-prep.ts +2 -4
  120. package/src/serve/text-completion-stage.ts +2 -2
  121. package/src/server.ts +64 -108
  122. package/src/spec/assistant-rows.ts +92 -0
  123. package/src/spec/assistant-source.ts +28 -18
  124. package/src/spec/cache-identity.ts +22 -0
  125. package/src/spec/deepspec-rows.ts +29 -0
  126. package/src/spec/deepspec-source.ts +12 -10
  127. package/src/spec/dflash-source.ts +28 -4
  128. package/src/spec/drafter.ts +153 -142
  129. package/src/spec/dspark/deepspec-module.ts +95 -120
  130. package/src/spec/dspark/module-dflash.ts +126 -170
  131. package/src/spec/glm52-mtp-graph.ts +29 -0
  132. package/src/spec/glm52-mtp-rows.ts +132 -0
  133. package/src/spec/glm52-mtp-source.ts +53 -136
  134. package/src/spec/glm52-mtp-state.ts +33 -0
  135. package/src/spec/ngram-source.ts +92 -20
  136. package/src/spec/projected-draft-rows.ts +73 -0
  137. package/src/spec/qwen-mtp-module.ts +171 -0
  138. package/src/spec/qwen-mtp-rows.ts +219 -0
  139. package/src/spec/qwen-mtp-source.ts +81 -226
  140. package/src/spec/qwen-mtp-state.ts +30 -0
  141. package/src/spec/serve-loop.ts +70 -68
  142. package/src/spec/source.ts +80 -15
  143. package/src/spec/two-model-rows.ts +61 -0
  144. package/src/spec/two-model.ts +43 -101
  145. package/src/ssd-cache.ts +33 -21
  146. package/src/ssd-durability.ts +11 -7
  147. package/src/web/app.js +57 -57
  148. package/src/backends/mlx/memory-guard.ts +0 -40
  149. package/src/spec/prefix-state.ts +0 -45
  150. /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
@@ -74,6 +74,10 @@ export interface DflashTrainOut { draftLogits: MlxArray; conf: MlxArray }
74
74
  * positions — ABSENT when the caller passed collectLogits:false (the loop
75
75
  * never materialized/concatenated the per-position logits). */
76
76
  export interface DflashDraftBlock { tokens: number[]; conf: number[]; draftLogits?: MlxArray }
77
+ export interface DflashDraftRows { tokens: number[][]; conf: number[][]; draftLogits?: MlxArray; }
78
+ export interface DflashContextAttention {
79
+ attend(layer: number, query: MlxArray, keys: MlxArray, values: MlxArray, scale: number): MlxArray;
80
+ }
77
81
  export interface DflashDraftOpts {
78
82
  sample?: DSparkSampleConfig;
79
83
  keys?: KeyStream;
@@ -92,6 +96,8 @@ export interface DflashDraftOpts {
92
96
  * path runs its own verify lm-head and never reads it — pass false there
93
97
  * to skip the dead concat + a host-unrelated GPU alloc every round. */
94
98
  collectLogits?: boolean;
99
+ /** Return confidence telemetry; pruning still computes it when needed. */
100
+ collectConfidence?: boolean;
95
101
  }
96
102
 
97
103
  const CDT = Dtype.float32;
@@ -245,7 +251,7 @@ export class DflashDrafter {
245
251
  }
246
252
 
247
253
  // --- Eq 3: one KV-injection layer. H_ctx read-only; H_d is queries+updated ---
248
- #layer(i: number, H_d: MlxArray, H_ctx: MlxArray, maskBias: MlxArray | null, A: number, Lctx: number): MlxArray {
254
+ #layer(i: number, H_d: MlxArray, H_ctx: MlxArray, maskBias: MlxArray | null, A: number, Lctx: number, context?: DflashContextAttention): MlxArray {
249
255
  const { dDraft, nHeads, gamma } = this.cfg;
250
256
  const hd = dDraft / nHeads;
251
257
  const eps = this.dims.eps;
@@ -253,24 +259,34 @@ export class DflashDrafter {
253
259
 
254
260
  const residual = H_d;
255
261
  const nd = ops.rmsNorm(H_d, this.get(`bb.${i}.attn_norm`), eps);
256
- const nc = ops.rmsNorm(H_ctx, this.get(`bb.${i}.attn_norm`), eps); // SAME norm as block
262
+ const nc = context ? null : ops.rmsNorm(H_ctx, this.get(`bb.${i}.attn_norm`), eps); // SAME norm as block
257
263
 
258
264
  // Q from block only
259
265
  let q = ops.matmul(nd, this.get(`bb.${i}.q`)); // [A,γ,d]
260
266
  q = disposing(q, ops.reshape(q, [A, gamma, nHeads, hd]));
261
267
  q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3])); // [A,nHeads,γ,hd]
262
268
 
263
- // K/V = [Wk·H_ctx ; Wk·H_d] (context first, then block)
264
- const kc = ops.matmul(nc, wk), kd = ops.matmul(nd, wk);
265
- const vc = ops.matmul(nc, wv), vd = ops.matmul(nd, wv);
266
- nd.dispose(); nc.dispose();
267
- const shape4 = (x: MlxArray) => { const r = ops.reshape(x, [A, Lctx + gamma, nHeads, hd]); x.dispose(); const t = ops.transposeAxes(r, [0, 2, 1, 3]); r.dispose(); return t; };
268
- let K = ops.concatAxis([kc, kd], 1); kc.dispose(); kd.dispose(); // [A,Lctx+γ,d]
269
- let V = ops.concatAxis([vc, vd], 1); vc.dispose(); vd.dispose();
270
- K = shape4(K); V = shape4(V); // [A,nHeads,Lctx+γ,hd]
271
-
272
- let attn = ops.sdpa(q, K, V, Math.pow(hd, -0.5), maskBias ? "array" : "", maskBias);
273
- q.dispose(); K.dispose(); V.dispose();
269
+ const kd = ops.matmul(nd, wk), vd = ops.matmul(nd, wv); nd.dispose();
270
+ let attn: MlxArray;
271
+ if (context) {
272
+ const shape = (array: MlxArray) => {
273
+ using rows = ops.reshape(array, [A, gamma, nHeads, hd]);
274
+ return ops.transposeAxes(rows, [0, 2, 1, 3]);
275
+ };
276
+ using keys = shape(kd), values = shape(vd); kd.dispose(); vd.dispose();
277
+ attn = context.attend(i, q, keys, values, Math.pow(hd, -0.5));
278
+ } else {
279
+ const kc = ops.matmul(nc!, wk), vc = ops.matmul(nc!, wv); nc!.dispose();
280
+ const shape = (array: MlxArray) => {
281
+ using rows = ops.reshape(array, [A, Lctx + gamma, nHeads, hd]);
282
+ return ops.transposeAxes(rows, [0, 2, 1, 3]);
283
+ };
284
+ using k = ops.concatAxis([kc, kd], 1), v = ops.concatAxis([vc, vd], 1);
285
+ kc.dispose(); kd.dispose(); vc.dispose(); vd.dispose();
286
+ using keys = shape(k), values = shape(v);
287
+ attn = ops.sdpa(q, keys, values, Math.pow(hd, -0.5), maskBias ? "array" : "", maskBias);
288
+ }
289
+ q.dispose();
274
290
  attn = disposing(attn, ops.transposeAxes(attn, [0, 2, 1, 3])); // [A,γ,nHeads,hd]
275
291
  attn = disposing(attn, ops.reshape(attn, [A, gamma, dDraft]));
276
292
  attn = disposing(attn, ops.matmul(attn, this.get(`bb.${i}.o`)));
@@ -290,17 +306,35 @@ export class DflashDrafter {
290
306
  }
291
307
 
292
308
  /** hCtx [A,Lctx,m*H], anchorEmb [A,H], ctxMask [A,Lctx]|null → block [A,γ,d]. */
293
- #backbone(hCtx: MlxArray, anchorEmb: MlxArray, ctxMask: MlxArray | null, A: number): MlxArray {
294
- const Lctx = hCtx.shape[1]!;
295
- const H_ctx = this.#buildContext(hCtx);
309
+ #backbone(hCtx: MlxArray | null, anchorEmb: MlxArray, ctxMask: MlxArray | null, A: number, context?: DflashContextAttention): MlxArray {
310
+ const Lctx = hCtx?.shape[1] ?? 0;
311
+ const H_ctx = context ? null : this.#buildContext(hCtx!);
296
312
  let H_d = this.#buildBlock(anchorEmb, A);
297
313
  const maskBias = this.#maskBias(ctxMask, A, Lctx);
298
- for (let i = 0; i < this.cfg.nLayers; i++) H_d = disposing(H_d, this.#layer(i, H_d, H_ctx, maskBias, A, Lctx));
299
- H_ctx.dispose(); maskBias?.dispose();
314
+ for (let i = 0; i < this.cfg.nLayers; i++) H_d = disposing(H_d, this.#layer(i, H_d, H_ctx!, maskBias, A, Lctx, context));
315
+ H_ctx?.dispose(); maskBias?.dispose();
300
316
  H_d = disposing(H_d, ops.rmsNorm(H_d, this.get("out_norm"), this.dims.eps));
301
317
  return H_d;
302
318
  }
303
319
 
320
+ /** Context projection is independent of the block and can be retained. */
321
+ projectContextRows(hidden: MlxArray): { k: MlxArray; v: MlxArray }[] {
322
+ const { nHeads, dDraft } = this.cfg, B = hidden.shape[0]!, N = hidden.shape[1]!;
323
+ using projected = this.#buildContext(hidden);
324
+ using pending = new DisposableStack();
325
+ const pairs = Array.from({ length: this.cfg.nLayers }, (_, layer) => {
326
+ using normalized = ops.rmsNorm(projected, this.get(`bb.${layer}.attn_norm`), this.dims.eps);
327
+ const plane = (name: string) => {
328
+ using flat = ops.matmul(normalized, this.get(name));
329
+ using rows = ops.reshape(flat, [B, N, nHeads, dDraft / nHeads]);
330
+ return pending.use(ops.transposeAxes(rows, [0, 2, 1, 3]));
331
+ };
332
+ return { k: plane(`bb.${layer}.k`), v: plane(`bb.${layer}.v`) };
333
+ });
334
+ pending.move();
335
+ return pairs;
336
+ }
337
+
304
338
  #baseLogits(model: DraftProjection, block: MlxArray): MlxArray {
305
339
  const hOut = ops.matmul(block, this.get("out_proj")); // [A,γ,H]
306
340
  const hBf = hOut.astype(model.embed.scales.dtype); hOut.dispose();
@@ -401,167 +435,89 @@ export class DflashDrafter {
401
435
  }
402
436
  }
403
437
 
404
- /** Inference: parallel backbone once, then sequential Markov sampling.
405
- * hCtx [1,Lctx,m*H] (full current context), anchorTok the bonus token.
406
- *
407
- * Host-sync discipline (docs/archive/investigations/dspark-handoff.md item 3): the
408
- * greedy path's token recurrence stays ON-DEVICE across positions — the
409
- * argmax result [1] uint32 feeds directly into the next position's
410
- * takeAxis, no itemUint32 in the loop. Per-position token arrays are
411
- * collected and resolved to host numbers with exactly ONE concat +
412
- * toIntTokens() after the loop. Confidence is the one read the pruning
413
- * ALGORITHM inherently needs per position (Alg 1 must decide whether to
414
- * stop NOW) — so it stays per-position whenever thresholds/minConf are
415
- * actually active; when pruning is inactive there is no such need, so
416
- * confidence is deferred the same way (collect [1] arrays, one concat +
417
- * one toFloat32 after the loop). The sampling path already forces a host
418
- * read every position (sampleToken draws from a device RNG key) — that
419
- * path is unchanged. */
420
- forwardInfer(model: DraftProjection, hCtx: MlxArray, anchorTok: number, gamma: number, opts: DflashDraftOpts = {}): DflashDraftBlock {
421
- const { dDraft, markovRank: r } = this.cfg;
422
- const V = this.dims.vocabSize;
423
- const sample = opts.sample && opts.sample.temperature > 0 ? opts.sample : null;
424
- const keys = opts.keys ?? new KeyStream(opts.sample?.seed ?? 0);
425
- const collectLogits = opts.collectLogits ?? true;
426
- const pruningActive = opts.thresholds !== undefined || opts.minConf !== undefined;
427
-
428
- const ids = ops.fromInt32([anchorTok], [1, 1]);
429
- const ae3 = model.embed.encode(ids); ids.dispose();
430
- const anchorEmb = ops.reshape(ae3, [1, this.dims.hiddenSize]); ae3.dispose();
431
- const block = this.#backbone(hCtx, anchorEmb, null, 1); // [1,γ,d]
432
- anchorEmb.dispose();
433
- const U = this.#baseLogits(model, block);
434
- const Uf = U.astype(CDT); U.dispose();
438
+ /** Single-request callers use the same graph and proposal head at B1. */
439
+ forwardInfer(model: DraftProjection, hCtx: MlxArray, anchor: number, gamma: number, opts: DflashDraftOpts = {}): DflashDraftBlock {
440
+ const result = this.forwardRows(model, hCtx, [anchor], gamma, opts);
441
+ return { tokens: result.tokens[0]!, conf: result.conf[0]!, draftLogits: result.draftLogits };
442
+ }
435
443
 
436
- const w1 = this.get("markov.w1"), w2 = this.get("markov.w2");
444
+ forwardRows(model: DraftProjection, hCtx: MlxArray | null, anchors: readonly number[], gamma: number,
445
+ opts: DflashDraftOpts = {}, context?: DflashContextAttention): DflashDraftRows {
446
+ const B = anchors.length, { dDraft, markovRank: r } = this.cfg, V = this.dims.vocabSize;
447
+ const sample = opts.sample && opts.sample.temperature > 0 ? opts.sample : null;
448
+ const keys = sample ? anchors.map((_, row) => row === 0 && opts.keys ? opts.keys : new KeyStream(opts.sample?.seed ?? 0)) : [];
449
+ const collect = opts.collectLogits ?? true, pruning = opts.thresholds !== undefined || opts.minConf !== undefined;
450
+ using ids = ops.fromInt32([...anchors], [B, 1]);
451
+ using embeddings = model.embed.encode(ids);
452
+ using anchorEmb = ops.reshape(embeddings, [B, this.dims.hiddenSize]);
453
+ using block = this.#backbone(hCtx, anchorEmb, null, B, context);
454
+ using raw = this.#baseLogits(model, block);
455
+ using base = raw.astype(CDT);
437
456
  const isRnn = this.cfg.seqHead === "rnn";
438
- const wH = isRnn ? this.get("rnn.wH") : null;
439
- const bH = isRnn ? this.get("rnn.bH") : null;
440
- const wO = isRnn ? this.get("rnn.wO") : null;
441
- let s: MlxArray | null = isRnn ? zerosArray([1, r]) : null; // s_{-1} = 0, carried across k
442
- const conf: number[] = [];
443
- const perPos: MlxArray[] = []; // per-position [1,1,V] draft logits (collectLogits only)
444
- const tokArrs: MlxArray[] = []; // per-position [1] device token ids, greedy path only
445
- const confArrs: MlxArray[] = []; // per-position [1] device confidences, deferred (no pruning) only
446
- let tokens: number[] = [];
447
- let prevIdx: MlxArray = ops.fromInt32([anchorTok], [1]); // device token id fed to takeAxis
448
- let ownPrevIdx = true; // false once prevIdx aliases a tokArrs/argmax entry we must not double-dispose
449
- // Iteration-local temps register here at creation so the catch can reach
450
- // them on a mid-flight throw; drained (length=0) at the bottom of each
451
- // iteration once everything in it was disposed on the normal path.
452
- // dispose() idempotence makes catch-side re-disposal of the already-freed
453
- // ones inert.
454
- const pending: MlxArray[] = [];
457
+ let state: MlxArray | null = isRnn ? zerosArray([B, r]) : null;
458
+ const tokens: MlxArray[] = [], confidence: MlxArray[] = [], logits: MlxArray[] = [];
459
+ const lengths: Array<number | undefined> = anchors.map(() => undefined);
460
+ const hostConfidence: number[][] = [];
461
+ const needConfidence = pruning || opts.collectConfidence !== false;
462
+ using first = ops.fromInt32([...anchors], [B]); let previous = first;
455
463
  try {
456
- for (let k = 0; k < gamma; k++) {
457
- const UkS = Uf.slice([0, k, 0], [1, k + 1, V]); pending.push(UkS);
458
- const Uk = ops.reshape(UkS, [1, V]); pending.push(Uk);
459
- UkS.dispose();
460
- const e1 = ops.takeAxis(w1, prevIdx, 0); pending.push(e1); // [1,r], shared embedding either way
461
- if (ownPrevIdx) { prevIdx.dispose(); ownPrevIdx = false; } // consumed; prevIdx reassigned below
462
- let Bk: MlxArray;
464
+ for (let step = 0; step < gamma; step++) {
465
+ using row = base.slice([0, step, 0], [B, step + 1, V]);
466
+ using flat = ops.reshape(row, [B, V]);
467
+ using e1 = ops.takeAxis(this.get("markov.w1"), previous, 0);
468
+ let bias: MlxArray;
463
469
  if (isRnn) {
464
- const sh = ops.matmul(s!, wH!); pending.push(sh);
465
- s!.dispose(); s = null;
466
- let sNext = ops.add(sh, e1); pending.push(sNext);
467
- sh.dispose();
468
- sNext = disposing(sNext, ops.add(sNext, bH!)); pending.push(sNext);
469
- sNext = disposing(sNext, ops.tanh(sNext)); pending.push(sNext);
470
- s = sNext; // s_k, survives to the next iteration (covered by the s slot from here)
471
- Bk = ops.matmul(s, wO!); pending.push(Bk);
472
- } else {
473
- Bk = ops.matmul(e1, w2); pending.push(Bk);
474
- }
475
- const logitsK = ops.add(Uk, Bk); pending.push(logitsK);
476
- Uk.dispose(); Bk.dispose();
477
- // prevIdx becomes the SINGLE tracked owner of the next token id the
478
- // moment it exists — no separate local for the catch to miss.
470
+ using recurrent = ops.matmul(state!, this.get("rnn.wH"));
471
+ using sum = ops.add(recurrent, e1);
472
+ using shifted = ops.add(sum, this.get("rnn.bH"));
473
+ const next = ops.tanh(shifted); state!.dispose(); state = next;
474
+ bias = ops.matmul(state, this.get("rnn.wO"));
475
+ } else bias = ops.matmul(e1, this.get("markov.w2"));
476
+ using correction = bias;
477
+ using scores = ops.add(flat, correction);
478
+ let token: MlxArray;
479
479
  if (sample) {
480
- const sc = processLogits(logitsK, sample); pending.push(sc);
481
- const tok = sampleToken(sc, keys.next()); sc.dispose();
482
- tokens.push(tok);
483
- prevIdx = ops.fromInt32([tok], [1]); ownPrevIdx = true;
484
- } else {
485
- const am = ops.argmaxAxis(logitsK, -1); // [1] uint32, ON-DEVICE — no itemUint32 here
486
- tokArrs.push(am);
487
- prevIdx = am; ownPrevIdx = false; // aliases tokArrs[k] — that collection owns it
488
- }
489
- if (collectLogits) perPos.push(ops.reshape(logitsK, [1, 1, V]));
490
- logitsK.dispose();
491
- const hkS = block.slice([0, k, 0], [1, k + 1, dDraft]); pending.push(hkS);
492
- const hk = ops.reshape(hkS, [1, dDraft]); pending.push(hk);
493
- hkS.dispose();
494
- const ci = ops.concatAxis([hk, e1], 1); pending.push(ci);
495
- hk.dispose(); e1.dispose();
496
- let cz = ops.matmul(ci, this.get("conf.w")); pending.push(cz);
497
- ci.dispose();
498
- cz = disposing(cz, ops.add(cz, this.get("conf.b"))); pending.push(cz);
499
- cz = disposing(cz, ops.sigmoid(cz)); pending.push(cz);
500
-
501
- // Alg 1 (single-user): drop position k when its predicted acceptance
502
- // is below the calibrated/manual threshold — the target then
503
- // verifies a shorter window. Position 0 always survives (sources
504
- // return ≥1 token). Pruning inherently needs the confidence value
505
- // THIS position, on the host, to decide whether to stop — no way
506
- // around that read when active. When inactive, defer it (device
507
- // array, resolved once after the loop) since nothing this iteration
508
- // depends on its value.
509
- const thr = opts.thresholds?.[k] ?? opts.minConf;
510
- if (pruningActive) {
511
- const cVal = cz.toFloat32()[0]!; cz.dispose();
512
- if (thr !== undefined && k > 0 && cVal < thr) {
513
- // Drop position k SYMMETRICALLY on both paths so tokens/conf/
514
- // draftLogits stay aligned (the DflashDraftBlock contract —
515
- // verifySampling slices draftLogits per token). Greedy: pop the
516
- // device array (prevIdx aliases it — one dispose via the owning
517
- // collection). Sample: pop the host token + free the standalone
518
- // fromInt32.
519
- if (!sample) tokArrs.pop()!.dispose(); // also frees prevIdx (same array)
520
- else { prevIdx.dispose(); tokens.pop(); }
521
- ownPrevIdx = false; // dead either way — post-loop/catch must not re-dispose
522
- if (collectLogits) perPos.pop()!.dispose(); // logits row for the dropped position
523
- break;
480
+ const sampled = anchors.map((_, index) => {
481
+ if (lengths[index] !== undefined) return 0;
482
+ using one = scores.slice([index, 0], [index + 1, V]);
483
+ using processed = processLogits(one, sample);
484
+ return sampleToken(processed, keys[index]!.next());
485
+ });
486
+ token = ops.fromInt32(sampled, [B]);
487
+ } else token = ops.argmaxAxis(scores, -1);
488
+ tokens.push(token); previous = token;
489
+ if (collect) logits.push(ops.reshape(scores, [B, 1, V]));
490
+ if (needConfidence) {
491
+ using h = block.slice([0, step, 0], [B, step + 1, dDraft]);
492
+ using hFlat = ops.reshape(h, [B, dDraft]);
493
+ using joined = ops.concatAxis([hFlat, e1], 1);
494
+ using product = ops.matmul(joined, this.get("conf.w"));
495
+ using shifted = ops.add(product, this.get("conf.b"));
496
+ const conf = ops.sigmoid(shifted); confidence.push(conf);
497
+ if (pruning) {
498
+ const values = conf.toFloat32(), threshold = opts.thresholds?.[step] ?? opts.minConf;
499
+ hostConfidence.push(Array.from(values));
500
+ if (step > 0 && threshold !== undefined) values.forEach((value, row) => {
501
+ if (lengths[row] === undefined && value < threshold) lengths[row] = step;
502
+ });
503
+ if (lengths.every(length => length !== undefined)) break;
524
504
  }
525
- conf.push(cVal);
526
- } else {
527
- confArrs.push(cz);
528
505
  }
529
- pending.length = 0; // everything above was disposed (or handed to a collection)
530
506
  }
531
- pending.length = 0; // early-break path: the broken iteration's temps are all freed too
532
- if (ownPrevIdx) prevIdx.dispose(); // last iteration's fresh sample-path array, unused past the loop
533
- s?.dispose(); // final on-device state (loop end or early break — both leave s live)
534
- Uf.dispose(); block.dispose();
535
-
536
- if (!sample) {
537
- const allToks = ops.concatAxis(tokArrs, 0); // [nKept] uint32, ONE host read below
538
- for (const a of tokArrs) a.dispose();
539
- tokens = allToks.toIntTokens(); allToks.dispose();
507
+ using packed = ops.concatAxis(tokens, 0); const values = packed.toIntTokens();
508
+ using packedConfidence = !pruning && confidence.length ? ops.concatAxis(confidence, 0) : null;
509
+ const conf = packedConfidence?.toFloat32();
510
+ const sizes = lengths.map(length => length ?? tokens.length);
511
+ let draftLogits: MlxArray | undefined;
512
+ if (collect) {
513
+ using full = ops.concatAxis(logits, 1);
514
+ draftLogits = full.slice([0, 0, 0], [B, Math.max(...sizes), V]);
540
515
  }
541
- if (!pruningActive && confArrs.length) {
542
- const allConf = ops.concatAxis(confArrs, 0); // [nKept,1]
543
- for (const a of confArrs) a.dispose();
544
- const flat = allConf.toFloat32(); allConf.dispose();
545
- for (const v of flat) conf.push(v);
546
- }
547
- const draftLogits = collectLogits ? ops.concatAxis(perPos, 1) : undefined;
548
- if (collectLogits) for (const a of perPos) a.dispose();
549
- return { tokens, conf, draftLogits };
550
- } catch (err) {
551
- // Cleanup on a mid-flight throw — the collections, the carried state,
552
- // AND the current iteration's registered temps (`pending`). dispose()
553
- // is idempotent (array.ts #disposed guard) so re-disposing an array the
554
- // try block already freed (or that aliases one of these collections,
555
- // e.g. prevIdx === tokArrs[k]) is inert, never a double-free bug.
556
- for (const a of pending) a.dispose();
557
- if (ownPrevIdx) prevIdx.dispose();
558
- for (const a of tokArrs) a.dispose();
559
- for (const a of confArrs) a.dispose();
560
- for (const a of perPos) a.dispose();
561
- s?.dispose();
562
- Uf.dispose(); block.dispose();
563
- throw err;
564
- }
516
+ return {
517
+ tokens: sizes.map((length, row) => Array.from({ length }, (_, step) => values[step * B + row]!)),
518
+ conf: sizes.map((length, row) => opts.collectConfidence === false ? [] : Array.from({ length }, (_, step) => pruning ? hostConfidence[step]![row]! : conf![step * B + row]!)), draftLogits
519
+ };
520
+ } finally { state?.dispose(); for (const array of [...tokens, ...confidence, ...logits]) array.dispose(); }
565
521
  }
566
522
 
567
523
  save(dir: string): void {
@@ -0,0 +1,29 @@
1
+ import type { MlxArray } from "../mlx/array";
2
+ import * as ops from "../mlx/ops";
3
+ import type { Glm52DecoderLayer, Glm52Model } from "../model/glm52";
4
+ import type { MLACache } from "../model/glm52-cache";
5
+ import { rmsNormF32Mlx } from "../model/glm52-mla";
6
+
7
+ /** Native MTP numerical graph. Tokens, anchor hidden and compressed state may
8
+ * have one or several rows; sampling and state retention belong to callers. */
9
+ export class Glm52MtpGraph {
10
+ constructor(readonly model: Glm52Model, readonly layer: Glm52DecoderLayer) {}
11
+
12
+ async forward(ids: MlxArray, hidden: MlxArray, cache: MLACache): Promise<MlxArray> {
13
+ const { glmConfig: config, weights } = this.model;
14
+ const prefix = `model.layers.${config.numHiddenLayers}`;
15
+ using embedded = weights.embedding(ids, "model.embed_tokens.weight", config.vocabSize, config.hiddenSize);
16
+ using embeddedNorm = rmsNormF32Mlx(embedded, weights.tensor(`${prefix}.enorm.weight`), config.rmsNormEps);
17
+ using hiddenNorm = rmsNormF32Mlx(hidden, weights.tensor(`${prefix}.hnorm.weight`), config.rmsNormEps);
18
+ using joined = ops.concatAxis([embeddedNorm, hiddenNorm], 2);
19
+ using projected = weights.linear(joined, `${prefix}.eh_proj.weight`, config.hiddenSize, 2 * config.hiddenSize);
20
+ return await this.layer.forwardAsync(projected, cache, null);
21
+ }
22
+
23
+ project(hidden: MlxArray): MlxArray {
24
+ const { glmConfig: config, weights } = this.model;
25
+ using normalized = rmsNormF32Mlx(hidden,
26
+ weights.tensor(`model.layers.${config.numHiddenLayers}.shared_head.norm.weight`), config.rmsNormEps);
27
+ return this.model.logitsFromHidden(normalized);
28
+ }
29
+ }
@@ -0,0 +1,132 @@
1
+ import type { MlxArray } from "../mlx/array";
2
+ import * as ops from "../mlx/ops";
3
+ import { Dtype } from "../mlx/ffi";
4
+ import { materializeCopy } from "../mlx/materialize";
5
+ import { MLACache } from "../model/glm52-cache";
6
+ import { toLogprobs } from "../sampler";
7
+ import { applyStateChanges, disposeResources } from "../engine/resources";
8
+ import type { PreparedStateChange } from "../contracts/resources";
9
+ import type { DraftRowSampling } from "./source";
10
+ import type { Glm52MtpGraph } from "./glm52-mtp-graph";
11
+
12
+ export interface Glm52MtpRowState {
13
+ readonly cache: MLACache;
14
+ readonly hidden: MlxArray;
15
+ readonly processedTokens: number;
16
+ }
17
+
18
+ /** Native Colibri draft history starts empty, independently of target prompt
19
+ * length. Accepted draft rows are rebuilt from true target hidden states. */
20
+ export class Glm52MtpRows {
21
+ #cache: MLACache;
22
+ #hidden: MlxArray | null = null;
23
+ #processed: number[] = [];
24
+ #drafts: number[][] = [];
25
+ #depth = 0;
26
+
27
+ constructor(readonly graph: Glm52MtpGraph, readonly sampling: DraftRowSampling | null) {
28
+ this.#cache = this.makeCache();
29
+ }
30
+ makeCache(): MLACache {
31
+ const c = this.graph.model.glmConfig;
32
+ return new MLACache({ kvLoraRank: c.kvLoraRank, ropeHeadDim: c.qkRopeHeadDim,
33
+ maxTokens: c.maxPositionEmbeddings, role: "mtp" });
34
+ }
35
+ get rowCount(): number { return this.#processed.length; }
36
+ append(states: readonly (Glm52MtpRowState | null)[]): void {
37
+ applyStateChanges([() => this.prepareAppend(states)]);
38
+ }
39
+ prepareAppend(states: readonly (Glm52MtpRowState | null)[]): PreparedStateChange {
40
+ if (!states.length) return { commit() {}, dispose() {} };
41
+ let cache: MLACache | null = this.makeCache(), hidden: MlxArray | null = null;
42
+ const held: { dispose(): void }[] = [];
43
+ const processed = [...this.#processed, ...states.map(state => state?.processedTokens ?? 0)];
44
+ try {
45
+ cache.mergeRows([...(this.rowCount ? [this.#cache] : []), ...states.map(state => {
46
+ if (state) return state.cache;
47
+ const empty = this.makeCache(); held.push(empty); return empty;
48
+ })]);
49
+ hidden = ops.concatAxis([...(this.#hidden ? [this.#hidden] : []), ...states.map(state => {
50
+ if (state) return state.hidden;
51
+ const zero = ops.zeros([1, 1, this.graph.model.glmConfig.hiddenSize], Dtype.float32);
52
+ held.push(zero); return zero;
53
+ })], 0);
54
+ } catch (error) { cache.dispose(); hidden?.dispose(); throw error; }
55
+ finally { disposeResources(held); }
56
+ return {
57
+ commit: () => {
58
+ const priorCache = this.#cache, priorHidden = this.#hidden;
59
+ this.#cache = cache!; this.#hidden = hidden; this.#processed = processed;
60
+ cache = priorCache; hidden = priorHidden;
61
+ },
62
+ dispose: () => { cache?.dispose(); hidden?.dispose(); cache = null; hidden = null; },
63
+ };
64
+ }
65
+ prefill(tokens: MlxArray, context: MlxArray): void {
66
+ const [batch, length] = tokens.shape as [number, number];
67
+ using tail = context.slice([0, length - 1, 0], [batch, length, context.shape[2]!]);
68
+ const hidden = materializeCopy(tail);
69
+ this.#hidden?.dispose(); this.#hidden = hidden;
70
+ this.#processed = this.#processed.map(count => count + length);
71
+ }
72
+ materialize(): void {
73
+ ops.evalAll([...this.#cache.state(), ...(this.#hidden ? [this.#hidden] : [])]);
74
+ }
75
+ filterRows(keep: readonly number[]): void {
76
+ if (!keep.length) { this.dispose(); return; }
77
+ using indices = ops.fromInt32([...keep], [keep.length]);
78
+ const hidden = ops.takeAxis(this.#hidden!, indices, 0);
79
+ try { this.#cache.filterRows(keep); } catch (error) { hidden.dispose(); throw error; }
80
+ this.#hidden!.dispose(); this.#hidden = hidden;
81
+ this.#processed = keep.map(row => this.#processed[row]!);
82
+ }
83
+ async draft(pending: readonly number[], depth: number, steps: readonly number[]): Promise<number[][]> {
84
+ const B = pending.length, tokens: MlxArray[] = [];
85
+ let ids: MlxArray | null = ops.fromInt32([...pending], [B, 1]), chained: MlxArray | null = null;
86
+ this.#cache.specRoundBegin();
87
+ try {
88
+ for (let position = 0; position < depth; position++) {
89
+ const hidden = await this.graph.forward(ids!, chained ?? this.#hidden!, this.#cache);
90
+ chained?.dispose(); chained = hidden;
91
+ using logits = this.graph.project(hidden);
92
+ using flat = ops.reshape(logits, [B, logits.shape.at(-1)!]);
93
+ using logprobs = toLogprobs(flat);
94
+ const next = this.sampling!.sample(logprobs, steps.map(step => step + position));
95
+ tokens.push(next); ids!.dispose(); ids = ops.reshape(next, [B, 1]);
96
+ }
97
+ using packed = depth ? ops.concatAxis(tokens, 0) : null;
98
+ const read = packed?.toIntTokens() ?? [];
99
+ this.#drafts = Array.from({ length: B }, (_, row) =>
100
+ Array.from({ length: depth }, (_, position) => read[position * B + row]!));
101
+ this.#depth = depth; return this.#drafts;
102
+ } finally { ids?.dispose(); chained?.dispose(); disposeResources(tokens); }
103
+ }
104
+ async commit(accepted: readonly number[], context: MlxArray): Promise<void> {
105
+ const B = accepted.length, width = Math.max(...accepted);
106
+ if (this.#depth) this.#cache.specRoundRollback(1);
107
+ else this.#cache.specRoundCommit();
108
+ if (width) {
109
+ using ids = ops.fromInt32(this.#drafts.flatMap(tokens => Array.from({ length: width }, (_, i) => tokens[i] ?? 0)), [B, width]);
110
+ using hidden = context.slice([0, 0, 0], [B, width, context.shape[2]!]);
111
+ this.#cache.specRoundBegin();
112
+ using output = await this.graph.forward(ids, hidden, this.#cache);
113
+ output.eval(); this.#cache.specRoundRollback(accepted);
114
+ }
115
+ using positions = ops.fromInt32([...accepted], [B, 1, 1]);
116
+ const hidden = ops.takeAlongAxis(context, positions, 1);
117
+ this.#hidden!.dispose(); this.#hidden = hidden;
118
+ this.#processed = this.#processed.map((count, row) => count + 1 + accepted[row]!);
119
+ this.#drafts = []; this.#depth = 0;
120
+ }
121
+ extractRow(row: number): Glm52MtpRowState {
122
+ const cache = this.#cache.extractRow(row);
123
+ try {
124
+ using hidden = this.#hidden!.slice([row, 0, 0], [row + 1, 1, this.#hidden!.shape[2]!]);
125
+ return { cache, hidden: materializeCopy(hidden), processedTokens: this.#processed[row]! };
126
+ } catch (error) { cache.dispose(); throw error; }
127
+ }
128
+ dispose(): void {
129
+ this.#cache.dispose(); this.#hidden?.dispose(); this.#hidden = null;
130
+ this.#processed = []; this.#drafts = []; this.#depth = 0;
131
+ }
132
+ }