mlx-bun 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +57 -32
  2. package/docs/README.md +55 -42
  3. package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
  4. package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
  5. package/docs/design/batching.md +220 -7
  6. package/docs/design/decode-speed-program.md +121 -66
  7. package/docs/design/generic-model-support.md +116 -0
  8. package/docs/design/kv-cache.md +380 -211
  9. package/docs/design/speculative-decoding.md +165 -26
  10. package/docs/design/turboquant.md +20 -7
  11. package/docs/design/unified-engine-frontier-plan.md +120 -13
  12. package/docs/planning/PRODUCT_ROADMAP.md +5 -6
  13. package/docs/planning/public-readiness-handoff.md +244 -0
  14. package/docs/planning/public-readiness.md +149 -0
  15. package/docs/reference/benchmarks.md +3463 -3489
  16. package/docs/reference/cli.md +62 -1
  17. package/docs/reference/distribution.md +29 -4
  18. package/docs/reference/environment.md +104 -8
  19. package/docs/reference/library-api.md +18 -4
  20. package/docs/reference/memory.md +1 -1
  21. package/docs/reference/models.md +17 -5
  22. package/docs/reference/server-api.md +181 -11
  23. package/docs/reference/server-config.md +232 -105
  24. package/package.json +2 -2
  25. package/src/audio/audiotoolbox.ts +112 -0
  26. package/src/audio/conformer.ts +8 -0
  27. package/src/audio/mic-capture.ts +98 -0
  28. package/src/audio/silero-vad.ts +379 -0
  29. package/src/audio/whisper-decode.ts +774 -0
  30. package/src/audio/whisper-fast.ts +732 -0
  31. package/src/audio/whisper-format.ts +73 -0
  32. package/src/audio/whisper-mel.ts +185 -0
  33. package/src/audio/whisper-timing.ts +274 -0
  34. package/src/audio/whisper-tokenizer.ts +239 -0
  35. package/src/audio/whisper-transcribe.ts +410 -0
  36. package/src/backends/mlx/autoregressive.ts +22 -1
  37. package/src/backends/mlx/batch-group.ts +111 -27
  38. package/src/backends/mlx/cache-layout.ts +3 -3
  39. package/src/backends/mlx/continuation-persistence.ts +5 -4
  40. package/src/backends/mlx/encoder-cache.ts +25 -0
  41. package/src/backends/mlx/fill-append.ts +21 -8
  42. package/src/backends/mlx/fill-group.ts +497 -0
  43. package/src/backends/mlx/gateway-binding.ts +56 -12
  44. package/src/backends/mlx/kv-maintenance.ts +30 -2
  45. package/src/backends/mlx/mixed-iteration.ts +62 -0
  46. package/src/backends/mlx/model-prompt.ts +46 -32
  47. package/src/backends/mlx/model-serving.ts +4 -3
  48. package/src/backends/mlx/paged-cache-codec.ts +27 -0
  49. package/src/backends/mlx/prefill-cohort.ts +15 -4
  50. package/src/backends/mlx/prefill-policy.ts +26 -0
  51. package/src/backends/mlx/prefill-rows.ts +57 -13
  52. package/src/backends/mlx/prompt-input.ts +29 -0
  53. package/src/backends/mlx/qwen-prompt-input.ts +40 -0
  54. package/src/backends/mlx/request-state-policy.ts +18 -5
  55. package/src/backends/mlx/rollback.ts +6 -3
  56. package/src/backends/mlx/serial-executor.ts +16 -7
  57. package/src/backends/mlx/speculative-group.ts +105 -26
  58. package/src/backends/mlx/speculative.ts +24 -7
  59. package/src/backends/mlx/target-layout-capability.ts +1 -1
  60. package/src/backends/mlx/transitioning-kv-rows.ts +8 -0
  61. package/src/cli.ts +322 -0
  62. package/src/contracts/execution.ts +10 -0
  63. package/src/contracts/object-cache.ts +8 -0
  64. package/src/contracts/pi-provider.ts +1 -1
  65. package/src/contracts/pi.ts +3 -1
  66. package/src/contracts/prefix-cache.ts +13 -3
  67. package/src/contracts/scheduling.ts +9 -0
  68. package/src/engine/execution-plan.ts +23 -12
  69. package/src/engine/execution-tasks.ts +44 -0
  70. package/src/engine/namespaced-cache.ts +13 -0
  71. package/src/engine/scheduler.ts +22 -2
  72. package/src/fill/fill-session.ts +14 -3
  73. package/src/generate.ts +28 -22
  74. package/src/grammar.ts +13 -0
  75. package/src/inference/prefill.ts +6 -0
  76. package/src/kv-scheme.ts +22 -0
  77. package/src/kv-store.ts +115 -79
  78. package/src/lab/README.md +5 -1
  79. package/src/lab/paged-kv/paged-attention.ts +117 -0
  80. package/src/lab/paged-kv/paged-kv-rows.ts +33 -5
  81. package/src/lab/paged-kv/paged-kv.ts +108 -42
  82. package/src/md-modules.d.ts +5 -0
  83. package/src/mlx/array.ts +37 -0
  84. package/src/mlx/ffi.ts +19 -0
  85. package/src/mlx/normalized-argmax.ts +116 -0
  86. package/src/mlx/ops.ts +97 -10
  87. package/src/model/batched-turboquant-kv.ts +6 -5
  88. package/src/model/delayed-quantized-kv.ts +3 -3
  89. package/src/model/delayed-rotating-quantized-kv.ts +1 -1
  90. package/src/model/delayed-turboquant-kv.ts +6 -4
  91. package/src/model/factory.ts +26 -0
  92. package/src/model/fingerprint.ts +12 -0
  93. package/src/model/full-prefill-row.ts +1 -1
  94. package/src/model/gemma4-base.ts +25 -6
  95. package/src/model/gemma4.ts +82 -2
  96. package/src/model/kv-attention-view.ts +4 -1
  97. package/src/model/profile.ts +29 -4
  98. package/src/model/quantized-append-attention.ts +31 -0
  99. package/src/model/qwen3-mrope.ts +7 -9
  100. package/src/model/qwen3_5.ts +99 -37
  101. package/src/model/support.ts +8 -0
  102. package/src/model/token-groups.ts +66 -0
  103. package/src/model/trellis-linear.ts +2 -2
  104. package/src/model/turboquant-codec.ts +6 -0
  105. package/src/model/whisper.ts +435 -0
  106. package/src/native/mic_capture.swift +92 -0
  107. package/src/native-pack.ts +6 -5
  108. package/src/pi-web.ts +9 -1
  109. package/src/prompt-cache.ts +265 -43
  110. package/src/runtime-config.ts +1 -5
  111. package/src/safetensors.ts +9 -2
  112. package/src/sampler.ts +5 -4
  113. package/src/serve/audio-routes.ts +323 -0
  114. package/src/serve/chat-request.ts +2 -0
  115. package/src/serve/chat-stage.ts +17 -12
  116. package/src/serve/completion-executor.ts +9 -1
  117. package/src/serve/discovery-routes.ts +17 -1
  118. package/src/serve/generated-token-history.ts +57 -0
  119. package/src/serve/generation-gateway.ts +52 -11
  120. package/src/serve/model-binding.ts +7 -1
  121. package/src/serve/preparation.ts +3 -3
  122. package/src/serve/prompt-response-trace.ts +14 -0
  123. package/src/serve/request-plan.ts +2 -0
  124. package/src/serve/request-prep.ts +38 -21
  125. package/src/serve/transcription-server.ts +64 -0
  126. package/src/serve/transcription-service.ts +502 -0
  127. package/src/server.ts +132 -164
  128. package/src/spec/assistant-rows.ts +1 -0
  129. package/src/spec/deepspec-rows.ts +1 -1
  130. package/src/spec/dflash-source.ts +1 -1
  131. package/src/spec/glm52-mtp-source.ts +1 -0
  132. package/src/spec/ngram-source.ts +27 -5
  133. package/src/spec/projected-draft-rows.ts +2 -2
  134. package/src/spec/qwen-mtp-rows.ts +11 -0
  135. package/src/spec/qwen-mtp-source.ts +3 -0
  136. package/src/spec/serve-loop.ts +5 -3
  137. package/src/spec/source.ts +14 -1
  138. package/src/spec/two-model-rows.ts +1 -0
  139. package/src/spec/two-model.ts +2 -2
  140. package/src/ssd-cache.ts +76 -17
  141. package/src/ssd-durability.ts +20 -22
  142. package/src/storage/host-buffer.ts +24 -0
  143. package/src/storage/kv-writer.ts +83 -0
  144. package/src/storage/kv-writer.worker.js +264 -0
  145. package/src/storage/retention-policy.ts +37 -0
  146. package/src/tiered-prompt-cache.ts +45 -0
  147. package/src/vision/embedder.ts +8 -0
  148. package/src/vision/prompt.ts +101 -65
  149. package/src/vision/qwen3vl-prompt.ts +101 -64
  150. package/src/vision/qwen3vl-tower.ts +11 -0
  151. package/src/vision/siglip.ts +8 -0
  152. package/src/web/app.html +12 -0
  153. package/src/web/app.js +160 -0
  154. package/src/web/src/chat.ts +4 -1
  155. package/src/web/src/composer.ts +3 -0
  156. package/src/web/src/voice.ts +141 -0
  157. package/src/web/sw.js +1 -1
  158. /package/docs/{planning → archive/planning}/release-notes-v0.4.0.md +0 -0
