mlx-bun 0.0.8 → 0.0.10

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 (144) hide show
  1. package/README.md +112 -20
  2. package/docs/design/batching-perf-path.md +232 -0
  3. package/docs/design/batching-v2-plan.md +515 -0
  4. package/docs/design/bucketing-stage.md +196 -0
  5. package/docs/design/compat-cli-surface-design.md +11 -11
  6. package/docs/design/docs-reorg-plan.md +1 -1
  7. package/docs/design/dreaming-nightly-pipeline.md +291 -0
  8. package/docs/design/dspark-speculative-decoding.md +106 -0
  9. package/docs/design/generic-model-support.md +381 -0
  10. package/docs/design/memory-inference-path.md +422 -0
  11. package/docs/design/memory-synthesis.md +287 -0
  12. package/docs/design/minicpm5-decode-megakernel.md +484 -0
  13. package/docs/design/mlx-lm-tool-parity-plan.md +535 -0
  14. package/docs/design/omlx-adoption-map.md +90 -0
  15. package/docs/design/orpo-dynamic-lambda.md +41 -0
  16. package/docs/design/orpo-training.md +35 -5
  17. package/docs/design/parallel-slots.md +12 -1
  18. package/docs/design/parity-tier-dag.md +13 -5
  19. package/docs/design/segmented-backward-training.md +15 -0
  20. package/docs/design/spec-decode-larger-targets.md +229 -0
  21. package/docs/design/ssd-kv-cold-tier.md +158 -0
  22. package/docs/design/tauri-desktop-app.md +2 -2
  23. package/docs/design/the-dreaming-handoff.md +141 -0
  24. package/docs/design/the-dreaming-master-plan.md +446 -0
  25. package/docs/design/write-pipeline-entity.md +468 -0
  26. package/docs/investigations/curve-bisector-routing.md +1 -1
  27. package/docs/investigations/decode-roofline-lookagain.md +348 -0
  28. package/docs/investigations/dspark-handoff.md +58 -0
  29. package/docs/investigations/kernel-perf-review-2026-07.md +171 -0
  30. package/docs/planning/PRODUCT_ROADMAP.md +2 -2
  31. package/docs/planning/curve-sampler-research-plan.md +331 -0
  32. package/docs/planning/memory-docs-and-dag-plan.md +185 -0
  33. package/docs/planning/pi-integration-review.md +178 -0
  34. package/docs/planning/release-notes-v0.0.10.md +57 -0
  35. package/docs/planning/release-notes-v0.0.9.md +122 -0
  36. package/docs/planning/web-ui-pass-plan.md +213 -0
  37. package/docs/planning/website-readme-pass-plan.md +224 -0
  38. package/docs/reference/cli.md +255 -0
  39. package/docs/reference/embedding.md +7 -2
  40. package/docs/reference/library-api.md +22 -3
  41. package/docs/reference/memory.md +5 -5
  42. package/docs/reference/models.md +137 -0
  43. package/docs/reference/orpo-quickstart.md +6 -0
  44. package/docs/reference/server-api.md +187 -9
  45. package/docs/reference/server-config.md +43 -9
  46. package/docs/reference/training.md +31 -12
  47. package/package.json +1 -1
  48. package/src/anthropic.ts +22 -0
  49. package/src/assets/curve-designer.html +1 -1
  50. package/src/chat-template.ts +25 -7
  51. package/src/cli.ts +1077 -90
  52. package/src/config.ts +4 -1
  53. package/src/download.ts +99 -8
  54. package/src/embed.ts +77 -0
  55. package/src/eval/perplexity.ts +164 -0
  56. package/src/eval/runner.ts +15 -6
  57. package/src/eval/tasks/bfcl.ts +1 -1
  58. package/src/generate.ts +149 -18
  59. package/src/harness-pi.ts +43 -17
  60. package/src/index.ts +2 -0
  61. package/src/kv-store.ts +240 -98
  62. package/src/lora.ts +2 -2
  63. package/src/memory/article.ts +558 -0
  64. package/src/memory/chunk-validation.ts +150 -0
  65. package/src/memory/chunk.ts +305 -12
  66. package/src/memory/cluster.ts +344 -17
  67. package/src/memory/coverage.ts +134 -0
  68. package/src/memory/crosslink.ts +0 -0
  69. package/src/memory/db.ts +184 -45
  70. package/src/memory/entity.ts +203 -0
  71. package/src/memory/gate.ts +187 -0
  72. package/src/memory/history.ts +152 -0
  73. package/src/memory/ingest.ts +324 -11
  74. package/src/memory/model.ts +321 -0
  75. package/src/memory/normalize.ts +671 -0
  76. package/src/memory/parse.ts +22 -0
  77. package/src/memory/pipeline.ts +242 -30
  78. package/src/memory/prompts.ts +42 -0
  79. package/src/memory/query.ts +334 -0
  80. package/src/memory/reconcile.ts +734 -0
  81. package/src/memory/redirect.ts +108 -0
  82. package/src/memory/reindex.ts +325 -0
  83. package/src/memory/resolve.ts +307 -0
  84. package/src/memory/route.ts +272 -0
  85. package/src/memory/schedule.ts +6 -4
  86. package/src/memory/stages.ts +774 -0
  87. package/src/memory/synthesize.ts +1629 -20
  88. package/src/memory/tools.ts +446 -96
  89. package/src/memory/vault.ts +462 -35
  90. package/src/memory/wikify.ts +1057 -16
  91. package/src/mlx/array.ts +21 -0
  92. package/src/mlx/ffi.ts +3 -0
  93. package/src/mlx/ops.ts +72 -0
  94. package/src/model/compiled-decode.ts +43 -8
  95. package/src/model/factory.ts +29 -8
  96. package/src/model/flash-attention.ts +4 -2
  97. package/src/model/fused-decode-kernel.ts +14 -7
  98. package/src/model/fused-mlp-kernel.ts +169 -0
  99. package/src/model/fused-swiglu-kernel.ts +240 -0
  100. package/src/model/gemma4-base.ts +96 -7
  101. package/src/model/gemma4.ts +33 -2
  102. package/src/model/minicpm5.ts +106 -8
  103. package/src/model/qwen3-delta.ts +48 -1
  104. package/src/model/qwen3.ts +276 -0
  105. package/src/model/qwen3_5.ts +11 -2
  106. package/src/model/steel-linear-kernel.ts +96 -0
  107. package/src/model/support.ts +38 -8
  108. package/src/model/universal/archs.ts +332 -0
  109. package/src/model/universal/dense.ts +576 -0
  110. package/src/model/universal/modules.ts +163 -0
  111. package/src/model/universal/rope.ts +366 -0
  112. package/src/pi-provider.ts +1 -1
  113. package/src/pi-terminal.ts +29 -8
  114. package/src/pi-web.ts +19 -2
  115. package/src/prompt-cache.ts +55 -9
  116. package/src/quantize/config-writer.ts +1 -1
  117. package/src/registry.ts +219 -8
  118. package/src/responses.ts +23 -0
  119. package/src/sampler.ts +220 -9
  120. package/src/serve/batch-scheduler.ts +307 -83
  121. package/src/serve/generation-gateway.ts +156 -29
  122. package/src/server.ts +746 -73
  123. package/src/spec/drafter.ts +81 -49
  124. package/src/spec/dspark/data-dflash.ts +156 -0
  125. package/src/spec/dspark/data.ts +201 -0
  126. package/src/spec/dspark/generate-dflash.ts +193 -0
  127. package/src/spec/dspark/generate.ts +290 -0
  128. package/src/spec/dspark/loss.ts +213 -0
  129. package/src/spec/dspark/module-dflash.ts +361 -0
  130. package/src/spec/dspark/module.ts +540 -0
  131. package/src/spec/dspark/sample.ts +112 -0
  132. package/src/spec/generate.ts +40 -19
  133. package/src/ssd-cache.ts +213 -0
  134. package/src/status-page.html +1 -1
  135. package/src/tokenizer.ts +7 -0
  136. package/src/train/flash-cce.ts +81 -23
  137. package/src/train/fuse.ts +243 -0
  138. package/src/train/index.ts +3 -0
  139. package/src/train/job.ts +8 -0
  140. package/src/train/loss.ts +341 -40
  141. package/src/train/prefix-shared.ts +62 -11
  142. package/src/train/segmented.ts +215 -55
  143. package/src/train/trainer.ts +112 -38
  144. package/src/web/skills/memory/SKILL.md +40 -17
