mlx-bun 0.0.9 → 0.0.11

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 (179) hide show
  1. package/README.md +66 -21
  2. package/THIRD_PARTY_LICENSES.md +1 -0
  3. package/docs/archive/mlx-bun-lab-report.html +275 -0
  4. package/docs/archive/wwdc-mlx-bun.html +848 -0
  5. package/docs/design/audio-input-plan.md +356 -0
  6. package/docs/design/batching-perf-path.md +255 -0
  7. package/docs/design/batching-v2-plan.md +23 -9
  8. package/docs/design/bucketing-stage.md +2 -1
  9. package/docs/design/compat-cli-surface-design.md +4 -3
  10. package/docs/design/decode-speed-program.md +172 -0
  11. package/docs/design/docs-reorg-plan.md +1 -1
  12. package/docs/design/dspark-serving-program.md +371 -0
  13. package/docs/design/dspark-speculative-decoding.md +167 -5
  14. package/docs/design/faithful-l1-consolidation.md +310 -0
  15. package/docs/design/generic-model-support.md +8 -5
  16. package/docs/design/grammar-spec-batching-integration.md +301 -0
  17. package/docs/design/minicpm5-decode-megakernel.md +16 -11
  18. package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
  19. package/docs/design/omlx-adoption-map.md +98 -0
  20. package/docs/design/orpo-training.md +34 -30
  21. package/docs/design/paged-kv-cache.md +214 -0
  22. package/docs/design/parallel-slots.md +82 -49
  23. package/docs/design/parity-tier-dag.md +43 -33
  24. package/docs/design/repo-cleanup-plan.md +189 -0
  25. package/docs/design/runtime-isolation.md +19 -3
  26. package/docs/design/segmented-backward-training.md +18 -2
  27. package/docs/design/ssd-kv-cold-tier.md +288 -0
  28. package/docs/design/structured-output.md +484 -0
  29. package/docs/design/the-dreaming-master-plan.md +2 -3
  30. package/docs/design/turboquant-kv.md +235 -0
  31. package/docs/design/unified-engine-frontier-plan.md +774 -0
  32. package/docs/design/web-chat-beat-matrix.md +315 -0
  33. package/docs/design/web-chat-redesign.md +1112 -0
  34. package/docs/investigations/decode-roofline-lookagain.md +92 -6
  35. package/docs/investigations/dspark-handoff.md +182 -16
  36. package/docs/investigations/kernel-perf-review-2026-07.md +105 -26
  37. package/docs/investigations/lab-build-journal.md +2 -2
  38. package/docs/investigations/openwiki-evaluation.md +249 -0
  39. package/docs/planning/IDEAS.md +14 -0
  40. package/docs/planning/PRODUCT_ROADMAP.md +2 -3
  41. package/docs/planning/chat-ui-vision-memory-first.md +199 -0
  42. package/docs/planning/chat-ui-vision.md +50 -0
  43. package/docs/planning/memory-docs-and-dag-plan.md +15 -6
  44. package/docs/planning/release-notes-v0.0.10.md +57 -0
  45. package/docs/planning/release-notes-v0.0.11.md +239 -0
  46. package/docs/reference/cli.md +35 -16
  47. package/docs/reference/distribution.md +13 -6
  48. package/docs/reference/embedding.md +35 -14
  49. package/docs/reference/features-matrix.md +97 -0
  50. package/docs/reference/library-api.md +30 -9
  51. package/docs/reference/memory.md +26 -6
  52. package/docs/reference/models.md +11 -1
  53. package/docs/reference/orpo-quickstart.md +3 -4
  54. package/docs/reference/server-api.md +634 -22
  55. package/docs/reference/server-config.md +251 -119
  56. package/docs/reference/training.md +20 -16
  57. package/package.json +5 -3
  58. package/src/anthropic.ts +13 -0
  59. package/src/audio/conformer.ts +647 -0
  60. package/src/audio/decode.ts +133 -0
  61. package/src/audio/features.ts +202 -0
  62. package/src/audio/transcode.ts +67 -0
  63. package/src/cli.ts +402 -107
  64. package/src/config.ts +49 -2
  65. package/src/download.ts +40 -0
  66. package/src/eval/README.md +7 -6
  67. package/src/eval/kl.ts +59 -3
  68. package/src/eval/quality-db.ts +2 -2
  69. package/src/eval/runner.ts +3 -3
  70. package/src/expert-offload.ts +15 -2
  71. package/src/flags.ts +13 -0
  72. package/src/generate.ts +330 -16
  73. package/src/grammar.ts +585 -0
  74. package/src/hub-rest.ts +242 -0
  75. package/src/kv-store.ts +618 -122
  76. package/src/lora.ts +8 -0
  77. package/src/md-modules.d.ts +42 -0
  78. package/src/media-fetch.ts +284 -0
  79. package/src/memory/rest.ts +245 -0
  80. package/src/memory/vault.ts +74 -0
  81. package/src/mlx/array.ts +127 -19
  82. package/src/mlx/autograd.ts +2 -2
  83. package/src/mlx/ffi.ts +27 -3
  84. package/src/mlx/metal-kernel.ts +7 -4
  85. package/src/mlx/ops.ts +179 -23
  86. package/src/mlx/turboquant-ops.ts +831 -0
  87. package/src/mlx/turboquant-tables.ts +237 -0
  88. package/src/model/batched-mask.ts +62 -1
  89. package/src/model/batched-quant.ts +206 -0
  90. package/src/model/batched-rotating-quant.ts +346 -0
  91. package/src/model/batched-rotating.ts +46 -1
  92. package/src/model/compiled-decode.ts +4 -15
  93. package/src/model/factory.ts +10 -4
  94. package/src/model/gemma4-base.ts +391 -34
  95. package/src/model/gemma4.ts +122 -33
  96. package/src/model/generated/gemma4-12b.ts +9 -13
  97. package/src/model/generated/gemma4-26b.ts +10 -5
  98. package/src/model/generated/gemma4-e4b.ts +14 -9
  99. package/src/model/minicpm5.ts +58 -102
  100. package/src/model/paged-kv.ts +253 -0
  101. package/src/model/qwen3-delta.ts +123 -15
  102. package/src/model/qwen3-moe.ts +404 -0
  103. package/src/model/qwen3.ts +9 -8
  104. package/src/model/qwen3_5.ts +99 -25
  105. package/src/model/support.ts +12 -1
  106. package/src/model/universal/dense.ts +40 -5
  107. package/src/model/universal/rope.ts +36 -21
  108. package/src/pi-web.ts +1299 -82
  109. package/src/preflight.ts +27 -8
  110. package/src/prompt-cache.ts +338 -32
  111. package/src/quantize/quantizer.ts +16 -1
  112. package/src/registry.ts +61 -3
  113. package/src/serve/batch-scheduler.ts +1001 -76
  114. package/src/serve/generation-gateway.ts +292 -30
  115. package/src/serve/isolate.ts +417 -0
  116. package/src/serve/lane-registry.ts +44 -0
  117. package/src/serve/session-search.ts +326 -0
  118. package/src/server.ts +1460 -105
  119. package/src/spec/assistant-source.ts +150 -0
  120. package/src/spec/deepspec-source.ts +143 -0
  121. package/src/spec/dflash-source.ts +124 -0
  122. package/src/spec/dspark/ab-stats.ts +136 -0
  123. package/src/spec/dspark/calibration.ts +112 -0
  124. package/src/spec/dspark/deepspec-module.ts +801 -0
  125. package/src/spec/dspark/generate-dflash.ts +72 -20
  126. package/src/spec/dspark/loader.ts +23 -0
  127. package/src/spec/dspark/module-dflash.ts +278 -36
  128. package/src/spec/dspark/quantize-drafter.ts +91 -0
  129. package/src/spec/generate.ts +59 -25
  130. package/src/spec/ngram-source.ts +132 -0
  131. package/src/spec/serve-loop.ts +536 -0
  132. package/src/spec/source.ts +93 -0
  133. package/src/spec/two-model.ts +171 -0
  134. package/src/ssd-cache.ts +285 -0
  135. package/src/tokenizer.ts +288 -2
  136. package/src/tool-approvals.ts +131 -0
  137. package/src/tool-call.ts +408 -20
  138. package/src/train/flash-cce.ts +29 -26
  139. package/src/train/job.ts +0 -2
  140. package/src/train/loss.ts +53 -11
  141. package/src/train/prefix-shared.ts +30 -7
  142. package/src/train/segmented.ts +150 -46
  143. package/src/train/trainer.ts +22 -15
  144. package/src/vision/prompt.ts +217 -48
  145. package/src/web/app.html +1184 -1789
  146. package/src/web/app.js +5667 -0
  147. package/src/web/icon.svg +13 -0
  148. package/src/web/manifest.webmanifest +24 -0
  149. package/src/web/src/adapters-panel.ts +288 -0
  150. package/src/web/src/api.ts +57 -0
  151. package/src/web/src/assistant.ts +485 -0
  152. package/src/web/src/chat.ts +1050 -0
  153. package/src/web/src/composer.ts +959 -0
  154. package/src/web/src/dataset.ts +132 -0
  155. package/src/web/src/finetune.ts +267 -0
  156. package/src/web/src/hub.ts +428 -0
  157. package/src/web/src/main.ts +98 -0
  158. package/src/web/src/markdown.ts +0 -0
  159. package/src/web/src/memory-panel.ts +572 -0
  160. package/src/web/src/model-picker.ts +159 -0
  161. package/src/web/src/palette.ts +346 -0
  162. package/src/web/src/protocol.ts +62 -0
  163. package/src/web/src/quantize.ts +167 -0
  164. package/src/web/src/rag.ts +274 -0
  165. package/src/web/src/sessions.ts +469 -0
  166. package/src/web/src/shell.ts +804 -0
  167. package/src/web/src/status.ts +252 -0
  168. package/src/web/src/ui-catalog.ts +92 -0
  169. package/src/web/sw.js +71 -0
  170. package/src/web/vendor/README.md +61 -0
  171. package/src/web/vendor/hljs-LICENSE +29 -0
  172. package/src/web/vendor/hljs-theme.css +59 -0
  173. package/src/web/vendor/hljs.js +26 -0
  174. package/src/model/fused-decode-kernel.ts +0 -312
  175. package/src/model/fused-geglu-kernel.ts +0 -198
  176. package/src/model/fused-mlp-kernel.ts +0 -169
  177. package/src/model/fused-swiglu-kernel.ts +0 -240
  178. package/src/model/steel-linear-kernel.ts +0 -96
  179. package/src/status-page.html +0 -497
