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,344 @@
1
+ # mlx-bun: gemma4.ts decompose & optimize — action plan
2
+
3
+ > This is the canonical **design/rationale** for the optimization work.
4
+ > Live **status/tracking** (checkboxes, findings) lives in PLAN.md under
5
+ > "Optimization plan Phase A–E".
6
+
7
+ **Premise (confirmed from the source read):** the current engine sits at
8
+ parity-to-ahead vs mlx-lm on the 12B *while still interpreting the decode
9
+ graph every token through hundreds of `bun:ffi` crossings, uncompiled.* The
10
+ largest remaining cost is host-side graph rebuilding that is identical
11
+ token-to-token — exactly what `mx.compile` exists to erase. Specialization and
12
+ custom kernels are real but smaller, and both are only *measurable* once the
13
+ host overhead is gone.
14
+
15
+ **Therefore the ordering is non-negotiable:** compile first (the big lever),
16
+ then decompose + specialize (enables the kernel and cleans the graph), then the
17
+ per-config fused kernel (the 2–3× on top). Doing it in any other order means
18
+ optimizing against a baseline that compile would have changed anyway.
19
+
20
+ **Invariant for the entire plan:** the compat-mode bit-exact parity gate
21
+ (`toBe(0)` logits, identical greedy trajectories vs the oracle venv) stays green
22
+ at *every* checkpoint. It is the instrument that makes refactoring safe to do
23
+ fast — any step that changes behavior turns it red the same hour. No step
24
+ proceeds with a red gate. No tolerance is ever added to make a refactor pass;
25
+ divergence is root-caused, never papered over.
26
+
27
+ ---
28
+
29
+ ## Phase A — Compile the decode step (on the current monolith, before any split)
30
+
31
+ **Why first:** this is the 14×-class lever for *this* runtime specifically,
32
+ because mlx-bun's per-step cost is FFI-crossing-heavy in a way Python's
33
+ interpreter loop is not. It is independent of decomposition. Landing it first
34
+ means every later phase is measured against a compiled baseline, so you learn
35
+ exactly how much headroom specialization actually has left to chase instead of
36
+ chasing host overhead that compile already removed.
37
+
38
+ **Steps:**
39
+
40
+ 1. **Characterize before touching anything.** Run `scripts/decode-split.ts`
41
+ (the per-step wall-time splitter that already exists) on all three models at
42
+ @600 and @8k. Record GPU-blocked time vs JS-graph-build time per step. This
43
+ is the control measurement; everything in Phase A is judged against it. The
44
+ prediction: e4b shows the largest JS-graph-build fraction (it has the
45
+ per-layer-input slicing loop no other model runs).
46
+
47
+ 2. **Wire `mx.compile` around the single-token decode graph.** The unit to
48
+ compile is one `forward(tokens=[next], cache)` step: embed → forwardLayers →
49
+ finalNorm → logitsFromHidden → argmax. At fixed L=1 every op is shape-stable
50
+ token-to-token; only input values and cache offsets change. Use
51
+ `shapeless=True` semantics so the same compiled graph serves any decode step.
52
+ The cache update is the one stateful part — confirm the compiled region
53
+ either includes the sliceUpdate writes or is structured so the cache mutation
54
+ happens at stable shapes (see step 4).
55
+
56
+ 3. **Keep prefill uncompiled (or separately compiled).** Prefill is
57
+ variable-length (L = prompt size), so it does not share a compiled graph with
58
+ decode. Leave prefill on the current path initially; a separate
59
+ fixed-tile-size compiled prefill is a later option, not part of this phase.
60
+
61
+ 4. **Resolve the cache-mutation/compile boundary.** The quantized caches do
62
+ `quantize → sliceUpdate(ring) → activeSlices` per step (≈670 FFI crossings
63
+ per token on the 12B across 48 layers). Decide whether these live inside the
64
+ compiled graph (preferred — collapses the crossings into one replay) or
65
+ immediately outside it. The ring-index bookkeeping is host-side integer math
66
+ and stays in JS; only the array ops compile.
67
+
68
+ 5. **Parity gate.** Compiled decode must produce bit-exact identical token
69
+ trajectories to the uncompiled path on the fixed prompt suite. Compile must
70
+ not change numerics — if it does, a fusion inside the compiled graph reordered
71
+ an op; root-cause it.
72
+
73
+ 6. **Measure.** Re-run `decode-split.ts`. Expected: JS-graph-build fraction
74
+ collapses, e4b ~5% residual shrinks or closes, 12B short-context moves
75
+ further ahead. Record the delta — this number tells you how much the rest of
76
+ the plan is even worth.
77
+
78
+ 7. **Re-run the fused-decode A/B *after* compile, not before.**
79
+ `MLX_BUN_FUSED_DECODE` measured ~3% *worse* uncompiled, almost certainly
80
+ because the tile loop's per-iteration host overhead dominates at L=1. Compiled,
81
+ that host cost vanishes and the flag may flip to a win with no kernel written.
82
+ This is a free re-measurement that may close part of the gap on its own.
83
+
84
+ **Phase A exit:** compiled decode green on parity, measured speedup recorded per
85
+ model, fused-decode A/B re-evaluated. Decision point: how much headroom remains?
86
+ That answer scopes Phases D–E.
87
+
88
+ ---
89
+
90
+ ## Phase B — Extract a shared, config-independent base
91
+
92
+ **Why:** before generating per-model files, separate the machinery that is
93
+ genuinely identical across all architectures from the parts that vary. This is
94
+ mechanical, parity-preserving code movement — no behavior change, gate stays
95
+ green throughout.
96
+
97
+ **Moves into `gemma4-base.ts` (or `src/model/gemma4/base.ts`):**
98
+
99
+ - Cache classes: `KVCache`, `QuantizedKVCache`, `RotatingKVCache`,
100
+ `RotatingQuantizedKVCache` — these are architecture-independent storage; which
101
+ *combination* a model uses varies, but the classes themselves don't.
102
+ - SDPA functions: `quantizedSdpaUnfused`, `quantizedSdpaTiled`,
103
+ `fusedSdpaSupported`, `quantizedSdpa`, plus `ops.sdpa` plain dispatch.
104
+ - Mask builders: `createCausalMask`, `bidirMask`, the `Mask`/`MaskMode` types.
105
+ - Primitives: `RMSNorm`, `QuantizedLinear`, `QuantizedEmbedding`,
106
+ `QuantizedSwitchLinear` and the LoRA machinery (`LoraWeights`, `LoraState`).
107
+ - Helpers: `disposing`, `mapTriple`, `disposeTriple`, `logitSoftcap`,
108
+ `argmaxLastPosition`, `lastPositionLogits`, `FINFO_MIN`.
109
+
110
+ **Stays model-specific (does NOT move to base):** `Attention`, `MLP`, MoE
111
+ (`Router`/`Experts`/`SwitchGLU`), `DecoderLayer`, `Gemma4Model.forwardLayers`,
112
+ `computePerLayerInputs` — these branch on architecture and become the
113
+ per-model generated code.
114
+
115
+ **Steps:**
116
+
117
+ 1. Create the base module, move the listed classes verbatim, fix imports.
118
+ 2. Run the full parity suite + all behavioral tests. Green = the move was
119
+ behavior-preserving. This is the only acceptance criterion for Phase B.
120
+ 3. No optimization in this phase. Pure extraction. Resist the urge to "improve
121
+ while moving" — that conflates a refactor bug with an optimization bug.
122
+
123
+ **Phase B exit:** base module extracted, every test green, monolith now contains
124
+ only the model-specific assembly that Phase C will generate.
125
+
126
+ ---
127
+
128
+ ## Phase C — Generate three per-architecture files (not author — generate)
129
+
130
+ **Reframe from the original request:** the per-model files are *outputs of a
131
+ generator*, not hand-written variants. Loop unrolling and branch elimination are
132
+ mechanical transforms; a codegen script performs them from each model's config.
133
+ Three files = three generator outputs. When the base algorithm changes, you
134
+ regenerate, not hand-edit three copies.
135
+
136
+ **The three targets (your actual runnable set):**
137
+
138
+ - `gemma4-12b.ts` — dense. Full-attention `KVCache` (or quantized variant),
139
+ GQA, no per-layer-input, no MoE, no KV-sharing. The simplest graph.
140
+ - `gemma4-e4b.ts` — the complex one. Per-layer-input gating, KV-sharing
141
+ (donor/sharer layers), sliding/rotating quantized cache. This is where the
142
+ per-step host overhead concentrated, so it's where specialization pays most.
143
+ - `gemma4-26b.ts` — MoE. Router + experts (`SwitchGLU`/`gather_qmm`), parallel
144
+ dense+routed branches.
145
+
146
+ **The generator (`scripts/gen-model.ts`):** reads `config.json` +
147
+ `kv_config.json` for a model and emits the specialized file. What it eliminates,
148
+ all resolvable at generation time from known constants:
149
+
150
+ - The per-layer `instanceof` cache-type check (gemma4.ts:1327) — each layer's
151
+ cache type is known from `layer_types`, so the generated layer calls the right
152
+ fetch directly, no runtime `instanceof`.
153
+ - The `if (this.router)` / `if (perLayerGate)` / KV-sharing forks in
154
+ `DecoderLayer.forward` and `forwardLayers` — each model takes exactly one
155
+ branch; the others are deleted from its file.
156
+ - The `forwardLayers` layer loop — unrolled into straight-line per-layer calls
157
+ with each layer's constants (type, cache index, donor index, sliding/full)
158
+ baked in. (Note: post-compile this is largely cosmetic for *speed* — compile
159
+ already erased the per-step cost — but it produces a cleaner compiled graph
160
+ and removes the `intermediates`/`previousKvs`/`cacheIndex` indirection arrays
161
+ and their per-step lookups.)
162
+ - The per-layer-input `slice`+`reshape` loop (gemma4.ts:1865) on non-e4b models:
163
+ deleted entirely (they don't have it). On e4b: the slice bounds are constant
164
+ per layer, so they're baked.
165
+
166
+ **Steps:**
167
+
168
+ 1. Write `gen-model.ts`: input = model dir (config + kv_config), output = one
169
+ `.ts` file importing from `gemma4-base.ts`. Start with the 12B (simplest) to
170
+ prove the generator end-to-end.
171
+ 2. Generated 12B must pass parity bit-exact against the oracle. This proves the
172
+ generator preserves behavior — the gate is the generator's test.
173
+ 3. Generate e4b and 26B. Each passes parity. The KV-sharing donor/sharer wiring
174
+ in e4b is the trickiest to generate correctly; its parity test is the proof.
175
+ 4. Dispatch: model load picks the generated module by config fingerprint
176
+ (arch + kv_config hash), with the **monolithic `gemma4.ts` retained as the
177
+ general fallback** for any model without a generated specialization. Nothing
178
+ is deleted; specialized files are opportunistic overrides.
179
+
180
+ **Phase C exit:** three generated, branchless, unrolled per-model files, each
181
+ bit-exact, dispatched by fingerprint, with the monolith as fallback. Measure
182
+ each vs its Phase-A compiled baseline — expect small wins (cleaner graph, fewer
183
+ indirection lookups), not large ones. The large one was compile.
184
+
185
+ ---
186
+
187
+ ## Phase D — Per-config kernel specialization (kv_config-driven)
188
+
189
+ **Why:** now that each model has its own file and the cache/SDPA dispatch is
190
+ already branch-free per model, bake the *quantization* constants. The
191
+ kv_config.json's per-layer (bits, group_size) becomes compile-time information
192
+ in the generated SDPA and cache calls.
193
+
194
+ **What gets specialized, from the 12B's kv_config as the worked example**
195
+ (42 layers @ 4-bit/gs64, 6 layers @ 8-bit/gs64, group_size uniformly 64):
196
+
197
+ - **Per-layer bit-width resolved at generation, not per-step.** The generated
198
+ layer for index 6 calls the 8-bit SDPA path; index 0 calls the 4-bit path.
199
+ No runtime bit-width parameter, no `fusedSdpaSupported` bit check — the
200
+ generator already knows.
201
+ - **group_size folded as a compile-time constant** (64 everywhere in this
202
+ config) — `dim / group_size`, `32 / bits` (elements-per-int) become literals.
203
+ - **nRep (GQA ratio) baked** — the `nRep > 1` branch and the `expandDims`
204
+ broadcast become a constant-folded reshape with the literal ratio.
205
+ - **head_dim baked** — sliding (256) vs full-attention (512) head dims are known
206
+ per layer.
207
+
208
+ **Important honesty:** Phase D is specialization of the *existing* op-graph
209
+ (constant-folding, branch deletion), still using `ops.quantizedMatmulQT` etc.
210
+ It is NOT yet a custom kernel. Expected gain is modest — it removes parameter
211
+ passing and branches the compiled graph would partly handle anyway. Its real job
212
+ is to make Phase E's custom kernel *possible* by having a single known
213
+ (bits, group_size, nRep, head_dim) per dispatch site.
214
+
215
+ **Steps:**
216
+
217
+ 1. Extend `gen-model.ts` to read kv_config and emit per-layer-specialized SDPA
218
+ dispatch (right bit-width path per layer, constants folded).
219
+ 2. Parity gate per generated model. Bit-exact required — constant-folding must
220
+ not change numerics.
221
+ 3. Measure vs Phase C. Record the (likely small) delta. If negligible, that's a
222
+ *finding*, not a failure — it means compile already captured it, and it tells
223
+ you the kernel in Phase E is the only remaining lever.
224
+
225
+ **Phase D exit:** each generated model has branch-free, constant-folded,
226
+ per-layer-specialized quant dispatch. Every dispatch site now has a single known
227
+ shape — the precondition for Phase E.
228
+
229
+ ---
230
+
231
+ ## Phase E — Per-config fused decode-SDPA kernel (the 2–3×, last)
232
+
233
+ **Why last:** this is the custom Metal kernel, the fun part, and it's the
234
+ *smallest* lever (the search and the turboquant retro both say the fused kernel
235
+ was 2–3× while the buffer/host path was the big win — which you've already
236
+ captured in Phases A and D). Doing it last means you measure its true
237
+ contribution against a compiled, specialized baseline instead of attributing
238
+ host-overhead noise to it.
239
+
240
+ **What the kernel does** (the mlx-qsdpa shape, specialized per bit-width):
241
+
242
+ - Decode-time (L=1) fused attention: stream the quantized KV through QK^T and
243
+ ×V in one Metal dispatch, dequantizing inline, **never materializing** the
244
+ `[B,H,1,N]` scores row or the dequantized cache.
245
+ - **nRep baked in** — no `expandDims` GQA broadcast (gemma4.ts:916); the kernel
246
+ loops the query-head group internally with the literal ratio.
247
+ - **bits/group_size as compile-time template parameters** — generate the 4-bit
248
+ variant and the 8-bit variant; dispatch the right one per layer from the
249
+ config (Phase D already resolved which).
250
+ - Online-softmax fused (score → max → exp → ×V accumulate) so the scores never
251
+ hit memory.
252
+
253
+ **Mechanism:** `mx.fast.metal_kernel` (the intended extension point — no MLX
254
+ fork). Lift the *dequant unpacking logic* from a known-correct reference
255
+ (mlx-qsdpa is open, `_BaseCache`-compatible, ~200 lines) — the bit-unpacking and
256
+ scale/zero-point application is the well-trodden, copyable part. The novel part
257
+ is the *fusion boundary* (not writing the intermediate) and the *Bun dispatch*
258
+ (every existing impl dispatches from Python; a Bun-dispatched fused quant-SDPA
259
+ is genuinely unoccupied territory).
260
+
261
+ **Correctness regime — this is where the gate model changes:**
262
+
263
+ - A fused kernel that's faster will **not** be bit-exact with the unfused path
264
+ (online softmax ≠ one-shot softmax in bf16 — this is already documented in the
265
+ tiled path's "tier b" note). So the compat bit-exact gate does NOT apply to
266
+ the kernel output directly.
267
+ - Instead: the kernel runs under the **perf-mode flag** and is gated against the
268
+ **frozen ground-truth snapshot** (capture today's compat-mode logits/
269
+ trajectories on the fixed prompt suite *before* starting Phase E — that's the
270
+ quality oracle once bit-exact-vs-optiq no longer applies).
271
+ - Perf-mode gate = bounded logit tolerance + token-trajectory agreement above a
272
+ set threshold, *labeled with the measured delta* (e.g. "+X% tok/s, −Y% on the
273
+ eval set"), the way a quant ships with its perplexity hit. "No guarantee of
274
+ identical output" means **bounded and measured**, never unchecked.
275
+ - Compat mode (bit-exact, unfused) stays in the codebase permanently as the
276
+ `-O0` reference and differential-testing oracle: when the kernel misbehaves,
277
+ flip to compat, same prompt, and the diff localizes kernel-vs-expectation.
278
+
279
+ **Steps:**
280
+
281
+ 1. **Freeze the ground-truth snapshot** (compat-mode logits + trajectories,
282
+ fixed prompt suite, all three models) before writing any kernel. This is the
283
+ perf-mode oracle.
284
+ 2. **Metal capture one decode step** (`mlx_metal_start_capture`) on the
285
+ compiled+specialized 12B to confirm the prize size: is the dequantized-cache
286
+ round-trip still fat after compile, or did fusion upstream already thin it?
287
+ If thin, the kernel ceiling is low — record and decide. If fat, proceed.
288
+ 3. **Toolchain derisk first:** get a trivial `mx.fast.metal_kernel` compiling,
289
+ dispatched from Bun, running, verified — *before* the GQA numerics. (RMSNorm
290
+ or a no-op fusion proves the `.metal` → mlx-c → `bun:ffi` pipeline end-to-end
291
+ so the real kernel debugs numerics OR plumbing, not both at once.)
292
+ 4. **Generate the 4-bit fused decode kernel** for the 42 four-bit layers (where
293
+ 42/48 of KV traffic lives and 4-bit's dequant round-trip is most lopsided
294
+ against compute). nRep, group_size baked.
295
+ 5. **A/B vs compiled+specialized baseline**, paired, clean machine, at @8k (where
296
+ KV traffic dominates and fusion shows). Isolate the kernel's contribution.
297
+ 6. **Decide on the 8-bit six.** 8-bit dequant is cheaper and the data fatter, so
298
+ the fusion win per-byte is smaller — possibly not worth a second kernel; stock
299
+ MLX path may stay for those layers. Measure before committing kernel #2.
300
+ 7. **Per-model:** repeat the generation for e4b and 26B configs only if the 12B
301
+ kernel shows a real win. The generator (Phase C/D) already produces the
302
+ dispatch; the kernel is one more generated, per-bit-width artifact keyed by
303
+ fingerprint, each with its own auto-generated parity/quality test.
304
+
305
+ **Phase E exit:** per-config fused decode kernel(s), perf-mode flag, gated
306
+ against the frozen quality oracle with the trade *labeled*, compat mode retained
307
+ as permanent reference. Measured isolated contribution recorded.
308
+
309
+ ---
310
+
311
+ ## Cross-cutting discipline (applies to every phase)
312
+
313
+ - **Parity gate green at every checkpoint.** Compat bit-exact for Phases A–D;
314
+ frozen-snapshot quality tolerance for Phase E's perf mode. Never add a
315
+ tolerance to make a refactor pass — root-cause divergence.
316
+ - **One flag, one measurement per lever.** Compile, fused-decode, each
317
+ specialization, the kernel — each is independently togglable and independently
318
+ measured ("run it both ways, compare"), so you always know what each bought.
319
+ Never measure two new levers tangled together.
320
+ - **Clean machine or it didn't happen.** Reboot + preflight-gate every quotable
321
+ benchmark; `sudo purge` for cold-start rows.
322
+ - **Losing experiments become documented default-off flags, never deletions.**
323
+ - **Nothing deleted, fallback always present.** The monolith stays as the
324
+ general fallback; specialized/generated files are opportunistic overrides
325
+ dispatched by config fingerprint. An unhandled config runs slow, never broken.
326
+ - **Generator over hand-edits.** Per-model files are regenerated from config,
327
+ not maintained by hand. A base-algorithm fix is one regeneration, not three
328
+ edits. Each generated artifact ships with its generated parity test.
329
+
330
+ ## Expected outcome, honestly stated
331
+
332
+ - **Phase A (compile):** the big win. Likely closes the e4b ~5% residual and
333
+ pushes the 12B further ahead. This is the lever that matters most and it's
334
+ pure host-side — no kernel, no Metal.
335
+ - **Phases C–D (decompose + specialize):** small wins (cleaner graph, deleted
336
+ branches/indirection), main value is enabling Phase E and improving
337
+ maintainability via codegen.
338
+ - **Phase E (kernel):** the 2–3× on the attention hot path, biggest at long
339
+ context, smallest lever of the three but the one with no upstream Bun-dispatched
340
+ equivalent — the genuinely novel contribution.
341
+
342
+ The sequence front-loads the largest, cheapest win (compile) and ends with the
343
+ most fun, most novel, smallest one (kernel), with every step gated by the oracle
344
+ that's kept the project honest from the start.