@@ -0,0 +1,31 @@
1
+ import type { MlxArray } from "../mlx/array";
2
+ import * as ops from "../mlx/ops";
3
+ import { quantizedSdpa } from "./gemma4-base";
4
+
5
+ /** Committed positions retain single-token attention arithmetic. Projection
6
+ * work can still share the span; each query sees its own causal KV prefix. */
7
+ export function quantizedAppendAttention(
8
+ q: MlxArray, keys: ops.QuantizedTensor, values: ops.QuantizedTensor,
9
+ scale: number, groupSize: number, bits: number,
10
+ ): MlxArray {
11
+ const [batch, heads, length, dim] = q.shape as [number, number, number, number];
12
+ const prefix = keys.packed.shape[2]! - length;
13
+ const outputs: MlxArray[] = [];
14
+ const trim = (tensor: ops.QuantizedTensor, end: number) => {
15
+ const slice = (a: MlxArray) => a.slice([0, 0, 0, 0],
16
+ [a.shape[0]!, a.shape[1]!, end, a.shape[3]!]);
17
+ return { packed: slice(tensor.packed), scales: slice(tensor.scales), biases: slice(tensor.biases) };
18
+ };
19
+ try {
20
+ for (let row = 0; row < length; row++) {
21
+ using queryView = q.slice([0, 0, row, 0], [batch, heads, row + 1, dim]);
22
+ using query = ops.contiguous(queryView);
23
+ const k = trim(keys, prefix + row + 1), v = trim(values, prefix + row + 1);
24
+ try { outputs.push(quantizedSdpa(query, k, v, scale, { mode: "", arr: null }, groupSize, bits)); }
25
+ finally {
26
+ for (const t of [k, v]) { t.packed.dispose(); t.scales.dispose(); t.biases.dispose(); }
27
+ }
28
+ }
29
+ return ops.concatAxis(outputs, 2);
30
+ } finally { for (const output of outputs) output.dispose(); }
31
+ }
@@ -30,20 +30,13 @@ export interface MropeRequestState {
30
30
  }