@@ -45,15 +45,77 @@ import {
45
45
  RMSNorm,
46
46
  RotatingKVCache,
47
47
  RotatingQuantizedKVCache,
48
+ TurboQuantKVCache,
48
49
  quantizedSdpa,
49
50
  type Cache,
50
51
  type Mask,
51
52
  type SharedKv,
52
53
  type LoraWeights,
53
54
  } from "./gemma4-base";
54
- import { fusedGeglu, fusedGegluDifferentiable, fusedGeluEnabled, fusedGeluSupported, fusedGeluTraining } from "./fused-geglu-kernel";
55
55
  import { Checkpoint } from "../mlx/checkpoint";
56
56
  import { flashAttention, getTrainingAttn, flashSupported } from "./flash-attention";
57
+ import { unrotateValues as tqUnrotateValues } from "../mlx/turboquant-ops";
58
+ import { CompiledFunction } from "../mlx/compile";
59
+ import { flagOn } from "../flags";
60
+
61
+ /** Verbatim port of mlx_lm/models/gemma4_text.py:
62
+ * `@partial(mx.compile, shapeless=True) def geglu(gate, x): return nn.gelu_approx(gate) * x`
63
+ * The oracle @mx.compile's this and uses it for EVERY MLP shape (dense MLP
64
+ * __call__ line 114, and the SwitchGLU GeGLU activation line 149-150). Our
65
+ * default MLP runs the SAME math UNFUSED (standalone ops.geluApprox + ops.mul),
66
+ * a different dispatched kernel set + the per-op host tax the MiniCPM5 swiglu
67
+ * fix measured at ~5%. This closure fuses geluApprox's ~9 element-wise ops + the
68
+ * final mul into one traced-once/replayed graph, matching the oracle's kernel
69
+ * set. `geluApprox` is composed op-for-op from the same primitives as the
70
+ * oracle's nn.gelu_approx (0.5·x·(1+tanh(√(2/π)(x+0.044715·x³)))), so the
71
+ * compiled graph is the oracle's geglu graph. Autograd-safe (verified: the vjp
72
+ * through this closure is bit-identical to the plain composition). */
73
+ /** True when gemma's geglu activation runs through the compiled closure (matching
74
+ * the oracle's @mx.compile geglu) instead of the spelled-out ops.geluApprox+ops.mul.
75
+ * DEFAULT ON: compiled geglu is bit-exact to mlx-lm's @mx.compile geglu (same
76
+ * libmlx graph → same kernel) AND dispatches one fused kernel set instead of the
77
+ * ~9-op path. `MLX_BUN_COMPILED_GEGLU=0` (--compiled-activations off) selects the
78
+ * uncompiled composition — same L1 parity, slower (the A/B opt-out). */
79
+ export function compiledGegluActive(): boolean {
80
+ return flagOn("MLX_BUN_COMPILED_GEGLU", true);
81
+ }
82
+
83
+ let _gegluClosure: CompiledFunction | null = null;
84
+ export function compiledGeglu(gate: MlxArray, up: MlxArray): MlxArray {
85
+ if (!_gegluClosure) {
86
+ _gegluClosure = new CompiledFunction((inputs) => {
87
+ const g = inputs[0]!, u = inputs[1]!; // nn.gelu_approx(gate) * x
88
+ const act = ops.geluApprox(g);
89
+ const out = ops.mul(act, u); act.dispose();
90
+ return [out];
91
+ });
92
+ }
93
+ return _gegluClosure.apply([gate, up])[0]!;
94
+ }
95
+
96
+ /** gemma4_text.py `@partial(mx.compile) def logit_softcap(softcap, x):
97
+ * return mx.tanh(x / softcap) * softcap`
98
+ * The oracle fuses divide+tanh+multiply into ONE kernel; our default
99
+ * `logitSoftcap` runs them UNFUSED (standalone div + tanh + mul → a separate
100
+ * `vn_Tanh` + `vsn_Divide`). Keyed by cap so the softcap is baked as a graph
101
+ * constant (matching the oracle's weak-scalar → the `…Divide…Tanh…Multiply…_VC_`
102
+ * kernel). The default (matches mlx-lm's dispatched kernel set). */
103
+ const _softcapClosures = new Map<number, CompiledFunction>();
104
+ export function compiledLogitSoftcap(x: MlxArray, cap: number): MlxArray {
105
+ let fn = _softcapClosures.get(cap);
106
+ if (!fn) {
107
+ fn = new CompiledFunction((inputs) => {
108
+ const xx = inputs[0]!; // tanh(x / softcap) * softcap
109
+ const capArr = ops.scalarLike(cap, xx);
110
+ const scaled = ops.div(xx, capArr);
111
+ const t = ops.tanh(scaled); scaled.dispose();
112
+ const out = ops.mul(t, capArr); t.dispose(); capArr.dispose();
113
+ return [out];
114
+ });
115
+ _softcapClosures.set(cap, fn);
116
+ }
117
+ return fn.apply([x])[0]!;
118
+ }
57
119
 
