mlx-bun 0.0.4 → 0.0.6

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 (90) hide show
  1. package/README.md +175 -93
  2. package/docs/dag/training-inference-map.html +575 -0
  3. package/docs/design/adapters-end-to-end.md +50 -46
  4. package/docs/design/compat-cli-surface-design.md +10 -8
  5. package/docs/design/docs-reorg-plan.md +14 -1
  6. package/docs/design/hlg-sampling.md +12 -2
  7. package/docs/design/memory-system.md +193 -295
  8. package/docs/design/optimization_plan.md +4 -2
  9. package/docs/design/orpo-dynamic-lambda.md +179 -0
  10. package/docs/design/orpo-future-enhancements.md +340 -0
  11. package/docs/design/orpo-training.md +1212 -0
  12. package/docs/design/parallel-slots.md +34 -26
  13. package/docs/design/parity-tier-dag.md +143 -0
  14. package/docs/design/runtime-isolation.md +1 -1
  15. package/docs/design/segmented-backward-training.md +58 -39
  16. package/docs/design/tauri-desktop-app.md +127 -0
  17. package/docs/design/w4a16-compute-precision-spike.md +232 -0
  18. package/docs/investigations/orpo-base-uf-experiment-and-directions.md +120 -0
  19. package/docs/investigations/orpo-flash-cce-pin-leak.md +167 -0
  20. package/docs/investigations/orpo-uf-testing-handoff.md +126 -0
  21. package/docs/{segmented-backward-handoff.md → investigations/segmented-backward-handoff.md} +1 -1
  22. package/docs/investigations/starter-model-port-handoff.md +32 -13
  23. package/docs/investigations/steel-flash-cce-handoff.md +489 -0
  24. package/docs/investigations/trainer-validation-experiment.md +173 -0
  25. package/docs/planning/IDEAS.md +29 -0
  26. package/docs/planning/PRODUCT_ROADMAP.md +147 -27
  27. package/docs/planning/chunk-task-roadmap.md +1 -1
  28. package/docs/planning/journal.md +161 -0
  29. package/docs/reference/distribution.md +61 -5
  30. package/docs/reference/library-api.md +29 -14
  31. package/docs/reference/memory.md +77 -0
  32. package/docs/reference/orpo-quickstart.md +130 -0
  33. package/docs/reference/server-api.md +105 -7
  34. package/docs/reference/server-config.md +12 -5
  35. package/docs/reference/training.md +296 -0
  36. package/package.json +1 -1
  37. package/src/cli.ts +651 -18
  38. package/src/eval/ifeval.ts +138 -0
  39. package/src/eval/runner.ts +76 -6
  40. package/src/eval/tasks/bfcl.ts +49 -15
  41. package/src/eval/tasks/gsm8k.ts +21 -9
  42. package/src/eval/tasks/hashhop.ts +33 -4
  43. package/src/eval/tasks/humaneval.ts +30 -8
  44. package/src/eval/tasks/ifeval.ts +52 -18
  45. package/src/eval/tasks/mmlu.ts +85 -74
  46. package/src/eval/ultrafeedback.ts +60 -0
  47. package/src/jobs/runner.ts +6 -0
  48. package/src/lora.ts +12 -7
  49. package/src/memory/chunk.ts +25 -0
  50. package/src/memory/cluster.ts +24 -0
  51. package/src/memory/db.ts +114 -0
  52. package/src/memory/ingest.ts +22 -0
  53. package/src/memory/pipeline.ts +63 -0
  54. package/src/memory/prompts.ts +23 -0
  55. package/src/memory/schedule.ts +163 -0
  56. package/src/memory/synthesize.ts +29 -0
  57. package/src/memory/tools.ts +399 -0
  58. package/src/memory/vault.ts +694 -0
  59. package/src/memory/wikify.ts +23 -0
  60. package/src/mlx/metal-kernel.ts +31 -4
  61. package/src/model/flash-attention.ts +37 -19
  62. package/src/model/fused-geglu-kernel.ts +111 -0
  63. package/src/model/gemma4-base.ts +39 -1
  64. package/src/model/gemma4.ts +196 -21
  65. package/src/model/minicpm5.ts +44 -3
  66. package/src/pi-provider.ts +29 -13
  67. package/src/pi-session.ts +81 -0
  68. package/src/pi-terminal.ts +13 -24
  69. package/src/pi-web.ts +246 -166
  70. package/src/quantize/job.ts +17 -2
  71. package/src/quantize/quantizer.ts +26 -2
  72. package/src/server.ts +216 -47
  73. package/src/tokenizer.ts +6 -2
  74. package/src/train/dataset.ts +29 -6
  75. package/src/train/flash-cce.ts +1175 -0
  76. package/src/train/job.ts +64 -3
  77. package/src/train/lora-params.ts +53 -4
  78. package/src/train/loss.ts +947 -0
  79. package/src/train/optimizer.ts +10 -3
  80. package/src/train/prefix-shared.ts +435 -0
  81. package/src/train/rank.ts +36 -0
  82. package/src/train/segmented.ts +1148 -3
  83. package/src/train/steel-qmm-header.ts +1461 -0
  84. package/src/train/trainer.ts +570 -54
  85. package/src/train/watch.ts +542 -0
  86. package/src/web/app.html +285 -19
  87. package/src/web/skills/memory/SKILL.md +97 -0
  88. package/src/web/skills/web-research/SKILL.md +3 -1
  89. package/src/web/skills.ts +46 -8
  90. /package/{docs → src/assets}/curve-designer.html +0 -0
