mlx-bun 0.0.4

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 (210) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +362 -0
  3. package/THIRD_PARTY_LICENSES.md +41 -0
  4. package/bin/mlx-bun.mjs +32 -0
  5. package/docs/curve-designer.html +303 -0
  6. package/docs/design/adapters-end-to-end.md +201 -0
  7. package/docs/design/compat-cli-surface-design.md +227 -0
  8. package/docs/design/docs-reorg-plan.md +152 -0
  9. package/docs/design/hlg-sampling.md +340 -0
  10. package/docs/design/memory-system.md +339 -0
  11. package/docs/design/optimization_plan.md +344 -0
  12. package/docs/design/parallel-slots.md +515 -0
  13. package/docs/design/runtime-isolation.md +247 -0
  14. package/docs/design/segmented-backward-training.md +508 -0
  15. package/docs/investigations/chunk-finetune-experiment.md +84 -0
  16. package/docs/investigations/curve-bisector-routing.md +123 -0
  17. package/docs/investigations/curve-runs/bisector-route.json +98 -0
  18. package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
  19. package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
  20. package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
  21. package/docs/investigations/curve-terrain.html +105 -0
  22. package/docs/investigations/expert-offload-single-user-moe.md +292 -0
  23. package/docs/investigations/hlg-ab.html +34 -0
  24. package/docs/investigations/hlg-explorer.html +238 -0
  25. package/docs/investigations/hlg-figs/cliff.svg +1 -0
  26. package/docs/investigations/hlg-figs/cross.svg +1 -0
  27. package/docs/investigations/hlg-figs/edges.svg +1 -0
  28. package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
  29. package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
  30. package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
  31. package/docs/investigations/hlg-figs/terrain.svg +1 -0
  32. package/docs/investigations/hlg-report.html +262 -0
  33. package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
  34. package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
  35. package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
  36. package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
  37. package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
  38. package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
  39. package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
  40. package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
  41. package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
  42. package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
  43. package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
  44. package/docs/investigations/hlg-runs/hlg-map.json +383 -0
  45. package/docs/investigations/hlg-runs/run0-original.log +159 -0
  46. package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
  47. package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
  48. package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
  49. package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
  50. package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
  51. package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
  52. package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
  53. package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
  54. package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
  55. package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
  56. package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
  57. package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
  58. package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
  59. package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
  60. package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
  61. package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
  62. package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
  63. package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
  64. package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
  65. package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
  66. package/docs/investigations/hlg-runs/runN-verify.log +116 -0
  67. package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
  68. package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
  69. package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
  70. package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
  71. package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
  72. package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
  73. package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
  74. package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
  75. package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
  76. package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
  77. package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
  78. package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
  79. package/docs/investigations/hlg-sampling-investigation.md +970 -0
  80. package/docs/investigations/lab-build-journal.md +157 -0
  81. package/docs/investigations/pi-builtin-investigation.md +308 -0
  82. package/docs/investigations/starter-model-port-handoff.md +323 -0
  83. package/docs/planning/IDEAS.md +26 -0
  84. package/docs/planning/PRODUCT_ROADMAP.md +426 -0
  85. package/docs/planning/ResearchTopics.md +119 -0
  86. package/docs/planning/chunk-task-roadmap.md +77 -0
  87. package/docs/planning/journal.md +155 -0
  88. package/docs/reference/distribution.md +121 -0
  89. package/docs/reference/embedding.md +168 -0
  90. package/docs/reference/library-api.md +153 -0
  91. package/docs/reference/server-api.md +224 -0
  92. package/docs/reference/server-config.md +243 -0
  93. package/docs/segmented-backward-handoff.md +148 -0
  94. package/package.json +68 -0
  95. package/src/anthropic.ts +491 -0
  96. package/src/chat-template.ts +186 -0
  97. package/src/cli.ts +1075 -0
  98. package/src/config.ts +229 -0
  99. package/src/curve-sampler.ts +122 -0
  100. package/src/dataset/generators.ts +1087 -0
  101. package/src/dataset/index.ts +12 -0
  102. package/src/dataset/job.ts +81 -0
  103. package/src/dataset/llm.ts +71 -0
  104. package/src/dataset/registry.ts +528 -0
  105. package/src/download.ts +283 -0
  106. package/src/eval/README.md +86 -0
  107. package/src/eval/kl-prompts.ts +29 -0
  108. package/src/eval/kl.ts +294 -0
  109. package/src/eval/quality-db.ts +92 -0
  110. package/src/eval/runner.ts +102 -0
  111. package/src/eval/score.ts +49 -0
  112. package/src/eval/tasks/bfcl.ts +500 -0
  113. package/src/eval/tasks/gsm8k.ts +85 -0
  114. package/src/eval/tasks/hashhop.ts +109 -0
  115. package/src/eval/tasks/humaneval.ts +252 -0
  116. package/src/eval/tasks/ifeval.ts +354 -0
  117. package/src/eval/tasks/mmlu.ts +145 -0
  118. package/src/evaldb.ts +199 -0
  119. package/src/expert-offload-build.ts +77 -0
  120. package/src/expert-offload.ts +61 -0
  121. package/src/expert-trace.ts +67 -0
  122. package/src/fit.ts +254 -0
  123. package/src/generate.ts +451 -0
  124. package/src/harness-pi.ts +177 -0
  125. package/src/hf-push.ts +628 -0
  126. package/src/index.ts +22 -0
  127. package/src/jobs/db.ts +139 -0
  128. package/src/jobs/index.ts +29 -0
  129. package/src/jobs/job-entry.ts +106 -0
  130. package/src/jobs/runner.ts +304 -0
  131. package/src/jobs/sse.ts +114 -0
  132. package/src/jobs/types.ts +58 -0
  133. package/src/kv-store.ts +185 -0
  134. package/src/lora.ts +314 -0
  135. package/src/md-modules.d.ts +7 -0
  136. package/src/mlx/array.ts +237 -0
  137. package/src/mlx/autograd.ts +267 -0
  138. package/src/mlx/checkpoint.ts +116 -0
  139. package/src/mlx/compile.ts +158 -0
  140. package/src/mlx/custom-vjp.ts +140 -0
  141. package/src/mlx/ffi.ts +433 -0
  142. package/src/mlx/metal-kernel.ts +130 -0
  143. package/src/mlx/ops.ts +707 -0
  144. package/src/mmap.ts +90 -0
  145. package/src/model/batched-mask.ts +204 -0
  146. package/src/model/batched-rotating.ts +300 -0
  147. package/src/model/compiled-decode.ts +681 -0
  148. package/src/model/factory.ts +27 -0
  149. package/src/model/fingerprint.ts +33 -0
  150. package/src/model/flash-attention.ts +674 -0
  151. package/src/model/fused-decode-kernel.ts +305 -0
  152. package/src/model/fused-geglu-kernel.ts +87 -0
  153. package/src/model/gemma4-base.ts +1616 -0
  154. package/src/model/gemma4.ts +1012 -0
  155. package/src/model/generated/gemma4-12b.ts +526 -0
  156. package/src/model/generated/gemma4-26b.ts +500 -0
  157. package/src/model/generated/gemma4-e4b.ts +757 -0
  158. package/src/model/generated/index.ts +18 -0
  159. package/src/model/minicpm5.ts +278 -0
  160. package/src/model/qwen3-delta.ts +224 -0
  161. package/src/model/qwen3_5.ts +429 -0
  162. package/src/model/support.ts +46 -0
  163. package/src/native-pack.ts +152 -0
  164. package/src/pi-provider.ts +95 -0
  165. package/src/pi-terminal.ts +264 -0
  166. package/src/pi-web.ts +924 -0
  167. package/src/preflight.ts +123 -0
  168. package/src/prompt-cache.ts +113 -0
  169. package/src/quantize/allocator.ts +627 -0
  170. package/src/quantize/calibration.ts +180 -0
  171. package/src/quantize/config-writer.ts +132 -0
  172. package/src/quantize/index.ts +57 -0
  173. package/src/quantize/job.ts +136 -0
  174. package/src/quantize/quantizer.ts +462 -0
  175. package/src/quantize/safetensors-writer.ts +171 -0
  176. package/src/quantize/sensitivity.ts +234 -0
  177. package/src/registry.ts +336 -0
  178. package/src/responses.ts +774 -0
  179. package/src/safetensors.ts +158 -0
  180. package/src/sampler.ts +517 -0
  181. package/src/serve/batch-scheduler.ts +362 -0
  182. package/src/serve/generation-gateway.ts +160 -0
  183. package/src/server.ts +1891 -0
  184. package/src/spec/drafter.ts +261 -0
  185. package/src/spec/generate.ts +221 -0
  186. package/src/status-page.html +497 -0
  187. package/src/tokenizer.ts +38 -0
  188. package/src/tool-call.ts +242 -0
  189. package/src/train/dataset.ts +461 -0
  190. package/src/train/export.ts +32 -0
  191. package/src/train/forward.ts +229 -0
  192. package/src/train/index.ts +43 -0
  193. package/src/train/job.ts +148 -0
  194. package/src/train/lora-params.ts +194 -0
  195. package/src/train/loss.ts +342 -0
  196. package/src/train/merge.ts +195 -0
  197. package/src/train/optimizer.ts +176 -0
  198. package/src/train/rank.ts +101 -0
  199. package/src/train/segmented.ts +520 -0
  200. package/src/train/trainer.ts +600 -0
  201. package/src/tui.ts +161 -0
  202. package/src/vision/embedder.ts +168 -0
  203. package/src/vision/preprocess.ts +247 -0
  204. package/src/vision/prompt.ts +158 -0
  205. package/src/vision/siglip.ts +599 -0
  206. package/src/web/app.html +2471 -0
  207. package/src/web/skills/web-research/SKILL.md +44 -0
  208. package/src/web/skills.ts +59 -0
  209. package/src/web-tools.ts +426 -0
  210. package/src/weights.ts +91 -0