58
120
  /** Optional gradient-checkpointing context for the training forward. When set,
59
121
  * forwardLayers wraps each layer in a Checkpoint so its interior activations
@@ -275,6 +337,14 @@ class Attention {
275
337
  groupSize: cache.groupSize, bits: cache.bits,
276
338
  offsetArr,
277
339
  };
340
+ } else if (cache instanceof TurboQuantKVCache) {
341
+ // Deferred-V read: values stay rotated; the attention output is
342
+ // un-rotated below (shared.vRotated) — including by KV-shared
343
+ // consumer layers, which see the flag through sharedIn.
344
+ const [keys, values] = cache.updateAndFetchDeferredV(kRoped, vT);
345
+ kRoped.dispose();
346
+ vT.dispose();
347
+ shared = { kind: "plain", keys, values, offset, offsetArr, vRotated: true };
278
348
  } else {
279
349
  const [keys, values] = cache.updateAndFetch(kRoped, vT);
280
350
  kRoped.dispose();
@@ -310,6 +380,11 @@ class Attention {
310
380
  attn = ops.sdpa(q, shared.keys, shared.values, 1.0, mask.mode, mask.arr);
311
381
  }
312
382
  q.dispose();
383
+ if (shared.kind === "plain" && shared.vRotated) {
384
+ // Attention is linear in V: one InvFWHT on the [B,H,L,D] output
385
+ // replaces one per cached token (TurboQuant deferred-V).
386
+ attn = disposing(attn, tqUnrotateValues(attn));
387
+ }
313
388
  const attnT = ops.transposeAxes(attn, [0, 2, 1, 3]);
314
389
  attn.dispose();
315
390
  const merged = ops.reshape(attnT, [B, L, -1]);
@@ -334,14 +409,11 @@ class MLP {
334
409
  forward(x: MlxArray): MlxArray {
335
410
  const g = this.gate.forward(x);
336
411
  const u = this.up.forward(x);
337
- // Fused GeGLU perf kernel (opt-in, not in a compiled trace): one pass
338
- // instead of ~9 element-wise kernels. Off → the bit-exact spelled path.
339
- // In training mode the differentiable wrapper (kernel forward + hand-derived
340
- // vjp) is used so autograd can flow AND the backward recomputes the gelu
341
- // from the primal instead of retaining the spelled-out intermediates.
342
412
  let h: MlxArray;
343
- if (fusedGeluEnabled() && !isCompiledTrace() && fusedGeluSupported(g)) {
344
- h = fusedGeluTraining() ? fusedGegluDifferentiable(g, u) : fusedGeglu(g, u);
413
+ if (compiledGegluActive() && !isCompiledTrace()) {
414
+ // Oracle geglu(gate_proj(x), up_proj(x)): @mx.compile'd gelu_approx·mul in
415
+ // one kernel set (the default; = mlx-lm). Autograd-safe → covers training.
416
+ h = compiledGeglu(g, u);
345
417
  g.dispose();
346
418
  u.dispose();
347
419
  } else {
@@ -464,12 +536,21 @@ class SwitchGLU {
464
536
  const xUp = this.up.forward(h, idx, doSort);
465
537
  const xGate = this.gate.forward(h, idx, doSort);
466
538
  h.dispose();
467
- // GeGLU activation: gelu_approx(gate) · up (same composition as MLP)
468
- const act = ops.geluApprox(xGate);
469
- xGate.dispose();
470
- const mid = ops.mul(act, xUp);
471
- act.dispose();
472
- xUp.dispose();
539
+ // GeGLU activation: gelu_approx(gate) · up (same composition as MLP). The
540
+ // oracle SwitchGLU uses the SAME @mx.compile'd geglu (activation=GeGLU(),
541
+ // switch_layers.py) → the compiled path routes through the shared closure.
542
+ let mid: MlxArray;
543
+ if (compiledGegluActive() && !isCompiledTrace()) {
544
+ mid = compiledGeglu(xGate, xUp);
545
+ xGate.dispose();
546
+ xUp.dispose();
547
+ } else {
548
+ const act = ops.geluApprox(xGate);
549
+ xGate.dispose();
550
+ mid = ops.mul(act, xUp);
551
+ act.dispose();
552
+ xUp.dispose();
553
+ }
473
554
  let y = this.down.forward(mid, idx, doSort);
474
555
  mid.dispose();
475
556
  if (idx !== indices) idx.dispose();
@@ -625,17 +706,12 @@ export class DecoderLayer {
625
706
  f.dispose();
626
707
 
627
708
  // per-layer input gating (reference gemma4_text DecoderLayer). gelu(gate)·pli
628
- // is the second GeGLU site — fused (differentiable) on the training path, the
629
- // bit-exact spelled-out gelu+mul on the inference path (unchanged).
709
+ // is the second GeGLU site — the bit-exact spelled-out gelu+mul.
630
710
  if (this.perLayerGate && this.perLayerProjection && this.postPerLayerNorm && perLayerInput) {
631
711
  const res2 = h;
632
712
  let gate = this.perLayerGate.forward(h);
633
- if (fusedGeluTraining() && fusedGeluEnabled() && !isCompiledTrace() && fusedGeluSupported(gate)) {
634
- gate = disposing(gate, fusedGegluDifferentiable(gate, perLayerInput));
635
- } else {
636
- gate = disposing(gate, ops.geluApprox(gate));
637
- gate = disposing(gate, ops.mul(gate, perLayerInput));
638
- }
713
+ gate = disposing(gate, ops.geluApprox(gate));
714
+ gate = disposing(gate, ops.mul(gate, perLayerInput));
639
715
  gate = disposing(gate, this.perLayerProjection.forward(gate));
640
716
  gate = disposing(gate, this.postPerLayerNorm.forward(gate));
641
717
  h = ops.add(res2, gate);
@@ -805,25 +881,35 @@ export class Gemma4Model {
805
881
  }
806
882
 
807
883
  /** Pre-merged (unscaled) input embeddings → hidden states. Used by the
808
- * vision path; `bidir` (bool [L]) marks image tokens, which attend
809
- * bidirectionally among themselves (use_bidirectional_attention:
810
- * "vision" — text stays causal). `ids` ([1, L], the spliced token ids)
811
- * is required for per-layer-input models (e2b/e4b): image-token
812
- * positions get token 0's per-layer embedding, and their vision content
884
+ * multimodal (vision/audio) path; `bidir` (bool [L]) marks image tokens,
885
+ * which attend bidirectionally among themselves (use_bidirectional_attention:
886
+ * "vision" — text stays causal; audio prompts pass NO bidir mask, and a
887
+ * mixed image+audio prompt drops the image overlay too, §3.3 Q1 of
888
+ * docs/design/audio-input-plan.md). `ids` ([1, L], the spliced token ids)
889
+ * is required for per-layer-input models (e2b/e4b): multimodal soft-token
890
+ * positions get token 0's per-layer embedding, and their tower content
813
891
  * enters only through the projection term (the merged hidden). Matches
814
- * optiq's frontend: `zeroed = where(text_mask, input_ids, 0)`. */
892
+ * optiq's frontend: `zeroed = where(text_mask, input_ids, 0)`.
893
+ *
894
+ * `multimodal` (bool [L]) is the per-layer-input ZEROING mask — the UNION
895
+ * of multimodal soft tokens (image | audio, §3.3 Q2), decoupled from the
896
+ * attention overlay: an audio-only prompt has no bidir mask but its audio
897
+ * positions must still be zeroed. When null (the legacy vision call
898
+ * shape), the zeroing mask falls back to `bidir` — vision passes the same
899
+ * image mask for both roles, so that path is unchanged. */
815
900
  forwardEmbeddings(
816
901
  embeds: MlxArray, cache: Cache[], bidir: MlxArray | null,
817
- ids: MlxArray | null = null,
902
+ ids: MlxArray | null = null, multimodal: MlxArray | null = null,
818
903
  ): MlxArray {
819
904
  const h = ops.mulScalar(embeds, this.embedScale);
820
905
  if (this.perLayerWidth === 0) return this.forwardLayers(h, cache, bidir, null);
821
906
 
822
907
  if (!ids)
823
- throw new Error("per-layer-input vision models require token ids");
908
+ throw new Error("per-layer-input multimodal models require token ids");
909
+ const zeroing = multimodal ?? bidir;
824
910
  let plIds = ids;
825
- if (bidir) {
826
- const cond = ops.reshape(bidir, ids.shape);
911
+ if (zeroing) {
912
+ const cond = ops.reshape(zeroing, ids.shape);
827
913
  const zero = ops.zeros(ids.shape, ids.dtype);
828
914
  plIds = ops.where(cond, zero, ids);
829
915
  cond.dispose();
@@ -1180,7 +1266,10 @@ export class Gemma4Model {
1180
1266
  logitsFromHidden(h: MlxArray): MlxArray {
1181
1267
  let logits = this.embed.asLinear(h);
1182
1268
  const softcap = this.config.text.finalLogitSoftcapping;
1183
- if (softcap !== null) logits = disposing(logits, logitSoftcap(logits, softcap));
1269
+ if (softcap !== null)
1270
+ logits = disposing(logits, compiledGegluActive() && !isCompiledTrace()
1271
+ ? compiledLogitSoftcap(logits, softcap) // fused, matches @mx.compile logit_softcap
1272
+ : logitSoftcap(logits, softcap));
1184
1273
  return logits;
1185
1274
  }
1186
1275
 
@@ -24,9 +24,6 @@ import {
24
24
  type Mask,
25
25
  type SharedKv,
26
26
  } from "../gemma4-base";
27
- import {
28
- fusedDecodeKernelSupported, fusedDecodeSdpa, perfKernelEnabled,
29
- } from "../fused-decode-kernel";
30
27
  import { Gemma4Model, type DecoderLayer } from "../gemma4";
31
28
 
32
29
  export const FINGERPRINT = "9f812d2eb461fcbe";
@@ -73,9 +70,7 @@ function donorSlidQuantG64B4VScal(layer: DecoderLayer, x: MlxArray, mask: Mask,
73
70
  q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
74
71
  q = disposing(q, a.rope(q, cache.ropeOffsetArr ?? offset));
75
72
  // dispatch-site constants: bits=4 group_size=64 nRep=2 head_dim=256
76
- const attn = perfKernelEnabled() && mask.mode === "" && fusedDecodeKernelSupported(q, 4, 64)
77
- ? fusedDecodeSdpa(q, kq, vq, 64, 4)
78
- : (L > 1 || process.env.MLX_BUN_FUSED_DECODE === "1") && fusedSdpaRuntimeOk(q, mask)
73
+ const attn = L > 1 && fusedSdpaRuntimeOk(q, mask)
79
74
  ? quantizedSdpaTiled(q, kq, vq, 1.0, mask, 64, 4)
80
75
  : quantizedSdpaUnfused(q, kq, vq, 1.0, mask, 64, 4);
81
76
  q.dispose();
@@ -129,9 +124,7 @@ function donorSlidQuantG64B8VScal(layer: DecoderLayer, x: MlxArray, mask: Mask,
129
124
  q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
130
125
  q = disposing(q, a.rope(q, cache.ropeOffsetArr ?? offset));
131
126
  // dispatch-site constants: bits=8 group_size=64 nRep=2 head_dim=256
132
- const attn = perfKernelEnabled() && mask.mode === "" && fusedDecodeKernelSupported(q, 8, 64)
133
- ? fusedDecodeSdpa(q, kq, vq, 64, 8)
134
- : (L > 1 || process.env.MLX_BUN_FUSED_DECODE === "1") && fusedSdpaRuntimeOk(q, mask)
127
+ const attn = L > 1 && fusedSdpaRuntimeOk(q, mask)
135
128
  ? quantizedSdpaTiled(q, kq, vq, 1.0, mask, 64, 8)
136
129
  : quantizedSdpaUnfused(q, kq, vq, 1.0, mask, 64, 8);
137
130
  q.dispose();
@@ -182,9 +175,7 @@ function donorFullQuantG64B4KeqVScal(layer: DecoderLayer, x: MlxArray, mask: Mas
182
175
  q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
183
176
  q = disposing(q, a.rope(q, cache.ropeOffsetArr ?? offset));
184
177
  // dispatch-site constants: bits=4 group_size=64 nRep=16 head_dim=512
185
- const attn = perfKernelEnabled() && mask.mode === "" && fusedDecodeKernelSupported(q, 4, 64)
186
- ? fusedDecodeSdpa(q, kq, vq, 64, 4)
187
- : (L > 1 || process.env.MLX_BUN_FUSED_DECODE === "1") && fusedSdpaRuntimeOk(q, mask)
178
+ const attn = L > 1 && fusedSdpaRuntimeOk(q, mask)
188
179
  ? quantizedSdpaTiled(q, kq, vq, 1.0, mask, 64, 4)
189
180
  : quantizedSdpaUnfused(q, kq, vq, 1.0, mask, 64, 4);
190
181
  q.dispose();
@@ -366,7 +357,12 @@ export class GeneratedGemma4 extends Gemma4Model {
366
357
  protected override forwardLayers(
367
358
  h0: MlxArray, cache: Cache[], bidir: MlxArray | null, ids: MlxArray | null,
368
359
  ): MlxArray {
369
- if (bidir !== null || !this.#matches(cache))
360
+ // hiddenTap (DSpark H_ctx capture) lives in the monolith's forwardLayers —
361
+ // this unrolled forward never calls captureLayer, so a tapped call MUST
362
+ // fall back or the spec path silently captures nothing and throws. Tapped
363
+ // forwards are only the spec path's prefill/verify; the monolith is the
364
+ // bit-exact reference, so this is correct-but-unspecialized.
365
+ if (bidir !== null || this.hiddenTap !== null || !this.#matches(cache))
370
366
  return super.forwardLayers(h0, cache, bidir, ids);
371
367
  generatedForwardUses++;
372
368
  const L = h0.shape[1]!;
@@ -70,7 +70,7 @@ function donorSlidQuantG64B8VMoeScal(layer: DecoderLayer, x: MlxArray, mask: Mas
70
70
  q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
71
71
  q = disposing(q, a.rope(q, cache.ropeOffsetArr ?? offset));
72
72
  // dispatch-site constants: bits=8 group_size=64 nRep=2 head_dim=256
73
- const attn = (L > 1 || process.env.MLX_BUN_FUSED_DECODE === "1") && fusedSdpaRuntimeOk(q, mask)
73
+ const attn = L > 1 && fusedSdpaRuntimeOk(q, mask)
74
74
  ? quantizedSdpaTiled(q, kq, vq, 1.0, mask, 64, 8)
75
75
  : quantizedSdpaUnfused(q, kq, vq, 1.0, mask, 64, 8);
76
76
  q.dispose();
@@ -134,7 +134,7 @@ function donorSlidQuantG64B4VMoeScal(layer: DecoderLayer, x: MlxArray, mask: Mas
134
134
  q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
135
135
  q = disposing(q, a.rope(q, cache.ropeOffsetArr ?? offset));
136
136
  // dispatch-site constants: bits=4 group_size=64 nRep=2 head_dim=256
137
- const attn = (L > 1 || process.env.MLX_BUN_FUSED_DECODE === "1") && fusedSdpaRuntimeOk(q, mask)
137
+ const attn = L > 1 && fusedSdpaRuntimeOk(q, mask)
138
138
  ? quantizedSdpaTiled(q, kq, vq, 1.0, mask, 64, 4)
139
139
  : quantizedSdpaUnfused(q, kq, vq, 1.0, mask, 64, 4);
140
140
  q.dispose();
@@ -195,7 +195,7 @@ function donorFullQuantG64B8KeqVMoeScal(layer: DecoderLayer, x: MlxArray, mask:
195
195
  q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
196
196
  q = disposing(q, a.rope(q, cache.ropeOffsetArr ?? offset));
197
197
  // dispatch-site constants: bits=8 group_size=64 nRep=8 head_dim=512
198
- const attn = (L > 1 || process.env.MLX_BUN_FUSED_DECODE === "1") && fusedSdpaRuntimeOk(q, mask)
198
+ const attn = L > 1 && fusedSdpaRuntimeOk(q, mask)
199
199
  ? quantizedSdpaTiled(q, kq, vq, 1.0, mask, 64, 8)
200
200
  : quantizedSdpaUnfused(q, kq, vq, 1.0, mask, 64, 8);
201
201
  q.dispose();
@@ -256,7 +256,7 @@ function donorFullQuantG64B4KeqVMoeScal(layer: DecoderLayer, x: MlxArray, mask:
256
256
  q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
257
257
  q = disposing(q, a.rope(q, cache.ropeOffsetArr ?? offset));
258
258
  // dispatch-site constants: bits=4 group_size=64 nRep=8 head_dim=512
259
- const attn = (L > 1 || process.env.MLX_BUN_FUSED_DECODE === "1") && fusedSdpaRuntimeOk(q, mask)
259
+ const attn = L > 1 && fusedSdpaRuntimeOk(q, mask)
260
260
  ? quantizedSdpaTiled(q, kq, vq, 1.0, mask, 64, 4)
261
261
  : quantizedSdpaUnfused(q, kq, vq, 1.0, mask, 64, 4);
262
262
  q.dispose();
@@ -394,7 +394,12 @@ export class GeneratedGemma4 extends Gemma4Model {
394
394
  protected override forwardLayers(
395
395
  h0: MlxArray, cache: Cache[], bidir: MlxArray | null, ids: MlxArray | null,
396
396
  ): MlxArray {
397
- if (bidir !== null || !this.#matches(cache))
397
+ // hiddenTap (DSpark H_ctx capture) lives in the monolith's forwardLayers —
398
+ // this unrolled forward never calls captureLayer, so a tapped call MUST
399
+ // fall back or the spec path silently captures nothing and throws. Tapped
400
+ // forwards are only the spec path's prefill/verify; the monolith is the
401
+ // bit-exact reference, so this is correct-but-unspecialized.
402
+ if (bidir !== null || this.hiddenTap !== null || !this.#matches(cache))
398
403
  return super.forwardLayers(h0, cache, bidir, ids);
399
404
  generatedForwardUses++;
400
405
  const L = h0.shape[1]!;
@@ -88,7 +88,7 @@ function donorSlidQuantG64B4VPliScal(layer: DecoderLayer, x: MlxArray, mask: Mas
88
88
  q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
89
89
  q = disposing(q, a.rope(q, cache.ropeOffsetArr ?? offset));
90
90
  // dispatch-site constants: bits=4 group_size=64 nRep=4 head_dim=256
91
- const attn = (L > 1 || process.env.MLX_BUN_FUSED_DECODE === "1") && fusedSdpaRuntimeOk(q, mask)
91
+ const attn = L > 1 && fusedSdpaRuntimeOk(q, mask)
92
92
  ? quantizedSdpaTiled(q, kq, vq, 1.0, mask, 64, 4)
93
93
  : quantizedSdpaUnfused(q, kq, vq, 1.0, mask, 64, 4);
94
94
  q.dispose();
@@ -151,7 +151,7 @@ function donorSlidQuantG64B8VPliScal(layer: DecoderLayer, x: MlxArray, mask: Mas
151
151
  q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
152
152
  q = disposing(q, a.rope(q, cache.ropeOffsetArr ?? offset));
153
153
  // dispatch-site constants: bits=8 group_size=64 nRep=4 head_dim=256
154
- const attn = (L > 1 || process.env.MLX_BUN_FUSED_DECODE === "1") && fusedSdpaRuntimeOk(q, mask)
154
+ const attn = L > 1 && fusedSdpaRuntimeOk(q, mask)
155
155
  ? quantizedSdpaTiled(q, kq, vq, 1.0, mask, 64, 8)
156
156
  : quantizedSdpaUnfused(q, kq, vq, 1.0, mask, 64, 8);
157
157
  q.dispose();
@@ -214,7 +214,7 @@ function donorFullQuantG64B4VPliScal(layer: DecoderLayer, x: MlxArray, mask: Mas
214
214
  q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
215
215
  q = disposing(q, a.rope(q, cache.ropeOffsetArr ?? offset));
216
216
  // dispatch-site constants: bits=4 group_size=64 nRep=4 head_dim=512
217
- const attn = (L > 1 || process.env.MLX_BUN_FUSED_DECODE === "1") && fusedSdpaRuntimeOk(q, mask)
217
+ const attn = L > 1 && fusedSdpaRuntimeOk(q, mask)
218
218
  ? quantizedSdpaTiled(q, kq, vq, 1.0, mask, 64, 4)
219
219
  : quantizedSdpaUnfused(q, kq, vq, 1.0, mask, 64, 4);
220
220
  q.dispose();
@@ -277,7 +277,7 @@ function donorFullQuantG64B8VPliScal(layer: DecoderLayer, x: MlxArray, mask: Mas
277
277
  q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
278
278
  q = disposing(q, a.rope(q, cache.ropeOffsetArr ?? offset));
279
279
  // dispatch-site constants: bits=8 group_size=64 nRep=4 head_dim=512
280
- const attn = (L > 1 || process.env.MLX_BUN_FUSED_DECODE === "1") && fusedSdpaRuntimeOk(q, mask)
280
+ const attn = L > 1 && fusedSdpaRuntimeOk(q, mask)
281
281
  ? quantizedSdpaTiled(q, kq, vq, 1.0, mask, 64, 8)
282
282
  : quantizedSdpaUnfused(q, kq, vq, 1.0, mask, 64, 8);
283
283
  q.dispose();
@@ -340,7 +340,7 @@ function donorSlidQuantG64B4VPliScalExp(layer: DecoderLayer, x: MlxArray, mask:
340
340
  q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
341
341
  q = disposing(q, a.rope(q, cache.ropeOffsetArr ?? offset));
342
342
  // dispatch-site constants: bits=4 group_size=64 nRep=4 head_dim=256
343
- const attn = (L > 1 || process.env.MLX_BUN_FUSED_DECODE === "1") && fusedSdpaRuntimeOk(q, mask)
343
+ const attn = L > 1 && fusedSdpaRuntimeOk(q, mask)
344
344
  ? quantizedSdpaTiled(q, kq, vq, 1.0, mask, 64, 4)
345
345
  : quantizedSdpaUnfused(q, kq, vq, 1.0, mask, 64, 4);
346
346
  q.dispose();
@@ -403,7 +403,7 @@ function donorFullQuantG64B4VPliScalExp(layer: DecoderLayer, x: MlxArray, mask:
403
403
  q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
404
404
  q = disposing(q, a.rope(q, cache.ropeOffsetArr ?? offset));
405
405
  // dispatch-site constants: bits=4 group_size=64 nRep=4 head_dim=512
406
- const attn = (L > 1 || process.env.MLX_BUN_FUSED_DECODE === "1") && fusedSdpaRuntimeOk(q, mask)
406
+ const attn = L > 1 && fusedSdpaRuntimeOk(q, mask)
407
407
  ? quantizedSdpaTiled(q, kq, vq, 1.0, mask, 64, 4)
408
408
  : quantizedSdpaUnfused(q, kq, vq, 1.0, mask, 64, 4);
409
409
  q.dispose();
@@ -449,7 +449,7 @@ function sharerSlidQuantG64B4VPliScal(layer: DecoderLayer, x: MlxArray, mask: Ma
449
449
  q = disposing(q, a.rope(q, shared.offsetArr ?? shared.offset));
450
450
  if (shared.kind !== "quant") throw new Error("generated sharer expected quant shared KV");
451
451
  // dispatch-site constants: bits=4 group_size=64 nRep=4 head_dim=256
452
- const attn = (L > 1 || process.env.MLX_BUN_FUSED_DECODE === "1") && fusedSdpaRuntimeOk(q, mask)
452
+ const attn = L > 1 && fusedSdpaRuntimeOk(q, mask)
453
453
  ? quantizedSdpaTiled(q, shared.keys, shared.values, 1.0, mask, 64, 4)
454
454
  : quantizedSdpaUnfused(q, shared.keys, shared.values, 1.0, mask, 64, 4);
455
455
  q.dispose();
@@ -494,7 +494,7 @@ function sharerFullQuantG64B4VPliScal(layer: DecoderLayer, x: MlxArray, mask: Ma
494
494
  q = disposing(q, a.rope(q, shared.offsetArr ?? shared.offset));
495
495
  if (shared.kind !== "quant") throw new Error("generated sharer expected quant shared KV");
496
496
  // dispatch-site constants: bits=4 group_size=64 nRep=4 head_dim=512
497
- const attn = (L > 1 || process.env.MLX_BUN_FUSED_DECODE === "1") && fusedSdpaRuntimeOk(q, mask)
497
+ const attn = L > 1 && fusedSdpaRuntimeOk(q, mask)
498
498
  ? quantizedSdpaTiled(q, shared.keys, shared.values, 1.0, mask, 64, 4)
499
499
  : quantizedSdpaUnfused(q, shared.keys, shared.values, 1.0, mask, 64, 4);
500
500
  q.dispose();
@@ -612,7 +612,12 @@ export class GeneratedGemma4 extends Gemma4Model {
612
612
  protected override forwardLayers(
613
613
  h0: MlxArray, cache: Cache[], bidir: MlxArray | null, ids: MlxArray | null,
614
614
  ): MlxArray {
615
- if (bidir !== null || ids === null || !this.#matches(cache))
615
+ // hiddenTap (DSpark H_ctx capture) lives in the monolith's forwardLayers —
616
+ // this unrolled forward never calls captureLayer, so a tapped call MUST
617
+ // fall back or the spec path silently captures nothing and throws. Tapped
618
+ // forwards are only the spec path's prefill/verify; the monolith is the
619
+ // bit-exact reference, so this is correct-but-unspecialized.
620
+ if (bidir !== null || ids === null || this.hiddenTap !== null || !this.#matches(cache))
616
621
  return super.forwardLayers(h0, cache, bidir, ids);
617
622
  generatedForwardUses++;
618
623
  const L = h0.shape[1]!;