@@ -0,0 +1,489 @@
1
+ # Steel flash-CCE — handoff
2
+
3
+ Porting MLX's quantized `steel` GEMM verbatim into our flash-CCE ORPO head, then
4
+ fusing the ORPO-specific epilogue (softcap + online-softmax logp / softmax−onehot
5
+ coeff + dh) on top — to get **MLX-GEMM speed AND the `[M,V]`-free memory property**
6
+ in one kernel. MLX ships no fused "quantized-matmul + softcap + response-logp" op, so
7
+ we build the composition; the heavy GEMM is theirs, verbatim.
8
+
9
+ ## HANDOFF — current state (2026-06-19): SHIPPED in v0.0.5
10
+
11
+ The whole ORPO training stack is **merged (PR #16) and released as v0.0.5** — `npm
12
+ i mlx-bun` / `brew install joshuarossi/tap/mlx-bun` (tap formula at 0.0.5, sha verified).
13
+ What shipped: the flash-CCE head (fwd+bwd), prefix-sharing, segmented composition, warm-start,
14
+ the `scripts/train-orpo.ts` launcher, and the IFEval/UltraFeedback eval pieces. Release notes:
15
+ GitHub release v0.0.5. Supported scope = **OptiQ-quantized MiniCPM5 + Gemma-4 (e4b/12B/26B)**.
16
+
17
+ **Post-merge PR-review fixes (CodeRabbit, commits 890f380 / 63759fa) — read these, they
18
+ caught real bugs:**
19
+ - 🔴 **Prefix-shared head-primal UAF** (`branchLogpMeanGathered`/`orpoLossPrefixShared`): the
20
+ flash/fused-head CustomVjp PRIMAL (`hResp`, and `h`) was disposed *before* the lazy backward
21
+ recomputes from it → use-after-free (same class as the segmented `headSink` one). Fix: push
22
+ primals into the sink (freed post-eval) on the fused/flash path. **Tests passed before only
23
+ by lazy-eval timing luck** — this was latent. Watch for this pattern anywhere a CustomVjp's
24
+ primal is freed inline.
25
+ - **Segmented "flash implies fused"** (`trainer.ts` `segFusedChunk`): `orpoFlashCe` alone left
26
+ the segmented non-prefix fallback on the full `[M,vocab]` head → e4b memory blowup. Now
27
+ `orpoFusedCe || orpoFlashCe` routes through the bounded chunked head.
28
+ - Warm-start made atomic (validate all targets before disposing any), job payload exposes
29
+ `warm_start_adapter`/`mlp_split`, script GPU inputs use `fromBytesCopy` (page-align footgun),
30
+ env-precondition enforcement + flag validation in the probes.
31
+
32
+ **Warm-start** (`warmStartFromAdapter` / launcher `RESUME=<adapter-dir>`): continue from a
33
+ checkpoint's LoRA weights (optimizer + LR schedule restart). Insurance for interrupted runs.
34
+
35
+ **The training dress-rehearsal (honest):** a CPM5 UltraFeedback ORPO run (full stack, lr 5e-5)
36
+ reached ~step 4820, **val 1.66 → ~1.50, then plateaued** — expected: UF is open-ended, its loss
37
+ floor is ~1.2-1.5, NOT the 0.3-0.4 you get on a narrow task. **IFEval moved 22.5% → 22.5%
38
+ (flat)** — a real null result: general preference data + a partial run is the wrong lever for
39
+ IFEval. The pipeline learns (val dropped); the metric just isn't the right one. Checkpoints
40
+ every 200 under `./adapters/cpm5-uf-8h/checkpoints/` (best-val ~`step-04200-val1.5008`).
41
+
42
+ **Gotchas the runs taught us:**
43
+ - **Agent-spawned background runs get REAPED** (~47 min observed; not a crash/OOM — confirmed
44
+ via logs + `pmset` showed no sleep). **Run long training detached from your OWN shell**
45
+ (`nohup … &`), never as a session background task.
46
+ - e4b training **requires** `MLX_BUN_PERF_KERNEL=0` + `MLX_BUN_FUSED_GELU=0` (the launcher
47
+ auto-sets them; the e4b parity probe now enforces them).
48
+ - Adapters live in **`~/.cache/mlx-bun/mlx-bun-finetunes/`** (launcher default), never the repo.
49
+
50
+ **THE next goal — the chunk segmenter (load-bearing).** UF was the rehearsal; the real run is
51
+ **distilling Opus/GPT-5.5 conversation-segmentation into a local model** (data at
52
+ `~/Code/lucien/benchmark/finetune/chunk-v3/dpo/orpo-curated-*.fixed.jsonl`, `{prompt:[sys,user],
53
+ chosen, rejected}` — already supported by the loader). Score by **boundary/label accuracy vs
54
+ the gold (chunk-eval), NOT val loss** (loss is dominated by low-entropy JSON/UUID tokens). This
55
+ is what localizes the Lucien synthesis pipeline's `chunk-recent` stage (the whole "100% local
56
+ memory" promise). ORPO is ideal here: chosen/rejected share the conversation + are both
57
+ well-formed JSON, so the contrast isolates the segmentation decision, and the gold makes
58
+ pair-ranking + eval objective. The stack is built for it (prefix-share the shared 8k
59
+ conversation once, segment the long context, flash the big vocab). See memory
60
+ [[training-tracks-are-appliance-components]]. After that: the e4b overnight, and the welcome
61
+ assistant (a UF-tuned greeter — system prompt carries the mlx-bun knowledge, not training).
62
+
63
+ **Run commands:** see [orpo-quickstart](../reference/orpo-quickstart.md) +
64
+ [training.md](../reference/training.md). The full stack is `scripts/train-orpo.ts` (defaults on);
65
+ resume with `RESUME=<ckpt-dir>`; run it detached (`nohup`).
66
+
67
+ ## TL;DR status
68
+
69
+ | Piece | State |
70
+ |---|---|
71
+ | **Steel GEMM port** (verbatim MLX BlockMMA/QuantizedBlockLoader/qmm_t) | ✅ done, compiles, matches `quantizedMatmul` 1e-5, **101ms == MLX 106ms** on e4b |
72
+ | **Forward** (steel GEMM + softcap + online-softmax → logp) | ✅ **in production** (`flashCceForward` default), parity PASS e4b/cpm/M=200, **180ms** (5× the old 848ms) |
73
+ | **Backward** (steel GEMM ×2 + softmax−onehot coeff + dh accumulate) | ✅ **IN PRODUCTION** (`flashCceBackward` default, `BWD_STEEL_SOURCE`/`bwdSteelKernel`). Parity PASS e4b (dh 0.40%) + cpm (dh 0.28%); **754ms** @ e4b M=512 — phase-2 W dequant now uses MLX's fused `QuantizedBlockLoader` (HTw=group_size, reduction_dim=0), **5× the old SG's 3687ms exact** (was 1851ms with the scalar dequant). Peak **0.928 GB flat @ M=8192** (`[M,V]`-free preserved). Old SG kept as `MLX_BUN_CCE_BWD_NOSTEEL=1` fallback. |
74
+ | Old shipping backward | the **OLD hand-rolled SG kernel** (`[M,V]`-free, correct, slow 3687ms) — now the fallback only |
75
+
76
+ **So: BOTH directions are now fast + live (steel fwd 180ms, steel bwd 754ms, both
77
+ `[M,V]`-free + exact). M1 + the phase-2 dequant optimization DONE.** Backward is now
78
+ ~2.1× the ~360ms ideal (down from ~5×). Everything below (M2 prefix-share, M3 e4b probe,
79
+ M3-composition segmented+prefix, M4A integration tests) is also DONE — see sections.
80
+
81
+ ## Why this exists (the journey, so you don't repeat it)
82
+
83
+ - Our hand-rolled simdgroup_matrix GEMM runs at **~8% of M1 Max peak**; MLX's `steel`
84
+ quantized GEMM runs at **~46%**. We spent a long time micro-optimizing the
85
+ hand-rolled kernel (token tiling, h-coalescing, dequant staging, bank-conflict
86
+ padding) — all marginal, because the gap is the **BlockMMA register-tiling +
87
+ K-pipeline**, not staging. The decisive move was: **stop hand-rolling, copy MLX
88
+ verbatim.** It works and hits their speed.
89
+ - **MLX's `quantizedMatmul` does the whole head ~3.8× faster than our old kernel AND
90
+ exact** (fwd 151 + bwd 330 = 481ms). That's the `orpoFusedCe` head, already in the
91
+ repo. The ONLY reason to keep flash-CCE: it's a **single kernel** → `[M,V]`-free,
92
+ no per-chunk host-loop allocator churn.
93
+ - **Memory caveat (important, was over-claimed then corrected):** the head is NOT the
94
+ memory wall at long seq — the layer activations are. Avoiding the full `[M,V]`
95
+ (8.6 GB at e4b/8192) IS essential, but BOTH flash-CCE (+0.17 GB) and a properly
96
+ streamed fused head (+0.27 GB) achieve it. flash-CCE backward peak is **0.93 GB flat
97
+ at M=8192** (weights-dominated; kernel scratch ~5 MB; linear in M, flat in V). So
98
+ option 2 (steel backward) is justified for **SPEED + single-kernel allocation
99
+ flatness**, not because the fused head "blows up" (it doesn't, if streamed right).
100
+
101
+ ## Files
102
+
103
+ - `src/train/steel-qmm-header.ts` — **the verbatim MLX steel port** (auto-assembled
104
+ from MLX `quantized.h` + `steel/{gemm/mma.h,gemm/loader.h,gemm/transforms.h,
105
+ utils/integral_constant.h,utils/type_traits.h}`). Exports `STEEL_QMM_HEADER` (the
106
+ MSL header string). Contains: `BlockMMA`, `MMATile`, `BaseMMAFrag`, `tile_matmad`,
107
+ `BlockLoader`, `QuantizedBlockLoader`, `dequantize`, `get_pack_factor`, `qmm_t_impl`.
108
+ - `src/train/flash-cce.ts` — the head. `FWD_STEEL_SOURCE` + `fwdSteelKernel(gs,bits)`
109
+ (the **live forward**, dispatched by `useSteel` default). The old SG/scalar/lane
110
+ forward kernels + the current backward (`BWD_SG_SOURCE` / `bwdSgKernel`, the
111
+ hand-rolled persistent-acc[80] kernel) are still there.
112
+ - `src/train/loss.ts` — `makeFlashCceHeadVjp` (CustomVjp: fwd→Σlogp stash lse+blockMax,
113
+ bwd→flashCceBackward). `ChunkCtx.flash`. Routed via `orpoFlashCe` config flag.
114
+ - `src/train/trainer.ts` / `job.ts` — `orpoFlashCe` / `orpo_flash_ce` knob.
115
+ - `tests/train-orpo-fused-ce.test.ts` — flash-head fwd-parity + end-to-end training
116
+ tests (MLX_BUN_TEST_TRAIN=1, MiniCPM5).
117
+ - **Validated experiment scripts** (preserved, portable imports):
118
+ - `scripts/experiments/qmm-port-test.ts` — steel GEMM correctness vs quantizedMatmul.
119
+ - `scripts/experiments/qmm-port-speed.ts` — steel GEMM speed vs MLX (101 vs 106ms).
120
+ - `scripts/experiments/steel-fwd-test.ts` — forward epilogue (logp) vs reference.
121
+ - `scripts/experiments/steel-coeff-test.ts` — **backward phase-1** (logit→coeff) PASS.
122
+ - `scripts/experiments/steel-bwd-test.ts` — **full backward dh** (phase-1+2) PASS 1.6e-5.
123
+ - `scripts/experiments/bwd-peak.ts` — clean isolated backward peak (0.93 GB @ M=8192).
124
+ - `scripts/experiments/fwd-gemm-bench.ts` — MLX QM baseline (fwd 151 / bwd 330ms).
125
+
126
+ ## The architecture (what's proven)
127
+
128
+ Tile constants: **BM=8 tokens, BN=32 vocab, BK=32 hidden, WM=1, WN=4** (4 simdgroups
129
+ along N), `BK_padded = BK + 16/sizeof(T)`. The `WM=1,WN=4` choice is THE unlock:
130
+ BlockMMA's output tile is then split so **each simdgroup owns a different N-slice** —
131
+ which for the dh GEMM means each simdgroup owns a different **H-slice**, i.e. exactly
132
+ the persistent `acc[80]` layout we already proved works (160 regs/lane). So steel's
133
+ BlockMMA and the `[M,V]`-free persistent accumulator ARE compatible.
134
+
135
+ **Forward** (`FWD_STEEL_SOURCE`, live): grid `[128, NBLK, ⌈M/32⌉]` (BM=32 there, not 8),
136
+ per (token-block z, vocab-block y) threadgroup loop BN vocab-tiles → steel K-loop →
137
+ `Ctile.store` to threadgroup `Ls` → softcap + online-softmax → `[M,NBLK]` partials →
138
+ MLX merge → logp. `load_safe` for the last token-block (M%BM); vocab aligned for e4b/cpm.
139
+
140
+ **Backward** (proven in `steel-bwd-test.ts`, BM=8/WM=1/WN=4): per (token-block, vocab-
141
+ block) threadgroup, persistent `dmma = BlockMMA<float,float,8,H,32,1,4,false,false>`
142
+ Ctile = the dh accumulator. Loop vocab-tiles: phase-1 `lmma` BlockMMA logit → `Ls` →
143
+ coeff epilogue (`g·(onehot−softmax(logit−lse))·sech²`) → threadgroup `coeffS[8,32]`;
144
+ dequant W non-transposed → `Wd[32,H]`; `dmma.mma(coeffS, Wd)` accumulates `coeff@W`.
145
+ After the loop `dmma.store_result(dh,...)`.
146
+
147
+ ## Gotchas (hard-won — read before touching the header/kernels)
148
+
149
+ 1. **String.raw escaping**: when writing the header to a `.ts` string, do NOT escape
150
+ backslashes (mangles macro `\` line-continuations). Only fails if content has a
151
+ backtick or `${` (the MLX code has neither).
152
+ 2. **MLX preamble**: `mx.fast.metal_kernel` already provides `METAL_FUNC` + `using
153
+ namespace metal` (redef warning, harmless) but NOT `STEEL_CONST`/`MLX_MTL_CONST`/
154
+ `SIMD_SIZE`/`Int<>`/`pointer_element_t` — we add the macros + `type_traits.h` +
155
+ `integral_constant.h` ourselves.
156
+ 3. **`#pragma METAL internals`**: `type_traits.h` and `integral_constant.h` need it
157
+ (struct inheritance / `is_integral`). Include them **verbatim with their own
158
+ self-managed enable/disable pragmas** — do NOT strip + wrap manually (type_traits'
159
+ trailing `disable` will turn it off before integral_constant compiles → "derived
160
+ classes not supported").
161
+ 4. **W dequant convention** (in MANUAL dequant, e.g. phase-2 `Wd`): use
162
+ `perWord = 32/bits` (4 elems per uint32 at 8-bit), NOT `get_pack_factor<bits,8>=1`
163
+ (that's MLX's byte-based packing for the QuantizedBlockLoader's internal uint8
164
+ addressing). Mixing these gave a 100%-wrong dh.
165
+ 5. **`BlockMMA.mma` reads `Bs[BK,BN]` full-width** from threadgroup — so you can't wrap
166
+ it for a full-H persistent accumulator (would need `W[32,2560]`=320KB tg). Either
167
+ keep H small (test) or tile H with manual `tile_matmad` (production).
168
+ 6. Watchdog: per-threadgroup vocab slice must stay bounded (`VOCAB_BLOCK=8192`).
169
+
170
+ ## NEXT STEPS — the plan (in order)
171
+
172
+ ### 1. H-tiling in the steel backward phase-2 (the one real remaining piece)
173
+ The test stages the full `W[32,H]`; at e4b H=2560 that's 320 KB > 32 KB threadgroup.
174
+ Production must tile H:
175
+ - dh accumulator = `MMATile<float, 1, H/32>` per simdgroup (= its H-quarter; 80 frags
176
+ @ e4b = the proven `acc[80]` register budget).
177
+ - per vocab-tile: loop H in `BKh` chunks → dequant `W[32, BKh]` → threadgroup (small) →
178
+ load coeff A-frags `MMATile<1,4>` + W B-frags `MMATile<4, BKh/8>` → `tile_matmad(
179
+ D_sub, A, B_sub, D_sub)` into the right sub-frags of the persistent D.
180
+ - The intricate bit is the manual sub-frag indexing into D (BlockMMA does this
181
+ internally; here we do `tile_matmad` by hand because D persists across vocab-tiles).
182
+ - Validate against `steel-bwd-test.ts` (extend it to H=2560, compare to QM(coeff,W,t=F)).
183
+
184
+ ### 2. Vocab-blocking grid + atomic dh
185
+ - Grid `[128, NBLK, ⌈M/BM⌉]`; each (token-block, vocab-block) computes its block's dh
186
+ contribution, **atomic-add** into device `dh` (across the NBLK blocks) — same pattern
187
+ the current `BWD_SG_SOURCE` already uses (`atomicOutputs`, `initValue:0`).
188
+ - Need `lse` (have it, forward returns it) + `blockMax` (optional, for the block-skip)
189
+ + `gv` (cotangent) passed in — same inputs as the current backward.
190
+
191
+ ### 3. Wire into `flashCceBackward`
192
+ - Add `BWD_STEEL_SOURCE` + `bwdSteelKernel(gs,bits)` (header = STEEL_QMM_HEADER + defines).
193
+ - Dispatch: `useSteel` default when H,V,blockV %32 (mirror the forward's gate).
194
+ - Keep the old `bwdSgKernel` as fallback (H%32!=0) + the float4 scalar as last resort.
195
+ - The CustomVjp (`makeFlashCceHeadVjp`) already calls `flashCceBackward` — no change there.
196
+
197
+ ### 4. Validate + benchmark
198
+ - Parity: `E4B=1 M=512 bun scripts/experiments/flash-cce-parity.ts` (+ cpm, + M=200).
199
+ Expect dh ~0.4% (bf16-class) vs autograd.
200
+ - Bench vs baselines: fused head **481ms** (speed), current flash-CCE (memory parity).
201
+ Target: steel backward in the same class as the forward (~hundreds of ms, vs 3375).
202
+ - Memory: `scripts/experiments/bwd-peak.ts` should stay ~0.93 GB @ 8192 (flat).
203
+ - **CAP ANY MEMORY PROBE well under 32 GB** and eval+free per iteration — a naive
204
+ full-graph host-loop allocation swapped the machine to 23 GB once. Single-kernel
205
+ flash probes are safe by construction.
206
+
207
+ ### 5. (optional) softcap + filter/block-skip parity in the steel backward
208
+ - Softcap sech² is in the coeff epilogue already (steel-bwd-test uses cap=0; cap>0 path
209
+ is written, validate it).
210
+ - The Apple-CCE coeff filter + blockMax skip are **opt-in, default-off** approximations
211
+ (data-dependent; flat-softmax probes showed dh sensitivity). They live in the OLD
212
+ backward; decide whether to port them to steel (probably not needed — steel is fast
213
+ enough exact).
214
+
215
+ ## Decision context (for when you pick the shipping config)
216
+
217
+ - **Now**: `orpoFlashCe` = steel forward (fast) + old SG backward (slow but `[M,V]`-free).
218
+ Trains, memory-flat. `orpoFusedCe` = MLX QM both ways (481ms, exact, `[chunk,V]`
219
+ transient).
220
+ - **After option 2 lands**: `orpoFlashCe` = steel fwd + steel bwd = fast AND `[M,V]`-free
221
+ AND single-kernel — strictly best; removes the speed/memory tradeoff.
222
+ - The one genuinely-unanswered question: does the fused head's host-loop chunked backward
223
+ churn MLX's allocator at real e4b/8192 training? An end-to-end training-step peak
224
+ measurement (model + activations + head, with segmented/checkpointed backward) decides
225
+ whether the single-kernel property matters in practice. Worth measuring once.
226
+
227
+ ---
228
+
229
+ # END-STATE ROADMAP — "CPM5 test run with everything → e4b overnight @ 8192"
230
+
231
+ **The actual goal.** Two deliverables Josh runs himself (we wire, we do NOT kick off
232
+ the overnight run — ground rule):
233
+ - **(A)** A short **MiniCPM5** test training session using ALL optimizations — proves
234
+ the whole stack composes and trains (loss decreases).
235
+ - **(B)** An **e4b overnight** ORPO run at seq **8192** — the real target.
236
+
237
+ "ALL of our stuff" = flash-CCE head (fwd+bwd at MLX speed, `[M,V]`-free) + prefix-sharing
238
+ (single fwd/bwd, prompt shared) + segmented/checkpointed backward (layer-activation
239
+ memory at long seq) + LoRA.
240
+
241
+ ## What's already wired (so you don't redo it)
242
+ - flash-CCE **forward**: live + default (`flashCceForward`).
243
+ - flash-CCE head CustomVjp (`makeFlashCceHeadVjp`) ↔ `orpoFlashCe` flag ↔ trainer.
244
+ - flash threaded into BOTH non-segmented (`fusedLogpMeanB1`) and **segmented**
245
+ (`fusedLogpMeanFromHidden`) head paths.
246
+ - Segmented backward exists (`SegmentedBackwardOrpo` / `…Gemma4`), `segmentSize` flag.
247
+ - ORPO B=1 path (`branchLogpMean`→fused/flash) + tests (MiniCPM5 flash training PASSES).
248
+ - prefix-sharing math (`orpoLossPrefixShared`, MiniCPM5, bit-exact, parity script).
249
+
250
+ ## Milestones (dependency order)
251
+
252
+ ### M1 — flash-CCE backward → production (the steel backward) ✅ DONE (2026-06-18)
253
+ H-tiled phase-2 (Design C: persistent `MMATile<1,H/32>` accumulator + per-H-tile temp
254
+ BlockMMA over bounded `Wd[32,128]` + lane-local frag accumulation) → vocab-blocking grid
255
+ + atomic dh → `BWD_STEEL_SOURCE`/`bwdSteelKernel` wired into `flashCceBackward` (`useSteel`
256
+ gate: H,V,blockV %32 + H%128). Parity PASS e4b (dh 0.40%) + cpm (dh 0.28%); 2.0× the old
257
+ SG (1851 vs 3687ms e4b M=512 exact); peak 0.928 GB flat @8192. Design validated first in
258
+ `scripts/experiments/steel-bwd-htile-test.ts` (H=2560, PASS 1.1e-4 rel). Both flash
259
+ directions now fast. KEY INSIGHT that avoided the "intricate manual tile_matmad": a TEMP
260
+ BlockMMA per H-tile whose Ctile is accumulated into the persistent D via `frag += frag`
261
+ (lane-local — the per-lane element layout is identical, so no get_coord math needed).
262
+
263
+ ### M2 — prefix-sharing → trainer (single fwd/bwd) ✅ DONE (2026-06-18)
264
+ - `orpoPrefixShared` trainer flag (+ `orpo_prefix_shared` job field) dispatches to
265
+ `orpoLossPrefixShared` (MiniCPM5) / `orpoLossPrefixSharedGemma` (Gemma4 — the sibling
266
+ already existed) via `splitPrefixBatch`, falling back to two-forward `orpoLoss` when a
267
+ row's chosen/rejected prompts differ. Guarded against `segmentSize>0` (composition is
268
+ M3). End-to-end CPM5 training test PASSES (loss decreases).
269
+ - **Composed with the flash head**: new `branchLogpMeanGathered` gathers each branch's
270
+ `[M,H]` response hiddens → `fusedRespLogpMean(flash)` per branch (reuses the analytic
271
+ per-branch VJP — cleaner than splitting one call's non-uniform length-normalized
272
+ cotangent). Validated: flash-routed prefix-share matches whole-vocab prefix-share to
273
+ **0.018%** (bf16-class) in `prefix-shared-parity.ts`. So prefix-share is now `[M,V]`-free
274
+ end to end (one model fwd + per-branch flash fwd/bwd).
275
+ - **e4b caveat unchanged**: e4b's bf16 length-sensitivity (~2–14%) is a numerical-parity
276
+ nicety, not correctness. MiniCPM5 is bit-exact (forward 0%, grads ~1% at P=512). NOTE:
277
+ for the e4b OVERNIGHT at seq 8192, prefix-share can't be used until M3 composes it with
278
+ the segmented backward — the overnight uses **segmented + flash** (validated config shape
279
+ below), with prefix-share an M3 follow-on.
280
+
281
+ ### M4A — CPM5 smoke test of the complete new system ✅ DONE (2026-06-18)
282
+ Automated end-to-end tests in `tests/train-orpo-fused-ce.test.ts` (run with
283
+ `MLX_BUN_TEST_TRAIN=1`), all PASS — each trains CPM5 20 iters, asserts finite +
284
+ decreasing loss + no OOM/NaN:
285
+ - **flash head** (steel fwd+bwd) + LoRA.
286
+ - **prefix-share + flash** (the single-concat-forward + `[M,V]`-free head).
287
+ - **segmented + flash** (the e4b-overnight config shape: gradient-checkpointed layer
288
+ activations + `[M,V]`-free head). These exercise every new piece composing in a real
289
+ ORPO step. (prefix-share + segmented together = M3, not yet composed.)
290
+
291
+ ### M3 — long-sequence memory @ e4b/8192 (the layer activations, NOT the head)
292
+ - Compose segmented/checkpointed backward (`segmentSize>0`) + flash head + prefix-share.
293
+ The head is ~free in memory (0.93 GB flat); the wall is the transformer activations,
294
+ which segmented backward already handles (peak 10.9→3.3 GB @2048 per the notes).
295
+ - **THE measurement that actually decides "does e4b train at 8192"**: a real end-to-end
296
+ training-step peak (model + activations + head) with segmented backward on. Do this
297
+ EARLY — it tells us the segment size needed to fit 8192 in 32 GB, and whether the head
298
+ choice (flash vs fused) matters at all. (Cap probes; never swap the machine.)
299
+
300
+ ### M3/M4B — e4b @ 8192 memory probe + overnight config ✅ PROBED (2026-06-18)
301
+ `scripts/bench-orpo.ts` sweeps the new-system configs (`flash head` / `segmented + flash`
302
+ / `prefix + flash`); `ONLY_NEW=1` skips the legacy baselines, `CONFIG="<substr>"` runs one,
303
+ and it now prints peak GB + a finite-loss check. The **`segmented + flash` row IS the
304
+ overnight config shape**.
305
+
306
+ **MEASURED on the M1 Max 32 GB (e4b, segmented+flash, SEG=2, `MLX_BUN_PERF_KERNEL=0
307
+ MLX_BUN_FUSED_GELU=0`):**
308
+ | SEQ | peak GB | ms/step |
309
+ |---|---|---|
310
+ | 1024 | 8.66 | 20.7k |
311
+ | 2048 | 10.17 | 40.4k |
312
+ | 4096 | 11.68 | 80.8k |
313
+ | **8192** | **16.14** | **175k** (loss 0.093→0.091 finite, decreasing) |
314
+
315
+ Footprint is **linear** in seq (~+1.5 GB / 1024 tokens — segmenting + flash attention keep
316
+ it linear, not quadratic; the head is flat 0.93 GB). **e4b @ 8192 fits in 16.14 GB with
317
+ ~16 GB headroom — the historical "e4b OOMs ≥2048" ceiling is BROKEN.** SEG=2 is the sweet
318
+ spot: SEG=8 @ 8192 hit **33.47 GB (swapped) and was slower** — bigger segments hold more
319
+ activations, so small segments win here. The run command:
320
+ ```
321
+ MODEL=<e4b-snapshot> CONFIG="segmented + flash" SEQ=8192 SEG=2 ITERS=3 \
322
+ MLX_BUN_PERF_KERNEL=0 MLX_BUN_FUSED_GELU=0 bun scripts/bench-orpo.ts
323
+ ```
324
+
325
+ **VALIDATED overnight config (segmented + flash + LoRA):**
326
+ ```
327
+ method:"orpo", orpoFlashCe:true (implies fused), segmentSize:2, orpoChunkSize:512,
328
+ maxSeqLen:8192, env MLX_BUN_PERF_KERNEL=0 + MLX_BUN_FUSED_GELU=0, rank/scale/lr + real
329
+ dpo data per the run, LoRA targets default. Peak ~16 GB, ~2.9 min/step on the M1 Max.
330
+ ```
331
+ Prefix-share is NOT in the overnight (segmented + prefix-share = the M3-composition below);
332
+ the overnight uses segmented + flash. **Josh starts the overnight** (ground rule).
333
+
334
+ ### M3-composition — segmented backward + prefix-share together ✅ LANDED MiniCPM5 (2026-06-18)
335
+ New `SegmentedBackwardOrpoPrefix` (`src/train/segmented.ts`): streams ONE concat
336
+ `[prompt; chosen; rejected]` through the segmented backward with `PrefixSharedCache`
337
+ (block-sparse mask) + `setMiniCpmPrefixPlan` active during the streaming forward AND every
338
+ segment's `mlx_vjp` recompute; the head VJP gathers chosen/rejected response positions
339
+ (`branchLogpMeanGathered`, the `[M,V]`-free per-branch flash head) and scatter-adds its dh
340
+ into the `[1,T,H]` boundary that seeds the segmented backward. Trainer composes it (old
341
+ guard removed) with a two-forward-segmented fallback on prompt mismatch. Validated
342
+ (`scripts/experiments/prefix-shared-segmented-parity.ts`): grads **2.27%** bf16-class
343
+ (single-segment diagnostic 1.16% → it's segment-boundary reassociation + flash-head 0.4%
344
+ stacking, not a bug), loss **0.052%**, peak **32% lower** (3.64→2.47 GB @ P=512; grows to
345
+ 59% lower @ P=1024) — prompt-encode-once now holds at long seq with flat ~2.5 GB activation.
346
+ Test #6 in `tests/train-orpo-fused-ce.test.ts` (PASS). **e4b (Gemma4) DEFERRED** — trainer
347
+ throws a clear error; the logical-position sliding-window prefix mask + donor-KV threading
348
+ across segmented donor boundaries is the remaining follow-on (the overnight uses
349
+ segmented+flash without prefix-share, which works).
350
+
351
+ ### M4 — runnable configs + the two runs
352
+ - **(A) MiniCPM5 test config**: short seq, small iters, `orpoFlashCe` (or prefix-share
353
+ mode) + segmented on, LoRA. Run `trainLora`; assert loss decreases. This is the
354
+ "everything composes" gate — basically the existing flash training test scaled up.
355
+ - **(B) e4b overnight config**: seq 8192, `segmentSize` tuned from M3, flash head,
356
+ prefix-share (or two-forward if e4b variance is a problem), LoRA, full dataset
357
+ (`scripts/curate-ultrafeedback.ts` / the dpo data). **Josh runs this** — we hand him a
358
+ validated config + command, we do not start it.
359
+
360
+ ## THE POINT: the runs are INTEGRATION TESTS of the NEW system
361
+ Josh does NOT care about training quality/results. The CPM5 run and the e4b run exist to
362
+ **exercise everything we just built — steel backward + flash head + prefix-sharing — end
363
+ to end in a real ORPO training step** and confirm the new code path RUNS (loss is finite,
364
+ decreases a bit, no crash/NaN/OOM). So we FINISH all the new pieces, wire them ALL the
365
+ way through, then run. Do NOT ship the old slow backward as a crutch — the steel backward
366
+ is part of what's being tested.
367
+
368
+ ## Order of execution (build the new system, then test it)
369
+ 1. **M1 — steel backward → production** (H-tile phase-2, vocab-block + atomic dh, wire
370
+ `bwdSteelKernel` into `flashCceBackward`, parity e4b/cpm). The new backward IS under
371
+ test; it must be in the path.
372
+ 2. **M2 — prefix-sharing → trainer + compose with flash head** (gather `[Rc+Rr]` → one
373
+ flash-head call → split by branch). MiniCPM5 first.
374
+ 3. **M4(A) — CPM5 test run = the smoke test of the complete new system**: flash fwd+bwd
375
+ (steel) + prefix-share + segmented + LoRA, all on. Run `trainLora` a handful of steps;
376
+ assert finite + decreasing loss + no OOM. THIS is the test — it validates every new
377
+ piece composes in a real step. If it passes, the new system works.
378
+ 4. **M3 — e4b @ 8192 dry-run**: same new system on e4b, seq 8192, segmented on. First a
379
+ memory/step-peak check (cap it; the head is flat 0.93 GB, the wall is activations →
380
+ tune `segmentSize`), then a few real steps to confirm it RUNS. (e4b prefix-share bf16
381
+ variance is fine — we're not checking quality; if it NaNs, fall back to two-forward.)
382
+ 5. **M4(B) — e4b overnight**: hand Josh a validated config + command for the longer run
383
+ (we wire + validate a few steps; **Josh starts the overnight** — ground rule).
384
+
385
+ **Risk register**: M1 mechanical (scoped — architecture+correctness already proven). M2/e4b
386
+ bf16 variance = non-blocking for a RUN test (two-forward fallback; quality irrelevant). M3
387
+ e4b/8192 memory = the one real unknown, but it's an activations/segmentSize question, not
388
+ a head question. The runs test integration; the pieces are individually validated.
389
+
390
+ ---
391
+
392
+ # GLOSSARY — every named piece, what it is, status, where
393
+
394
+ Status legend: ✅ done+live · 🟢 done, not production-wired · 🟡 partial · 🔬 proven in a
395
+ test only · 📦 pre-existing (not ours this arc) · 🪦 dead end.
396
+
397
+ ## A. The "head" = how we turn response hiddens `[M,H]` into ORPO log-probs
398
+ (M = number of response tokens; V = vocab; H = hidden.) These are *alternative
399
+ implementations of the same math* (`logp_t = target_logit − logsumexp_v`).
400
+ - **Full-logits head** 📦 — the naive reference: materialize `[M,V]` logits, logsumexp.
401
+ Correct, huge memory. Only used as the parity oracle.
402
+ - **Chunked head** (`orpoChunkSize`) 📦 — token-chunked + per-chunk `Checkpoint`; bounds
403
+ `[M,V]`→`[chunk,V]`. Older, autograd through the head.
404
+ - **Fused linear-CE head** (`orpoFusedCe`, `fusedRespLogpMean`) 📦✅ — a `CustomVjp` with
405
+ an ANALYTIC softmax−onehot backward (no autograd through the head), using **MLX
406
+ `quantizedMatmul`** for both GEMMs, token-chunked. Fast (**481ms** e4b), exact,
407
+ `[chunk,V]` transient. The speed baseline we're trying to match.
408
+ - **Vocab-blocked head** (`makeVocabBlockedHeadVjp`) 📦 — pure-MLX online-softmax over
409
+ vocab blocks; `[chunk, vocabBlock]` only. Slow-ish; the MLX-only CCE.
410
+ - **flash-CCE head** (`orpoFlashCe`, `makeFlashCceHeadVjp`) — **OURS**, the Metal-kernel
411
+ head: the vocab loop lives INSIDE one Metal kernel, so neither `[M,V]` nor a
412
+ dequantized `[V,H]` ever touches HBM → `[M,V]`-free, single kernel. Forward ✅ live,
413
+ backward 🟡 (old slow kernel live; steel kernel 🔬 proven, not wired). `flashCceForward`
414
+ + `flashCceBackward` in `src/train/flash-cce.ts`.
415
+
416
+ ## B. flash-CCE kernel internals (`src/train/flash-cce.ts`) — OURS
417
+ The flash head dispatches to one of several kernel implementations of fwd/bwd:
418
+ - **`FWD_STEEL_SOURCE` / `fwdSteelKernel`** ✅ — the NEW steel-GEMM forward (default). 180ms.
419
+ - `FWD_SG_SOURCE` / `fwdSgKernel` 🪦-ish — old hand-rolled simdgroup_matrix forward (848ms).
420
+ - `FWD_SOURCE` / `fwdKernel` — scalar fallback (H%32≠0).
421
+ - `FWD_LANE_SOURCE` 🪦 — lane-sliced (2.6× slower, dead end, kept gated).
422
+ - **`BWD_SG_SOURCE` / `bwdSgKernel`** ✅(live) — CURRENT production backward: hand-rolled
423
+ simdgroup_matrix, persistent `acc[80]` dh accumulator, phase-1 (logit→coeff) +
424
+ phase-2 (dh=coeffᵀ@W). Correct, `[M,V]`-free, **slow (3375ms)**.
425
+ - `BWD_SOURCE` / `bwdKernel` — float4 scalar backward fallback.
426
+ - **STEEL BACKWARD** 🔬 — the NEW one: `scripts/experiments/steel-bwd-test.ts`, validated
427
+ dh 1.6e-5, NOT yet a production kernel (`BWD_STEEL_SOURCE` is M1's job).
428
+
429
+ ## C. The steel port (`src/train/steel-qmm-header.ts`) — OURS (verbatim from MLX)
430
+ MLX's quantized `steel` GEMM, copied verbatim so we run at MLX speed (~46% peak vs our
431
+ hand-rolled 8%). The `STEEL_QMM_HEADER` string contains:
432
+ - **`BlockMMA`** — the tiled matmul (BM×BN output, BK contraction, WM×WN simdgroups, the
433
+ K-pipeline). The thing that makes MLX fast.
434
+ - **`MMATile` / `BaseMMAFrag` / `tile_matmad`** — the 8×8 simdgroup-matrix fragment
435
+ abstraction BlockMMA is built on (used directly for the persistent dh accumulator).
436
+ - **`QuantizedBlockLoader`** — loads packed quantized weights and dequantizes them
437
+ straight into the threadgroup matmul tile (fused dequant, no separate pass).
438
+ - **`BlockLoader`** — loads the dense input (X / coeff) into the tile.
439
+ - **`qmm_t_impl`** — MLX's full quantized-matmul orchestration (we use it for the
440
+ forward; the backward composes BlockMMA + tile_matmad ourselves).
441
+ - Tile params we use: **BM=8/32, BN=32, BK=32, WM=1, WN=4** (the `WM=1,WN=4` is the key
442
+ that makes BlockMMA's output tiling match our `[M,V]`-free per-simdgroup H-slice accum).
443
+
444
+ ## D. The epilogue (what we FUSE on top of the steel GEMM) — OURS
445
+ MLX has no "quant-matmul + softcap + response-logp" op, so we add:
446
+ - **Softcap** — `cap·tanh(raw/cap)` (Gemma final-logit cap; e4b cap=30, cpm none) + its
447
+ `sech²` chain-rule term in the backward.
448
+ - **Online softmax** — running `(max, sumexp)` over vocab blocks → `lse` (forward).
449
+ - **Gather / target capture** — pick the target token's logit per row.
450
+ - **softmax−onehot coeff** — the backward gradient `coeff = g·(onehot − softmax)·sech²`.
451
+ - **MLX merge** — combine the `[M,NBLK]` per-vocab-block partials → `logp`/`lse` in MLX.
452
+
453
+ ## E. Backward approximations (opt-in, default-off, data-dependent) — OURS
454
+ Both are Apple-CCE-style skips that proved unreliable on flat/synthetic softmax, so they
455
+ are OFF by default and live in the OLD backward; probably NOT needed once steel is fast.
456
+ - **Coeff filter** (`MLX_BUN_CCE_BWD_FILTER_EPS`) — skip phase-2 chunks whose whole coeff
457
+ block < eps.
458
+ - **blockMax vocab-block skip** (`MLX_BUN_CCE_BWD_BLOCK_EPS`) — skip whole cold
459
+ (token-block, vocab-block) programs using the forward's `blockMax`.
460
+
461
+ ## F. Model-level optimizations (above the head)
462
+ - **Prefix-sharing** (`src/train/prefix-shared.ts`, `orpoLossPrefixShared`,
463
+ `blockSparseMask`, `PrefixSharedCache`) 🟢 — OURS (earlier this arc). One forward over
464
+ `[prompt ; chosen ; rejected]` with a block-sparse mask (chosen & rejected each attend
465
+ the shared prompt but NOT each other) → prompt encoded ONCE, single fwd/bwd for the
466
+ pair. Bit-exact for MiniCPM5; e4b has a bf16 length-sensitivity caveat. Built + parity
467
+ script, NOT wired into the trainer.
468
+ - **Segmented backward** (`src/train/segmented.ts`, `SegmentedBackwardOrpo*`,
469
+ `segmentSize`) 📦✅ — gradient checkpointing for the TRANSFORMER LAYER activations (the
470
+ real memory wall at long seq; orthogonal to the head). Composes with the flash head
471
+ (`fusedLogpMeanFromHidden` carries the `flash` flag).
472
+
473
+ ## G. Wiring / config layer
474
+ - **`makeFlashCceHeadVjp`** (`loss.ts`) — wraps flashCceForward/Backward as a `CustomVjp`
475
+ (fwd→Σlogp, stash lse+blockMax; bwd→dh×cotangent). OURS.
476
+ - **`ChunkCtx`** (`loss.ts`) — the head config struct: `{fused, flash, chunkSize,
477
+ vocabBlock, sink}`.
478
+ - **`orpoLoss` / `branchLogpMean`** (`loss.ts`) — the ORPO loss entry; B=1 routes to the
479
+ fused/flash head, B>1 to full-logits.
480
+ - **Trainer flags** (`trainer.ts` / `job.ts`): `orpoFusedCe`, `orpoFlashCe`, `segmentSize`,
481
+ `orpoChunkSize`, `orpoLambda`. CLI: `--orpo-flash-ce` etc.
482
+
483
+ ## H. Baselines / numbers to remember
484
+ - MLX `quantizedMatmul` raw GEMM (the speed reference): e4b fwd **151ms**, bwd **330ms**.
485
+ - Fused head total: **481ms** exact.
486
+ - Our steel forward: **180ms** (raw GEMM 101ms; +epilogue). Old SG forward: 848ms.
487
+ - Our backward: old SG **3375ms** (live); steel target ~hundreds of ms (M1).
488
+ - flash-CCE backward peak: **0.93 GB flat @ M=8192** (weights-dominated; kernel ~5MB;
489
+ linear in M, flat in V).