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
@@ -11,6 +11,8 @@
11
11
  // row's logical position without reconstructing per-head K/V.
12
12
 
13
13
  import { MlxArray } from "../mlx/array";
14
+ import { FullPrefillPadding } from "./full-prefill-padding";
15
+ import type { PrefillPadding } from "./gemma4-base";
14
16
  import { Dtype } from "../mlx/ffi";
15
17
  import * as ops from "../mlx/ops";
16
18
  import {
@@ -210,6 +212,8 @@ export class Glm52Cache implements BatchableCache {
210
212
  batchSize: number | null = null;
211
213
  rowOffsets: number[] = [];
212
214
  leftPad: number[] = [];
215
+ #beforeRound: number[] | undefined;
216
+ readonly #padding = new FullPrefillPadding(1);
213
217
 
214
218
  constructor(geometry: MLACacheGeometry) {
215
219
  positiveInteger("MLA kvLoraRank", geometry.kvLoraRank);
@@ -482,6 +486,56 @@ export class Glm52Cache implements BatchableCache {
482
486
  this.rowOffsets = this.rowOffsets.map((value) => value - n);
483
487
  }
484
488
 
489
+ preparePrefill(padding: PrefillPadding): void { this.#padding.prepare(this, padding); }
490
+ finalizePrefill(): void {
491
+ const next = this.#padding.finalize(this.state(), this);
492
+ if (!next) return;
493
+ this.latent?.dispose(); this.rope?.dispose();
494
+ this.latent = next[0] ?? null; this.rope = next[1] ?? null;
495
+ if (this.dsa && next[2]) this.dsa.restoreState(next[2], this.offset);
496
+ }
497
+
498
+ specRoundBegin(): void { this.#beforeRound = [...this.rowOffsets]; }
499
+ specRoundCommit(): void { this.#beforeRound = undefined; }
500
+
501
+ /** Keep each accepted prefix in compressed form. Right alignment lets the
502
+ * next shared append reuse the existing MLA/DSA attention geometry. */
503
+ specRoundRollback(keep: number | readonly number[]): void {
504
+ const nextOffsets = this.#beforeRound!.map((before, row) => before +
505
+ (typeof keep === "number" ? keep : keep[row]!));
506
+ const rejected = this.rowOffsets.map((offset, row) => offset - nextOffsets[row]!);
507
+ if (rejected.every(count => count === rejected[0])) {
508
+ const count = this.batchSize!;
509
+ this.trim(rejected[0]!);
510
+ if (this.batchSize === null) {
511
+ const empty = this.makeEmptyBatch();
512
+ try { this.mergeRows(Array.from({ length: count }, () => empty)); }
513
+ finally { empty.dispose(); }
514
+ }
515
+ this.#beforeRound = undefined; return;
516
+ }
517
+ const width = this.offset;
518
+ const pads = this.leftPad.map((pad, row) => pad + rejected[row]!);
519
+ const removable = Math.min(...pads);
520
+ using indices = ops.fromInt32(rejected.flatMap(count =>
521
+ Array.from({ length: width }, (_, column) => (column - count + width) % width)),
522
+ [this.batchSize!, width, 1]);
523
+ const next: MlxArray[] = [];
524
+ try {
525
+ for (const array of this.state()) {
526
+ using shifted = ops.takeAlongAxis(array, indices, 1);
527
+ next.push(shifted.slice([0, removable, 0], [this.batchSize!, width, array.shape[2]!]));
528
+ }
529
+ } catch (error) { for (const array of next) array.dispose(); throw error; }
530
+ this.latent!.dispose(); this.rope!.dispose();
531
+ this.latent = next[0]!; this.rope = next[1]!;
532
+ if (this.dsa) this.dsa.restoreState(next[2]!, width - removable);
533
+ this.offset = width - removable;
534
+ this.rowOffsets = nextOffsets;
535
+ this.leftPad = pads.map(pad => pad - removable);
536
+ this.#beforeRound = undefined;
537
+ }
538
+
485
539
  /** Exact logical bytes of valid f32 compressed state. */
486
540
  get byteLength(): number {
487
541
  if (this.batchSize === null) return 0;
@@ -529,8 +583,16 @@ export class Glm52Cache implements BatchableCache {
529
583
  ) {
530
584
  throw new Error("MLA mergeRows cache geometry does not match");
531
585
  }
532
- if (generic.batchSize === null || generic.offset === 0)
533
- throw new Error("MLA mergeRows cannot merge an empty row");
586
+ if (generic.batchSize === null || generic.offset === 0) {
587
+ const count = generic.batchSize ?? 1;
588
+ for (let row = 0; row < count; row++) {
589
+ states.push({ latent: ops.zeros([1, 0, this.kvLoraRank], Dtype.float32),
590
+ rope: ops.zeros([1, 0, this.ropeHeadDim], Dtype.float32),
591
+ dsa: this.dsa ? ops.zeros([1, 0, this.dsa.headDim], Dtype.float32) : null });
592
+ offsets.push(0);
593
+ }
594
+ continue;
595
+ }
534
596
  for (let row = 0; row < generic.batchSize; row++) {
535
597
  states.push(generic.fetchRow(row));
536
598
  offsets.push(generic.rowOffset(row));
@@ -577,7 +639,10 @@ export class Glm52Cache implements BatchableCache {
577
639
  this.offset = width;
578
640
  this.rowOffsets = [...offsets];
579
641
  this.leftPad = offsets.map((value) => width - value);
580
- if (this.dsa) this.dsa.restoreState(dsa!, width);
642
+ if (this.dsa) {
643
+ if (width) this.dsa.restoreState(dsa!, width);
644
+ else { this.dsa.data = dsa!; this.dsa.batchSize = offsets.length; }
645
+ }
581
646
  } finally {
582
647
  for (const state of states) {
583
648
  state.latent.dispose();
@@ -591,6 +656,7 @@ export class Glm52Cache implements BatchableCache {
591
656
  extractRow(row: number): Glm52Cache {
592
657
  if (this.batchSize === null) throw new Error("MLA cache is empty");
593
658
  validateRowIndex(row, this.batchSize, "MLA extract");
659
+ if (this.rowOffsets[row] === 0) return this.makeEmptyBatch();
594
660
  const state = this.fetchRow(row);
595
661
  const copy = (array: MlxArray): MlxArray => ops.mulScalar(array, 1);
596
662
  const latent = copy(state.latent);
@@ -666,7 +732,15 @@ export class Glm52Cache implements BatchableCache {
666
732
  this.offset = nextWidth;
667
733
  this.rowOffsets = nextOffsets;
668
734
  this.leftPad = selectedPad.map((value) => value - removablePad);
669
- if (this.dsa) this.dsa.restoreState(dsa!, nextWidth);
735
+ this.#padding.filter(keep);
736
+ if (this.dsa) {
737
+ if (nextWidth) this.dsa.restoreState(dsa!, nextWidth);
738
+ else {
739
+ // A retained cold row still owns batch membership, even though the
740
+ // strict persisted-state restore contract requires nonempty tokens.
741
+ this.dsa.dispose(); this.dsa.data = dsa!; this.dsa.batchSize = keep.length;
742
+ }
743
+ }
670
744
  }
671
745
 
672
746
  projectedBytes(tokens: number): number {
@@ -709,6 +783,8 @@ export class Glm52Cache implements BatchableCache {
709
783
  this.batchSize = null;
710
784
  this.rowOffsets = [];
711
785
  this.leftPad = [];
786
+ this.#beforeRound = undefined;
787
+ this.#padding.clear();
712
788
  }
713
789
  }
714
790
 
@@ -484,7 +484,7 @@ export class Glm52Mla {
484
484
  }
485
485
  return this.attendAbsorbed(projection, state, selectedPositions);
486
486
  }
487
- return this.attendReconstructed(projection, state);
487
+ return this.attendReconstructed(projection, state, cache.leftPad);
488
488
  } finally {
489
489
  disposeState(state);
490
490
  disposeProjection(projection);
@@ -495,6 +495,7 @@ export class Glm52Mla {
495
495
  attendReconstructed(
496
496
  projection: Pick<Glm52MlaProjection, "qNope" | "qRope">,
497
497
  state: Pick<MLACompressedState, "latent" | "rope">,
498
+ leftPadding: readonly number[] = [],
498
499
  ): MlxArray {
499
500
  const [batch, tokens] = projection.qNope.shape;
500
501
  const keys = state.latent.shape[1]!;
@@ -570,14 +571,19 @@ export class Glm52Mla {
570
571
 
571
572
  const prefixTokens = keys - tokens!;
572
573
  let maskedScores = scores;
573
- if (tokens! > 1) {
574
- const maskValues = new Float32Array(tokens! * keys);
575
- for (let queryToken = 0; queryToken < tokens!; queryToken++) {
574
+ const padded = leftPadding.some(value => value > 0);
575
+ if (tokens! > 1 || padded) {
576
+ const maskRows = padded ? batch! : 1;
577
+ const maskValues = new Float32Array(maskRows * tokens! * keys);
578
+ for (let row = 0; row < maskRows; row++) for (let queryToken = 0; queryToken < tokens!; queryToken++) {
576
579
  const lastVisible = prefixTokens + queryToken;
580
+ const start = (row * tokens! + queryToken) * keys;
581
+ for (let keyToken = 0; keyToken < (leftPadding[row] ?? 0); keyToken++)
582
+ maskValues[start + keyToken] = -Infinity;
577
583
  for (let keyToken = lastVisible + 1; keyToken < keys; keyToken++)
578
- maskValues[queryToken * keys + keyToken] = -Infinity;
584
+ maskValues[start + keyToken] = -Infinity;
579
585
  }
580
- const mask = MlxArray.fromFloat32(maskValues, [1, 1, tokens!, keys]);
586
+ const mask = MlxArray.fromFloat32(maskValues, [maskRows, 1, tokens!, keys]);
581
587
  maskedScores = ops.add(scores, mask);
582
588
  mask.dispose();
583
589
  scores.dispose();
@@ -144,8 +144,6 @@ class Glm52DsaIndexer {
144
144
  forceDense = false,
145
145
  ): DsaStep {
146
146
  const [batch, tokens, hidden] = input.shape;
147
- if (batch! > 1 && tokens !== 1)
148
- throw new Error("batched DSA supports ordinary single-token decode only");
149
147
  if (hidden !== this.config.hiddenSize)
150
148
  throw new Error(`DSA hidden width ${hidden} != ${this.config.hiddenSize}`);
151
149
 
@@ -228,7 +226,10 @@ class Glm52DsaIndexer {
228
226
  }
229
227
  const contextLength = allKeys.shape[1]!;
230
228
  let selectedPositions: MlxArray | Glm52MlaBatchedSelection | null;
231
- if (batch! > 1) {
229
+ if (forceDense || contextLength <= this.config.indexTopk) {
230
+ state.selectFullDense(this.layer, contextLength);
231
+ selectedPositions = null;
232
+ } else if (batch! > 1) {
232
233
  const rowSelections: Array<MlxArray | null> = [];
233
234
  for (let row = 0; row < batch!; row++) {
234
235
  const rowContext = (cache.rowOffsets[row] ?? 0) + tokens!;
@@ -269,15 +270,6 @@ class Glm52DsaIndexer {
269
270
  scores.dispose();
270
271
  }
271
272
  selectedPositions = { rows: rowSelections };
272
- } else if (forceDense) {
273
- state.selectFullDense(this.layer, contextLength);
274
- selectedPositions = null;
275
- } else if (contextLength <= this.config.indexTopk) {
276
- state.selectFull(
277
- this.layer,
278
- new Float32Array(contextLength),
279
- );
280
- selectedPositions = null;
281
273
  } else {
282
274
  if (tokens !== 1) {
283
275
  if (allKeys !== key) allKeys.dispose();
@@ -533,6 +525,7 @@ export class Glm52DecoderLayer {
533
525
  this.inputNorm,
534
526
  this.config.rmsNormEps,
535
527
  );
528
+ const batch = input.shape[0]!;
536
529
  const tokens = input.shape[1]!;
537
530
  const denseBenchmarkPrefill =
538
531
  this.denseBenchmarkPrefillTokens > 0 &&
@@ -552,10 +545,10 @@ export class Glm52DecoderLayer {
552
545
  for (let token = 0; token < tokens; token++) {
553
546
  const row = normalized.slice(
554
547
  [0, token, 0],
555
- [1, token + 1, this.config.hiddenSize],
548
+ [batch, token + 1, this.config.hiddenSize],
556
549
  );
557
550
  let dsaKey: MlxArray | null = null;
558
- let positions: MlxArray | null = null;
551
+ let positions: MlxArray | Glm52MlaBatchedSelection | null = null;
559
552
  try {
560
553
  if (this.dsa) {
561
554
  const step = this.dsa.projectAndSelect(
@@ -564,18 +557,12 @@ export class Glm52DecoderLayer {
564
557
  dsaState,
565
558
  );
566
559
  dsaKey = step.key;
567
- if (
568
- step.selectedPositions !== null &&
569
- !(step.selectedPositions instanceof MlxArray)
570
- ) {
571
- throw new Error("serial DSA verification received batched selection");
572
- }
573
560
  positions = step.selectedPositions;
574
561
  } else {
575
- positions = dsaState.selectSharedPositions(
576
- this.layer,
577
- cache.offset + 1,
578
- );
562
+ positions = batch > 1
563
+ ? { rows: Array.from({ length: batch }, (_, row) =>
564
+ dsaState.selectSharedPositions(this.layer, (cache.rowOffsets[row] ?? 0) + 1, row)) }
565
+ : dsaState.selectSharedPositions(this.layer, cache.offset + 1);
579
566
  }
580
567
  rows.push(this.mla.forward(
581
568
  row,
@@ -609,7 +596,9 @@ export class Glm52DecoderLayer {
609
596
  dsaKey = step.key;
610
597
  positions = step.selectedPositions;
611
598
  } else if (dsaState) {
612
- positions = cache.batchSize !== null && cache.batchSize > 1
599
+ positions = denseBenchmarkPrefill || cache.offset + tokens <= this.config.indexTopk
600
+ ? null
601
+ : cache.batchSize !== null && cache.batchSize > 1
613
602
  ? {
614
603
  rows: cache.rowOffsets.map((offset, row) =>
615
604
  dsaState.selectSharedPositions(
@@ -871,18 +860,13 @@ export class Glm52Model {
871
860
  tokenCount > GLM52_DSA_SPARSE_DECODE_BATCH_MAX &&
872
861
  firstOffset + tokenCount > this.glmConfig.indexTopk
873
862
  ) {
874
- if (ids.shape.length !== 2 || ids.shape[0] !== 1) {
875
- throw new Error(
876
- "G2 DSA sparse prefill requires serial [1,T] token ids",
877
- );
878
- }
879
- // Colibri selects independently for every query row. G2 preserves that
880
- // behavior with a deliberately serial correctness fallback once a
881
- // multi-token prefill crosses index_topk; G6 may vectorize it.
863
+ // Bound sparse-selection state for long prefills while retaining all
864
+ // request rows in each forward. Short verify windows keep the residual
865
+ // and expert graph batched across both requests and query positions.
882
866
  const rows: MlxArray[] = [];
883
867
  try {
884
868
  for (let token = 0; token < tokenCount; token++) {
885
- const row = ids.slice([0, token], [1, token + 1]);
869
+ const row = ids.slice([0, token], [ids.shape[0]!, token + 1]);
886
870
  try {
887
871
  rows.push(this.forwardHidden(row, cache));
888
872
  } finally {
@@ -922,15 +906,10 @@ export class Glm52Model {
922
906
  tokenCount > GLM52_DSA_SPARSE_DECODE_BATCH_MAX &&
923
907
  firstOffset + tokenCount > this.glmConfig.indexTopk
924
908
  ) {
925
- if (ids.shape.length !== 2 || ids.shape[0] !== 1) {
926
- throw new Error(
927
- "G3 DSA sparse prefill requires serial [1,T] token ids",
928
- );
929
- }
930
909
  const rows: MlxArray[] = [];
931
910
  try {
932
911
  for (let token = 0; token < tokenCount; token++) {
933
- const row = ids.slice([0, token], [1, token + 1]);
912
+ const row = ids.slice([0, token], [ids.shape[0]!, token + 1]);
934
913
  try {
935
914
  rows.push(await this.forwardHiddenAsync(row, cache));
936
915
  } finally {
@@ -0,0 +1,67 @@
1
+ import type { MlxArray } from "../mlx/array";
2
+ import * as ops from "../mlx/ops";
3
+ import type { Cache, KvAttentionView, KvDonorAttention } from "./gemma4-base";
4
+ import { quantizedSdpa, disposeTriple } from "./gemma4-base";
5
+
6
+ export function captureKvDonorAttention(cache: Cache): KvDonorAttention {
7
+ if (cache.captureDonorAttention) return cache.captureDonorAttention();
8
+ const view = cache.captureDonorRows!();
9
+ return {
10
+ width: view.keys.shape[2]!, dtype: view.keys.dtype,
11
+ offsets: view.offsets, starts: view.starts, ends: view.ends,
12
+ attend: (q, scale, mask) => ops.sdpa(q, view.keys, view.values, scale, mask.mode, mask.arr),
13
+ dispose() { view.keys.dispose(); view.values.dispose(); },
14
+ };
15
+ }
16
+
17
+ export function quantizedDonorAttention(keys: ops.QuantizedTensor, values: ops.QuantizedTensor,
18
+ validity: Pick<KvDonorAttention, "offsets" | "starts" | "ends">,
19
+ groupSize: number, bits: number): KvDonorAttention {
20
+ return {
21
+ width: keys.packed.shape[2]!, dtype: keys.scales.dtype, ...validity,
22
+ attend: (q, scale, mask) => quantizedSdpa(q, keys, values, scale, mask, groupSize, bits),
23
+ dispose() { disposeTriple(keys); disposeTriple(values); },
24
+ };
25
+ }
26
+
27
+ export function captureKvAttention(cache: Cache, k: MlxArray, v: MlxArray): KvAttentionView {
28
+ if (cache.attentionState) return cache.attentionState.appendAndFetch(k, v);
29
+ const quantized = cache.quantizedAttention;
30
+ if (quantized) {
31
+ const { groupSize, bits } = quantized;
32
+ const [keys, values] = quantized.updateAndFetchQuantized(k, v);
33
+ return {
34
+ attend: (q, scale, mask) => quantizedSdpa(q, keys, values, scale, mask, groupSize, bits),
35
+ dispose() { disposeTriple(keys); disposeTriple(values); },
36
+ };
37
+ }
38
+ const [keys, values] = cache.updateAndFetch(k, v);
39
+ return {
40
+ attend: (q, scale, mask) => ops.sdpa(q, keys, values, scale, mask.mode, mask.arr),
41
+ dispose() { keys.dispose(); values.dispose(); },
42
+ };
43
+ }
44
+
45
+
46
+ /** Takes ownership of already aligned per-row donor views. Each storage codec
47
+ * keeps its own attention arithmetic while the caller supplies one query batch. */
48
+ export function combineKvDonorAttention(rows: readonly KvDonorAttention[]): KvDonorAttention {
49
+ return { width: rows[0]!.width, dtype: rows[0]!.dtype,
50
+ offsets: rows.flatMap(row => [...row.offsets]), starts: rows.flatMap(row => [...row.starts]),
51
+ ends: rows.flatMap(row => [...row.ends]),
52
+ attend(q, scale, mask) {
53
+ const outputs: MlxArray[] = [];
54
+ try {
55
+ for (const [index, row] of rows.entries()) {
56
+ using query = q.slice([index, 0, 0, 0], [index + 1, q.shape[1]!, q.shape[2]!, q.shape[3]!]);
57
+ const from = mask.arr?.shape.map(() => 0), to = mask.arr ? [...mask.arr.shape] : undefined;
58
+ if (from && to && to.length === 4 && to[0]! > 1) { from[0] = index; to[0] = index + 1; }
59
+ using arr = mask.arr ? mask.arr.slice(from!, to!) : null;
60
+ outputs.push(row.attend(query, scale, { mode: mask.mode, arr }));
61
+ }
62
+ return outputs.length === 1 ? ops.contiguous(outputs[0]!) : ops.concatAxis(outputs, 0);
63
+ } finally { for (const output of outputs) output.dispose(); }
64
+ },
65
+ dispose() { for (const row of rows) row.dispose(); },
66
+ };
67
+ }
@@ -14,7 +14,6 @@ import {
14
14
  KVCache,
15
15
  LoraState,
16
16
  QuantizedKVCache,
17
- TurboQuantKVCache,
18
17
  QuantizedEmbedding,
19
18
  QuantizedLinear,
20
19
  quantizedSdpa,
@@ -143,23 +142,28 @@ export class LlamaAttention {
143
142
  q = disposing(q, ropeStep(q));
144
143
  k = disposing(k, ropeStep(k));
145
144
  let attn: MlxArray;
146
- if (cache instanceof QuantizedKVCache) {
147
- const [keys, values] = cache.updateAndFetchQuantized(k, v);
145
+ if (cache.attentionState) {
146
+ const view = cache.attentionState.appendAndFetch(k, v);
147
+ k.dispose(); v.dispose();
148
+ try { attn = view.attend(q, this.scale, mask); } finally { view.dispose(); }
149
+ } else if (cache.quantizedAttention) {
150
+ const quantized = cache.quantizedAttention;
151
+ const [keys, values] = quantized.updateAndFetchQuantized(k, v);
148
152
  k.dispose();
149
153
  v.dispose();
150
- attn = quantizedSdpa(q, keys, values, this.scale, mask, cache.groupSize, cache.bits);
154
+ attn = quantizedSdpa(q, keys, values, this.scale, mask, quantized.groupSize, quantized.bits);
151
155
  disposeTriple(keys);
152
156
  disposeTriple(values);
153
- } else if (cache instanceof TurboQuantKVCache) {
157
+ } else if (cache.rotatedValueAttention) {
154
158
  // Deferred-V: sdpa runs on rotated values, then one InvFWHT on the
155
159
  // output (linear in V) — see TurboQuantKVCache.updateAndFetchDeferredV.
156
- const [keys, values] = cache.updateAndFetchDeferredV(k, v);
160
+ const [keys, values] = cache.rotatedValueAttention.updateAndFetchDeferredV(k, v);
157
161
  k.dispose();
158
162
  v.dispose();
159
163
  const rotated = ops.sdpa(q, keys, values, this.scale, mask.mode, mask.arr);
160
164
  keys.dispose();
161
165
  values.dispose();
162
- attn = tqUnrotateValues(rotated);
166
+ attn = cache.rotatedValueAttention.captureValueTransform?.()(rotated) ?? tqUnrotateValues(rotated);
163
167
  rotated.dispose();
164
168
  } else {
165
169
  const [keys, values] = cache.updateAndFetch(k, v);
@@ -0,0 +1,74 @@
1
+ import { MlxArray } from "../mlx/array";
2
+ import { Dtype } from "../mlx/ffi";
3
+ import { MetalKernel } from "../mlx/metal-kernel";
4
+ import * as ops from "../mlx/ops";
5
+ import { computeG } from "./qwen3-delta";
6
+
7
+ // The same recurrent arithmetic as gated_delta_step, with no query/output
8
+ // calculation. Each row replays only its retained prefix of the verify window.
9
+ const SOURCE = String.raw`
10
+ const int T = Tin[0];
11
+ auto n = thread_position_in_grid.z;
12
+ auto b_idx = n / Hv;
13
+ auto hv_idx = n % Hv;
14
+ auto hk_idx = hv_idx / (Hv / Hk);
15
+ constexpr int n_per_t = Dk / 32;
16
+ auto dk_idx = thread_position_in_threadgroup.x;
17
+ auto dv_idx = thread_position_in_grid.y;
18
+ auto k_ = k + b_idx * T * Hk * Dk + hk_idx * Dk;
19
+ auto v_ = v + b_idx * T * Hv * Dv + hv_idx * Dv;
20
+ auto g_ = g + b_idx * T * Hv;
21
+ auto beta_ = beta + b_idx * T * Hv;
22
+ auto i_state = state_in + (n * Dv + dv_idx) * Dk;
23
+ auto o_state = state_out + (n * Dv + dv_idx) * Dk;
24
+ float state[n_per_t];
25
+ for (int i = 0; i < n_per_t; ++i)
26
+ state[i] = static_cast<float>(i_state[n_per_t * dk_idx + i]);
27
+ const int keep = lengths[b_idx];
28
+ for (int t = 0; t < keep; ++t) {
29
+ float kv_mem = 0.0f;
30
+ for (int i = 0; i < n_per_t; ++i) {
31
+ auto s_idx = n_per_t * dk_idx + i;
32
+ state[i] = state[i] * g_[hv_idx];
33
+ kv_mem += state[i] * k_[s_idx];
34
+ }
35
+ kv_mem = simd_sum(kv_mem);
36
+ auto delta = (v_[dv_idx] - kv_mem) * beta_[hv_idx];
37
+ for (int i = 0; i < n_per_t; ++i) {
38
+ auto s_idx = n_per_t * dk_idx + i;
39
+ state[i] = state[i] + k_[s_idx] * delta;
40
+ }
41
+ k_ += Hk * Dk;
42
+ v_ += Hv * Dv;
43
+ g_ += Hv;
44
+ beta_ += Hv;
45
+ }
46
+ for (int i = 0; i < n_per_t; ++i)
47
+ o_state[n_per_t * dk_idx + i] = state[i];
48
+ `;
49
+
50
+ let kernel: MetalKernel | undefined;
51
+
52
+ /** State-only prefix replay. Inputs keep their original [B,T,...] strides;
53
+ * lengths are request-owned accepted counts, never scheduling decisions. */
54
+ export function gatedDeltaState(
55
+ k: MlxArray, v: MlxArray, a: MlxArray, b: MlxArray,
56
+ aLog: MlxArray, dtBias: MlxArray, state: MlxArray | null,
57
+ lengths: readonly number[],
58
+ ): MlxArray {
59
+ const [B, T, Hk, Dk] = k.shape as [number, number, number, number];
60
+ const [, , Hv, Dv] = v.shape as [number, number, number, number];
61
+ using beta = ops.sigmoid(b);
62
+ using g = computeG(aLog, a, dtBias);
63
+ using zero = state ? null : ops.zeros([B, Hv, Dv, Dk], Dtype.float32);
64
+ using t = ops.fromInt32([T], [1]);
65
+ using kept = ops.fromInt32([...lengths], [B]);
66
+ kernel ??= new MetalKernel({ name: "gated_delta_prefix_state",
67
+ inputNames: ["k", "v", "g", "beta", "state_in", "Tin", "lengths"],
68
+ outputNames: ["state_out"], source: SOURCE, ensureRowContiguous: true });
69
+ return kernel.apply([k, v, g, beta, state ?? zero!, t, kept], {
70
+ outputs: [{ shape: [B, Hv, Dv, Dk], dtype: Dtype.float32 }],
71
+ grid: [32, Dv, B * Hv], threadGroup: [32, 4, 1],
72
+ templateInts: { Dk, Dv, Hk, Hv },
73
+ })[0]!;
74
+ }