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.
- package/LICENSE +21 -0
- package/README.md +362 -0
- package/THIRD_PARTY_LICENSES.md +41 -0
- package/bin/mlx-bun.mjs +32 -0
- package/docs/curve-designer.html +303 -0
- package/docs/design/adapters-end-to-end.md +201 -0
- package/docs/design/compat-cli-surface-design.md +227 -0
- package/docs/design/docs-reorg-plan.md +152 -0
- package/docs/design/hlg-sampling.md +340 -0
- package/docs/design/memory-system.md +339 -0
- package/docs/design/optimization_plan.md +344 -0
- package/docs/design/parallel-slots.md +515 -0
- package/docs/design/runtime-isolation.md +247 -0
- package/docs/design/segmented-backward-training.md +508 -0
- package/docs/investigations/chunk-finetune-experiment.md +84 -0
- package/docs/investigations/curve-bisector-routing.md +123 -0
- package/docs/investigations/curve-runs/bisector-route.json +98 -0
- package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
- package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
- package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
- package/docs/investigations/curve-terrain.html +105 -0
- package/docs/investigations/expert-offload-single-user-moe.md +292 -0
- package/docs/investigations/hlg-ab.html +34 -0
- package/docs/investigations/hlg-explorer.html +238 -0
- package/docs/investigations/hlg-figs/cliff.svg +1 -0
- package/docs/investigations/hlg-figs/cross.svg +1 -0
- package/docs/investigations/hlg-figs/edges.svg +1 -0
- package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
- package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
- package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
- package/docs/investigations/hlg-figs/terrain.svg +1 -0
- package/docs/investigations/hlg-report.html +262 -0
- package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
- package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
- package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
- package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
- package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
- package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
- package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
- package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
- package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
- package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
- package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
- package/docs/investigations/hlg-runs/hlg-map.json +383 -0
- package/docs/investigations/hlg-runs/run0-original.log +159 -0
- package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
- package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
- package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
- package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
- package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
- package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
- package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
- package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
- package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
- package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
- package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
- package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
- package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
- package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
- package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
- package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
- package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
- package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
- package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
- package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
- package/docs/investigations/hlg-runs/runN-verify.log +116 -0
- package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
- package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
- package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
- package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
- package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
- package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
- package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
- package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
- package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
- package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
- package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
- package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
- package/docs/investigations/hlg-sampling-investigation.md +970 -0
- package/docs/investigations/lab-build-journal.md +157 -0
- package/docs/investigations/pi-builtin-investigation.md +308 -0
- package/docs/investigations/starter-model-port-handoff.md +323 -0
- package/docs/planning/IDEAS.md +26 -0
- package/docs/planning/PRODUCT_ROADMAP.md +426 -0
- package/docs/planning/ResearchTopics.md +119 -0
- package/docs/planning/chunk-task-roadmap.md +77 -0
- package/docs/planning/journal.md +155 -0
- package/docs/reference/distribution.md +121 -0
- package/docs/reference/embedding.md +168 -0
- package/docs/reference/library-api.md +153 -0
- package/docs/reference/server-api.md +224 -0
- package/docs/reference/server-config.md +243 -0
- package/docs/segmented-backward-handoff.md +148 -0
- package/package.json +68 -0
- package/src/anthropic.ts +491 -0
- package/src/chat-template.ts +186 -0
- package/src/cli.ts +1075 -0
- package/src/config.ts +229 -0
- package/src/curve-sampler.ts +122 -0
- package/src/dataset/generators.ts +1087 -0
- package/src/dataset/index.ts +12 -0
- package/src/dataset/job.ts +81 -0
- package/src/dataset/llm.ts +71 -0
- package/src/dataset/registry.ts +528 -0
- package/src/download.ts +283 -0
- package/src/eval/README.md +86 -0
- package/src/eval/kl-prompts.ts +29 -0
- package/src/eval/kl.ts +294 -0
- package/src/eval/quality-db.ts +92 -0
- package/src/eval/runner.ts +102 -0
- package/src/eval/score.ts +49 -0
- package/src/eval/tasks/bfcl.ts +500 -0
- package/src/eval/tasks/gsm8k.ts +85 -0
- package/src/eval/tasks/hashhop.ts +109 -0
- package/src/eval/tasks/humaneval.ts +252 -0
- package/src/eval/tasks/ifeval.ts +354 -0
- package/src/eval/tasks/mmlu.ts +145 -0
- package/src/evaldb.ts +199 -0
- package/src/expert-offload-build.ts +77 -0
- package/src/expert-offload.ts +61 -0
- package/src/expert-trace.ts +67 -0
- package/src/fit.ts +254 -0
- package/src/generate.ts +451 -0
- package/src/harness-pi.ts +177 -0
- package/src/hf-push.ts +628 -0
- package/src/index.ts +22 -0
- package/src/jobs/db.ts +139 -0
- package/src/jobs/index.ts +29 -0
- package/src/jobs/job-entry.ts +106 -0
- package/src/jobs/runner.ts +304 -0
- package/src/jobs/sse.ts +114 -0
- package/src/jobs/types.ts +58 -0
- package/src/kv-store.ts +185 -0
- package/src/lora.ts +314 -0
- package/src/md-modules.d.ts +7 -0
- package/src/mlx/array.ts +237 -0
- package/src/mlx/autograd.ts +267 -0
- package/src/mlx/checkpoint.ts +116 -0
- package/src/mlx/compile.ts +158 -0
- package/src/mlx/custom-vjp.ts +140 -0
- package/src/mlx/ffi.ts +433 -0
- package/src/mlx/metal-kernel.ts +130 -0
- package/src/mlx/ops.ts +707 -0
- package/src/mmap.ts +90 -0
- package/src/model/batched-mask.ts +204 -0
- package/src/model/batched-rotating.ts +300 -0
- package/src/model/compiled-decode.ts +681 -0
- package/src/model/factory.ts +27 -0
- package/src/model/fingerprint.ts +33 -0
- package/src/model/flash-attention.ts +674 -0
- package/src/model/fused-decode-kernel.ts +305 -0
- package/src/model/fused-geglu-kernel.ts +87 -0
- package/src/model/gemma4-base.ts +1616 -0
- package/src/model/gemma4.ts +1012 -0
- package/src/model/generated/gemma4-12b.ts +526 -0
- package/src/model/generated/gemma4-26b.ts +500 -0
- package/src/model/generated/gemma4-e4b.ts +757 -0
- package/src/model/generated/index.ts +18 -0
- package/src/model/minicpm5.ts +278 -0
- package/src/model/qwen3-delta.ts +224 -0
- package/src/model/qwen3_5.ts +429 -0
- package/src/model/support.ts +46 -0
- package/src/native-pack.ts +152 -0
- package/src/pi-provider.ts +95 -0
- package/src/pi-terminal.ts +264 -0
- package/src/pi-web.ts +924 -0
- package/src/preflight.ts +123 -0
- package/src/prompt-cache.ts +113 -0
- package/src/quantize/allocator.ts +627 -0
- package/src/quantize/calibration.ts +180 -0
- package/src/quantize/config-writer.ts +132 -0
- package/src/quantize/index.ts +57 -0
- package/src/quantize/job.ts +136 -0
- package/src/quantize/quantizer.ts +462 -0
- package/src/quantize/safetensors-writer.ts +171 -0
- package/src/quantize/sensitivity.ts +234 -0
- package/src/registry.ts +336 -0
- package/src/responses.ts +774 -0
- package/src/safetensors.ts +158 -0
- package/src/sampler.ts +517 -0
- package/src/serve/batch-scheduler.ts +362 -0
- package/src/serve/generation-gateway.ts +160 -0
- package/src/server.ts +1891 -0
- package/src/spec/drafter.ts +261 -0
- package/src/spec/generate.ts +221 -0
- package/src/status-page.html +497 -0
- package/src/tokenizer.ts +38 -0
- package/src/tool-call.ts +242 -0
- package/src/train/dataset.ts +461 -0
- package/src/train/export.ts +32 -0
- package/src/train/forward.ts +229 -0
- package/src/train/index.ts +43 -0
- package/src/train/job.ts +148 -0
- package/src/train/lora-params.ts +194 -0
- package/src/train/loss.ts +342 -0
- package/src/train/merge.ts +195 -0
- package/src/train/optimizer.ts +176 -0
- package/src/train/rank.ts +101 -0
- package/src/train/segmented.ts +520 -0
- package/src/train/trainer.ts +600 -0
- package/src/tui.ts +161 -0
- package/src/vision/embedder.ts +168 -0
- package/src/vision/preprocess.ts +247 -0
- package/src/vision/prompt.ts +158 -0
- package/src/vision/siglip.ts +599 -0
- package/src/web/app.html +2471 -0
- package/src/web/skills/web-research/SKILL.md +44 -0
- package/src/web/skills.ts +59 -0
- package/src/web-tools.ts +426 -0
- 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.
|