31
31
 
32
32
  export interface MropeForwardState {
33
- /** Effective positions [3, 1, L] int32 for the CURRENT forward window. */
33
+ /** Effective positions [3, B, L] int32 for the current forward window. */
34
34
  posIds: MlxArray;
35
35
  /** Borrowed model-owned f32 inv_freq (mropeInvFreq) — not disposed here. */
36
36
  invFreq: MlxArray;
37
37
  rotaryDims: number;
38
38
  }
39
39
 
40
- /** Active per-forward mRoPE cos/sin, read by Qwen3Attention.forward. Null on
41
- * the text-only path (serial lane only — vision requests never batch). */
42
- export let activeMrope: MropeForwardState | null = null;
43
- export function setActiveMrope(s: MropeForwardState | null): void {
44
- activeMrope = s;
45
- }
46
-
47
40
  const MROPE_SECTION = [11, 11, 10] as const;
48
41
 
49
42
  /** Interleaved frequency→stream selector (rope_utils
@@ -85,6 +78,11 @@ export function buildMropePositions(
85
78
  state: MropeRequestState, offset: number, L: number,
86
79
  invFreq: MlxArray, rotaryDims: number,
87
80
  ): MropeForwardState {
81
+ return { posIds: mropePositionIds(state, offset, L), invFreq, rotaryDims };
82
+ }
83
+
84
+ /** Request-owned prompt positions; the returned native array belongs to the caller. */
85
+ export function mropePositionIds(state: MropeRequestState, offset: number, L: number): MlxArray {
88
86
  const P = state.positions;
89
87
  const promptLen = P[0].length;
90
88
  const joint = new Int32Array(3 * L);
@@ -94,7 +92,7 @@ export function buildMropePositions(
94
92
  joint[axis * L + l] = idx < promptLen ? P[axis]![idx]! : idx + state.delta;
95
93
  }
96
94
  }
97
- return { posIds: MlxArray.fromInt32(joint, [3, 1, L]), invFreq, rotaryDims };
95
+ return MlxArray.fromInt32(joint, [3, 1, L]);
98
96
  }