@@ -0,0 +1,681 @@
1
+ // Compiled decode step (Phase A of docs/design/optimization_plan.md).
2
+ //
3
+ // The single-token decode graph is identical token-to-token except for
4
+ // integer state (RoPE offset, cache write position, active length). This
5
+ // module wraps one whole step — embed → all layers → finalNorm → logits —
6
+ // in an mx.compile'd closure (src/mlx/compile.ts) so the per-step graph
7
+ // is REPLAYED in C++ instead of rebuilt through ~2000 bun:ffi crossings.
8
+ //
9
+ // How the per-step state crosses into a fixed graph:
10
+ // - RoPE offset and ring write positions enter as int32 ARRAY inputs
11
+ // (fast_rope_dynamic / slice_update_dynamic) — values change, graph
12
+ // doesn't.
13
+ // - Shapes that grow (the active KV prefix) enter as inputs and the
14
+ // closure is compiled shapeless=true: dim sizes may vary per step,
15
+ // ndim/dtype may not.
16
+ //
17
+ // The trace runs the UNMODIFIED Gemma4Model.forwardHidden against trace
18
+ // adapters that subclass the real cache classes — so the compiled graph
19
+ // is the production op sequence by construction, not a reimplementation.
20
+ // Per-cache fetch strategy (see DecodeStepPlan in gemma4.ts):
21
+ // - "concat": graph returns this step's (quantized) KV row; the buffer
22
+ // write stays OUTSIDE, right after the step — the buffer keeps a single
23
+ // reference at its slice_update, so mlx donates it (no per-step copy).
24
+ // - "ring": write in-graph at a dynamic position, read the full updated
25
+ // ring — bit-exact with the rotating steady state, where a concat
26
+ // would permute KV positions and change reduction order.
27
+ //
28
+ // Anything this module can't express falls back to the uncompiled path
29
+ // (generate.ts catches and disables for the generation): active LoRA
30
+ // adapters (their weights would bake into the trace as constants),
31
+ // unknown cache classes, diverged cache offsets.
32
+
33
+ import { MlxArray } from "../mlx/array";
34
+ import { CompiledFunction } from "../mlx/compile";
35
+ import * as ops from "../mlx/ops";
36
+ import { perfKernelEnabled } from "./fused-decode-kernel";
37
+ import {
38
+ Gemma4Model,
39
+ KVCache,
40
+ QuantizedKVCache,
41
+ RotatingKVCache,
42
+ RotatingQuantizedKVCache,
43
+ setCompiledTrace,
44
+ type Cache,
45
+ type DecodeStepPlan,
46
+ type SharedKv,
47
+ } from "./gemma4";
48
+
49
+ type AnyCache = KVCache | QuantizedKVCache | RotatingKVCache | RotatingQuantizedKVCache;
50
+
51
+ const fullSlice = (a: MlxArray): MlxArray => {
52
+ const stop = a.shape;
53
+ return a.slice(stop.map(() => 0), stop);
54
+ };
55
+
56
+ const catAxis2 = (a: ops.QuantizedTensor, b: ops.QuantizedTensor): ops.QuantizedTensor => ({
57
+ packed: ops.concatAxis([a.packed, b.packed], 2),
58
+ scales: ops.concatAxis([a.scales, b.scales], 2),
59
+ biases: ops.concatAxis([a.biases, b.biases], 2),
60
+ });
61
+
62
+ // --- trace adapters ---------------------------------------------------------
63
+ // Subclasses so Attention.forward's instanceof dispatch (quantized vs
64
+ // plain) routes exactly as it does for the real cache. `offset` is set
65
+ // for completeness only: at L=1 every makeMask returns mode "" and the
66
+ // RoPE offset rides the ropeOffsetArr input, so the baked value cannot
67
+ // leak into the graph. Adapters live only for the duration of one trace.
68
+
69
+ /** Growing plain cache (KVCache, or RotatingKVCache before the window
70
+ * fills): fetch = concat(active prefix, new row); new row is a closure
71
+ * output for the outside write. */
72
+ class TraceConcatPlain extends KVCache {
73
+ override readonly ropeOffsetArr: MlxArray;
74
+ outs: MlxArray[] = [];
75
+ constructor(
76
+ offset: number,
77
+ readonly activeK: MlxArray,
78
+ readonly activeV: MlxArray,
79
+ ropeOffsetArr: MlxArray,
80
+ ) {
81
+ super();
82
+ this.offset = offset;
83
+ this.ropeOffsetArr = ropeOffsetArr;
84
+ }
85
+
86
+ override updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
87
+ // same-shape reshape: an owned alias that survives the caller's
88
+ // dispose of k/v, so the row can be a closure output
89
+ this.outs = [ops.reshape(k, k.shape), ops.reshape(v, v.shape)];
90
+ return [
91
+ ops.concatAxis([this.activeK, k], 2),
92
+ ops.concatAxis([this.activeV, v], 2),
93
+ ];
94
+ }
95
+ }
96
+
97
+ /** Growing quantized cache: quantize in-graph, fetch = per-component
98
+ * concat, the six quantized row components are closure outputs. */
99
+ class TraceConcatQuant extends QuantizedKVCache {
100
+ override readonly ropeOffsetArr: MlxArray;
101
+ outs: MlxArray[] = [];
102
+ constructor(
103
+ offset: number, groupSize: number, bits: number,
104
+ readonly activeKq: ops.QuantizedTensor,
105
+ readonly activeVq: ops.QuantizedTensor,
106
+ ropeOffsetArr: MlxArray,
107
+ ) {
108
+ super(groupSize, bits);
109
+ this.offset = offset;
110
+ this.ropeOffsetArr = ropeOffsetArr;
111
+ }
112
+
113
+ override updateAndFetchQuantized(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor] {
114
+ const kq = ops.quantize(k, this.groupSize, this.bits);
115
+ const vq = ops.quantize(v, this.groupSize, this.bits);
116
+ this.outs = [kq.packed, kq.scales, kq.biases, vq.packed, vq.scales, vq.biases];
117
+ return [catAxis2(this.activeKq, kq), catAxis2(this.activeVq, vq)];
118
+ }
119
+ }
120
+
121
+ /** Rotating plain cache at steady state: write in-graph at the dynamic
122
+ * ring position, fetch the full updated buffer (mirrors #updateInPlace →
123
+ * #fetchAll); updated buffers are closure outputs. */
124
+ class TraceRingPlain extends RotatingKVCache {
125
+ override readonly ropeOffsetArr: MlxArray;
126
+ outs: MlxArray[] = [];
127
+ constructor(
128
+ offset: number, maxSize: number,
129
+ readonly bufK: MlxArray,
130
+ readonly bufV: MlxArray,
131
+ readonly writePosArr: MlxArray,
132
+ ropeOffsetArr: MlxArray,
133
+ ) {
134
+ super(maxSize);
135
+ this.offset = offset;
136
+ this.ropeOffsetArr = ropeOffsetArr;
137
+ }
138
+
139
+ override updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
140
+ const updK = ops.sliceUpdateDynamic(this.bufK, k, this.writePosArr, [2]);
141
+ const updV = ops.sliceUpdateDynamic(this.bufV, v, this.writePosArr, [2]);
142
+ this.outs = [updK, updV];
143
+ return [fullSlice(updK), fullSlice(updV)];
144
+ }
145
+ }
146
+
147
+ /** Rotating quantized cache at steady state: quantize + six dynamic
148
+ * writes in-graph, fetch full updated buffers (mirrors the oracle's
149
+ * _update_in_place → _active_slices). */
150
+ class TraceRingQuant extends RotatingQuantizedKVCache {
151
+ override readonly ropeOffsetArr: MlxArray;
152
+ outs: MlxArray[] = [];
153
+ constructor(
154
+ offset: number, maxSize: number, groupSize: number, bits: number,
155
+ readonly bufKq: ops.QuantizedTensor,
156
+ readonly bufVq: ops.QuantizedTensor,
157
+ readonly writePosArr: MlxArray,
158
+ ropeOffsetArr: MlxArray,
159
+ ) {
160
+ super(maxSize, groupSize, bits);
161
+ this.offset = offset;
162
+ this.ropeOffsetArr = ropeOffsetArr;
163
+ }
164
+
165
+ override updateAndFetchQuantized(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor] {
166
+ const kq = ops.quantize(k, this.groupSize, this.bits);
167
+ const vq = ops.quantize(v, this.groupSize, this.bits);
168
+ const upd = (buf: ops.QuantizedTensor, row: ops.QuantizedTensor): ops.QuantizedTensor => ({
169
+ packed: ops.sliceUpdateDynamic(buf.packed, row.packed, this.writePosArr, [2]),
170
+ scales: ops.sliceUpdateDynamic(buf.scales, row.scales, this.writePosArr, [2]),
171
+ biases: ops.sliceUpdateDynamic(buf.biases, row.biases, this.writePosArr, [2]),
172
+ });
173
+ const updK = upd(this.bufKq, kq);
174
+ const updV = upd(this.bufVq, vq);
175
+ for (const t of [kq, vq])
176
+ for (const a of [t.packed, t.scales, t.biases]) a.dispose();
177
+ this.outs = [updK.packed, updK.scales, updK.biases, updV.packed, updV.scales, updV.biases];
178
+ return [
179
+ { packed: fullSlice(updK.packed), scales: fullSlice(updK.scales), biases: fullSlice(updK.biases) },
180
+ { packed: fullSlice(updV.packed), scales: fullSlice(updV.scales), biases: fullSlice(updV.biases) },
181
+ ];
182
+ }
183
+ }
184
+
185
+ // --- slot layout ------------------------------------------------------------
186
+
187
+ interface SlotDesc {
188
+ kind: "p-cat" | "q-cat" | "p-ring" | "q-ring";
189
+ offset: number;
190
+ maxSize: number;
191
+ groupSize: number;
192
+ bits: number;
193
+ }
194
+
195
+ function slotDesc(c: AnyCache, plan: DecodeStepPlan): SlotDesc {
196
+ const cat = plan.fetch === "concat";
197
+ if (c instanceof QuantizedKVCache)
198
+ return { kind: "q-cat", offset: c.offset, maxSize: 0, groupSize: c.groupSize, bits: c.bits };
199
+ if (c instanceof RotatingQuantizedKVCache)
200
+ return { kind: cat ? "q-cat" : "q-ring", offset: c.offset, maxSize: c.maxSize, groupSize: c.groupSize, bits: c.bits };
201
+ if (c instanceof RotatingKVCache)
202
+ return { kind: cat ? "p-cat" : "p-ring", offset: c.offset, maxSize: c.maxSize, groupSize: 0, bits: 0 };
203
+ return { kind: "p-cat", offset: c.offset, maxSize: 0, groupSize: 0, bits: 0 };
204
+ }
205
+
206
+ /** Graph-shape signature: anything that changes the traced op sequence
207
+ * must appear here (cache kinds/quant params and the env flags read
208
+ * inside quantizedSdpa's dispatch). Offsets/capacities don't — they're
209
+ * array values or shapeless dims. */
210
+ function closureKey(descs: SlotDesc[]): string {
211
+ const flags =
212
+ `fd=${process.env.MLX_BUN_FUSED_DECODE === "1" ? 1 : 0}` +
213
+ `,nf=${process.env.MLX_BUN_NO_FUSED_SDPA === "1" ? 1 : 0}` +
214
+ `,pk=${perfKernelEnabled() ? 1 : 0}`;
215
+ return descs.map((d) => `${d.kind}:${d.groupSize}:${d.bits}`).join(",") + "|" + flags;
216
+ }
217
+
218
+ function makeTraceFn(model: Gemma4Model, descs: SlotDesc[]) {
219
+ return (inputs: MlxArray[]): MlxArray[] => {
220
+ const ropeOff = inputs[1]!;
221
+ let pos = 2;
222
+ const triple = (at: number): ops.QuantizedTensor => ({
223
+ packed: inputs[at]!, scales: inputs[at + 1]!, biases: inputs[at + 2]!,
224
+ });
225
+ const adapters: (TraceConcatPlain | TraceConcatQuant | TraceRingPlain | TraceRingQuant)[] = [];
226
+ for (const d of descs) {
227
+ switch (d.kind) {
228
+ case "p-cat":
229
+ adapters.push(new TraceConcatPlain(d.offset, inputs[pos]!, inputs[pos + 1]!, ropeOff));
230
+ pos += 2;
231
+ break;
232
+ case "q-cat":
233
+ adapters.push(new TraceConcatQuant(d.offset, d.groupSize, d.bits, triple(pos), triple(pos + 3), ropeOff));
234
+ pos += 6;
235
+ break;
236
+ case "p-ring":
237
+ adapters.push(new TraceRingPlain(d.offset, d.maxSize, inputs[pos]!, inputs[pos + 1]!, inputs[pos + 2]!, ropeOff));
238
+ pos += 3;
239
+ break;
240
+ case "q-ring":
241
+ adapters.push(new TraceRingQuant(d.offset, d.maxSize, d.groupSize, d.bits, triple(pos), triple(pos + 3), inputs[pos + 6]!, ropeOff));
242
+ pos += 7;
243
+ break;
244
+ }
245
+ }
246
+ setCompiledTrace(true);
247
+ try {
248
+ const ids = ops.reshape(inputs[0]!, [1, 1]);
249
+ const h = model.forwardHidden(ids, adapters as unknown as Cache[]);
250
+ ids.dispose();
251
+ const logits = model.logitsFromHidden(h);
252
+ h.dispose();
253
+ return [logits, ...adapters.flatMap((a) => a.outs)];
254
+ } finally {
255
+ setCompiledTrace(false);
256
+ }
257
+ };
258
+ }
259
+
260
+ // --- segmented mode (dense models) ------------------------------------------
261
+ // Measured (PLAN Phase A findings): the concat fetch materializes a copy
262
+ // of each growing cache's active window every step — per-op encode
263
+ // overhead + 2× byte traffic + allocator churn from monotonically
264
+ // growing transient sizes. Ring-phase caches (write-in-graph, read the
265
+ // updated buffer) cost nothing extra. So for models without KV sharing /
266
+ // per-layer inputs / MoE, the compiled graph is SEGMENTED at growing-
267
+ // cache layers: those layers run uncompiled (today's exact view-based
268
+ // ops — no copies, bit-exact trivially) and everything between them
269
+ // replays compiled. At steady state on the 12B that is 6 JS layers and
270
+ // 7 compiled segments per step.
271
+
272
+ /** Non-mutating phase check (segmented mode decides layer placement
273
+ * BEFORE any cache bookkeeping runs). Mirrors prepareDecodeStep's
274
+ * outcome: rotating caches go ring once the next write lands at or past
275
+ * the window; everything else grows. Under the perf kernel, quantized
276
+ * layers run as JS layers regardless — the CustomKernel primitive has
277
+ * no output_shapes and cannot live inside a compiled closure. */
278
+ function decodePhase(c: AnyCache): "concat" | "ring" {
279
+ if (perfKernelEnabled() && (c instanceof QuantizedKVCache || c instanceof RotatingQuantizedKVCache))
280
+ return "concat";
281
+ if (c instanceof RotatingKVCache || c instanceof RotatingQuantizedKVCache)
282
+ return c.offset + 1 < c.maxSize ? "concat" : "ring";
283
+ return "concat";
284
+ }
285
+
286
+ function disposeShared(s: SharedKv): void {
287
+ if (s.kind === "plain") {
288
+ s.keys.dispose();
289
+ s.values.dispose();
290
+ } else {
291
+ for (const t of [s.keys, s.values])
292
+ for (const a of [t.packed, t.scales, t.biases]) a.dispose();
293
+ }
294
+ }
295
+
296
+ /** Trace one compiled segment: layers [from, to) — all ring caches —
297
+ * with embed before (first) and finalNorm+logits after (last).
298
+ * Input layout: [idsOrH, ropeOffset, ...ring slots in layer order].
299
+ * Output layout: [hOrLogits, ...ring buffer updates]. */
300
+ function makeSegmentTraceFn(
301
+ model: Gemma4Model, descs: SlotDesc[], from: number, to: number,
302
+ first: boolean, last: boolean,
303
+ ) {
304
+ return (inputs: MlxArray[]): MlxArray[] => {
305
+ setCompiledTrace(true);
306
+ try {
307
+ const ropeOff = inputs[1]!;
308
+ let pos = 2;
309
+ const triple = (at: number): ops.QuantizedTensor => ({
310
+ packed: inputs[at]!, scales: inputs[at + 1]!, biases: inputs[at + 2]!,
311
+ });
312
+ const adapters: (TraceRingPlain | TraceRingQuant)[] = [];
313
+ for (const d of descs) {
314
+ if (d.kind === "p-ring") {
315
+ adapters.push(new TraceRingPlain(d.offset, d.maxSize, inputs[pos]!, inputs[pos + 1]!, inputs[pos + 2]!, ropeOff));
316
+ pos += 3;
317
+ } else {
318
+ adapters.push(new TraceRingQuant(d.offset, d.maxSize, d.groupSize, d.bits, triple(pos), triple(pos + 3), inputs[pos + 6]!, ropeOff));
319
+ pos += 7;
320
+ }
321
+ }
322
+
323
+ let h: MlxArray;
324
+ if (first) {
325
+ const ids = ops.reshape(inputs[0]!, [1, 1]);
326
+ const embedded = model.embed.encode(ids);
327
+ ids.dispose();
328
+ h = ops.mulScalar(embedded, model.embedScale);
329
+ embedded.dispose();
330
+ } else {
331
+ // owned alias of the carried hidden state (constant [1,1,H])
332
+ h = ops.reshape(inputs[0]!, inputs[0]!.shape);
333
+ }
334
+
335
+ for (let i = from; i < to; i++) {
336
+ const layer = model.layers[i]!;
337
+ const adapter = adapters[i - from]!;
338
+ const window = layer.layerType === "sliding_attention" ? model.windowSize : null;
339
+ const mask = adapter.makeMask(1, window); // N=1 → mode ""
340
+ const { h: next, shared } = layer.forward(h, mask, adapter as unknown as Cache, null, null);
341
+ h.dispose();
342
+ h = next;
343
+ disposeShared(shared);
344
+ }
345
+
346
+ let out = h;
347
+ if (last) {
348
+ const normed = model.finalNorm.forward(h);
349
+ h.dispose();
350
+ out = model.logitsFromHidden(normed);
351
+ normed.dispose();
352
+ }
353
+ return [out, ...adapters.flatMap((a) => a.outs)];
354
+ } finally {
355
+ setCompiledTrace(false);
356
+ }
357
+ };
358
+ }
359
+
360
+ // --- runner -----------------------------------------------------------------
361
+
362
+ const runners = new WeakMap<Gemma4Model, CompiledDecode>();
363
+
364
+ export class CompiledDecode {
365
+ /** Total compiled steps executed (tests assert the compiled path ran
366
+ * rather than silently falling back). */
367
+ static stepsExecuted = 0;
368
+ /** Total UNEXPECTED retraces observed (a shapeless closure tracing more
369
+ * than once). Must stay 0: a retrace means per-step JS graph builds
370
+ * sneaked back in, which both wrecks the perf win and signals shape
371
+ * drift. Surfaced loudly; tests assert on it. */
372
+ static unexpectedRetraces = 0;
373
+
374
+ #closures = new Map<string, CompiledFunction>();
375
+ /** Segmented mode: per layout key, one closure per segment (null for
376
+ * empty middle segments, which are identity). */
377
+ #segClosures = new Map<string, (CompiledFunction | null)[]>();
378
+ /** Closure keys whose apply failed (e.g. a primitive without
379
+ * output_shapes under shapeless replay): never re-trace these — the
380
+ * caller falls back to uncompiled without paying a trace per
381
+ * generation. */
382
+ #broken = new Set<string>();
383
+ /** Dense models (no KV sharing, per-layer inputs, or MoE) use the
384
+ * segmented form — growing-cache layers uncompiled, ring runs
385
+ * compiled. Models with cross-layer plumbing keep the whole-graph
386
+ * form (measured net-win on e4b; the concat copies it pays are the
387
+ * price of the sharing). */
388
+ readonly #segmented: boolean;
389
+
390
+ private constructor(readonly model: Gemma4Model) {
391
+ const t = model.config.text;
392
+ this.#segmented =
393
+ t.numKvSharedLayers === 0 && !t.enableMoeBlock && model.perLayerWidth === 0;
394
+ }
395
+
396
+ static for(model: Gemma4Model): CompiledDecode {
397
+ let r = runners.get(model);
398
+ if (!r) {
399
+ r = new CompiledDecode(model);
400
+ runners.set(model, r);
401
+ }
402
+ return r;
403
+ }
404
+
405
+ /** Compilable this step? (Cheap; checked per generation setup.) */
406
+ static supports(caches: Cache[]): boolean {
407
+ return caches.every(
408
+ (c) =>
409
+ c instanceof KVCache ||
410
+ c instanceof QuantizedKVCache ||
411
+ c instanceof RotatingKVCache ||
412
+ c instanceof RotatingQuantizedKVCache,
413
+ );
414
+ }
415
+
416
+ /** One decode step: consumes the pending token array (uint32 [1],
417
+ * unevaluated is fine), advances every cache by one position, returns
418
+ * the logits node plus the cache-update nodes that must ride the same
419
+ * async_eval. Throws on unsupported state — caller falls back to the
420
+ * uncompiled path (any growth already done is benign). */
421
+ step(cur: MlxArray, caches: Cache[]): { logits: MlxArray; evalWith: MlxArray[] } {
422
+ const offset0 = caches[0]!.offset;
423
+ for (const c of caches)
424
+ if (c.offset !== offset0)
425
+ throw new Error("compiled decode: cache offsets diverged");
426
+ return this.#segmented
427
+ ? this.#stepSegmented(cur, caches as AnyCache[], offset0)
428
+ : this.#stepWhole(cur, caches as AnyCache[], offset0);
429
+ }
430
+
431
+ #stepWhole(cur: MlxArray, anyCaches: AnyCache[], offset0: number): { logits: MlxArray; evalWith: MlxArray[] } {
432
+ const plans = anyCaches.map((c) => c.prepareDecodeStep());
433
+ const descs = anyCaches.map((c, i) => slotDesc(c, plans[i]!));
434
+ const key = closureKey(descs);
435
+ if (this.#broken.has(key)) throw new Error(`compiled decode: known-broken closure ${key}`);
436
+
437
+ // gather inputs — [cur, ropeOffset, ...per-cache slots]
438
+ const inputs: MlxArray[] = [cur];
439
+ const temps: MlxArray[] = [];
440
+ const ropeOff = ops.fromInt32([offset0], []);
441
+ temps.push(ropeOff);
442
+ inputs.push(ropeOff);
443
+ const writePosCache = new Map<number, MlxArray>();
444
+ const writePos = (v: number): MlxArray => {
445
+ let a = writePosCache.get(v);
446
+ if (!a) {
447
+ a = ops.fromInt32([v], [1]);
448
+ writePosCache.set(v, a);
449
+ temps.push(a);
450
+ }
451
+ return a;
452
+ };
453
+ const activeView = (a: MlxArray, len: number): MlxArray => {
454
+ const stop = [...a.shape];
455
+ stop[2] = len;
456
+ const view = a.slice(stop.map(() => 0), stop);
457
+ temps.push(view);
458
+ return view;
459
+ };
460
+ for (let i = 0; i < anyCaches.length; i++) {
461
+ const c = anyCaches[i]!;
462
+ const p = plans[i]!;
463
+ const d = descs[i]!;
464
+ if (d.kind === "p-cat") {
465
+ const pc = c as KVCache | RotatingKVCache;
466
+ inputs.push(activeView(pc.keys!, p.activeLen), activeView(pc.values!, p.activeLen));
467
+ } else if (d.kind === "q-cat") {
468
+ const qc = c as QuantizedKVCache | RotatingQuantizedKVCache;
469
+ for (const t of [qc.keys!, qc.values!])
470
+ inputs.push(
471
+ activeView(t.packed, p.activeLen),
472
+ activeView(t.scales, p.activeLen),
473
+ activeView(t.biases, p.activeLen),
474
+ );
475
+ } else if (d.kind === "p-ring") {
476
+ const rc = c as RotatingKVCache;
477
+ inputs.push(rc.keys!, rc.values!, writePos(p.writePos));
478
+ } else {
479
+ const rq = c as RotatingQuantizedKVCache;
480
+ inputs.push(
481
+ rq.keys!.packed, rq.keys!.scales, rq.keys!.biases,
482
+ rq.values!.packed, rq.values!.scales, rq.values!.biases,
483
+ writePos(p.writePos),
484
+ );
485
+ }
486
+ }
487
+
488
+ let closure = this.#closures.get(key);
489
+ if (!closure) {
490
+ closure = new CompiledFunction(makeTraceFn(this.model, descs), true);
491
+ this.#closures.set(key, closure);
492
+ }
493
+
494
+ let outs: MlxArray[];
495
+ try {
496
+ outs = closure.apply(inputs);
497
+ } catch (e) {
498
+ this.#broken.add(key);
499
+ throw e;
500
+ } finally {
501
+ for (const t of temps) t.dispose();
502
+ }
503
+ if (closure.traceCount > 1) {
504
+ CompiledDecode.unexpectedRetraces++;
505
+ console.warn(
506
+ `compiled decode: closure retraced (count=${closure.traceCount}) — ` +
507
+ `input ndim/dtype drifted for key ${key}`,
508
+ );
509
+ }
510
+
511
+ const logits = outs[0]!;
512
+ const evalWith: MlxArray[] = [];
513
+ let oi = 1;
514
+ for (let i = 0; i < anyCaches.length; i++) {
515
+ const c = anyCaches[i]!;
516
+ const d = descs[i]!;
517
+ if (d.kind === "p-cat") {
518
+ evalWith.push(...(c as KVCache | RotatingKVCache).writeDecodeStep(outs[oi]!, outs[oi + 1]!));
519
+ oi += 2;
520
+ } else if (d.kind === "q-cat") {
521
+ evalWith.push(...(c as QuantizedKVCache | RotatingQuantizedKVCache).writeDecodeStep(outs.slice(oi, oi + 6)));
522
+ oi += 6;
523
+ } else if (d.kind === "p-ring") {
524
+ // ring updates are ancestors of the logits (the in-graph fetch
525
+ // reads the updated buffer) — no explicit eval root needed
526
+ (c as RotatingKVCache).adoptDecodeStep(outs[oi]!, outs[oi + 1]!);
527
+ oi += 2;
528
+ } else {
529
+ (c as RotatingQuantizedKVCache).adoptDecodeStep(outs.slice(oi, oi + 6));
530
+ oi += 6;
531
+ }
532
+ }
533
+ CompiledDecode.stepsExecuted++;
534
+ return { logits, evalWith };
535
+ }
536
+
537
+ #stepSegmented(cur: MlxArray, anyCaches: AnyCache[], offset0: number): { logits: MlxArray; evalWith: MlxArray[] } {
538
+ const phases = anyCaches.map(decodePhase);
539
+ // ring caches need their host bookkeeping (growth/trim/rotation) now;
540
+ // growing caches keep it inside their layer's real updateAndFetch
541
+ const plans = anyCaches.map((c, i) =>
542
+ phases[i] === "ring" ? c.prepareDecodeStep() : null,
543
+ );
544
+
545
+ // layout: consecutive ring layers form compiled segments, each
546
+ // growing-cache layer runs uncompiled between them
547
+ const segs: { from: number; to: number }[] = [];
548
+ const jsLayers: number[] = [];
549
+ let runStart = 0;
550
+ for (let i = 0; i < anyCaches.length; i++) {
551
+ if (phases[i] === "concat") {
552
+ segs.push({ from: runStart, to: i });
553
+ jsLayers.push(i);
554
+ runStart = i + 1;
555
+ }
556
+ }
557
+ segs.push({ from: runStart, to: anyCaches.length });
558
+
559
+ // layout key: ring slot tags at their positions; js layers only by
560
+ // position (their quant params live outside the closures)
561
+ const key =
562
+ "seg|" +
563
+ anyCaches
564
+ .map((c, i) => (phases[i] === "ring" ? `${slotDesc(c, plans[i]!).kind}:${slotDesc(c, plans[i]!).groupSize}:${slotDesc(c, plans[i]!).bits}` : "js"))
565
+ .join(",") +
566
+ `|fd=${process.env.MLX_BUN_FUSED_DECODE === "1" ? 1 : 0}` +
567
+ `,nf=${process.env.MLX_BUN_NO_FUSED_SDPA === "1" ? 1 : 0}` +
568
+ `,pk=${perfKernelEnabled() ? 1 : 0}`;
569
+ if (this.#broken.has(key)) throw new Error(`compiled decode: known-broken closure ${key}`);
570
+
571
+ let closures = this.#segClosures.get(key);
572
+ if (!closures) {
573
+ closures = segs.map(() => null);
574
+ this.#segClosures.set(key, closures);
575
+ }
576
+
577
+ const ropeOff = ops.fromInt32([offset0], []);
578
+ const writePosCache = new Map<number, MlxArray>();
579
+ const stepTemps: MlxArray[] = [ropeOff];
580
+ const writePos = (v: number): MlxArray => {
581
+ let a = writePosCache.get(v);
582
+ if (!a) {
583
+ a = ops.fromInt32([v], [1]);
584
+ writePosCache.set(v, a);
585
+ stepTemps.push(a);
586
+ }
587
+ return a;
588
+ };
589
+
590
+ const evalWith: MlxArray[] = [];
591
+ let carried = cur; // seg 0 consumes the token array; later, hidden state
592
+ let carriedOwned = false; // cur belongs to the caller
593
+ try {
594
+ for (let s = 0; s < segs.length; s++) {
595
+ const { from, to } = segs[s]!;
596
+ const firstSeg = s === 0;
597
+ const lastSeg = s === segs.length - 1;
598
+ if (firstSeg || lastSeg || from < to) {
599
+ const descs = [];
600
+ const inputs: MlxArray[] = [carried, ropeOff];
601
+ for (let i = from; i < to; i++) {
602
+ const c = anyCaches[i]!;
603
+ const p = plans[i]!;
604
+ descs.push(slotDesc(c, p));
605
+ if (c instanceof RotatingQuantizedKVCache) {
606
+ inputs.push(
607
+ c.keys!.packed, c.keys!.scales, c.keys!.biases,
608
+ c.values!.packed, c.values!.scales, c.values!.biases,
609
+ writePos(p.writePos),
610
+ );
611
+ } else {
612
+ const rc = c as RotatingKVCache;
613
+ inputs.push(rc.keys!, rc.values!, writePos(p.writePos));
614
+ }
615
+ }
616
+ let closure = closures[s];
617
+ if (!closure) {
618
+ closure = new CompiledFunction(
619
+ makeSegmentTraceFn(this.model, descs, from, to, firstSeg, lastSeg),
620
+ true,
621
+ );
622
+ closures[s] = closure;
623
+ }
624
+ let outs: MlxArray[];
625
+ try {
626
+ outs = closure.apply(inputs);
627
+ } catch (e) {
628
+ this.#broken.add(key);
629
+ throw e;
630
+ }
631
+ if (closure.traceCount > 1) {
632
+ CompiledDecode.unexpectedRetraces++;
633
+ console.warn(`compiled decode: segment ${s} retraced for key ${key}`);
634
+ }
635
+ if (carriedOwned) carried.dispose();
636
+ carried = outs[0]!;
637
+ carriedOwned = true;
638
+ // ring updates are ancestors of the carried hidden state (the
639
+ // in-graph fetch reads the updated buffer): no eval roots
640
+ let oi = 1;
641
+ for (let i = from; i < to; i++) {
642
+ const c = anyCaches[i]!;
643
+ if (c instanceof RotatingQuantizedKVCache) {
644
+ c.adoptDecodeStep(outs.slice(oi, oi + 6));
645
+ oi += 6;
646
+ } else {
647
+ (c as RotatingKVCache).adoptDecodeStep(outs[oi]!, outs[oi + 1]!);
648
+ oi += 2;
649
+ }
650
+ }
651
+ }
652
+ if (s < jsLayers.length) {
653
+ const li = jsLayers[s]!;
654
+ const layer = this.model.layers[li]!;
655
+ const c = anyCaches[li]!;
656
+ const window = layer.layerType === "sliding_attention" ? this.model.windowSize : null;
657
+ const mask = c.makeMask(1, window); // N=1 → mode ""
658
+ const { h: next, shared } = layer.forward(carried, mask, c, null, null);
659
+ if (carriedOwned) carried.dispose();
660
+ carried = next;
661
+ carriedOwned = true;
662
+ disposeShared(shared);
663
+ mask.arr?.dispose();
664
+ }
665
+ }
666
+ } finally {
667
+ for (const t of stepTemps) t.dispose();
668
+ }
669
+ CompiledDecode.stepsExecuted++;
670
+ return { logits: carried, evalWith };
671
+ }
672
+
673
+ /** Free compiled closures (model unload). */
674
+ dispose(): void {
675
+ for (const c of this.#closures.values()) c.dispose();
676
+ this.#closures.clear();
677
+ for (const arr of this.#segClosures.values())
678
+ for (const c of arr) c?.dispose();
679
+ this.#segClosures.clear();
680
+ }
681
+ }