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,348 @@
1
+ # Decode perf "look again" — adversarial re-exam of the M=1 floor claim (2026-07-01)
2
+
3
+ > **2026-07-02 CORRECTION — the host-graph-tax claim is itself overturned; see
4
+ > §7.** The fix-plan items 1a–1d were built on "host graph build is serial with
5
+ > the GPU"; the overlap spike (decode-overlap-probe.ts) shows the pipelined loop
6
+ > ALREADY hides the build. The §3 wall times and %-of-roofline stand; the
7
+ > §4(b)/(c) decomposition does not — the whole gap is GPU-side.
8
+
9
+ **Machine:** Apple M1 Max, 32 GB (MacBookPro18,2), `sysctl` verified. All eval-DB
10
+ rows are also M1 Max — same box. **All numbers below are directional (session,
11
+ loaded-machine caveat); quotable numbers remain benchmark.sh-gated.** GPU idle
12
+ verified before every probe (one contaminated bandwidth run was discarded and
13
+ re-run; another agent's `bun test` was live during it).
14
+
15
+ **Mandate:** re-attack the PLAN-archive megakernel post-mortem conclusion —
16
+ "mlx per-op decode = 4.62 ms vs ~4.5 ms pure weight-read floor (~0.12 ms
17
+ overhead, near optimal)" — the way the ORPO backward was attacked.
18
+
19
+ ## Verdict up front
20
+
21
+ **The floor claim was wrong.** The ~4.5 ms "pure weight-read floor" for CPM5 was
22
+ never derived from measured bandwidth and overstates the true floor by ~70%:
23
+
24
+ - CPM5 reads **0.694 GB** of weights/token (safetensors-header accounting:
25
+ 0.448 GiB layers + 0.198 GiB untied lm_head; the 0.198 GiB embed table is
26
+ row-gathered, not streamed). The old "~1 GB" figure counted the embed table.
27
+ - Measured achieved bandwidth on this M1 Max: **~337 GB/s raw streaming read**
28
+ (4 GiB bf16 sum), **~250–295 GB/s in mlx's own `quantized_matmul` GEMV** at
29
+ decode shapes (large matrices). 4.5 ms for 0.694 GB implies 154 GB/s — no
30
+ measurement supports that.
31
+ - True CPM5 floor: **~2.6 ms/tok** at GEMV-achieved BW (~2.1 ms at raw-read BW).
32
+ Production-shaped decode measures **4.06 ms/tok**. That is **~64% of the
33
+ measured roofline**, not 97%.
34
+
35
+ The megakernel was still right to shelve (it was slower for its own reasons),
36
+ but "mlx is already at the floor" is false for every model except the 12B.
37
+ There is ~1.4 ms/tok recoverable on CPM5 (+35–55%), ~5–7 ms on e4b (+30–50%),
38
+ ~8 ms on the 26B (+40–60%), and ~3 ms (~9%) on the 12B.
39
+
40
+ Two inconsistencies in the record corroborate that the old floor was never
41
+ measured: `megakernel-perf.ts`'s own comment says "floor ~394 tok/s" (2.54 ms —
42
+ 0.694 GB at the M4 Pro's 273 GB/s spec sheet), while the post-mortem says
43
+ ~4.5 ms; and the production eval-DB h2h rows (255–266 tok/s = 3.76–3.92 ms)
44
+ were ALREADY faster than the claimed 4.5 ms "floor" when the post-mortem was
45
+ written.
46
+
47
+ ## 1. Measured bandwidth (this machine, idle, `decode-roofline-bw.ts`)
48
+
49
+ | probe | result |
50
+ |---|---|
51
+ | raw streaming read, bf16 sum 1/2/4 GiB | 299 / 320 / **337 GB/s** |
52
+ | qmv 4-bit GEMV 540 MiB (12B tied head) | 2.12 ms → **268 GB/s** |
53
+ | qmv 4-bit GEMV 360 MiB (e4b tied head) | 1.31 ms → **289 GB/s** |
54
+ | qmv 4-bit GEMV 108 MiB (CPM5 head) | 0.45 ms → **249 GB/s** |
55
+ | qmv 8-bit GEMV 60 MiB | **295 GB/s** |
56
+ | qmv 4-bit GEMV 32 MiB (12B MLP) | **256 GB/s** |
57
+ | qmv 4-bit GEMV 3.8 MiB (CPM5 MLP) | 0.022 ms → **183 GB/s** |
58
+ | qmv 4-bit GEMV 1.3 MiB (small qkv) | 0.013 ms → **100 GB/s** |
59
+ | bf16 GEMV 113 MiB | **296 GB/s** |
60
+ | dependent tiny-op chain (in one eval) | **~10–15 µs/op** incl. host encode |
61
+
62
+ GEMVs were chained back-to-back inside one eval over distinct matrix copies
63
+ (cycling ≥ SLC) — the in-step cost, not eval round-trip latency. Methodology
64
+ trap worth recording: mlx is lazy — disposing unevaluated outputs silently
65
+ skips their compute; the first draft of this probe "measured" 4.4 TB/s.
66
+
67
+ So: mlx's `qmv_fast` reaches **74–88% of raw machine bandwidth on large
68
+ matrices** and degrades hard below ~10 MiB (fixed per-kernel cost). Small-model
69
+ decode is a mix of both regimes.
70
+
71
+ ## 2. Weights + KV bytes per decode token (safetensors headers, exact)
72
+
73
+ | model | streamed weights/tok | how |
74
+ |---|---|---|
75
+ | CPM5-1B | **0.694 GB** | 0.448 layers + 0.198 lm_head (untied; embed row-gathered) |
76
+ | e4b | **3.536 GB** | 2.547 layers + 0.664 tied head + 0.082 altup; 2.789 GiB per-layer embeds are ROW-gathered; vision/audio 0.891 GiB untouched |
77
+ | 12B | **8.860 GB** | 7.255 layers + 0.996 tied head |
78
+ | 26B-A4B | **3.444 GB** | 1.597 dense + 0.730 tied head + 8/128 × 14.091 GiB experts (top_k 8 of 128, all 30 layers) |
79
+
80
+ KV read/token (bf16): CPM5 24.6 KB/pos (24L·2kv·128d); e4b 7 full + 35
81
+ sliding@512 (2kv·256d) → 45 MB @600, 154 MB @8k; 12B 8 full + 40 sliding@1024
82
+ (8kv·256d) → 236 MB @600, 872 MB @8k; 26B 5 full + 25 sliding@1024 → 147 MB @600.
83
+
84
+ ## 3. Roofline table (floor at 270 GB/s GEMV-achieved; ceiling at 337 GB/s raw)
85
+
86
+ Measured with `decode-roofline-step.ts` (mirrors generateInner's pipelined
87
+ loop; greedy; bf16 KV; clearCache cadence; two passes each; uncompiled arm
88
+ unless noted).
89
+
90
+ | model @ctx | bytes/tok | floor ms (270) | ceiling tok/s (337) | measured ms (tok/s) | GPU ms | host graph ms | read ms | % of floor-roofline |
91
+ |---|---|---|---|---|---|---|---|---|
92
+ | CPM5 @132 | 0.70 GB | 2.58 | 483 | 4.06 (246) | 3.0 | 1.0 | 0.02 | **64%** |
93
+ | CPM5 @4k | 0.80 GB | 2.94 | 424 | 5.03 (199) | 4.0 | 1.0 | 0.02 | **58%** |
94
+ | e4b @600 | 3.58 GB | 13.3 | 94 | 18.9 (53.0) | 15.1 | 3.6 | 0.03 | **70%** |
95
+ | e4b @600 compiled | 3.58 GB | 13.3 | 94 | 18.1 (55.2) | 16.8 | 1.2 | 0.03 | 73% |
96
+ | e4b @8k | 3.69 GB | 13.7 | 91 | 21.3 (46.9) | 17.4 | 3.5 | 0.03 | **64%** |
97
+ | e4b @8k compiled | 3.69 GB | 13.7 | 91 | 21.5 (46.5) | 20.2 | 1.3 | 0.03 | 64% |
98
+ | 12B @600 | 9.10 GB | 33.7 | 37 | 36.8 (27.2) | 33.0 | 3.4 | 0.03 | **92%** |
99
+ | 12B @8k | 9.73 GB | 36.0 | 35 | 37.5–40.9 (26.6–24.4) | 33.2–36.1 | 3.4 | 0.03 | **~93%** |
100
+ | 12B @8k compiled | 9.73 GB | 36.0 | 35 | 39.4–41.6 | 37.0–39.7 | 1.9 | 0.03 | (thermal drift muddies the arm A/B) |
101
+ | 26B @600 | 3.59 GB | 13.3 | 92 | 21.5–22.3 (46.5–44.8) | 17.3 | 3.7 | 0.02 | **60–62%** |
102
+
103
+ (12B pass-to-pass drift @8k was ~8% — machine warming across the long runs;
104
+ within-run medians are the trustworthy split.)
105
+
106
+ **The inversion:** the bigger the model, the closer to the roofline. The 12B's
107
+ GPU time IS the bandwidth floor (its GEMVs are all in the 250–295 GB/s regime
108
+ and its dispatch count amortizes); the small/sparse models leave 30–40% on the
109
+ table. The old conclusion was accidentally ~true for the 12B only — it was
110
+ derived on CPM5, where it is most wrong.
111
+
112
+ ## 4. Gap decomposition
113
+
114
+ Per-step wall time = GPU step + host graph build + readback, **serial** (PLAN's
115
+ structural finding re-confirmed: async_eval hides only the ~0.03 ms token read,
116
+ never the graph build — measured directly in every row above).
117
+
118
+ **(a) Weight+KV read floor** — §3 table. Dominant everywhere; the only true
119
+ "physics".
120
+
121
+ **(b) GPU overhead above floor** (dispatch gaps + small-GEMV inefficiency +
122
+ elementwise/norm/rope kernels):
123
+ - CPM5: 0.4 ms @132 rising to 1.0 @4k (KV-append/attention kernels are in the
124
+ 100–183 GB/s small-buffer regime).
125
+ - e4b: **1.8 @600 → 3.7 ms @8k** — the worst dense offender. 42 layers of
126
+ per-layer-input machinery (gate/proj GEMVs ~1–2 MiB each = the 100 GB/s
127
+ regime, ×42), altup ops, KV-share plumbing → dispatch count several hundred
128
+ per token at ~5–15 µs each.
129
+ - 12B: ~0 (at floor).
130
+ - 26B: **~4.0 ms** — gather-qmm expert GEMVs: each expert slice is ~3.3 MiB
131
+ (2816×704×3 mats, 4-bit) read at the ~180 GB/s small-buffer rate, ×8 experts
132
+ ×30 layers, plus router argpartition/take chains.
133
+ - Corroboration: the dependent tiny-op chain costs ~10–15 µs/op; a ~200–400
134
+ dispatch decode step predicts 2–5 ms of pure serialization if nothing
135
+ overlaps, consistent with the observed overheads.
136
+
137
+ **(c) Host JS loop** — the single biggest NON-physics term across models:
138
+ graph build 1.0 ms (CPM5) / 3.4–3.7 ms (e4b, 12B, 26B) per token, serial with
139
+ the GPU. Sampler readback is already free (0.02–0.03 ms, pipelined).
140
+ clearCache cadence already mlx-lm-faithful.
141
+
142
+ **(d) Compiled decode reality check** (suspect d):
143
+ - e4b (whole-graph form): graph 3.5→1.2 ms but GPU +1.7 ms (the concat-fetch
144
+ materializes each growing cache's window every step) → net **+3.6% @600,
145
+ ~0 @8k**. The trade is real and grows with context.
146
+ - 12B (segmented form): **@600 it compiles NOTHING** — sliding rings only
147
+ enter ring phase at offset ≥ window (1024), and the 8 full-attention layers
148
+ are concat-phase (never compiled) forever. So at short context the default
149
+ MLX_BUN_COMPILED_DECODE=1 delivers zero and the full 3.4 ms host tax stands.
150
+ @8k it halves the host term (3.6→1.9 ms); net effect within thermal noise
151
+ today, historically +0–1%.
152
+ - CPM5 / Qwen / 26B-MoE: **no compiled decode exists at all** (gemma4-dense
153
+ gate in generate.ts:512-516). CPM5 pays 25% of its step in host graph build.
154
+
155
+ ## 5. Suspect-by-suspect verdicts (2026-07-01 kernel review)
156
+
157
+ **(a) Backlog #4 — fused decode kernel's 6 ensureRowContiguous copies/layer/step: CONFIRMED structurally.**
158
+ `fused-decode-kernel.ts:227,234` build both kernels with
159
+ `ensureRowContiguous: true`; the dispatch site passes step-padded fetch VIEWS,
160
+ non-contiguous whenever offset < capacity → mlx copies all six
161
+ packed/scales/biases arrays per quantized full-attention layer per step.
162
+ Order-of-magnitude at 12B @8k (8-bit KV): ~38 MB/layer × 8 full layers ≈
163
+ 300 MB/step ≈ 1–2 ms ≈ 3–5% — matches the review's 1–3% @8k and grows linearly
164
+ with context. Note this only helps the kv=config arm, which today is SLOWER
165
+ than bf16 KV (eval DB: 12B @16k 24.7 vs 27.3; CPM5 209 vs 255) — this fix
166
+ narrows the quantized-KV speed tax, it does not move the bf16 headline.
167
+
168
+ **(b) qmv_fast port → live kernel: NOTHING TO FLOW.** Live weight GEMVs go
169
+ through `ops.quantizedMatmul` (gemma4-base.ts:121,198) = mlx's own `qmv_fast`
170
+ — the thing the megakernel port was catching up TO (0.70→0.94x, never above
171
+ 1.0). The live perf kernel (fused SDPA) already uses the same quantized.h qdot
172
+ pattern (fused-decode-kernel.ts:47-55 comment, predates the megakernel per
173
+ PLAN.md:1109-1130). Review's claim verified correct; no action.
174
+
175
+ **(c) "Megakernel could win at M=K": AGREE WITH THE REVIEW — recorded
176
+ misleadingly.** The post-mortem's own numbers show the megakernel losing at
177
+ M=1 even with zero barriers (4.95 vs 4.62) on the atomic-coherence tax and a
178
+ 0.94x GEMV. At M=K, mlx ALSO amortizes: the live spec-decode verify is already
179
+ one batched forward sharing the weight read, and mlx's steel GEMM kernels are
180
+ strongest exactly at small-M GEMM. Nothing measured supports the megakernel
181
+ overtaking at M=K. The banked learning should read: "M=K amortizes the
182
+ per-forward fixed cost for ANY engine; the megakernel's specific coordination
183
+ primitives were measured net-negative." The real M=K lever is the small-L
184
+ flash-decoding SDPA kernel (review opportunity) + spec decode.
185
+
186
+ **(d) Compiled decode: see §4(d).** On by default where applicable, but it
187
+ delivers far less than its billing: nothing at 12B short-context (window
188
+ phase-gating), a wash on e4b @8k (concat tax), absent on CPM5/MoE. The 3.4 ms
189
+ host term it was built to kill is still ~85–100% alive at short context.
190
+
191
+ **(e) e4b prefill gap: NOT REPRODUCIBLE ON THIS MACHINE — the RESULTS.md row
192
+ is suspect, profile on the other laptop before building anything.**
193
+ - RESULTS.md says e4b prefill 304 (ours) vs 373 (mlx-lm). This machine's eval
194
+ DB has NO such rows (e4b prefill rows: ours 505–725 tok/s @600–16k; the only
195
+ oracle e4b rows are @24–31 ctx where "prefill tok/s" is meaningless).
196
+ - Measured today, same prompt, this machine: ours 689 tok/s @3266 (probe,
197
+ in-process), mlx-lm 715–763 tok/s @3266 (two runs, cross-process). Gap ≈
198
+ −4 to −10% directional, thermally muddy — nowhere near −20%.
199
+ - Flag arms @2048: FUSED_GELU=0 changes prefill ~nothing (2.31→2.36 s) but
200
+ costs −7% DECODE (18.4→19.9 ms — the fused GeGLU is a decode win); fused
201
+ SDPA off: no prefill change.
202
+ - Conclusion: the 304/373 row most likely came from the M4 Pro or a stale
203
+ build. Re-benchmark there before spending on backlog #10. (Also noted: the
204
+ oracle venv's mlx-lm cannot even load the 12B — `gemma4_unified not
205
+ supported` — so the 12B baseline rows came through some other path.)
206
+ - Bonus decode observation while pairing: mlx-lm e4b decode measured 61.3
207
+ tok/s over 8 tokens but 34.0 tok/s over 64 tokens in back-to-back runs —
208
+ cross-process decode comparisons at this granularity are thermal noise;
209
+ trust the recorded paired result (−5% @short) and the in-process split.
210
+
211
+ ## 6. Ranked fix plan (estimated wins are directional, this machine)
212
+
213
+ 1. **Kill/hide the serial host graph-build tax** — the largest cross-model,
214
+ non-physics term (CPM5 25%, e4b 17%, 26B 17%, 12B 9% of step):
215
+ 1a. **CPM5 (and Qwen): extend CompiledDecode beyond gemma4.** CPM5 is
216
+ dense, no per-layer inputs, no MoE — the EASY shape (whole-graph, no
217
+ concat-tax caveat beyond growing KV, same as 12B's ring layers after
218
+ window). Worth ~0.8 ms/tok ≈ **+20–25% CPM5 decode**. Effort M.
219
+ 1b. **Research spike: overlap graph build with the GPU instead of
220
+ compiling it away.** Today asyncEvalAll(step n+1) blocks until step n
221
+ drains and the build happens outside that window. If the build of step
222
+ n+2 can run while step n's buffer drains (build-two-ahead, or move the
223
+ blocking wait off the loop thread), the host term hides under the GPU
224
+ for EVERY model with zero numerics risk — this would also beat mlx-lm's
225
+ loop, which pays the same serial cost (their pybind build is just
226
+ cheaper). Effort S to spike, M to land. **This is the "no way we can't
227
+ be faster" candidate.**
228
+ 1c. **e4b: SharedKv segmented compile plumbing** (already-scoped review
229
+ item) so e4b gets graph 3.5→~1.2 without the whole-graph concat tax:
230
+ ~**+10–12% e4b**, more at long context. Effort L.
231
+ 1d. **12B short-context: compile the concat phase too** (or accept 1b as
232
+ the fix). The current segmented form compiles nothing below the 1024
233
+ window and never compiles the 8 full-attention layers. ~+9% @600.
234
+ 2. **26B GPU overhead (~4 ms/tok over floor):** ~~profile gather-qmm expert
235
+ reads~~ **PROFILED 2026-07-02** (moe-expert-read-profile.ts, exact 26B
236
+ shapes; real decode @600 re-measured 21.0–22.6 ms/tok ✓). The gap is real
237
+ and located, but BOTH cheap candidates are refuted: expert-contiguous
238
+ layout does nothing (a chain with ONE expert repeated ×8 — 1/8 the unique
239
+ bytes — runs at the same speed: not bandwidth/locality-bound), and merging
240
+ gate+up stacks to cut dispatches is SLOWER (9.6 vs 8.1 ms). Root cause:
241
+ **mx.gather_qmm at M=1 is compute-bound** (~99 GB/s effective vs the qmv
242
+ kernel's ~180 on identical bytes; upstream python mlx 0.31.2 reproduces
243
+ 6.18 vs 4.01 ms) — it lacks a qmv-class fast path, so each per-expert
244
+ [1,2816]→[704] product runs on mostly-idle GEMM tiles. The router chain
245
+ adds a secondary ~1.6–2.6 ms of tiny-op dispatches.
246
+ **Custom-kernel attempt (same day): built, correct, SHELVED.** A gather-qmv
247
+ Metal kernel (scripts/experiments/moe-qmv-kernel.ts + moe-qmv-parity.ts)
248
+ passes bounded-divergence on all three 26B dispatch patterns, but five
249
+ structural variants (occupancy/staging/bank-conflict/mask-trick sweeps —
250
+ post-mortem table in the kernel header) all measure SLOWER than gather_qmm
251
+ (best 12.5 vs 8–10 ms on the 30-layer chain), and the killer measurement is
252
+ structural: **mx.fast.metal_kernel dispatches cost ~60–95 µs each in a
253
+ dependent chain, nearly independent of the work inside** — at 3
254
+ dispatches/layer × 30 layers that fixed cost alone eats the entire ~4 ms
255
+ prize. Remaining routes, in order: (a) UPSTREAM — give mlx's gather_qmm an
256
+ M=1 qmv-class path in C++ (no dispatch overhead; natural companion to the
257
+ quantized_matmul M=2/3 bug report), (b) a fused whole-MLP kernel (gate+up+
258
+ gelu in one dispatch, down in a second → 60 dispatches/step) with a
259
+ qmv-class body — a dedicated fused-decode-v2-style session starting from
260
+ the post-mortem map.
261
+ 3. **e4b GPU overhead (1.8–3.7 ms):** dispatch-count reduction on the
262
+ per-layer-input machinery (the ~1–2 MiB gate/proj GEMVs and altup
263
+ elementwise chains; batch the 42 per-layer gate GEMVs into one [42·width]
264
+ GEMM where semantics allow). **+8–15% e4b.** Effort M–L.
265
+ 4. **Backlog #4 (ensureRowContiguous full-capacity buffers + activeN):**
266
+ validated; ~3–5% on the kv=config arm @8k, ~4x that @32k. Small, safe,
267
+ makes quantized KV stop losing to bf16 on speed. Effort S.
268
+ 5. **CPM5 @4k KV-path overhead (GPU 3.0→4.0 ms while KV adds only 0.35 ms of
269
+ bytes):** the attention/append kernels at CPM5's small KV shapes run in the
270
+ ~100–180 GB/s regime; candidates: the existing fused-decode kernel path for
271
+ CPM5 (currently gemma-shaped sites only?) or upstream sdpa-vector tuning.
272
+ ~+10% CPM5 @4k+. Effort M (profile first).
273
+ 6. **Spec decode (M=K)** remains the only lever that beats the 12B's
274
+ bandwidth wall (weight read shared across K tokens) — unchanged priority,
275
+ now with the roofline to prove why nothing else can.
276
+
277
+ ## 7. The overlap spike (2026-07-02) — fix 1b REFUTED; §4(c) decomposition corrected
278
+
279
+ Fix 1b asked: can the host graph build hide under the GPU (build-two-ahead /
280
+ off-thread dispatch)? `decode-overlap-probe.ts` answers two ways — the
281
+ decisive one is DIRECT REMOVAL: take the per-step host build out of the loop
282
+ entirely (pre-build all N step graphs up front, chained through the lazy
283
+ argmax outputs — identical graph structure to production — then run
284
+ dispatch+read with zero interleaved build) and see if decode gets faster.
285
+
286
+ M1 Max, greedy, uncompiled, all arms token-IDENTICAL to baseline:
287
+
288
+ | arm | CPM5 @132 (spin 2 ms) | e4b @600 (spin 4 ms) |
289
+ |---|---|---|
290
+ | baseline (pipelined) | 4.05 ms/tok (graph 0.86, dispatch 3.13) | 19.02→18.06 across passes (graph ~3.3–3.6, dispatch ~14) |
291
+ | **prebuilt (host build REMOVED)** | **4.11 (+0.07)** | **19.03 (+0.01)** |
292
+ | prebuilt-1buf (also no per-step dispatch boundaries) | 4.33 | 19.73 |
293
+ | serial (evalAll, no pipeline) | **+1.75 ms** | **+3.36 ms** |
294
+ | spin-build / spin-pre / spin-post (+W injected host work) | **+0.4 ms** (not +2) | **+1.0–1.4 ms** (not +4) |
295
+ | chain-2 / chain-3 (deeper pipeline) | ±0.01 ms | +0.02 / −0.96 (== pass drift) |
296
+
297
+ - **Removing the work gains nothing**: with the entire 0.9 ms (CPM5) /
298
+ 3.6 ms (e4b) per-token build lifted out of the loop, decode speed is
299
+ unchanged (+0.07 / +0.01 ms, inside pass-to-pass drift). Even the one-buffer
300
+ floor — no host work AND no per-step dispatch boundaries — is no faster.
301
+ **The build already overlaps; wall = GPU step time.**
302
+ - The injection arms agree from the other direction: extra host work is
303
+ ABSORBED (dispatch shrinks 3.13→1.6 exactly as `gpu − host` predicts) — the
304
+ GPU is busy with step n while the host builds step n+1.
305
+ - The serial anchor costs MORE than the build itself (CPM5 +1.75 vs 0.86;
306
+ e4b +3.36 vs 3.6): the pipelined loop hides the build AND the per-step
307
+ launch gap. The loop shape is already right; mlx-lm's identical shape gets
308
+ the identical hiding.
309
+ - Deeper pipelining (chain-2/3) buys exactly nothing — there is no residual
310
+ host term to hide.
311
+
312
+ **Corrections to this doc:**
313
+ - §4(c) "host JS loop — the single biggest NON-physics term" is WRONG as an
314
+ additive claim. The "host graph ms" column was measured as time-in-JS-calls,
315
+ but that window runs concurrently with the GPU; "GPU ms" (= the dispatch
316
+ wait) is `gpu − graph`, so summing them reconstructs the wall without any
317
+ recoverable host term. The true GPU step time ≈ the measured wall itself.
318
+ - §6.1a (CPM5 CompiledDecode "+20–25%"): premise gone. Any compiled-decode win
319
+ is GPU-side (fewer/denser dispatches from C++ replay), not host removal —
320
+ re-bill as profile-first, expected small. This also re-explains §4(d):
321
+ e4b @600 compiled wall 18.9→18.1 means compiled made the GPU ~0.8 ms faster
322
+ (net of concat tax), not "host −2.4, GPU +1.7".
323
+ - §6.1b (this spike): answered, REFUTED — nothing to build.
324
+ - §6.1c/1d (SharedKv segmented-compile plumbing ~L effort, 12B concat-phase
325
+ compile): the host-tax share of their billing is gone; only GPU-side effects
326
+ remain. De-prioritize pending a GPU-side profile.
327
+ - §6 items 2 (26B expert reads), 3 (e4b dispatch count), 4 (backlog #4
328
+ contiguity), 5 (CPM5 KV-path), 6 (spec decode) are UNAFFECTED — they were
329
+ always GPU-side, and they are now the entire recoverable gap.
330
+ - 12B/26B not re-probed (structurally the same loop; 12B was at-wall under
331
+ either reading; 26B's 3.7 ms graph ≪ its ~18 ms step has the same slack).
332
+
333
+ ## Probes added (scripts/experiments/, tsc-clean)
334
+ - `decode-overlap-probe.ts` — direct-removal (prebuilt / prebuilt-1buf) +
335
+ spin-injection + serial-anchor + chain-depth arms for the overlap question
336
+ (§7).
337
+ - `decode-roofline-bw.ts` — machine bandwidth: raw read, decode-shaped qmv
338
+ GEMV (multi-copy, one-eval chaining), bf16 GEMV, dispatch chain.
339
+ - `decode-roofline-step.ts` — factory-generic pipelined decode split
340
+ (graph/dispatch/read) with `--compiled` arm, any model/context.
341
+
342
+ ## Standing-rule notes
343
+ - Pre-existing tsc error in `src/eval/runner.ts:161` (kvBits on a Pick union)
344
+ in Josh's working tree — not introduced here, not fixed here (outside the
345
+ edit boundary for this task).
346
+ - RESULTS.md "Direct" table's e4b prefill row contradicts this machine's eval
347
+ DB (no supporting rows) — per the verify-against-evaldb rule it needs a
348
+ machine label or a re-run on the M4 Pro.
@@ -0,0 +1,58 @@
1
+ # DSpark — handoff
2
+
3
+ **State:** faithful DSpark (DFlash KV-injection + Markov + confidence) is **built
4
+ and proven architecturally correct** (overfit τ=3.24, paper-range). It does not
5
+ yet net a speedup on e4b — that's a data-scale + target-speed problem, not an
6
+ architecture problem. Full design + measured numbers:
7
+ [docs/design/dspark-speculative-decoding.md](../design/dspark-speculative-decoding.md).
8
+
9
+ ## What happened (short version)
10
+ - Built v1 first as a single-vector shortcut — it dropped the paper's core KV
11
+ injection and was never really DSpark. Chunk (τ≈1.26) and article (τ≈1.16)
12
+ runs on it were misleading.
13
+ - Rebuilt the **faithful** DFlash (multi-layer H_ctx injected into every draft
14
+ layer, 5 layers) in parallel `*-dflash.ts` files (v1 kept as baseline; repo
15
+ stays green).
16
+ - Found + fixed an **off-by-one in the p^t target** (TV loss trained against a
17
+ distribution shifted one token — present in v1 too, so all prior τ were bogus).
18
+ - **Overfit test settled it:** 3 articles, eval on same → per-pos ~0.75, τ=3.24.
19
+ The architecture reaches the paper's numbers; the plateau at ~0.17 on 160
20
+ articles is data-starvation, not capacity (confirmed: width 1024 vs 2560 both
21
+ plateau ~0.17).
22
+ - **Target-speed is the other half.** Measured decode: e4b 45.9, 12b 27.5, 27B
23
+ ~15 tok/s. The fixed draft overhead only amortizes on a slow target — so e4b
24
+ is ~worst-case for spec decode; the **27B agentic workload is the real target**
25
+ (τ≈3 there could plausibly net ~2–3× → 15 → ~35–45 tok/s).
26
+
27
+ ## Verified / working
28
+ - Parity-safe `hiddenTap` multi-layer extraction (`gemma4.ts`), no-op when off.
29
+ - `trim(n, bypass=true)` physical-slice rollback past the sliding window (`gemma4-base.ts`) — lossless gate held on 6K context.
30
+ - CPU smoke 16/16 (`dspark-dflash-smoke.ts`): KV-injection forward, autograd, inference, multi-layer variable-context data round-trip.
31
+ - Training `--resume` (warm-start from checkpoint) — the GPU runs kept getting killed ~step 5000; resume survives that.
32
+
33
+ ## Next (in priority order)
34
+ 1. **Retarget to a slow model (27B or 12b).** The drafter reads dims from the
35
+ model + tapLayers is config, so it ports — but needs regen+train on that
36
+ model. 27B on 32GB is memory-tight (17.75GB weights); may need the serve
37
+ process stopped to free the ~14GB for regen. This is where a real speedup lives.
38
+ 2. **Scale training data** — thousands of on-distribution generations (not 160)
39
+ to lift generalizing τ toward the overfit ~0.75.
40
+ 3. **Tighten the draft inference loop** — per-position host syncs + double 262K
41
+ LM-head make the draft heavy; needed for τ to translate to wall-clock.
42
+ 4. **Finish the paper:** RNN head (Eq 6), STS calibration (§3.2.1), hardware-aware
43
+ prefix scheduler (Alg 1 — single-user = confidence-scheduled draft-length pruning).
44
+ 5. **Rename** `dflash`→`dspark` (faithful module IS DSpark; v1 = legacy single-vector).
45
+
46
+ ## Key files
47
+ `src/spec/dspark/{module,data,generate}-dflash.ts`, `{loss,sample}.ts`;
48
+ `src/model/gemma4.ts` (hiddenTap), `gemma4-base.ts` (trim bypass);
49
+ `scripts/dspark-{regen,train,measure}-dflash.ts`, `dspark-dflash-smoke.ts`.
50
+
51
+ ## Memory / gotchas
52
+ - `dspark-drafter-built-v1`, `dont-inflate-scope-build-the-paper` (the lesson:
53
+ build the paper's mechanism first — the faithful build was ~1hr; the wrong
54
+ builds cost the session).
55
+ - e4b hidden_size = **2560** (not 2816 — a different config block). Module reads dims live.
56
+ - Memory admission guard (`server.ts:1538`) rejects `prompt+max_tokens > maxSafeContext`
57
+ because a mid-gen GPU OOM SIGKILLs the server. 27B/32GB caps ~22.5k; a client
58
+ sending `max_tokens=64k` trips it — lower max_tokens (or make it a runtime stop).
@@ -0,0 +1,171 @@
1
+ # Kernel/perf deep review — 2026-07-01 (condensed, tracked record)
2
+
3
+ A 25-agent adversarially-verified review of every Metal/fused kernel and
4
+ training memory path (7 areas; every suspected bug survived an independent
5
+ refutation pass before making this list). Raw structured output (all seven
6
+ area reviews + full mechanisms): `reports/kernel-perf-review-2026-07-01.json`
7
+ (gitignored, local). This file is the durable record.
8
+
9
+ ## Area verdicts (one line each)
10
+
11
+ - **flash-CCE head** (steel fwd + H-tiled bwd): done unusually well — verbatim
12
+ vendored MLX steel machinery, exact-by-default backward with autograd + FD
13
+ parity — marred by decision drift: the PLAN-decided 1.35× filter win was
14
+ never landed and the in-code comments argued three contradictory positions
15
+ (comments fixed 2026-07-01; the FLIP LANDED 2026-07-02 — see backlog #1).
16
+ - **Training flash-attention kernel**: faithful port with exemplary post-bug
17
+ discipline, correctly opt-in — but it copies a naive oracle (~30× slower)
18
+ and its tests covered none of the production configs (extended 2026-07-01:
19
+ D=256, sliding-window, non-tile T; ≥2K e4b revalidation still Josh-gated).
20
+ - **Inference fused paths**: disciplined and honest (everything measured,
21
+ gated, losses documented) — one real latent bug (fixed) + the open 26B
22
+ perf-kernel checkbox.
23
+ - **Quantized-KV attention**: line-faithful ports, tol=0 oracle gates, no
24
+ materialized dequant on the serving path — optiq's streaming per-layer
25
+ conversion was only half-ported (fixed 2026-07-01) and FUSED_DECODE×
26
+ compiled-decode composes unsafely (OPEN — see below).
27
+ - **Training memory machinery (segmented backward)**: the strongest subsystem
28
+ reviewed — the mlx_vjp-per-segment design is the correct primitive, disposal
29
+ exception-safe in all five classes.
30
+ - **Metal infra + megakernel learnings**: apply() is careful post-incident
31
+ design; the megakernel was shelved on decisive numbers, but the banked
32
+ "could win at M=K" learning is recorded misleadingly (decode look-again
33
+ agrees: re-word it — mlx amortizes at M=K too).
34
+ - **Cross-cutting flags**: unusually disciplined for a 65-flag surface — but
35
+ the --l2 tier promise was violated by its own default (FIXED 2026-07-01:
36
+ perf kernel demoted to --l3) and the v1 batch scheduler lacked the serial
37
+ loop's optimizations (batching-v2 wave, in flight).
38
+
39
+ ## Confirmed bugs — status ledger
40
+
41
+ FIXED in commit 381382c (2026-07-01): same-ms batch seed collision ·
42
+ streaming per-layer KV-quant conversion · segmented compiled-decode mid-step
43
+ double-write · --l2 tier violation (perf kernel → l3; evidence: the frozen
44
+ "optiq" goldens were mlx-bun's own compat engine, gate was ≥56/64 argmax) ·
45
+ generate --l2/--l3 silent L1 degrade · unguarded TRAIN_ATTN=flash on Gemma ·
46
+ evalSftLoss error swallowing · tokens_per_sec metric · inverted comments
47
+ (gemma4.ts:298, flash-cce filter trio) · silent steel fallback (now warns;
48
+ H%128 throw narrowed) · flash-attn test blindness.
49
+
50
+ FIXED 2026-07-02: **FUSED_DECODE=1 inside whole-graph compiled decode** baked
51
+ the tile loop at trace-time N — q-cat adapters grow N per step under shapeless
52
+ replay, so the newest KV rows were silently never attended. REPRODUCED on e4b
53
+ (42 layers traced tile=true at N=17; diverges within 3 tokens then
54
+ repetition-loops). The 12B was never affected — its segmented form runs concat
55
+ layers as uncompiled js layers. Fix: generate() refuses to compile under
56
+ MLX_BUN_FUSED_DECODE=1 (explicit opt-in wins, like LoRA/MoE) + quantizedSdpa
57
+ throws if the tiled path is ever selected inside a trace (lands in the
58
+ transactional fallback, never silent). Regression in
59
+ tests/compiled-decode.test.ts, whose e4b block also now uses the dynamic
60
+ snapshot resolver — the old hard-coded hash silently skipped it on this
61
+ machine, which is how the combo went unexercised.
62
+
63
+ NEW BUG (found 2026-07-02 by the backlog-#8 gate test; FIXED in mlx-bun,
64
+ UPSTREAM in mlx): **mx.quantized_matmul(transpose=false) returns wrong values
65
+ at row count 2 or 3** — relnorm ~0.33-0.39 on our dylib, ~0.98-1.14 on stock
66
+ mlx 0.31.2 (oracle venv repro); M=1 and M>=4 exact, transpose=true exact at
67
+ every M. mlx's autograd computes dh for a transpose=true head qmm via an
68
+ internal transpose=false call at the cotangent's row count, so EVERY training
69
+ head silently produced wrong dh for 2-3-token responses, 2-3-token tail
70
+ chunks, and tiny prompt spans (f32 ground truth: dh relnorm up to 0.99 at the
71
+ affected positions, cos ~0.2). Serving/decode unaffected (transpose=true).
72
+ Fix: (a) loss.ts `logitsFromHiddenPadM` pads differentiated head spans to 4
73
+ rows (pad cotangents are exactly zero) — applied at all seven head sites;
74
+ (b) ops.quantizedMatmul pads direct transpose=false calls (the fused head's
75
+ analytic backward). Verified vs f32-dequant ground truth (chunk-4 dh
76
+ 0.47→0.006) + M-sweeps. Worth filing upstream with the 10-line python repro.
77
+
78
+ ## The ranked optimization backlog (task #12)
79
+
80
+ 1. ~~**[S] Land the coeff filter at eps~1e-5**~~ **LANDED 2026-07-02** — BOTH
81
+ skips default 1e-5 (coeff filter + blockMax block skip; env=0 restores exact).
82
+ All gates run on the M1 Max with real chunk-ORPO data
83
+ (flash-cce-filter-realdata.ts, extended: E4B=1 + blockEps sweep + Vjp
84
+ full-logits fidelity): filter CPM5 0.343%/1.41×, e4b 0.158%/1.70×; blockMax
85
+ skip ~lossless (≤0.004%) and real text DOES go cold (the synthetic probe was
86
+ the artifact) — **combined 1.71× (CPM5) / 3.16× (e4b)**; teacher-forced
87
+ fidelity cosine ≥0.99993, filter adds ≤0.07% over the eps=0 reassociation
88
+ floor (standing test in tests/train-orpo-fused-ce.test.ts). The "eps=0
89
+ byte-identical" gate resolved structurally: eps=0 compiles the identical
90
+ pre-flip kernel, but run-to-run dh is never byte-stable (atomic-add
91
+ reassociation) — byte-replay was the wrong spelling of that gate.
92
+ 2. ~~**[M] planSegments full-attention isolation**~~ **REFUTED BY MEASUREMENT
93
+ 2026-07-02** — built, A/B'd @8K on real chunk rows: ZERO peak win (18.09 vs
94
+ 18.02 GB; scripts/experiments/seg-isolation-smoke.ts + MLX_BUN_SEG_MEM_LOG
95
+ phase probes). The §5 cost-model premise is false: mlx's sdpa BACKWARD
96
+ materializes O(L²) scores for EVERY layer (~3.5 GB/layer @8K e4b — the
97
+ sliding window is just an additive mask; sliding pair +7.1 GB ≈ full+sliding
98
+ +7.15). Worst segment = layer count alone, so **segment_size is the whole
99
+ knob: seg1 measures 14.59 GB @8K (+3% step time, loss identical) — which
100
+ DOES fit the 24 GB M4 Pro today, no code needed.** Inter-segment clearCache
101
+ also measured no-effect (watermark is live memory). Real next levers: the
102
+ head vjp's ~3 GB [M,V] spike (backlog #8) and an O(L)-memory attention
103
+ backward (the flash-attention track).
104
+ 3. ~~**[S] Auto-dispatch the training head by M**~~ **LANDED 2026-07-02** —
105
+ fusedRespLogpMean routes requested-flash batches with M <
106
+ MLX_BUN_FLASH_MIN_M (default 1024) to the exact fused head. Fresh crossover
107
+ sweep (head-dispatch-sweep.ts, M1 Max — the review's 1.9× @M=512 was
108
+ pre-filter-flip): fused wins e4b M=64 1.6× / M=512 1.13×, tie @1024; the
109
+ #1-flip made flash FASTER at long M too (e4b ≥2048; 8K 10.7 vs 13.2 s) on
110
+ top of its memory win. CPM5: fused faster at every M — flash stays a pure
111
+ memory lever there (≥1024 honors the caller's intent). Dispatch proven by
112
+ bit-identical-to-fused loss in tests/train-orpo-fused-ce.test.ts.
113
+ 4. ~~**[S] Fused decode full-capacity KV buffers + activeN**~~ **REFUTED BY
114
+ MEASUREMENT 2026-07-02** — the variant was built (an int32 activeN input,
115
+ capacity-strided rows, fetch attaches full buffers), gated BYTE-IDENTICAL
116
+ on all three recorded dispatch shapes and perf-oracle-green — and then
117
+ measured SLOWER end-to-end: interleaved in-process A/B on the 12B uniform
118
+ kv4 (tokens identical across arms, M1 Max) gave 24.64 vs 26.18 tok/s @8k
119
+ (0.94) and 23.07 vs 23.83 @22k (0.97) — the "~4× at 32k" scaling is
120
+ refuted too. Two holes in the estimate: the 12B's full-attention dispatch
121
+ shape is KV=1 (H16 D512), where the fetch view is a CONTIGUOUS PREFIX and
122
+ the six-copies model never applied; and kernel-only micro-A/Bs proved
123
+ unstable (±50% run-to-run) — the numbers behind "1-3%" were never
124
+ end-to-end. REVERTED; evidence + re-application recipe:
125
+ scripts/experiments/fused-decode-activen-ab.ts.
126
+ 5. **[M] Collapse per-token host syncs in the spec/DSpark loop** — ~2γ+1
127
+ round-trips → ~2 per verify cycle; directly attacks DSpark's fixed draft
128
+ overhead.
129
+ 6. **[M] Pipeline the batched decode loop + clearCache cadence** — folded into
130
+ the batching-v2 wave (docs/design/batching-v2-plan.md).
131
+ 7. **[S] Enable the 26B perf kernel** (open PLAN Phase E box; oracle frozen).
132
+ 8. ~~**[S] Bound the SFT segmented head**~~ **LANDED 2026-07-02** — both SFT
133
+ segmented classes now use boundedSftCe (token-chunked Checkpoint head, the
134
+ proven ORPO plumbing; MLX_BUN_SEG_HEAD/MLX_BUN_SEG_HEAD_CHUNK knobs).
135
+ Head-only A/B at e4b M=6000: whole-vocab head 16.60 GB peak vs bounded
136
+ 6.60 GB (**~10 GB head-vjp transient eliminated**), loss identical, dh
137
+ relnorm 0.00000; head time +45% (Checkpoint recompute, small share of a
138
+ step). The @8K smoke's short responses were never head-bound (peak set by
139
+ the per-layer sdpa backward — see #2), so its wall peak moved little; the
140
+ win is exactly the long-response regime the review named. Gate test:
141
+ tests/train-orpo-fused-ce.test.ts "bounded SFT head".
142
+ **Landing this exposed a NEW upstream mlx bug (see ledger below).**
143
+ 9. ~~**[S] Segmented-step overhead pass**~~ **LANDED 2026-07-02** — all three
144
+ fixes: MlxArray.detachCopy (ONE mlx→mlx copy; the old rawBytes+
145
+ fromBytesCopy route was two copies via the JS heap), detachLeaves (ONE
146
+ evalAll barrier per segment's whole vjp set, applied in all six segmented
147
+ backward loops), and a step-shared block-sparse-mask memo for the CPM5
148
+ prefix classes (prefixSharedCaches — makeMask hands out slice views).
149
+ Gate: loss + grads BYTE-IDENTICAL on CPM5-SFT / CPM5-ORPO-prefix / e4b-SFT
150
+ fixed steps. Step time (short-seq fixtures, M1 Max): CPM5 ORPO-prefix
151
+ 238→158 ms (−34%), e4b SFT 2065→1272 ms (−38%); e4b @8K flat (GPU-bound,
152
+ as predicted) with peak unchanged (14.49 GB).
153
+ 10. **[M] e4b prefill gap** — profile-first; NOTE: the decode look-again could
154
+ NOT reproduce RESULTS.md's 304-vs-373 row on the M1 Max (no eval-DB
155
+ backing) — re-bench on the M4 Pro before building anything.
156
+
157
+ ## Superseding context: the decode roofline look-again
158
+
159
+ The review's framing assumed the megakernel post-mortem's "M=1 decode is at
160
+ the floor". The follow-up investigation (docs/investigations/
161
+ decode-roofline-lookagain.md) OVERTURNED that: only the 12B is at the wall
162
+ (~92-93% of measured roofline); CPM5 58-64%, e4b 64-70%, 26B 60-62%.
163
+ **2026-07-02 second correction (lookagain §7):** the "host JS graph build =
164
+ #1 recoverable term" part of that verdict was itself wrong — the overlap
165
+ spike (decode-overlap-probe.ts, spin-injection + serial anchor) proved the
166
+ pipelined loop ALREADY hides the build; wall = GPU step time. The lookagain's
167
+ host-side fixes (1a–1d: CPM5 compiled-decode port, build-ahead spike, SharedKv
168
+ segmented-compile, 12B concat-phase compile) are refuted or de-prioritized;
169
+ its GPU-side fixes (26B gather-qmm bandwidth, e4b dispatch batching, CPM5
170
+ KV-path, backlog #4 contiguity, spec decode) are now the ENTIRE recoverable
171
+ gap and still compose with the backlog above.
@@ -423,9 +423,9 @@ Examples:
423
423
 
424
424
  ```sh
425
425
  mlx-bun --model gemma-4-12b --context 65536
426
- mlx-bun serve --no-ui --port 8090
426
+ mlx-bun serve --no-ui --port 8080
427
427
  mlx-bun pi --model qwen-profile
428
- mlx-bun harness pi --base-url http://localhost:8090/v1
428
+ mlx-bun harness pi --base-url http://localhost:8080/v1
429
429
  ```
430
430
 
431
431
  ## Research loop behind the product