99
97
 
100
98
  // The reference language-side apply is a CUSTOM METAL KERNEL (rope_utils
@@ -10,8 +10,11 @@
10
10
  // shared maybeQuantizeKv path).
11
11
 
12
12
  import type { ModelConfig } from "../config";
13
+ import { quantizedAppendAttention } from "./quantized-append-attention";
13
14
  import { gatedDeltaState } from "./qwen3-delta-state";
14
15
  import type { Weights } from "../weights";
16
+ import { runtimeFlag } from "../runtime-config";
17
+ import { mapTokenGroups, type TokenGroup } from "./token-groups";
15
18
  import { MlxArray } from "../mlx/array";
16
19
  import { Dtype, deviceArchitecture } from "../mlx/ffi";
17
20
  import * as ops from "../mlx/ops";
@@ -36,8 +39,8 @@ import { materializeCopy } from "../mlx/materialize";
36
39
  import { gatedDeltaUpdate, SSMCache } from "./qwen3-delta";
37
40
  import type { QwenConvolution } from "./qwen-conv";
38
41
  import {
39
- activeMrope, applyInterleavedRope, buildMropePositions, mropeInvFreq,
40
- setActiveMrope, type MropeRequestState,
42
+ applyInterleavedRope, buildMropePositions, mropeInvFreq,
43
+ type MropeRequestState, type MropeForwardState,
41
44
  } from "./qwen3-mrope";
42
45
 
43
46
  const PREFIX = "language_model";
@@ -406,7 +409,8 @@ export class Qwen3Attention {
406
409
  this.kNorm = new RMSNorm(weights.tensor(`${prefix}.k_norm.weight`), t.rmsNormEps);
407
410
  }
408
411
 
