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
@@ -215,6 +215,23 @@ export interface ContextKV {
215
215
  v: MlxArray; // [1, nKvHeads, ctxLen, headDim] (v ≡ k when attention_k_eq_v)
216
216
  }
217
217
 
218
+ /** Storage owns context validity; the graph supplies only Q and new block KV. */
219
+ export interface DeepspecContextAttention {
220
+ attend(layer: number, query: MlxArray, blockKeys: MlxArray, blockValues: MlxArray): MlxArray;
221
+ }
222
+ export function plainDeepspecContext(context: readonly ContextKV[]): DeepspecContextAttention {
223
+ return { attend(layer, query, blockKeys, blockValues) {
224
+ using keys = ops.concatAxis([context[layer]!.k, blockKeys], 2);
225
+ using values = ops.concatAxis([context[layer]!.v, blockValues], 2);
226
+ return ops.sdpa(query, keys, values, 1, "", null);
227
+ } };
228
+ }
229
+ export interface DraftRowsResult {
230
+ tokens: number[][];
231
+ conf: number[][];
232
+ baseLogits?: MlxArray;
233
+ }
234
+
218
235
  export interface DraftBlockResult {
219
236
  /** Sequentially-sampled draft tokens, length 0..gamma (0 iff confidence
220
237
  * truncation fires at the very first position — EMPTY proposal). */
@@ -273,8 +290,8 @@ export class DeepspecDrafter {
273
290
  this.embedScale = Math.sqrt(cfg.hidden_size);
274
291
 
275
292
  const T = (name: string) => w.tensor(name);
276
- // Lazy transpose VIEW — no contiguous copy. The src/spec/drafter.ts
277
- // pattern (contiguous + eval) would materialize every matmul weight a
293
+ // Lazy transpose VIEW — no contiguous copy. Materializing would store
294
+ // every matmul weight a
278
295
  // second time: on the real 6.86 GB checkpoint that's ~+4.8 GB resident
279
296
  // (lm_head alone ~2 GB), decisive on a 24 GB box. mlx's steel GEMM
280
297
  // dispatches a transposed-B operand natively, so the view costs nothing
@@ -450,13 +467,18 @@ export class DeepspecDrafter {
450
467
  * concat-then-scale-less-norm, over k_proj's output — v gets NO rope).
451
468
  */
452
469
  projectContextKV(contextHidden: MlxArray, positions: number[]): ContextKV[] {
453
- const L = contextHidden.shape[1]!;
454
- if (positions.length !== L)
455
- throw new Error(`projectContextKV: ${positions.length} positions for ${L} context rows`);
470
+ if (positions.length !== contextHidden.shape[1])
471
+ throw new Error(`projectContextKV: ${positions.length} positions for ${contextHidden.shape[1]} context rows`);
472
+ assertContiguous(positions);
473
+ return this.projectContextKVRows(contextHidden, positions[0]!);
474
+ }
475
+
476
+ projectContextKVRows(contextHidden: MlxArray, position: number | MlxArray): ContextKV[] {
477
+ const B = contextHidden.shape[0]!, L = contextHidden.shape[1]!;
456
478
  const out: ContextKV[] = [];
457
479
  for (const layer of this.#layers) {
458
480
  const kFlat = matmulW(contextHidden, layer.kProj); // [1,L,nKvHeads*headDim]
459
- const k4 = ops.reshape(kFlat, [1, L, this.nKvHeads, this.headDim]);
481
+ const k4 = ops.reshape(kFlat, [B, L, this.nKvHeads, this.headDim]);
460
482
  kFlat.dispose();
461
483
  // v ≡ k pre-norm (attention_k_eq_v: v_ctx = k_ctx, same tensor before
462
484
  // the two norms diverge — v_norm has no weight).
@@ -469,14 +491,9 @@ export class DeepspecDrafter {
469
491
  const vT = ops.transposeAxes(vNormed, [0, 2, 1, 3]);
470
492
  vNormed.dispose();
471
493
 
472
- // RoPE over context rows at their absolute positions. mx.fast.rope's
473
- // static `offset` assumes contiguous positions starting at offset; our
474
- // context rows ARE contiguous (0..L-1 by construction — the taps are
475
- // read at the same prompt/verified positions every round), so a single
476
- // rope(offset=positions[0]) call matches apply_gemma4_rotary_pos_emb
477
- // row-for-row. Guard the assumption explicitly.
478
- assertContiguous(positions);
479
- const kRoped = ops.rope(kT, this.headDim, null, positions[0]!, this.ropeFreqs);
494
+ const kRoped = typeof position === "number"
495
+ ? ops.rope(kT, this.headDim, null, position, this.ropeFreqs)
496
+ : ops.ropeDynamic(kT, this.headDim, null, position, this.ropeFreqs);
480
497
  kT.dispose();
481
498
  out.push({ k: kRoped, v: vT });
482
499
  }
@@ -489,22 +506,19 @@ export class DeepspecDrafter {
489
506
  * transcribed inline per-layer below. Non-causal SDPA (scale=1.0) over
490
507
  * [ctx rows ++ block rows]; q uses only the block's own positions.
491
508
  *
492
- * blockIds: token ids for the noise block, length gamma — [anchor, MASK,
509
+ * blockIds: token ids for each noise block, shape [B, gamma] — [anchor, MASK,
493
510
  * MASK, ..., MASK] (create_noise_embed: only position 0 gets the real
494
511
  * anchor token when block_keep_mask; MASK elsewhere — at inference every
495
512
  * anchor is "kept" so this is always [anchor, mask_token_id × (γ-1)]).
496
- * blockPositions: absolute positions for the γ block rows (contiguous,
497
- * anchorPos..anchorPos+γ-1 — create_position_ids's per-block arange).
513
+ * position: each row's initial absolute position. RoPE advances contiguous
514
+ * positions within the block.
498
515
  */
499
- #forwardBackbone(ctxKV: ContextKV[], blockIds: number[], blockPositions: number[]): MlxArray {
500
- const G = blockIds.length;
501
- assertContiguous(blockPositions);
502
- const idsArr = ops.fromInt32(blockIds, [1, G]);
503
- const idsU32 = idsArr.astype(Dtype.uint32);
504
- idsArr.dispose();
516
+ #forwardBackbone(context: DeepspecContextAttention, blockIds: MlxArray, position: number | MlxArray): MlxArray {
517
+ const B = blockIds.shape[0]!, G = blockIds.shape[1]!;
518
+ const idsU32 = blockIds.astype(Dtype.uint32);
505
519
  const embRaw = gatherW(this.#embed, idsU32); // [1,G,hidden] (gather over vocab axis)
506
520
  idsU32.dispose();
507
- const embFlat = ops.reshape(embRaw, [1, G, this.hidden]);
521
+ const embFlat = ops.reshape(embRaw, [B, G, this.hidden]);
508
522
  embRaw.dispose();
509
523
  // Gemma4TextScaledWordEmbedding.forward: embed(ids) * embed_scale
510
524
  let h = ops.mulScalar(embFlat, this.embedScale);
@@ -512,31 +526,30 @@ export class DeepspecDrafter {
512
526
 
513
527
  for (let li = 0; li < this.#layers.length; li++) {
514
528
  const layer = this.#layers[li]!;
515
- const { k: kCtx, v: vCtx } = ctxKV[li]!;
516
- const ctxLen = kCtx.shape[2]!;
517
529
 
518
530
  const residual = h;
519
531
  const x = this.#rms(h, layer.inputNorm);
520
532
 
521
533
  // q = q_norm(q_proj(x).view(B,q_len,nHeads,headDim)).transpose(1,2)
522
534
  const qFlat = matmulW(x, layer.qProj); // [1,G,nHeads*headDim]
523
- const q4 = ops.reshape(qFlat, [1, G, this.nHeads, this.headDim]);
535
+ const q4 = ops.reshape(qFlat, [B, G, this.nHeads, this.headDim]);
524
536
  qFlat.dispose();
525
537
  const qNormed = this.#rms(q4, layer.qNorm);
526
538
  q4.dispose();
527
539
  const qT = ops.transposeAxes(qNormed, [0, 2, 1, 3]); // [1,nHeads,G,headDim]
528
540
  qNormed.dispose();
529
541
  // rope on q uses the LAST q_len positions (cos[:, -q_len:, :]) — here
530
- // q_len == G == the whole block, so "last q_len" is just the block's
531
- // own positions, contiguous from blockPositions[0].
532
- const qRoped = ops.rope(qT, this.headDim, null, blockPositions[0]!, this.ropeFreqs);
542
+ // q_len == G == the whole block, starting at each row's position.
543
+ const qRoped = typeof position === "number"
544
+ ? ops.rope(qT, this.headDim, null, position, this.ropeFreqs)
545
+ : ops.ropeDynamic(qT, this.headDim, null, position, this.ropeFreqs);
533
546
  qT.dispose();
534
547
 
535
548
  // k_noise = k_proj(x); norm+rope IDENTICALLY to the cached context
536
549
  // rows (concat-then-norm == norm-then-concat per the RMSNorm argument
537
550
  // in projectContextKV's doc comment).
538
551
  const kNoiseFlat = matmulW(x, layer.kProj);
539
- const kNoise4 = ops.reshape(kNoiseFlat, [1, G, this.nKvHeads, this.headDim]);
552
+ const kNoise4 = ops.reshape(kNoiseFlat, [B, G, this.nKvHeads, this.headDim]);
540
553
  kNoiseFlat.dispose();
541
554
  const vNoiseNormed = this.#rms(kNoise4, null);
542
555
  const kNoiseNormed = this.#rms(kNoise4, layer.kNorm);
@@ -545,26 +558,18 @@ export class DeepspecDrafter {
545
558
  kNoiseNormed.dispose();
546
559
  const vNoiseT = ops.transposeAxes(vNoiseNormed, [0, 2, 1, 3]);
547
560
  vNoiseNormed.dispose();
548
- const kNoiseRoped = ops.rope(kNoiseT, this.headDim, null, blockPositions[0]!, this.ropeFreqs);
561
+ const kNoiseRoped = typeof position === "number"
562
+ ? ops.rope(kNoiseT, this.headDim, null, position, this.ropeFreqs)
563
+ : ops.ropeDynamic(kNoiseT, this.headDim, null, position, this.ropeFreqs);
549
564
  kNoiseT.dispose();
550
565
  x.dispose();
551
566
 
552
- // k = cat([k_ctx, k_noise], dim=seq); v = cat([v_ctx, v_noise], dim=seq)
553
- const kFull = ops.concatAxis([kCtx, kNoiseRoped], 2); // [1,nKvHeads,ctxLen+G,headDim]
554
- kNoiseRoped.dispose();
555
- const vFull = ops.concatAxis([vCtx, vNoiseT], 2);
556
- vNoiseT.dispose();
557
-
558
- // non-causal SDPA, scale=1.0 (QK-norm replaces the usual 1/sqrt(d))
559
- const attnOut = ops.sdpa(qRoped, kFull, vFull, 1.0, "", null);
560
- qRoped.dispose();
561
- kFull.dispose();
562
- vFull.dispose();
563
- void ctxLen;
567
+ const attnOut = context.attend(li, qRoped, kNoiseRoped, vNoiseT);
568
+ qRoped.dispose(); kNoiseRoped.dispose(); vNoiseT.dispose();
564
569
 
565
570
  const attnT = ops.transposeAxes(attnOut, [0, 2, 1, 3]); // [1,G,nHeads,headDim]
566
571
  attnOut.dispose();
567
- const attnFlat = ops.reshape(attnT, [1, G, this.nHeads * this.headDim]);
572
+ const attnFlat = ops.reshape(attnT, [B, G, this.nHeads * this.headDim]);
568
573
  attnT.dispose();
569
574
  let attn = matmulW(attnFlat, layer.oProj);
570
575
  attnFlat.dispose();
@@ -613,12 +618,10 @@ export class DeepspecDrafter {
613
618
  * bf16 `logits + bias` sum, and softcapped logits in [-30,30] have bf16
614
619
  * steps ~0.125-0.25, so an f32 sum here reorders near-ties vs torch and
615
620
  * derails the sequential block (2026-07-06 port review, fidelity fix). */
616
- #markovStepBias(prevTok: number): MlxArray {
621
+ #markovStepBias(previous: MlxArray): MlxArray {
617
622
  if (!this.#markovW1 || !this.#markovW2)
618
623
  throw new Error("markovStepBias: markov_rank == 0 (no markov head)");
619
- const idxArr = ops.fromInt32([prevTok], [1]);
620
- const idxU32 = idxArr.astype(Dtype.uint32);
621
- idxArr.dispose();
624
+ const idxU32 = previous.astype(Dtype.uint32);
622
625
  const e1 = gatherW(this.#markovW1, idxU32); // [1,rank]
623
626
  idxU32.dispose();
624
627
  const bias = matmulW(e1, this.#markovW2); // [1,vocab], model dtype
@@ -631,16 +634,14 @@ export class DeepspecDrafter {
631
634
  * is nn.Linear(confDim,1) — proj.weight is [1,confDim]; we compute
632
635
  * features @ proj.weight^T + bias without transposing storage (row-vector
633
636
  * dot via matmul against a [confDim,1] view). */
634
- #confidence(h1: MlxArray, prevTok: number): number {
637
+ #confidence(h1: MlxArray, previous: MlxArray): MlxArray {
635
638
  if (!this.#confProj || !this.#confB)
636
639
  throw new Error("confidence: confidence head disabled");
637
640
  let feat = h1;
638
641
  let ownFeat = false;
639
642
  if (this.cfg.confidence_head_with_markov) {
640
643
  if (!this.#markovW1) throw new Error("confidence_head_with_markov requires markov_rank > 0");
641
- const idxArr = ops.fromInt32([prevTok], [1]);
642
- const idxU32 = idxArr.astype(Dtype.uint32);
643
- idxArr.dispose();
644
+ const idxU32 = previous.astype(Dtype.uint32);
644
645
  const e1 = gatherW(this.#markovW1, idxU32); // [1,rank]
645
646
  idxU32.dispose();
646
647
  const e1f = e1.dtype === h1.dtype ? e1 : disposing(e1, e1.astype(h1.dtype));
@@ -659,9 +660,7 @@ export class DeepspecDrafter {
659
660
  z = disposing(z, ops.add(z, this.#confB));
660
661
  if (z.dtype !== Dtype.float32) z = disposing(z, z.astype(Dtype.float32));
661
662
  z = disposing(z, ops.sigmoid(z));
662
- const v = z.toFloat32()[0]!;
663
- z.dispose();
664
- return v;
663
+ return z;
665
664
  }
666
665
 
667
666
  /**
@@ -701,73 +700,49 @@ export class DeepspecDrafter {
701
700
  * anchorPos: its absolute sequence position (blockPositions[0]).
702
701
  */
703
702
  draftBlock(ctxKV: ContextKV[], anchorTok: number, anchorPos: number): DraftBlockResult {
704
- const G = this.gamma;
705
- const V = this.cfg.vocab_size;
706
- const blockIds = [anchorTok, ...Array(G - 1).fill(this.cfg.mask_token_id)];
707
- const blockPositions = Array.from({ length: G }, (_, i) => anchorPos + i);
708
-
709
- const h = this.#forwardBackbone(ctxKV, blockIds, blockPositions); // [1,G,hidden]
710
- const baseLogitsRaw = this.#computeLogits(h); // [1,G,vocab], softcapped, model dtype
711
-
712
- // Pass 1: sequential Markov-biased argmax sampling over the FULL block —
713
- // in MODEL dtype end to end (reference precision: modeling.py
714
- // compute_logits → apply_step_logits → sample_tokens all run bf16; the
715
- // argmax is over the bf16 sum. f32 here reordered near-ties vs torch —
716
- // 2026-07-06 port review, fidelity fix).
717
- const sampledTokens: number[] = [];
718
- let prevTok = anchorTok;
719
- for (let k = 0; k < G; k++) {
720
- const base1 = baseLogitsRaw.slice([0, k, 0], [1, k + 1, V]);
721
- const baseFlat = ops.reshape(base1, [1, V]);
722
- base1.dispose();
723
- let stepLogits = baseFlat;
724
- if (this.#markovW1) {
725
- const bias = this.#markovStepBias(prevTok); // [1,V], model dtype
726
- stepLogits = ops.add(baseFlat, bias);
727
- baseFlat.dispose();
728
- bias.dispose();
729
- }
730
- const am = ops.argmaxAxis(stepLogits, -1);
731
- const tok = ops.itemUint32(am);
732
- am.dispose();
733
- stepLogits.dispose();
734
- sampledTokens.push(tok);
735
- prevTok = tok;
736
- }
703
+ const result = this.draftRows(plainDeepspecContext(ctxKV), [anchorTok], anchorPos, true);
704
+ return { tokens: result.tokens[0]!, conf: result.conf[0]!, baseLogits: result.baseLogits! };
705
+ }
737
706
 
738
- // The returned artifact is an f32 COPY (callers read/compare host-side);
739
- // sampling above never touches it.
740
- const baseLogits =
741
- baseLogitsRaw.dtype === Dtype.float32 ? baseLogitsRaw : disposing(baseLogitsRaw, baseLogitsRaw.astype(Dtype.float32));
742
-
743
- // Pass 2: confidence over the full block, prev_token_ids = [anchor,
744
- // sampled[:-1]] (_predict_confidence_logits) — independent of pass 1's
745
- // truncation decision, computed for every position.
746
- let proposalLen = G;
747
- if (this.#confProj) {
748
- const confVals: number[] = [];
749
- let confPrev = anchorTok;
750
- for (let k = 0; k < G; k++) {
751
- const h1 = h.slice([0, k, 0], [1, k + 1, this.hidden]);
752
- const h1Flat = ops.reshape(h1, [1, this.hidden]);
753
- h1.dispose();
754
- const cVal = this.#confidence(h1Flat, confPrev);
755
- h1Flat.dispose();
756
- confVals.push(cVal);
757
- confPrev = sampledTokens[k]!;
707
+ /** The graph and sequential proposal head run at the active row count.
708
+ * Confidence selects each row's length after the full device chain. */
709
+ draftRows(context: DeepspecContextAttention, anchors: readonly number[], position: number | MlxArray,
710
+ collectLogits = false): DraftRowsResult {
711
+ const B = anchors.length, G = this.gamma, V = this.cfg.vocab_size;
712
+ using ids = ops.fromInt32(anchors.flatMap(anchor => [anchor, ...Array(G - 1).fill(this.cfg.mask_token_id)]), [B, G]);
713
+ using hidden = this.#forwardBackbone(context, ids, position);
714
+ using base = this.#computeLogits(hidden);
715
+ using anchorIds = ops.fromInt32([...anchors], [B]);
716
+ const sampled: MlxArray[] = [], confidence: MlxArray[] = [];
717
+ let previous = anchorIds;
718
+ try {
719
+ for (let step = 0; step < G; step++) {
720
+ using row = base.slice([0, step, 0], [B, step + 1, V]);
721
+ using logits = ops.reshape(row, [B, V]);
722
+ using bias = this.#markovW1 ? this.#markovStepBias(previous) : null;
723
+ using corrected = bias ? ops.add(logits, bias) : null;
724
+ const token = ops.argmaxAxis(corrected ?? logits, -1);
725
+ sampled.push(token); previous = token;
758
726
  }
759
- if (this.cfg.confidence_threshold > 0) {
760
- const idx = confVals.findIndex((c) => c < this.cfg.confidence_threshold);
761
- proposalLen = idx === -1 ? G : idx;
727
+ // Serving does not consume confidence when truncation is disabled.
728
+ if (this.#confProj && (collectLogits || this.cfg.confidence_threshold > 0)) for (let step = 0; step < G; step++) {
729
+ using row = hidden.slice([0, step, 0], [B, step + 1, this.hidden]);
730
+ using flat = ops.reshape(row, [B, this.hidden]);
731
+ confidence.push(this.#confidence(flat, step ? sampled[step - 1]! : anchorIds));
762
732
  }
763
- const tokens = sampledTokens.slice(0, proposalLen);
764
- const conf = confVals.slice(0, proposalLen);
765
- h.dispose();
766
- return { tokens, conf, baseLogits };
767
- }
768
-
769
- h.dispose();
770
- return { tokens: sampledTokens, conf: [], baseLogits };
733
+ using packed = ops.concatAxis(sampled, 0);
734
+ const tokens = packed.toIntTokens();
735
+ using packedConfidence = confidence.length ? ops.concatAxis(confidence, 0) : null;
736
+ const conf = packedConfidence?.toFloat32();
737
+ const rows = anchors.map((_, row) => {
738
+ const values = conf ? Array.from({ length: G }, (_, step) => conf[step * B + row]!) : [];
739
+ const below = this.cfg.confidence_threshold > 0 ? values.findIndex(value => value < this.cfg.confidence_threshold) : -1;
740
+ const length = below < 0 ? G : below;
741
+ return { tokens: Array.from({ length }, (_, step) => tokens[step * B + row]!), conf: values.slice(0, length) };
742
+ });
743
+ return { tokens: rows.map(row => row.tokens), conf: rows.map(row => row.conf),
744
+ ...(collectLogits ? { baseLogits: base.astype(Dtype.float32) } : {}) };
745
+ } finally { for (const array of [...sampled, ...confidence]) array.dispose(); }
771
746
  }
772
747
 
773
748
  dispose(): void {