@@ -0,0 +1,484 @@
1
+ # MiniCPM5 Decode Megakernel — end-to-end build plan (handoff)
2
+
3
+ **Read this top to bottom, then execute the phases in order. Each phase has an
4
+ exact validation gate; do not advance until it passes. At the end you have one
5
+ resident Metal kernel that computes the entire decode forward (all 24 layers →
6
+ logits) in a single dispatch, never handing MLX's planner a graph to mis-plan.**
7
+
8
+ This document is self-contained. You should not need to ask anything to execute it.
9
+
10
+ ---
11
+
12
+ ## 0. Why this exists (the problem, settled)
13
+
14
+ The inference forward composes per-op MLX nodes (`quantized_matmul`, `sdpa`, …).
15
+ We tried to insert our own fused `fast.metal_kernel`s (steel `BlockMMA` GEMM) into
16
+ that forward to fuse+speed it up. **Confirmed dead end:** MLX's lazy buffer planner
17
+ corrupts *chained* `BlockMMA` custom kernels (nondeterministic NaN/garbage/crash) —
18
+ the pointwise `fused-gelu` chains fine, ours do not, and ≥2 of ours in one lazy
19
+ graph corrupt. The only robust fixes are (a) a per-kernel eval boundary (24 syncs/
20
+ forward → net **slower**, 0.5–0.8×), or (b) **don't give the planner anything to
21
+ chain: one resident kernel for the whole forward.** This plan builds (b).
22
+
23
+ The training path already proved the shape: its forward is MLX ops (which chain
24
+ fine) + **one** terminal fused kernel (flash-CCE loss), `detachLeaf` at coarse
25
+ segment boundaries (`src/train/segmented.ts`). Our megakernel is the inference
26
+ analogue — but because inference has no terminal heavy-reduction, the "one kernel"
27
+ must be the *whole forward*, resident, coordinating its own stages.
28
+
29
+ **Scope = DECODE (single new token, M=1).** This is the only case physically
30
+ expressible as one dispatch: a single Metal dispatch cannot barrier across
31
+ threadgroups, so cross-token prefill attention can't be one kernel — but one token
32
+ attending to the *cached* KV has no cross-token dependency. Decode is also the
33
+ latency path that *is* serving speed (`[[serving-speed-is-the-user-metric]]`).
34
+ Prefill stays on the existing (correct, fast, parity) MLX-ops path.
35
+
36
+ **Honest perf expectation:** decode is weight-bandwidth-bound (reading ~1 GB of
37
+ 4/8-bit weights per token is the floor for any method). The megakernel's win is
38
+ (1) eliminating ~240 per-op dispatch launches/token, (2) no intermediate device
39
+ round-trips (hidden stays resident), (3) zero MLX-planner involvement. Expect a
40
+ modest but real decode-latency win (overhead elimination), not a bandwidth
41
+ miracle. The *architectural* win (no corruption, fully ours) is the point.
42
+
43
+ ---
44
+
45
+ ## 1. Success criteria (definition of done)
46
+
47
+ 1. A function `decodeMegakernel(model, tokenId, position) → logits[V]` that runs the
48
+ entire MiniCPM5 decode forward in **one** `MetalKernel.apply` dispatch.
49
+ 2. **Correctness gate (L1, bf16 KV):** teacher-forced greedy trajectory matches
50
+ `goldens/minicpm5-parity.json` — argmax agreement **≥ 98/100**, mean per-step
51
+ logit maxDiff small (this is an L3 path: ~1–2 bf16 ULP/op accumulating; it will
52
+ NOT be bit-exact, gate by teacher-forced agreement + KL like
53
+ `tests/perf-kernel-oracle.test.ts`, threshold ≥ 56/64-style). DETERMINISTIC
54
+ across runs (no nondeterminism — that was the corruption signature).
55
+ 3. **Perf gate:** decode tok/s ≥ the serial baseline on a paired same-process A/B
56
+ (`scripts/experiments/` style). Not slower. (Stretch: measurably faster.)
57
+ 4. Default **OFF** behind `MLX_BUN_MEGAKERNEL=1`; the existing forward untouched;
58
+ `bun test tests/minicpm5-*.test.ts` stays green with it off.
59
+ 5. Wired into the real decode loop (`src/generate.ts`) for `--megakernel` / env.
60
+
61
+ ---
62
+
63
+ ## 2. Hard facts (MiniCPM5-1B-OptiQ-4bit) — do not re-derive
64
+
65
+ **Snapshot:** `/Users/joshrossi/.cache/huggingface/hub/models--mlx-community--MiniCPM5-1B-OptiQ-4bit/snapshots/664aabaed233c653f82716d8dc822234d0091f78`
66
+
67
+ **Config:** `hidden_size H=1536`, `num_hidden_layers=24`, `num_attention_heads=16`,
68
+ `num_key_value_heads=2` (GQA, `nRep=8`), `head_dim=128`, `intermediate_size I=4608`,
69
+ `vocab_size V=130560`, `rms_norm_eps=1e-6`, **`rope_theta=5_000_000`** (NOT 10000 —
70
+ the code reads `t.ropeParameters.full_attention?.ropeTheta`), `tie_word_embeddings=false`.
71
+ `attention scale = head_dim^-0.5 = 128^-0.5`. Plain Llama: **no MiniCPM residual
72
+ scaling** (verified — `LlamaLayer.forward` is `inputNorm→attn→+x→postAttnNorm→mlp→+h`).
73
+
74
+ **Per-layer attention output dims:** q_proj out = `16*128=2048`; k_proj/v_proj out =
75
+ `2*128=256`; o_proj in = `2048`, out = `1536`. MLP: gate/up out = `4608`, down in =
76
+ `4608`, out = `1536`. lm_head out = `130560`, in = `1536`. embed = quantized 8-bit.
77
+
78
+ **Per-TENSOR mixed precision (weight quant):** every linear has its OWN `{bits,
79
+ group_size}` baked in `config.quantization` (group_size always 64; bits ∈ {4,8}).
80
+ This is why the megakernel must be **generated/de-branched per model** — bits is a
81
+ compile-time literal per (layer, matrix), not a runtime value. Full table is in
82
+ `config.json` under `quantization`; read it with `quantFor(config.quantization,
83
+ path)` (`src/model/gemma4-base.ts`). Examples: L0 all 8-bit; L1 q4/k4/v8/o8/g4/u4/d8;
84
+ L7 q8/k4/v4/o8/g4/u8/d4; L23 all 8-bit. **Read the table programmatically per build —
85
+ do not hardcode.**
86
+
87
+ **Quantized weight layout** (`QuantizedLinear`, `src/model/gemma4-base.ts:76`):
88
+ - `.w`: packed `uint32` `[N_out, K_in*bits/32]`. `.scales`,`.biases`: `bf16`
89
+ `[N_out, K_in/group_size]` (affine: `deq = scale*q + bias`).
90
+ - Dequant math is in `STEEL_QMM_HEADER` (`dequantize<T,N,bits>`, `get_pack_factor`,
91
+ `get_bytes_per_pack`) — `src/train/steel-qmm-header.ts`. REUSE IT.
92
+
93
+ **KV cache:** L1 path = `KVCache` (bf16, `src/model/gemma4-base.ts:243`); its
94
+ `updateAndFetch(k,v)` appends `[1,heads,1,D]` at `cache.offset` and returns the full
95
+ `[1,heads,offset+1,D]`. `cache.offset` is the position. L2 path = per-layer
96
+ `QuantizedKVCache` (mixed bits from `config.kvQuant`) — DEFER to Phase 4.
97
+
98
+ **RoPE** (`ops.rope`, `src/mlx/ops.ts:100`): `rope(x[B,heads,L,D], dims=head_dim,
99
+ base=rope_theta, offset, scale=null)`. For decode L=1, offset=position. The
100
+ in-kernel formula (standard NeoX/Llama interleaved-half): for dim pair `i<D/2`,
101
+ `theta_i = position / base^(2i/D)`; rotate `(x[i], x[i+D/2])` by `theta_i`. **Match
102
+ `ops.rope`'s exact convention** — confirm against `src/mlx/ops.ts` rope binding and
103
+ a 1-token parity probe before trusting it (Phase 2 gate catches mismatch).
104
+
105
+ **Validation goldens (ALREADY EXIST, no regen needed):**
106
+ - L1: `goldens/minicpm5-parity.json` (prompt_ids `[0,608,4894,304,6918,357]`,
107
+ greedy_ids[100], vocab 130560) + `goldens/minicpm5-logits-step{0..99}.bin`
108
+ (f32 last-position logits). Produced by `scripts/regen-minicpm5-goldens.ts`.
109
+ - L2: `goldens/minicpm5-kv-parity.json` + `…-kv-logits-step*.bin`.
110
+
111
+ **Reference forward to copy (the source of truth for correctness):**
112
+ `src/model/minicpm5.ts` — `LlamaAttention.forward` (95–193), `LlamaMLP.forward`,
113
+ `LlamaLayer.forward`, `MiniCPM5Model.forwardHidden/forwardLayers/logitsFromHidden`.
114
+ Transcribe THIS op-for-op into the kernel.
115
+
116
+ ---
117
+
118
+ ## 3. Already-built, validated building blocks (reuse, don't rebuild)
119
+
120
+ All default-OFF, `bun test tests/minicpm5-*.test.ts` green. From this session:
121
+ - `src/train/steel-qmm-header.ts` — `STEEL_QMM_HEADER`: MLX's verbatim quantized
122
+ `BlockMMA` GEMM + `dequantize`/`QuantizedBlockLoader`/`BlockLoader`. The GEMM
123
+ building blocks for every projection. `qmm_t_impl` (line 1334) is the full GEMM
124
+ reference (takes dims as `constant int&` — for our metal_kernel we inline the
125
+ `BlockMMA` loop with templateInt dims instead, as `FWD_STEEL_SOURCE` does).
126
+ - `src/model/fused-mlp-kernel.ts` — `fusedMlp`: whole MLP (gate+up+silu+down +
127
+ absorbed residual) in ONE dispatch, `hidden` never materialized, per-loader
128
+ mixed-precision bits baked, atomic-accumulated f32 output. **1.9–3.4× standalone
129
+ @ M=8–256, ~1 ULP correct.** This is a working prototype of one megakernel STAGE
130
+ and the dequant/GEMV patterns to copy. (Note: it currently corrupts when CHAINED
131
+ in the lazy graph — that's the whole reason for the megakernel; the kernel math
132
+ itself is correct, proven by `scripts/experiments/fused-mlp-check.ts`.)
133
+ - `src/model/steel-linear-kernel.ts` — `steelLinear`: plain quantized linear as our
134
+ dispatch (q/k/v/o/down), ~1 ULP vs `ops.quantizedMatmul`. The projection GEMV
135
+ pattern.
136
+ - `src/mlx/metal-kernel.ts` — `MetalKernel`: `apply(inputs, {outputShapeFn|outputs,
137
+ grid, threadGroup, templateInts, templateDtypes, atomicOutputs, initValue})`.
138
+ `*_shape` helpers available in-kernel. `metalCapture` for Xcode GPU traces.
139
+ - `src/train/segmented.ts:81` `detachLeaf` (contiguous→eval→`fromBytesCopy`) — the
140
+ graph-break primitive (replicated in `minicpm5.ts`).
141
+
142
+ **Probe scripts (templates for new validation):** `scripts/experiments/swiglu-*.ts`,
143
+ `fused-mlp-*.ts`, `steel-linear-check.ts`, `swiglu-teacherforced.ts` (the
144
+ teacher-forced gate harness — copy it).
145
+
146
+ ---
147
+
148
+ ## 4. Architecture of the megakernel
149
+
150
+ ### 4.1 The shape
151
+
152
+ One resident dispatch. State lives in **device** buffers (shared across
153
+ threadgroups); the kernel walks layer 0→23 as a sequence of **stages**, with a
154
+ **grid-wide barrier** between stages so stage N+1 sees all of stage N.
155
+
156
+ Per layer, the stages (each a grid-wide-parallel step, barrier after):
157
+ 1. `xn = RMSNorm(hidden, inputNorm_w)` → device `xn[H]`
158
+ 2. `q = Wq·xn` (`[2048]`), `k = Wk·xn` (`[256]`), `v = Wv·xn` (`[256]`) — quantized
159
+ GEMVs, outputs split across threadgroups → device
160
+ 3. `rope(q, pos)`, `rope(k, pos)` (per head, D=128)
161
+ 4. write `k,v` into KV cache at `pos` (device)
162
+ 5. **attention:** for each of 16 q-heads (GQA: head h uses kv-head `h/8`), online
163
+ softmax of `q_h · K[0..pos]` over the `pos+1` cached keys (scale `128^-0.5`),
164
+ weighted sum of `V[0..pos]` → `attn[2048]`
165
+ 6. `o = Wo·attn` (`[1536]`); `hidden += o` (residual)
166
+ 7. `hn = RMSNorm(hidden, postAttnNorm_w)` → device `hn[H]`
167
+ 8. MLP: `gate = Wg·hn`,`up = Wu·hn` (`[4608]`), `silu(gate)·up`, `down = Wd·(…)`
168
+ (`[1536]`); `hidden += down` (residual) — the `fusedMlp` body, inlined
169
+ After 24 layers: `RMSNorm(hidden, finalNorm_w)` → `lmHead·hidden` → `logits[V]`.
170
+
171
+ ### 4.2 The grid-wide barrier (the crux)
172
+
173
+ A single dispatch has no built-in cross-threadgroup barrier. Implement a
174
+ **persistent-kernel software barrier** via two device atomics (`atomic_uint`):
175
+ `arrive` (count) + `sense` (phase). Each barrier: `tg_leader` does
176
+ `atomic_fetch_add(arrive,1)`; if it was the last (`==G`), reset `arrive=0` and flip
177
+ `sense`; all threadgroups spin-read `sense` until it flips; `threadgroup_barrier`
178
+ to fan back into the threadgroup. **HARD REQUIREMENT:** all `G` threadgroups must be
179
+ **co-resident** or this deadlocks (a threadgroup waiting on one never scheduled).
180
+ So `G` must be ≤ the GPU's max concurrent threadgroups.
181
+
182
+ Pick `G` conservatively for the M4 Pro (~16–20 GPU cores). Phase 3 determines the
183
+ safe `G` empirically (start `G=1`, raise until just before the largest occupancy;
184
+ if any deadlock/timeout, lower). Use `dispatchThreads`/grid = `G * TGN`. Provide an
185
+ env `MLX_BUN_MEGAKERNEL_G` to sweep. Document the chosen `G`.
186
+
187
+ > **De-risking:** Phase 2 builds `G=1` (single threadgroup, `threadgroup_barrier`
188
+ > only — NO software barrier, hidden in threadgroup memory) to get CORRECTNESS
189
+ > first. Phase 3 generalizes to `G>1` with the software barrier for SPEED. Keep the
190
+ > per-stage math identical between them.
191
+
192
+ ### 4.3 Weight access — the repack (Phase 1)
193
+
194
+ A `metal_kernel` can't take ~500 tensors. Repack at load time into a SMALL fixed set
195
+ of device buffers + an offset/spec table:
196
+ - `WBYTES`: one `uint8` buffer = every quantized weight's packed bytes concatenated
197
+ in a fixed order (L0.q, L0.k, … L23.lm_head). Plus `SCALES` (bf16 concat),
198
+ `BIASES` (bf16 concat). Plus `NORMS` (bf16 concat of all RMSNorm weights).
199
+ - `META`: a `uint32`/`int32` table the kernel indexes by `(layer, matrix)` giving:
200
+ byte-offset into WBYTES, scales offset, biases offset, N, K. **bits/group_size are
201
+ baked as template literals per (layer,matrix) in the GENERATED kernel**, not in
202
+ META (they drive the dequant template instantiation).
203
+ - Because bits varies per matrix, the kernel is **generated** (like
204
+ `scripts/gen-model.ts`): emit a `forwardLayer_L<i>` inline body per layer with that
205
+ layer's 7 matrices' bits baked, OR a single body that `switch`es on a baked
206
+ per-layer-constant array. Prefer generation: produces
207
+ `src/model/generated/minicpm5-megakernel.ts` with all bits as literals.
208
+
209
+ KV cache buffers: `KBUF`,`VBUF` device `[24, max_seq, 2, 128]` bf16 (L1). `pos` is a
210
+ scalar input. Logits out: `[V]` f32.
211
+
212
+ ---
213
+
214
+ ## 5. Phased plan (execute in order; gate each)
215
+
216
+ ### Phase 1 — Weight repack (foundation)
217
+ **File:** `src/model/megakernel-pack.ts`.
218
+ **Do:** `packMiniCpm5(model) → { wbytes, scales, biases, norms, meta, layout }`.
219
+ Walk `model.layers[i]` (`.attn.{qProj,kProj,vProj,oProj}`, `.mlp.{gate,up,down}`,
220
+ `.inputNorm`,`.postAttnNorm`) + `model.finalNorm`,`model.lmHead`,`model.embed`. For
221
+ each quantized linear concat `.w` bytes → WBYTES, `.scales`→SCALES, `.biases`→BIASES
222
+ (record offsets, N, K, bits, group in `layout`). Concat norm weights → NORMS. Use
223
+ `MlxArray.rawBytes()` (eval first) to get bytes; build with `Bun`'s buffers; upload
224
+ as `MlxArray.fromBytesCopy`.
225
+ **Gate:** unit test — for 3 sample (layer,matrix), slice WBYTES/SCALES/BIASES at the
226
+ recorded offset, rebuild a `QuantizedLinear`, run `ops.quantizedMatmul` on a fixed
227
+ input, assert **byte-identical** to the original `.forward`. (`scripts/experiments/
228
+ megapack-check.ts`.) This catches layout bugs before any kernel.
229
+
230
+ ### Phase 2 — Single-threadgroup correct megakernel (G=1)
231
+ **Files:** `src/model/megakernel-kernel.ts` (the `MetalKernel` + dispatch),
232
+ `scripts/gen-minicpm5-megakernel.ts` (emits the per-layer-bits-baked SOURCE into
233
+ `src/model/generated/minicpm5-megakernel.ts`).
234
+ **Design:** ONE threadgroup (`G=1`, `TGN=256` threads). `hidden[H]` in threadgroup
235
+ memory (6 KB). Loop layers; each stage uses `threadgroup_barrier`. GEMVs: the 256
236
+ threads cover the N outputs in chunks (each thread does `ceil(N/256)` dot-products
237
+ over K, dequantizing weight via `STEEL_QMM_HEADER`'s `dequantize`). RMSNorm: parallel
238
+ sum-of-squares reduction in threadgroup. Attention: loop heads; per head, online
239
+ softmax over `pos+1` cached keys (threads split the key positions, threadgroup
240
+ reduce). Inline the `fusedMlp` math for stage 8 (gate/up into threadgroup, silu·up,
241
+ down). KV read/write from `KBUF`/`VBUF`. Final: finalNorm + lmHead GEMV → logits[V]
242
+ (threads split V).
243
+ **Transcribe** `src/model/minicpm5.ts` op-for-op; match dtypes: accumulate in f32,
244
+ round to bf16 at each op boundary to track the reference (`(T)` casts, as `fusedMlp`
245
+ does). RoPE: match `ops.rope` exactly (verify with a single-layer probe).
246
+ **Gate (THE correctness gate):** `scripts/experiments/megakernel-teacherforced.ts`
247
+ (copy `swiglu-teacherforced.ts`): drive 100 decode steps via the megakernel,
248
+ teacher-forced on `goldens/minicpm5-parity.json`, KV cache in `KBUF`/`VBUF`. Require
249
+ **≥98/100 argmax agreement, deterministic across 3 runs, no NaN.** If an early step
250
+ mismatches, bisect by stage: add a debug output for `hidden` after layer L (env
251
+ `MLX_BUN_MEGA_DUMP_LAYER=L`) and compare to the reference model's per-layer hidden
252
+ (capture via the wrapper trick in `scripts/experiments/swiglu-layerN.ts`).
253
+
254
+ ### Phase 3 — Persistent multi-threadgroup (G>1) for SPEED
255
+ **Do:** generalize Phase 2 to `G` threadgroups. Move `hidden`,`xn`,`hn`,`q/k/v/attn/
256
+ gate/up` to DEVICE scratch (shared). Implement the §4.2 software barrier (`arrive`/
257
+ `sense` atomics, leader-flips-phase, spin). Split each stage's outputs across
258
+ threadgroups by `tid.x` block (GEMV: threadgroup `g` does N-rows `[g*N/G,
259
+ (g+1)*N/G)`). Determine safe `G`: sweep `MLX_BUN_MEGAKERNEL_G` from 1 up; the largest
260
+ that (a) never deadlocks/timeouts and (b) maximizes tok/s. Add a 1-second host-side
261
+ watchdog around the dispatch during the sweep to catch deadlock (kill + report).
262
+ **Gate:** same teacher-forced ≥98/100 + deterministic, AND paired tok/s ≥ baseline.
263
+ If `G>1` corrupts (barrier race), the barrier impl is wrong — verify the leader
264
+ reset ordering and that EVERY thread (not just leaders) waits on `sense` before
265
+ reading shared state, with a `threadgroup_barrier` + `mem_flags::mem_device` after.
266
+
267
+ ### Phase 3 — RESULTS (2026-06-24)
268
+
269
+ Multi-threadgroup persistent kernel built + correct. Findings, in order discovered:
270
+
271
+ - **Occupancy sweep:** TGN must be a **power of 2** (the RMSNorm tree reduction
272
+ `for(s=TGN/2;s>0;s>>=1)` silently drops lanes otherwise — TGN=192 gave KL 0.54).
273
+ Smaller TGN packs more co-resident threadgroups (TGN=128 scales past where TGN=256
274
+ collapses). Peak total threads ≈ 12–16k; G beyond co-residency **deadlocks** the
275
+ software barrier (G=144 @ TGN=128 hung). Sweep needs a host watchdog.
276
+ - **Barrier cost** (measured, `megakernel-barrier-time.ts`): ~3.8 µs/barrier @ G=48,
277
+ ~145 barriers/token after fusion → ~0.5 ms = ~10% of decode. Not the dominant cost.
278
+ - **bits-literal specialization REJECTED for the *naive* GEMV.** Templating on BITS
279
+ and/or K bloated code/registers → lowered max co-resident G → regressed peak
280
+ (164→102–156; full-K-unroll collapsed to 16). The kernel was occupancy/bandwidth-
281
+ bound, not branch-bound. **Lesson: feed the bottleneck (DRAM-latency-hiding =
282
+ occupancy×ILP), not instruction count.**
283
+ - **THE win — port mlx's `qmv_fast` decode GEMV verbatim** (`quantized.h`
284
+ qmv_fast_impl/load_vector/qdot → `qmv4`/`load_vec_tg`/`qdot_t`). Three things ours
285
+ lacked: (1) `load_vector` pre-scales x so `qdot` is mask-only (no per-nibble
286
+ shifts); (2) affine folds once per group as `scale*accum + sum*bias`; (3) **4 rows
287
+ per simdgroup reusing a register-resident x_thread** → 4× arithmetic per x-load +
288
+ 4 independent accumulators hiding FMA/load latency. Our weight layout already
289
+ matches mlx's, all N,K divide the fast-path blocks → no safe-tail path. This is the
290
+ M=1 decode primitive that was never ported (steel-qmm-header is the M>1 GEMM/BlockMMA
291
+ path). **Jumped 0.70× → ~0.93–0.95× baseline; KL improved to 9.5e-4; removed the
292
+ G≥24 down-tiling constraint (any G now correct).**
293
+ - **Barrier fusion:** RMSNorm computed in-place per-threadgroup (`rms_norm_local`,
294
+ every TG normalizes the full hidden — tiny, removes the rms→GEMV grid barrier +
295
+ d_nrm device traffic) and SwiGLU `silu(gate)·up` folded into the down-proj's input
296
+ staging. ~145 barriers/token (from ~194). Perf-neutral at G=32 (barriers already
297
+ hidden) but removes device round-trips.
298
+ - **Generated (layer-unrolled, constants-baked) kernel built** (`genSourceMt`, env
299
+ `MLX_BUN_MEGAKERNEL_GEN=1`): per-(layer,matrix) bits/offsets/N/K + layer index L as
300
+ compile-time literals, no META lookups, no runtime bits dispatch. Layer loop
301
+ unrolled (sequential → registers reused, NO occupancy hit, unlike the inner-K
302
+ unroll). **Correct (KL identical) but perf-NEUTRAL** (202.4 vs 202.9 tok/s) —
303
+ confirms the kernel is bandwidth/barrier-bound, not lookup-bound. Kept behind the
304
+ flag as the architecturally-correct path.
305
+ - **Peak config:** TGN=256, G=32. Paired same-process A/B: megakernel **~202 tok/s**
306
+ vs baseline **~216 tok/s = ~0.94×**. Correctness 97/100 teacher-forced (3 argmax
307
+ near-ties; KL 9.7e-4), deterministic, no NaN.
308
+ - **Remaining gap (~5%) is the resident-kernel structural tax** (~6 grid barriers/
309
+ layer for the row-split GEMV producer→consumer chain + per-stage staging), which is
310
+ largely fundamental to multi-threadgroup decode. Candidate lever to cross baseline:
311
+ fold rope-q into attention to drop one barrier/layer. Needs a CLEAN-machine paired
312
+ measurement (Josh-gated) — the session machine is loaded, only the ratio is trustworthy.
313
+
314
+ ### Phase 4 — L2 quantized KV (IN PROGRESS 2026-06-24, spec ready)
315
+ Swap `KBUF/VBUF` for the per-layer quantized-KV format. **Facts gathered:**
316
+ - CPM5 `kv_config.json`: per-layer `bits ∈ {4,8}`, `group_size=64`, quantized along
317
+ **head_dim=128** (so 2 groups per head per k/v). Loaded as `config.kvQuant`.
318
+ - Layout (`QuantizedKVCache`, gemma4-base.ts:407): per (layer,pos) store quantized
319
+ triple along the KVDIM=256 axis: `packed` uint32 `[256*bits/32]`, `scales`/`biases`
320
+ bf16 `[256/64=4]`. Affine `deq = scale*q + bias` (== our weight dequant).
321
+ - **mlx quantize formula to replicate EXACTLY** (`affine_quantize`, quantized.h:2432;
322
+ this is what the golden used via optiq `updateAndFetchQuantized`):
323
+ ```
324
+ n_bins=(1<<bits)-1; eps=1e-7
325
+ wmin=min(group), wmax=max(group)
326
+ scale=max((wmax-wmin)/n_bins, eps); side=|wmin|>|wmax|; scale = side? scale : -scale
327
+ edge = side? wmin : wmax; q0=round(edge/scale)
328
+ scale = q0==0? scale : edge/q0; bias = q0==0? 0 : edge
329
+ q = min(round((w-bias)/scale), n_bins) // dequant: scale*q+bias
330
+ ```
331
+ **Implementation steps:**
332
+ 1. ✅ **DONE — quantize formula validated** (`scripts/experiments/kv-quant-formula-check.ts`):
333
+ the affine_quantize transcription matches `ops.quantize` to bf16 ULP (scale/bias diff
334
+ 1.8e-3 @4-bit, 6e-5 @8-bit; dequant residual = expected bf16-storage rounding). The
335
+ riskiest piece is de-risked. Compute scale/bias in f32, store bf16 (as mlx does).
336
+ 2. **Host:** `MegakernelRunner` holds per-layer quantized KBUF/VBUF (packed/scales/
337
+ biases) sized to kvSeq; bits per-layer baked (literal in the generated kernel).
338
+ 3. **Kernel write:** after rope, each head's roped k/v (128 dims) → 2 groups of 64 →
339
+ compute wmin/wmax (threadgroup or per-head reduce) → scale/bias → pack q → store at
340
+ current pos. Replaces the bf16 `knewOut/vnewOut` emit.
341
+ 4. **Kernel read (attend_simd):** dequant EVERY position incl. current (optiq quantizes
342
+ then reads back, so the current entry must also round-trip) from packed+scales+biases.
343
+
344
+ **Increment 1 (DONE, bf16-storage round-trip): 93/100, KL 1.49e-2, deterministic, no NaN**
345
+ (`kv_rt_group<KVBITS>` after rope, `megakernel-kv-teacherforced.ts`,
346
+ `MLX_BUN_MEGAKERNEL_KVQUANT=1`). Quantize formula correct. **Gap diagnosed:** optiq's
347
+ `quantizedSdpaUnfused` uses `quantizedMatmulQT`, which dequants K/V **to f32 on-the-fly
348
+ inside the matmul** — never storing a bf16 dequantized value. Increment 1 stores
349
+ `bf16(scale·q+bias)` in the bf16 cache → ONE extra bf16 rounding optiq lacks → KL
350
+ 15× the L1 path. NOT a formula bug; a storage-precision artifact of the shortcut.
351
+ **Increment 2a — PARITY fix (store-int-q, the cleaner path):** the quantized int `q`
352
+ (0..15 / 0..255) is EXACTLY representable in bf16 (ints ≤256 are exact). So store `q`
353
+ itself in the existing bf16 kcache/vcache (lossless) + per-group scale/bias in small
354
+ side buffers `[NLAYERS,KVSEQ,nKvHeads·headDim/64=4]` bf16; `attend_simd<KVBITS>` computes
355
+ `scale·q+bias` in f32 on read = optiq's exact f32 dequant, ZERO extra rounding → should
356
+ reach the attend_simd ceiling (~97, == L1). Much smaller than bit-packing: kv_rt_group
357
+ stores `q` (not dequant) to d_k/d_v + emits scale/bias new-rows; host adds 4 scale/bias
358
+ buffers + merges; attend dequants (current-pos scale/bias from device scratch, cached
359
+ from the side-buffer inputs). NO memory win (q stored loosely as bf16), but full optiq
360
+ parity — the Phase 4 correctness goal.
361
+ **Increment 2a — DONE, store-int-q: 93/100, KL 1.38e-2, deterministic, no NaN.**
362
+ Stores int q in kcache/vcache (exact in bf16) + bf16 scale/bias; `attend_simd_q<KVBITS>`
363
+ dequants to f32 on read. **Found + fixed a real bug:** the current-pos scale/bias were
364
+ first written to a NON-ATOMIC output (`sbNew`) read cross-threadgroup by attention →
365
+ incoherent (stale) per [[metal-persistent-kernel-coherence]] → 78/100. Fix: route
366
+ current-pos scale/bias through ATOMIC scratch `d_sb` (A_LD/A_ST), copy to `sbNew` only
367
+ for the host merge; cached scale/bias read from the `sb` INPUT (coherent). → 93/100.
368
+ **Surprise / honest correction:** 2a removed increment-1's extra bf16-rounding but
369
+ landed at the SAME 93/100 — so **dequant precision was NOT the gap.** The residual
370
+ (~14× L1's KL) is a genuine divergence between our quantized KV and optiq's, cause
371
+ still OPEN: formula validated vs `ops.quantize` (bf16 ULP), all 24 layers' bits match
372
+ kv_config, coherence fixed. Candidate causes to investigate: optiq may keep recent
373
+ token(s) unquantized (a bf16 staging window) rather than quantize-on-write; or its
374
+ fused quantized-SDPA accumulates differently than our dequant-then-online-softmax.
375
+ **ROOT CAUSE FOUND (decisive, in-process vs our bit-exact reference):** the reference
376
+ `QuantizedKVCache`+`quantizedSdpaUnfused` is bit-exact with the golden (maxDiff 0,
377
+ `tests/minicpm5-kv-parity.test.ts`). Comparing the megakernel's dequantized layer-0 K
378
+ to the reference's: **max diff 0.166 (≈ one quant step).** Comparing the *pre-quant*
379
+ bf16 roped K (L1): **max diff 1 bf16 ULP** (9.0 vs 9.0625; 425/1536 elems > 1 ULP) —
380
+ the `qmv4`-vs-`ops.quantizedMatmul` GEMV rounding difference, i.e. exactly what gives
381
+ L1 its 9.7e-4. **Quantization is DISCONTINUOUS**, so that 1-ULP K difference lands on a
382
+ q-level / group-min-max boundary and flips a level → a full quant-step dequant error →
383
+ amplified 9.7e-4 → 1.38e-2. **NOT a quant bug — the megakernel's intrinsic ~1-ULP GEMV
384
+ difference amplified by quant discontinuity. 93/100 is the L2 ceiling (as 97/100 is L1's);
385
+ bit-exact L2 would need a bit-exact GEMV, defeating qmv4.** Gate L2 by KL + agreement
386
+ (L3 class), not the bit-exact golden. Scripts: `megakernel-kv-{cmpkv,cmpl1,perstep}.ts`.
387
+ **Increment 2b — MEMORY win (later):** bit-pack `q` into uint32 (flat KPACK/VPACK,
388
+ per-layer literal offsets) to actually shrink 4-bit KV. Same logits as 2a. Deferred.
389
+ **Build on the GENERATED kernel** (`genSourceMt`, `MLX_BUN_MEGAKERNEL_GEN`): the
390
+ per-layer KV `bits` is fixed on disk (`kv_config.json`), so bake it as a compile-time
391
+ literal per layer (same "pre-determined" principle as the weight bits) — the quantize/
392
+ dequant helpers are `template<int KVBITS>`, and each unrolled layer emits its literal
393
+ `KVBITS` (and `group_size=64`). No runtime kv-bits table lookup. This is why Phase 4
394
+ makes the generated path the primary one (the looped kernel would need a runtime kv
395
+ table). Phase 4 therefore promotes `genSourceMt` from flag-gated to the L2 default.
396
+ **Gate:** `goldens/minicpm5-kv-parity.json` + `…-kv-logits-step*.bin` (already exist),
397
+ teacher-forced agreement + KL. Defer until L1 solid (L1 IS solid — parity, Phase 3).
398
+
399
+ ### Phase 5 — Integration + ship
400
+ **Do:** `MiniCPM5Model.decodeStep(tokenId, pos) → logits` using the megakernel when
401
+ `MLX_BUN_MEGAKERNEL=1`; allocate the packed buffers + KV scratch once at model load
402
+ (or first decode), reuse across steps. Wire into `src/generate.ts` decode loop
403
+ (n23–n44 in `docs/dag/training-inference-map.html`) — the megakernel REPLACES the
404
+ per-step `forwardHidden→logitsFromHidden` at decode (M=1) only; prefill unchanged.
405
+ Guard: bf16 KV (L1) only, no adapters, no vision. Add `--megakernel` CLI flag.
406
+ **Gate:** `bun test tests/minicpm5-*.test.ts` green (off); a new
407
+ `tests/minicpm5-megakernel.test.ts` (teacher-forced gate, env-gated like
408
+ `tests/perf-kernel-oracle.test.ts`); paired decode tok/s A/B logged. Update
409
+ `STATUS.md` + `benchmarks/RESULTS.md` (preflight-gated `benchmark.sh` for the
410
+ quotable number — `[[dirty-machine-numbers-are-garbage]]`).
411
+
412
+ ---
413
+
414
+ ## 5.5 Methodology for the next megakernel (learned 2026-06-24)
415
+
416
+ **Copy mlx's exact code verbatim into one file → prove bit-identical (per-op harness,
417
+ maxDiff 0 vs `ops.*`) → static-analyze that one file → THEN optimize incrementally,
418
+ re-running the bit-exact harness after each step.** Why: a hand-port of `qmv_fast` was
419
+ 1-ULP-correct but NOT bit-identical to what `ops.quantizedMatmul` actually dispatches
420
+ for **4-bit M=1** (it uses a different kernel/accumulation order — `qmv4-bitexact-check.ts`
421
+ shows 8-bit bit-exact, 4-bit off by exactly 1 bf16 ULP on 12/256 rows, e.g. 1552 vs 1544
422
+ = adjacent bf16 values). Invisible at L1 (9.7e-4) but quantization's DISCONTINUITY
423
+ amplifies it into the L2 gap. A provably-identical starting copy + per-step gating would
424
+ catch any divergence the moment it's introduced, not 24 layers + a quant step downstream.
425
+ Steps: (1) copy mlx `quantized.h` GEMV kernels INCLUDING the per-(bits,M,N,K) dispatch
426
+ (`qmv_fast`/`qmv`/`qmv_quad`), `rms_norm`, `rope`, `sdpa`; (2) per-op maxDiff-0 harness
427
+ before fusing; (3) analyze the single fused file; (4) optimize with the harness as a
428
+ regression gate. This megakernel was built L3-first (KL-gated, NOT bit-exact by design),
429
+ so its 1-ULP/op drift is within contract — bit-exact is a focused REBUILD, not a patch
430
+ (and may cost qmv4's speed if mlx's real 4-bit kernel is the slower qmv_quad).
431
+
432
+ ## 6. Pitfalls (learned this session — do not relearn)
433
+
434
+ - **Chained custom kernels corrupt under MLX's planner.** The ENTIRE reason for one
435
+ kernel. Don't reintroduce intermediate custom-kernel boundaries.
436
+ - **`templateDtypes:{T:x.dtype}` is required** or `T` is undeclared (Metal compile
437
+ error). **`.template store<…>`** needed for dependent names when `T` is a template
438
+ param. Bake dims as `templateInts` / read `x_shape[0]`, don't pass a runtime
439
+ `shape` input array (extra lazy nodes).
440
+ - **Match the reference's bf16 rounding op-for-op** (round to `T` at each boundary) —
441
+ `metal::precise::exp`/`tanh` are the only residual vs the reference; that's L3 and
442
+ fine. Do NOT compute everything in f32 then round once (drifts).
443
+ - **`atomicOutputs:true` + `initValue:0`** for any atomic-accumulated output.
444
+ - **GQA:** 16 q-heads, 2 kv-heads, `nRep=8` → q-head `h` reads kv-head `h/8`.
445
+ - **`rope_theta=5e6`, not 10000.** scale `=128^-0.5`. No MiniCPM residual scaling.
446
+ - **Software barrier deadlocks if `G` > co-resident threadgroups.** Start G=1.
447
+ - **`detachLeaf` ≠ `evalAll`:** eval alone doesn't break the graph; only a fresh
448
+ `fromBytesCopy` leaf does. (Relevant only if you keep any boundary.)
449
+ - **`MlxArray` has `.astype`, not `ops.astype`; `ops.reshape`, not `.reshape`.**
450
+ - Oracle venv for any golden regen: `/Users/joshrossi/Code/mlx-lm/.venv/bin/python`,
451
+ `MLX_BUN_ORACLE_VENV=…`, `HF_HUB_DISABLE_XET=1`. (Goldens already exist; only
452
+ regen if you change the prompt.)
453
+
454
+ ---
455
+
456
+ ## 7. File manifest (what you create)
457
+
458
+ ```
459
+ src/model/megakernel-pack.ts # Phase 1: weight repack + layout table
460
+ scripts/gen-minicpm5-megakernel.ts # Phase 2: emit per-layer-baked kernel SOURCE
461
+ src/model/generated/minicpm5-megakernel.ts # (generated) the kernel SOURCE + bits
462
+ src/model/megakernel-kernel.ts # Phase 2/3: MetalKernel + decodeMegakernel()
463
+ scripts/experiments/megapack-check.ts # Phase 1 gate
464
+ scripts/experiments/megakernel-teacherforced.ts # Phase 2/3 correctness gate
465
+ scripts/experiments/megakernel-perf.ts # Phase 3/5 paired tok/s A/B
466
+ tests/minicpm5-megakernel.test.ts # Phase 5 CI gate (env-gated)
467
+ ```
468
+ Modify: `src/model/minicpm5.ts` (add `decodeStep` megakernel branch),
469
+ `src/generate.ts` (decode-loop branch), `src/cli.ts` (`--megakernel`).
470
+
471
+ ## 8. Order of operations (the literal checklist)
472
+
473
+ 1. [ ] Phase 1: `megakernel-pack.ts` + `megapack-check.ts` → byte-identical gate green.
474
+ 2. [ ] Phase 2: generator + `G=1` kernel; `megakernel-teacherforced.ts` ≥98/100,
475
+ deterministic ×3, no NaN. Bisect by layer if it fails.
476
+ 3. [ ] Phase 3: `G>1` + software barrier; sweep `MLX_BUN_MEGAKERNEL_G`; same
477
+ correctness gate + tok/s ≥ baseline. Record chosen `G`.
478
+ 4. [ ] Phase 4 (optional): L2 quantized KV vs kv-parity golden.
479
+ 5. [ ] Phase 5: wire into `decodeStep`/`generate.ts`, env+CLI gate, CI test,
480
+ benchmark, update `STATUS.md`/`RESULTS.md`/memory
481
+ `[[cpm5-generate-and-fuse-inference]]`.
482
+
483
+ **Done = one resident dispatch computes the MiniCPM5 decode forward, ≥98/100
484
+ teacher-forced, deterministic, ≥ baseline tok/s, default-off, CI-gated.**