409
- forward(x: MlxArray, mask: Mask, cache: Cache, independentRows = false): MlxArray {
412
+ forward(x: MlxArray, mask: Mask, cache: Cache, independentRows = false,
413
+ mrope: MropeForwardState | null = null): MlxArray {
410
414
  const [B, L] = x.shape as [number, number, number];
411
415
 
412
416
  // q_proj emits 2× head_dim per head → split into queries + gate.
@@ -433,11 +437,10 @@ export class Qwen3Attention {
433
437
  // position as ropeOffsetArr (rows have different prompt lengths); the
434
438
  // dynamic-offset kernel is the same fast::rope, bit-exact vs the static
435
439
  // form (tests/unit/compile.test.ts). Serial lane: scalar offset, unchanged.
436
- // Vision requests (serial lane only) install activeMrope for the current
437
- // forward: 3D interleaved positions via the manual apply — the reference
440
+ // Media forwards supply their own 3D interleaved positions. The reference
438
441
  // never uses the fused fast-rope kernel when position_ids are supplied,
439
442
  // so this IS the oracle's own arithmetic. Text-only stays on ops.rope.
440
- const mr = activeMrope;
443
+ const mr = mrope;
441
444
  const offArr = (cache as { ropeOffsetArr?: MlxArray }).ropeOffsetArr;
442
445
  if (mr) {
443
446
  q = disposing(q, applyInterleavedRope(q, mr));
@@ -456,12 +459,14 @@ export class Qwen3Attention {
456
459
  if (cache.attentionState) {
457
460
  const view = cache.attentionState.appendAndFetch(k, v);
458
461
  k.dispose(); v.dispose();
459
- try { attn = view.attend(q, this.scale, mask); } finally { view.dispose(); }
462
+ try { attn = view.attend(q, this.scale, mask, independentRows); } finally { view.dispose(); }
460
463
  } else if (quantized) {
461
464
  const [keys, values] = quantized.updateAndFetchQuantized(k, v);
462
465
  k.dispose();
463
466
  v.dispose();
464
- attn = quantizedSdpa(q, keys, values, this.scale, mask, quantized.groupSize, quantized.bits);
467
+ attn = independentRows && L > 1
468
+ ? quantizedAppendAttention(q, keys, values, this.scale, quantized.groupSize, quantized.bits)
469
+ : quantizedSdpa(q, keys, values, this.scale, mask, quantized.groupSize, quantized.bits);
465
470
  disposeTriple(keys);
466
471
  disposeTriple(values);
467
472
  } else {
@@ -559,23 +564,27 @@ export class Qwen3Layer {
559
564
  this.postAttnNorm = new RMSNorm(weights.tensor(`${prefix}.post_attention_layernorm.weight`), config.text.rmsNormEps);
560
565
  }
561
566
 
562
- forward(x: MlxArray, faMask: Mask, cache: Cache, independentRows = false, ssmMask?: MlxArray | null): MlxArray {
563
- const xn = this.inputNorm.forward(x);
564
- const r = this.isLinear
567
+ forward(x: MlxArray, faMask: Mask, cache: Cache, independentRows = false, ssmMask?: MlxArray | null,
568
+ mrope: MropeForwardState | null = null): MlxArray {
569
+ using h = this.forwardAttn(x, faMask, cache, independentRows, ssmMask, mrope);
570
+ return this.forwardMlp(h, independentRows);
571
+ }
572
+
573
+ forwardAttn(x: MlxArray, faMask: Mask, cache: Cache, independentRows = false, ssmMask?: MlxArray | null,
574
+ mrope: MropeForwardState | null = null): MlxArray {
575
+ using xn = this.inputNorm.forward(x);
576
+ using r = this.isLinear
565
577
  ? this.linearAttn!.forward(xn, cache as SSMCache, independentRows, ssmMask)
566
- : this.selfAttn!.forward(xn, faMask, cache, independentRows);
567
- xn.dispose();
568
- const h = ops.add(x, r);
569
- r.dispose();
570
- const hn = this.postAttnNorm.forward(h);
578
+ : this.selfAttn!.forward(xn, faMask, cache, independentRows, mrope);
579
+ return ops.add(x, r);
580
+ }
581
+
582
+ forwardMlp(h: MlxArray, independentRows = false): MlxArray {
583
+ using hn = this.postAttnNorm.forward(h);
571
584
  // RMSNorm allocates an aligned row-contiguous output. Pass that layout
572
585
  // proof so Trellis can match native small-prefill matmul without an eval.
573
- const m = this.mlp.forward(hn, true, independentRows);
574
- hn.dispose();
575
- const out = ops.add(h, m);
576
- h.dispose();
577
- m.dispose();
578
- return out;
586
+ using m = this.mlp.forward(hn, true, independentRows);
587
+ return ops.add(h, m);
579
588
  }
580
589
  }
581
590
 
@@ -669,10 +678,16 @@ export class Qwen35Model {
669
678
  return null;
670
679
  }
671
680
  return {
672
- maxChunkSize: (state: readonly Cache[]) => qwenAppendChunkSize(state[0]!.offset),
681
+ affineKvBits: [4, 8],
682
+ turboQuantFormats: [{ kBits: 8, vBits: 3 }],
683
+ maxChunkSize: (state: readonly Cache[], rows = 1) => rows === 1 ? qwenAppendChunkSize(state[0]!.offset) : 1,
673
684
  forwardHidden: (ids: MlxArray, cache: Cache[]): MlxArray => {
685
+ // Wider cohorts retain their ordinary one-position numerical graph.
686
+ // The specialized multi-position append remains qualified at B=1.
687
+ if (ids.shape.length === 2 && ids.shape[0]! > 1 && ids.shape[1] === 1)
688
+ return this.forwardHidden(ids, cache);
674
689
  if (ids.shape.length !== 2 || ids.shape[0] !== 1 || ids.shape[1]! > 4)
675
- throw new Error("Qwen committed-token append supports one row and at most four positions");
690
+ throw new Error("Qwen committed-token append supports one position per shared row or up to four positions at B=1");
676
691
  const hidden = this.embed.encode(ids);
677
692
  return this.forwardLayers(hidden, cache, true);
678
693
  },
@@ -684,10 +699,61 @@ export class Qwen35Model {
684
699
  return this.forwardLayers(h, cache);
685
700
  }
686
701
 
702
+ /** Borrowed request positions, including different positions for each row. */
703
+ forwardHiddenAtPositions(ids: MlxArray, cache: Cache[], positions: MlxArray): MlxArray {
704
+ return this.forwardLayers(this.embed.encode(ids), cache, false, positions);
705
+ }
706
+
707
+ forwardEmbeddingsAtPositions(embeds: MlxArray, cache: Cache[], positions: MlxArray): MlxArray {
708
+ return this.forwardLayers(ops.contiguous(embeds), cache, false, positions);
709
+ }
710
+
711
+ /** Attention and DeltaNet retain disjoint row state. Only tokenwise MLP
712
+ * work is eligible for packing; verification can preserve its geometry. */
713
+ forwardHiddenMixed(work: readonly TokenGroup[]): MlxArray[] {
714
+ if (work.length === 1 && !work[0]!.captureLayer) return [this.forwardHidden(work[0]!.ids, work[0]!.cache)];
715
+ const groups: Array<{ h: MlxArray; mask?: Mask; ssmMask?: MlxArray | null }> = [];
716
+ const results: MlxArray[] = [];
717
+ const pack = runtimeFlag("MLX_BUN_MIXED_PACKED_MLP", true);
718
+ const bounded = work.some(group => group.ids.shape[1]! > TRELLIS_MATVEC_MAX_M);
719
+ try {
720
+ for (const { ids, cache } of work) {
721
+ const group: typeof groups[number] = { h: this.embed.encode(ids) }; groups.push(group);
722
+ group.mask = cache[this.faIdx]!.makeMask(ids.shape[1]!, null);
723
+ group.ssmMask = (cache[0] as SSMCache).prefillPadding?.makeMask(ids.shape[1]!);
724
+ }
725
+ for (const [i, layer] of this.layers.entries()) {
726
+ const mids: MlxArray[] = [];
727
+ try {
728
+ for (const [row, group] of groups.entries())
729
+ mids.push(layer.forwardAttn(group.h, group.mask!, work[row]!.cache[i]!, false, group.ssmMask));
730
+ const outputs = mapTokenGroups(work, mids, hidden => layer.forwardMlp(hidden), pack);
731
+ for (const [row, group] of groups.entries()) { group.h.dispose(); group.h = outputs[row]!; }
732
+ // Materialize every layer's recurrent tail as well as the residual.
733
+ // It is not a dependency of h and otherwise pins prefill buffers.
734
+ if (bounded) {
735
+ const caches = work.map(group => group.cache[i]!);
736
+ const state = caches.map(cache => cache.state());
737
+ try { ops.evalAll([...groups.map(group => group.h), ...state.flat()]); }
738
+ finally { for (const [row, cache] of caches.entries())
739
+ if (cache.stateNeedsDispose) for (const value of state[row]!) value.dispose(); }
740
+ }
741
+ for (const [row, group] of groups.entries()) work[row]!.captureLayer?.(i, group.h);
742
+ } finally { for (const mid of mids) mid.dispose(); }
743
+ }
744
+ for (const [row, group] of groups.entries()) {
745
+ const hidden = this.finalNorm.forward(group.h); results.push(hidden);
746
+ work[row]!.captureLayer?.(this.layers.length, hidden);
747
+ }
748
+ return results;
749
+ } catch (error) { for (const result of results) result.dispose(); throw error; }
750
+ finally { for (const group of groups) { group.h.dispose(); group.mask?.arr?.dispose(); group.ssmMask?.dispose(); } }
751
+ }
752
+
687
753
  /** Active vision mRoPE request state (serial lane; set by the generation
688
754
  * gateway around a vision request's run, null for text-only — which keeps
689
- * the bit-exact fast-rope path). While set, forwardLayers installs the
690
- * per-forward interleaved cos/sin consumed by every full-attn layer. */
755
+ * the bit-exact fast-rope path). The explicit serial executor scopes this value.
756
+ * Shared media forwards pass request-owned positions directly. */
691
757
  mrope: MropeRequestState | null = null;
692
758
  #mropeInvFreq: MlxArray | null = null;
693
759
 
@@ -722,7 +788,8 @@ export class Qwen35Model {
722
788
  tap.captured.set(i, copy);
723
789
  }
724
790
 
725
- protected forwardLayers(h0: MlxArray, cache: Cache[], independentRows = false): MlxArray {
791
+ protected forwardLayers(h0: MlxArray, cache: Cache[], independentRows = false,
792
+ positions?: MlxArray): MlxArray {
726
793
  const L = h0.shape[1]!;
727
794
  // One full-attention mask shared by all full layers (same offset); linear
728
795
  // layers see no ssm mask at B=1.
@@ -731,15 +798,13 @@ export class Qwen35Model {
731
798
  // Vision requests: one interleaved-mRoPE cos/sin table per forward,
732
799
  // shared by all 12 full-attention layers (positions are layer-invariant).
733
800
  let mropeFwd: ReturnType<typeof buildMropePositions> | null = null;
734
- if (this.mrope) {
801
+ if (positions || this.mrope) {
735
802
  const t = this.config.text;
736
803
  const ropeDims = Math.trunc(t.headDim * t.partialRotaryFactor);
737
804
  const base = t.ropeParameters.full_attention?.ropeTheta ?? 10000;
738
805
  this.#mropeInvFreq ??= mropeInvFreq(ropeDims, base);
739
- mropeFwd = buildMropePositions(
740
- this.mrope, cache[this.faIdx]!.offset, L, this.#mropeInvFreq, ropeDims,
741
- );
742
- setActiveMrope(mropeFwd);
806
+ mropeFwd = positions ? { posIds: positions, invFreq: this.#mropeInvFreq, rotaryDims: ropeDims }
807
+ : buildMropePositions(this.mrope!, cache[this.faIdx]!.offset, L, this.#mropeInvFreq, ropeDims);
743
808
  }
744
809
  let h: MlxArray | null = h0;
745
810
  const prof = (globalThis as Record<string, unknown>).__deltaProf as
@@ -748,7 +813,7 @@ export class Qwen35Model {
748
813
  try {
749
814
  for (let i = 0; i < this.layers.length; i++) {
750
815
  const tl = prof ? performance.now() : 0;
751
- const next = this.layers[i]!.forward(h, faMask, cache[i]!, independentRows, ssmMask);
816
+ const next = this.layers[i]!.forward(h, faMask, cache[i]!, independentRows, ssmMask, mropeFwd);
752
817
  h.dispose();
753
818
  h = next;
754
819
  if (L > TRELLIS_MATVEC_MAX_M) {
@@ -772,10 +837,7 @@ export class Qwen35Model {
772
837
  h = null; // consumed — the finally must not double-free
773
838
  return out;
774
839
  } finally {
775
- if (mropeFwd) {
776
- setActiveMrope(null);
777
- mropeFwd.posIds.dispose();
778
- }
840
+ if (mropeFwd && !positions) mropeFwd.posIds.dispose();
779
841
  // A mid-loop layer throw must not strand the mask array or the
780
842
  // in-flight [1,L,H] residual (2026-08-18 review).
781
843
  faMask.arr?.dispose();
@@ -61,6 +61,13 @@ export function isDiffusionGemmaConfig(config: ModelConfig): boolean {
61
61
  return config.modelType === "diffusion_gemma";
62
62
  }
63
63
 
64
+ /** Whisper (model_type `whisper`, mlx-whisper ModelDimensions config):
65
+ * encoder-decoder speech recognition — audio in, text out. Routed through
66
+ * the transcription engine (src/audio/whisper-*.ts), never the chat loop. */
67
+ export function isWhisperConfig(config: ModelConfig): boolean {
68
+ return config.modelType === "whisper" && typeof config.raw.n_audio_ctx === "number";
69
+ }
70
+
64
71
  /** Support tier for a registry record (docs/design/generic-model-support.md):
65
72
  * "targeted" = dedicated/generated forward + L2/L3 paths;
66
73
  * "generic" = the Tier-0 universal module (L1 monolith only);
@@ -74,6 +81,7 @@ export function supportTier(modelType: string, repoId = ""): "targeted" | "gener
74
81
  if (modelType === "qwen3") return "targeted"; // plain Qwen3 (embedding backbone)
75
82
  if (modelType === "qwen3_moe") return "targeted"; // Qwen3-MoE (sparse experts)
76
83
  if (modelType === "glm_moe_dsa") return "targeted"; // dedicated GLM-5.2 MLA/DSA port
84
+ if (modelType === "whisper") return "targeted"; // speech-to-text (transcription engine)
77
85
  if (modelType === "llama" && repoId.toLowerCase().includes("minicpm5-1b-optiq-4bit"))
78
86
  return "targeted";
79
87
  // Tier-0 generic fallback: the universal-dense descriptor table.
@@ -0,0 +1,66 @@
1
+ import type { MlxArray } from "../mlx/array";
2
+ import * as ops from "../mlx/ops";
3
+ import type { Cache } from "./gemma4-base";
4
+
5
+ export interface TokenWorkOptions {
6
+ /** Verification keeps its established GEMV/GEMM geometry. Other groups may
7
+ * still pack their tokenwise work independently. */
8
+ readonly preserveTokenGeometry?: boolean;
9
+ /** Borrows a residual stream; the method copies only the layers it needs.
10
+ * The layer-count index denotes the post-final-norm output. */
11
+ readonly captureLayer?: (layer: number, hidden: MlxArray) => void;
12
+ }
13
+
14
+ /** Independent sequences presented to one model execution. Each item keeps
15
+ * its existing rectangular row geometry and owns a disjoint cache. */
16
+ export interface TokenGroup extends TokenWorkOptions {
17
+ readonly ids: MlxArray;
18
+ readonly cache: Cache[];
19
+ }
20
+
21
+ /** Apply a tokenwise block while respecting each method's numerical policy. */
22
+ export function mapTokenGroups(
23
+ groups: readonly TokenGroup[], inputs: readonly MlxArray[],
24
+ operation: (hidden: MlxArray) => MlxArray, pack = true,
25
+ ): MlxArray[] {
26
+ const results: MlxArray[] = [];
27
+ const packedRows = groups.flatMap((group, row) => pack && !group.preserveTokenGeometry ? [row] : []);
28
+ try {
29
+ const packed = packedRows.length ? mapPackedTokens(packedRows.map(row => inputs[row]!), operation) : [];
30
+ for (const [index, row] of packedRows.entries()) results[row] = packed[index]!;
31
+ for (let row = 0; row < groups.length; row++) results[row] ??= operation(inputs[row]!);
32
+ return results;
33
+ } catch (error) { for (const result of results) result?.dispose(); throw error; }
34
+ }
35
+
36
+ export interface MixedTokenModel {
37
+ /** Borrows inputs/state; returns one owned hidden array per input group. */
38
+ forwardHiddenMixed(groups: readonly TokenGroup[]): MlxArray[];
39
+ }
40
+
41
+ /** Pack real tokens for a tokenwise operation, then restore each group's
42
+ * geometry. Attention and recurrent operations must run outside this port.
43
+ * No padding, host readback, or evaluation is introduced here. */
44
+ export function mapPackedTokens(
45
+ inputs: readonly MlxArray[], operation: (packed: MlxArray) => MlxArray,
46
+ ): MlxArray[] {
47
+ if (inputs.length === 1) return [operation(inputs[0]!)];
48
+ const flat = inputs.map(input => ops.reshape(input, [1, -1, input.shape.at(-1)!]));
49
+ let packed: MlxArray;
50
+ try { packed = ops.concatAxis(flat, 1); }
51
+ finally { for (const input of flat) input.dispose(); }
52
+ using joined = packed;
53
+ using output = operation(joined);
54
+ const results: MlxArray[] = [];
55
+ let position = 0;
56
+ try {
57
+ for (const input of inputs) {
58
+ const [batch, length] = input.shape;
59
+ const end = position + batch! * length!;
60
+ using part = output.slice([0, position, 0], [1, end, output.shape[2]!]);
61
+ results.push(ops.reshape(part, [batch!, length!, output.shape[2]!]));
62
+ position = end;
63
+ }
64
+ return results;
65
+ } catch (error) { for (const result of results) result.dispose(); throw error; }
66
+ }
@@ -41,7 +41,7 @@ import * as ops from "../mlx/ops";
41
41
  import { MetalKernel } from "../mlx/metal-kernel";
42
42
  import type { Weights } from "../weights";
43
43
  import { quantFor, type ModelConfig, type QuantSpec } from "../config";
44
- import { runtimeFlag, runtimeNumber } from "../runtime-config";
44
+ import { runtimeFlag, runtimeNumber, runtimeValue } from "../runtime-config";
45
45
  import { lut1mad, wordsPerBlock } from "../quantize/trellis";
46
46
  import { QuantizedLinear } from "./gemma4-base";
47
47
  import { REDUCE_SHARED_M_SOURCE, GATEUP_SHARED_M_SOURCE } from "./trellis-shared-m";
@@ -475,7 +475,7 @@ export function expandTrellis(codes: MlxArray, scales: MlxArray, g: TrellisGeome
475
475
  export type TrellisMode = "kernel" | "expand";
476
476
 
477
477
  export function trellisModeFromEnv(): TrellisMode {
478
- const v = process.env.MLX_BUN_TRELLIS;
478
+ const v = runtimeValue("MLX_BUN_TRELLIS");
479
479
  if (v === "expand") return "expand";
480
480
  return "kernel";
481
481
  }
@@ -1,7 +1,13 @@
1
+ import { runtimeConfig, type RuntimeConfig } from "../runtime-config";
1
2
  import type { MlxArray } from "../mlx/array";
2
3
  import * as tq from "../mlx/turboquant-ops";
3
4
  import type { KvCodec } from "../backends/mlx/kv-codec";
4
5
 
6
+ /** Resolve once when a cache is created; storage copies retain this policy. */
7
+ export function turboQuantFusedDecode(config: RuntimeConfig = runtimeConfig()): boolean {
8
+ return config.flag("MLX_BUN_TURBOQUANT_FUSED_DECODE", true);
9
+ }
10
+
5
11
  /** One TurboQuant-encoded (K, V) storage tuple — the 5 arrays kv-store.ts
6
12
  * and toQuantized/fromKVCache pass around. Not `ops.QuantizedTensor`
7
13
  * (mlx's affine int4/int8 scheme): this is the rotation + Lloyd-Max