mlx-bun 0.0.9 → 0.0.11

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 (179) hide show
  1. package/README.md +66 -21
  2. package/THIRD_PARTY_LICENSES.md +1 -0
  3. package/docs/archive/mlx-bun-lab-report.html +275 -0
  4. package/docs/archive/wwdc-mlx-bun.html +848 -0
  5. package/docs/design/audio-input-plan.md +356 -0
  6. package/docs/design/batching-perf-path.md +255 -0
  7. package/docs/design/batching-v2-plan.md +23 -9
  8. package/docs/design/bucketing-stage.md +2 -1
  9. package/docs/design/compat-cli-surface-design.md +4 -3
  10. package/docs/design/decode-speed-program.md +172 -0
  11. package/docs/design/docs-reorg-plan.md +1 -1
  12. package/docs/design/dspark-serving-program.md +371 -0
  13. package/docs/design/dspark-speculative-decoding.md +167 -5
  14. package/docs/design/faithful-l1-consolidation.md +310 -0
  15. package/docs/design/generic-model-support.md +8 -5
  16. package/docs/design/grammar-spec-batching-integration.md +301 -0
  17. package/docs/design/minicpm5-decode-megakernel.md +16 -11
  18. package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
  19. package/docs/design/omlx-adoption-map.md +98 -0
  20. package/docs/design/orpo-training.md +34 -30
  21. package/docs/design/paged-kv-cache.md +214 -0
  22. package/docs/design/parallel-slots.md +82 -49
  23. package/docs/design/parity-tier-dag.md +43 -33
  24. package/docs/design/repo-cleanup-plan.md +189 -0
  25. package/docs/design/runtime-isolation.md +19 -3
  26. package/docs/design/segmented-backward-training.md +18 -2
  27. package/docs/design/ssd-kv-cold-tier.md +288 -0
  28. package/docs/design/structured-output.md +484 -0
  29. package/docs/design/the-dreaming-master-plan.md +2 -3
  30. package/docs/design/turboquant-kv.md +235 -0
  31. package/docs/design/unified-engine-frontier-plan.md +774 -0
  32. package/docs/design/web-chat-beat-matrix.md +315 -0
  33. package/docs/design/web-chat-redesign.md +1112 -0
  34. package/docs/investigations/decode-roofline-lookagain.md +92 -6
  35. package/docs/investigations/dspark-handoff.md +182 -16
  36. package/docs/investigations/kernel-perf-review-2026-07.md +105 -26
  37. package/docs/investigations/lab-build-journal.md +2 -2
  38. package/docs/investigations/openwiki-evaluation.md +249 -0
  39. package/docs/planning/IDEAS.md +14 -0
  40. package/docs/planning/PRODUCT_ROADMAP.md +2 -3
  41. package/docs/planning/chat-ui-vision-memory-first.md +199 -0
  42. package/docs/planning/chat-ui-vision.md +50 -0
  43. package/docs/planning/memory-docs-and-dag-plan.md +15 -6
  44. package/docs/planning/release-notes-v0.0.10.md +57 -0
  45. package/docs/planning/release-notes-v0.0.11.md +239 -0
  46. package/docs/reference/cli.md +35 -16
  47. package/docs/reference/distribution.md +13 -6
  48. package/docs/reference/embedding.md +35 -14
  49. package/docs/reference/features-matrix.md +97 -0
  50. package/docs/reference/library-api.md +30 -9
  51. package/docs/reference/memory.md +26 -6
  52. package/docs/reference/models.md +11 -1
  53. package/docs/reference/orpo-quickstart.md +3 -4
  54. package/docs/reference/server-api.md +634 -22
  55. package/docs/reference/server-config.md +251 -119
  56. package/docs/reference/training.md +20 -16
  57. package/package.json +5 -3
  58. package/src/anthropic.ts +13 -0
  59. package/src/audio/conformer.ts +647 -0
  60. package/src/audio/decode.ts +133 -0
  61. package/src/audio/features.ts +202 -0
  62. package/src/audio/transcode.ts +67 -0
  63. package/src/cli.ts +402 -107
  64. package/src/config.ts +49 -2
  65. package/src/download.ts +40 -0
  66. package/src/eval/README.md +7 -6
  67. package/src/eval/kl.ts +59 -3
  68. package/src/eval/quality-db.ts +2 -2
  69. package/src/eval/runner.ts +3 -3
  70. package/src/expert-offload.ts +15 -2
  71. package/src/flags.ts +13 -0
  72. package/src/generate.ts +330 -16
  73. package/src/grammar.ts +585 -0
  74. package/src/hub-rest.ts +242 -0
  75. package/src/kv-store.ts +618 -122
  76. package/src/lora.ts +8 -0
  77. package/src/md-modules.d.ts +42 -0
  78. package/src/media-fetch.ts +284 -0
  79. package/src/memory/rest.ts +245 -0
  80. package/src/memory/vault.ts +74 -0
  81. package/src/mlx/array.ts +127 -19
  82. package/src/mlx/autograd.ts +2 -2
  83. package/src/mlx/ffi.ts +27 -3
  84. package/src/mlx/metal-kernel.ts +7 -4
  85. package/src/mlx/ops.ts +179 -23
  86. package/src/mlx/turboquant-ops.ts +831 -0
  87. package/src/mlx/turboquant-tables.ts +237 -0
  88. package/src/model/batched-mask.ts +62 -1
  89. package/src/model/batched-quant.ts +206 -0
  90. package/src/model/batched-rotating-quant.ts +346 -0
  91. package/src/model/batched-rotating.ts +46 -1
  92. package/src/model/compiled-decode.ts +4 -15
  93. package/src/model/factory.ts +10 -4
  94. package/src/model/gemma4-base.ts +391 -34
  95. package/src/model/gemma4.ts +122 -33
  96. package/src/model/generated/gemma4-12b.ts +9 -13
  97. package/src/model/generated/gemma4-26b.ts +10 -5
  98. package/src/model/generated/gemma4-e4b.ts +14 -9
  99. package/src/model/minicpm5.ts +58 -102
  100. package/src/model/paged-kv.ts +253 -0
  101. package/src/model/qwen3-delta.ts +123 -15
  102. package/src/model/qwen3-moe.ts +404 -0
  103. package/src/model/qwen3.ts +9 -8
  104. package/src/model/qwen3_5.ts +99 -25
  105. package/src/model/support.ts +12 -1
  106. package/src/model/universal/dense.ts +40 -5
  107. package/src/model/universal/rope.ts +36 -21
  108. package/src/pi-web.ts +1299 -82
  109. package/src/preflight.ts +27 -8
  110. package/src/prompt-cache.ts +338 -32
  111. package/src/quantize/quantizer.ts +16 -1
  112. package/src/registry.ts +61 -3
  113. package/src/serve/batch-scheduler.ts +1001 -76
  114. package/src/serve/generation-gateway.ts +292 -30
  115. package/src/serve/isolate.ts +417 -0
  116. package/src/serve/lane-registry.ts +44 -0
  117. package/src/serve/session-search.ts +326 -0
  118. package/src/server.ts +1460 -105
  119. package/src/spec/assistant-source.ts +150 -0
  120. package/src/spec/deepspec-source.ts +143 -0
  121. package/src/spec/dflash-source.ts +124 -0
  122. package/src/spec/dspark/ab-stats.ts +136 -0
  123. package/src/spec/dspark/calibration.ts +112 -0
  124. package/src/spec/dspark/deepspec-module.ts +801 -0
  125. package/src/spec/dspark/generate-dflash.ts +72 -20
  126. package/src/spec/dspark/loader.ts +23 -0
  127. package/src/spec/dspark/module-dflash.ts +278 -36
  128. package/src/spec/dspark/quantize-drafter.ts +91 -0
  129. package/src/spec/generate.ts +59 -25
  130. package/src/spec/ngram-source.ts +132 -0
  131. package/src/spec/serve-loop.ts +536 -0
  132. package/src/spec/source.ts +93 -0
  133. package/src/spec/two-model.ts +171 -0
  134. package/src/ssd-cache.ts +285 -0
  135. package/src/tokenizer.ts +288 -2
  136. package/src/tool-approvals.ts +131 -0
  137. package/src/tool-call.ts +408 -20
  138. package/src/train/flash-cce.ts +29 -26
  139. package/src/train/job.ts +0 -2
  140. package/src/train/loss.ts +53 -11
  141. package/src/train/prefix-shared.ts +30 -7
  142. package/src/train/segmented.ts +150 -46
  143. package/src/train/trainer.ts +22 -15
  144. package/src/vision/prompt.ts +217 -48
  145. package/src/web/app.html +1184 -1789
  146. package/src/web/app.js +5667 -0
  147. package/src/web/icon.svg +13 -0
  148. package/src/web/manifest.webmanifest +24 -0
  149. package/src/web/src/adapters-panel.ts +288 -0
  150. package/src/web/src/api.ts +57 -0
  151. package/src/web/src/assistant.ts +485 -0
  152. package/src/web/src/chat.ts +1050 -0
  153. package/src/web/src/composer.ts +959 -0
  154. package/src/web/src/dataset.ts +132 -0
  155. package/src/web/src/finetune.ts +267 -0
  156. package/src/web/src/hub.ts +428 -0
  157. package/src/web/src/main.ts +98 -0
  158. package/src/web/src/markdown.ts +0 -0
  159. package/src/web/src/memory-panel.ts +572 -0
  160. package/src/web/src/model-picker.ts +159 -0
  161. package/src/web/src/palette.ts +346 -0
  162. package/src/web/src/protocol.ts +62 -0
  163. package/src/web/src/quantize.ts +167 -0
  164. package/src/web/src/rag.ts +274 -0
  165. package/src/web/src/sessions.ts +469 -0
  166. package/src/web/src/shell.ts +804 -0
  167. package/src/web/src/status.ts +252 -0
  168. package/src/web/src/ui-catalog.ts +92 -0
  169. package/src/web/sw.js +71 -0
  170. package/src/web/vendor/README.md +61 -0
  171. package/src/web/vendor/hljs-LICENSE +29 -0
  172. package/src/web/vendor/hljs-theme.css +59 -0
  173. package/src/web/vendor/hljs.js +26 -0
  174. package/src/model/fused-decode-kernel.ts +0 -312
  175. package/src/model/fused-geglu-kernel.ts +0 -198
  176. package/src/model/fused-mlp-kernel.ts +0 -169
  177. package/src/model/fused-swiglu-kernel.ts +0 -240
  178. package/src/model/steel-linear-kernel.ts +0 -96
  179. package/src/status-page.html +0 -497
@@ -1,5 +1,11 @@
1
1
  # Decode perf "look again" — adversarial re-exam of the M=1 floor claim (2026-07-01)
2
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
+
3
9
  **Machine:** Apple M1 Max, 32 GB (MacBookPro18,2), `sysctl` verified. All eval-DB
4
10
  rows are also M1 Max — same box. **All numbers below are directional (session,
5
11
  loaded-machine caveat); quotable numbers remain benchmark.sh-gated.** GPU idle
@@ -225,12 +231,33 @@ is suspect, profile on the other laptop before building anything.**
225
231
  1d. **12B short-context: compile the concat phase too** (or accept 1b as
226
232
  the fix). The current segmented form compiles nothing below the 1024
227
233
  window and never compiles the 8 full-attention layers. ~+9% @600.
228
- 2. **26B GPU overhead (~4 ms/tok over floor):** profile gather-qmm expert
229
- reads (each expert slice ~3.3 MiB → the ~180 GB/s small-buffer regime);
230
- candidates: expert-contiguous layout so 8 experts read as fewer/larger
231
- spans, the open perf-kernel enablement (review #7, +1–3%), router-chain op
232
- fusion. Potential **+15–25% 26B decode** if expert reads reach big-GEMV
233
- bandwidth. Effort M (profile first).
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.
234
261
  3. **e4b GPU overhead (1.8–3.7 ms):** dispatch-count reduction on the
235
262
  per-layer-input machinery (the ~1–2 MiB gate/proj GEMVs and altup
236
263
  elementwise chains; batch the 42 per-layer gate GEMVs into one [42·width]
@@ -247,7 +274,66 @@ is suspect, profile on the other laptop before building anything.**
247
274
  bandwidth wall (weight read shared across K tokens) — unchanged priority,
248
275
  now with the roofline to prove why nothing else can.
249
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
+
250
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).
251
337
  - `decode-roofline-bw.ts` — machine bandwidth: raw read, decode-shaped qmv
252
338
  GEMV (multi-copy, one-eval chaining), bf16 GEMV, dispatch chain.
253
339
  - `decode-roofline-step.ts` — factory-generic pipelined decode split
@@ -1,11 +1,176 @@
1
1
  # DSpark — handoff
2
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:
3
+ **State (2026-07-06): the DSpark paper is CODE-COMPLETE and serve-integrated.**
4
+ Architecture proven correct (overfit τ=3.24, paper-range); serve-integrated
5
+ behind `--draft-model` (Phase 1 — the `DraftSource` seam is KV-borrowing-ready;
6
+ sibling AssistantSource verified bit-lossless on real weights,
7
+ `tests/spec-serve-assistant.test.ts`); and every remaining paper component
8
+ landed in Phase 2 the same day: Alg-1 confidence-scheduled draft-length pruning
9
+ (checkpoint-driven, zero change for uncalibrated checkpoints), STS calibration
10
+ (§3.2.1, `calibration.ts` + `scripts/dspark-calibrate.ts`), the RNN sequential
11
+ head (Eq 6, `--seq-head rnn`, init-equivalent to Markov), the tightened draft
12
+ loop (on-device token chaining + deferred conf reads + `collectLogits:false`
13
+ on serve — bit-identity pinned), and the `dspark` variant rename + central
14
+ loader. Suites: smoke 21/21 · dspark tests 17/17 · adjacent server suites
15
+ green. **What remains is exactly the GPU work below** — data scale + 12B
16
+ retarget + train + calibrate + measure; e4b not netting a speedup is a
17
+ data/target-speed problem, not architecture. Full design:
7
18
  [docs/design/dspark-speculative-decoding.md](../design/dspark-speculative-decoding.md).
8
19
 
20
+ ## PATH A (preferred, no training) — DeepSeek's published 12B drafter
21
+
22
+ DeepSeek released a TRAINED drafter for our exact target:
23
+ `deepseek-ai/dspark_gemma4_12b_block7` (6.86 GB bf16, γ=7; DeepSpec repo is
24
+ the MIT reference impl + the oracle — see the "DeepSpec ground truth" section
25
+ of the design doc). Once the deepspec-variant port lands:
26
+
27
+ ```sh
28
+ # 1. download the trained drafter (~6.9 GB)
29
+ mlx-bun get deepseek-ai/dspark_gemma4_12b_block7
30
+ # 2. oracle gate: dump their reference trace (temp 0 = RNG-free deterministic)
31
+ # in a torch venv, then compare ours round-for-round (scripts staged)
32
+ # 3. measure + serve (auto-detected via config.json architectures field)
33
+ mlx-bun serve --model gemma-4-12B-it-OptiQ-4bit --draft-model <snapshot-of-dspark_gemma4_12b_block7>
34
+ ```
35
+
36
+ ### Oracle protocol (step 2, expanded)
37
+
38
+ Two staged scripts implement the temp-0 deterministic trace protocol against
39
+ DeepSpec's own reference implementation (github.com/deepseek-ai/DeepSpec, MIT).
40
+ At temperature 0 their leaky-rejection verify degenerates to exact argmax
41
+ token-match — the whole round trace is RNG-free and reproducible, so a
42
+ plain round-for-round diff is a real bit-exact gate (same discipline as the
43
+ D2 diffusion oracle, minus RNG parity entirely — see the design doc's
44
+ "DeepSpec ground truth" section).
45
+
46
+ ```sh
47
+ # a. dump the reference trace — needs its OWN torch venv (NOT the mlx-lm
48
+ # oracle venv at /Users/joshrossi/Code/mlx-lm/.venv, which has no
49
+ # torch/deepspec and shouldn't); see the script header for the exact
50
+ # venv-setup + pip install steps.
51
+ .venv-deepspec/bin/python scripts/oracle-dspark-deepspec.py \
52
+ --target <snapshot-dir-of-google/gemma-4-12B-it> \
53
+ --drafter <snapshot-of-dspark_gemma4_12b_block7> \
54
+ --data prompts.jsonl --n 8 --max-new-tokens 128 \
55
+ --temperature 0 --confidence-threshold 0 \
56
+ --out goldens/dspark-deepspec/trace-thr0.jsonl
57
+
58
+ # b. compare our port round-for-round
59
+ bun scripts/dspark-deepspec-compare.ts \
60
+ --fixture goldens/dspark-deepspec/trace-thr0.jsonl \
61
+ --target gemma-4-12B-it-bf16 --drafter <snapshot-of-dspark_gemma4_12b_block7>
62
+ ```
63
+
64
+ Run the pair twice — once at `--confidence-threshold 0` (no pruning), once at
65
+ `0.5` — to gate both the base draft loop and the confidence-truncation path.
66
+
67
+ Fixture location: `goldens/dspark-deepspec/*.jsonl` follows the house
68
+ manifest convention (goldens/README.md) — these are machine-independent
69
+ **JSON manifests** (prompts + token-id traces, no tensor blobs), so they are
70
+ the **tracked** kind, not the `.bin` machine-specific kind. Regenerate by
71
+ rerunning step (a); there is no `scripts/regen-*` wrapper for this family yet
72
+ since the oracle run needs its own GPU-hosting torch venv, not the mlx-lm
73
+ oracle venv this repo otherwise standardizes on.
74
+
75
+ **The OptiQ-4bit caveat is load-bearing, not a footnote**: the fixture above
76
+ is generated against the bf16 HF target. A `dspark-deepspec-compare.ts` run
77
+ with `--target` pointing at our bf16 12B is the TRUE bit-exact gate. A run
78
+ with `--target` pointing at the OptiQ-4bit-quantized 12B we actually serve is
79
+ a **separate acceptance-rate measurement** — tapped hiddens differ
80
+ numerically at 4-bit, so round-for-round divergence there is expected, not a
81
+ bug; pass `--acceptance-only` on that arm so the script reports an aggregate
82
+ accept-length reading instead of a PASS/FAIL assert.
83
+
84
+ Caveat: trained against the bf16 HF target; our serving 12B is OptiQ-4bit —
85
+ tapped hiddens differ numerically, so measure acceptance rather than assuming
86
+ their 60–85%.
87
+
88
+ **FIRST LIVE RUN (2026-07-07, loaded machine — directional only):** τ ≈ 2.8
89
+ committed/target-forward (200 tok / 72 verify rounds; per-token acceptance
90
+ 26–33% — the predicted OptiQ-4bit degradation), output coherent, losslessness
91
+ holding. **But wall-clock LOST ~3.4×** (spec 14.6 vs serial 49.8 agg tok/s;
92
+ the 20 s bench TTFT is conc-4 queueing on the serial-forced spec lane, not
93
+ prefill). The tax is the drafter itself. **The full step-by-step program for
94
+ the next pass is
95
+ [docs/design/dspark-serving-program.md](../design/dspark-serving-program.md)**
96
+ (phases, gates, dependency graph; pickup at TurboQuant merge, only its
97
+ Phase 5 waits for it). Lever summary:
98
+ 1. **Quantize the drafter** — the big one, and the ideal quantization target
99
+ PERIOD: drafter numerics only move ACCEPTANCE, never correctness
100
+ (losslessness is structural), so aggressive bpw is safe and the gate is a
101
+ single acceptance A/B. 6.9 GB bf16 → ~1.8 GB @4-bit kills both the
102
+ ~3 GB/round bandwidth tax (2 GB lm_head + 7×134 MB markov_w2 reads) and
103
+ the 14 GB two-model memory pressure on 24 GB. Uniform 4-bit (mlx-native)
104
+ first as the baseline; **TurboQuant (Phase 13), when it lands, competes
105
+ on acceptance-per-byte against that baseline** — the drafter is its
106
+ lowest-risk first customer. Enabler either way: quantized-weight support
107
+ in deepspec-module's forward (quantized_matmul, the house pattern).
108
+ 2. **Tighten deepspec draftBlock** — ~14 host syncs/round (7 argmax + 7
109
+ confidence reads); replay the on-device chaining + deferred-conf
110
+ tightening already done for our own module (threshold=0 needs NO conf
111
+ reads).
112
+ 3. **captureLayer in the GENERATED gemma forwards** — tapped verifies
113
+ currently fall back to the monolith (correct-but-unspecialized); teach
114
+ scripts/gen-model.ts to emit the tap so spec verifies keep the fast path.
115
+ 4. **γ sweep** — γ=7 at ~30% acceptance wastes drafts;
116
+ `--num-draft-tokens 2..3` may net better wall-clock free of any code.
117
+
118
+ ## PATH B — train our own module (custom targets / research)
119
+
120
+ The payoff target is **12B** (27B is memory-infeasible to train on 24 GB; the
121
+ pipeline stays dim-generic so 27B remains a future recipe). All three scripts
122
+ take `--model` and (new, 2026-07-06) `--tap-layers` — the retarget is now a
123
+ pure parameterization, no code edits. **`--tap-layers` MUST be identical across
124
+ regen and train** (the shard feature dim is `m*H`, `m = tapLayers.length`).
125
+
126
+ 12B tap set: use **`5,17,29,41,46`** — DeepSeek's actual trained
127
+ `target_layer_ids` for this model (their layer-output convention matches our
128
+ tapLayers indexing; audited 2026-07-06). Our earlier `24,37,47,48` guess is
129
+ superseded.
130
+
131
+ ```sh
132
+ # 0. one-time: cache the 12B target + a prompt/topic corpus (thousands of
133
+ # on-distribution topics — the data-scale lever; 160 only reached per-pos ~0.17).
134
+ mlx-bun get mlx-community/gemma-4-12B-it-OptiQ-4bit
135
+ # (12B target may need the serve process stopped to free memory for regen.)
136
+
137
+ # 1. regen multi-layer training shards from the 12B target's own generations
138
+ bun scripts/dspark-regen-dflash.ts \
139
+ --model gemma-4-12B-it-OptiQ-4bit --tap-layers 5,17,29,41,46 \
140
+ --topics <thousands-of-topics.txt> --out data/dspark-12b --max-resp 320
141
+
142
+ # 2. train the drafter against the 12B (warm-start survivable via --resume;
143
+ # GPU runs kept getting killed ~step 5000 pre-resume). Optional A/B:
144
+ # --seq-head rnn trains the Eq-6 RNN head instead of Markov (Eq 5).
145
+ bun scripts/dspark-train-dflash.ts \
146
+ --model gemma-4-12B-it-OptiQ-4bit --tap-layers 5,17,29,41,46 \
147
+ --data data/dspark-12b --out ckpt/dspark-12b \
148
+ --iters 8000 --batch 8 [--seq-head rnn] [--ddraft 2560] [--resume ckpt/dspark-12b]
149
+
150
+ # 3. STS-calibrate (§3.2.1): fit per-position confidence thresholds on real
151
+ # verify outcomes and stamp them into dspark.json — this ACTIVATES the
152
+ # Alg-1 confidence scheduler at decode (uncalibrated = fixed-γ, no change).
153
+ bun scripts/dspark-calibrate.ts \
154
+ --model gemma-4-12B-it-OptiQ-4bit \
155
+ --drafter ckpt/dspark-12b --data <prompts.jsonl> [--target 0.5]
156
+
157
+ # 4. measure live τ + tok/s vs vanilla 12B decode (clean machine — dirty
158
+ # numbers are garbage). Run BOTH pre- and post-calibration for the
159
+ # scheduler's own A/B (MLX_BUN_DSPARK_MINCONF=<c> = manual uniform knob).
160
+ bun scripts/dspark-measure-dflash.ts \
161
+ --model gemma-4-12B-it-OptiQ-4bit \
162
+ --drafter ckpt/dspark-12b --data <prompts.jsonl>
163
+
164
+ # 5. serve it (auto-detected as kind=dspark via dspark.json; γ pinned to cfg.gamma)
165
+ mlx-bun serve --model gemma-4-12B-it-OptiQ-4bit --draft-model ckpt/dspark-12b
166
+ ```
167
+
168
+ Decision gate (write it down, RESULTS.md only if clean-machine): does 12B live
169
+ τ net a wall-clock win? If the draft loop dominates, do the loop-tightening
170
+ (per-position host syncs + double 262K LM-head) first — see the open items in
171
+ the design doc. Memory guard (`server.ts`) caps context to fit; a client
172
+ sending huge `max_tokens` trips admission — lower it.
173
+
9
174
  ## What happened (short version)
10
175
  - Built v1 first as a single-vector shortcut — it dropped the paper's core KV
11
176
  injection and was never really DSpark. Chunk (τ≈1.26) and article (τ≈1.16)
@@ -31,20 +196,21 @@ architecture problem. Full design + measured numbers:
31
196
  - Training `--resume` (warm-start from checkpoint) — the GPU runs kept getting killed ~step 5000; resume survives that.
32
197
 
33
198
  ## 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).
199
+ 1. **Retarget to 12B + scale data + train + calibrate + measure** — the GPU
200
+ recipe above is the complete remaining work. (27B stays a future recipe:
201
+ memory-infeasible to train on the 24 GB M4 Pro.)
202
+ 2. ~~Tighten the draft inference loop~~ **DONE 2026-07-06** (on-device token
203
+ chaining, deferred conf reads, `collectLogits:false`; bit-identity pinned).
204
+ 3. ~~Finish the paper~~ **DONE 2026-07-06**: RNN head (Eq 6, `--seq-head rnn`),
205
+ STS calibration (§3.2.1), Alg-1 confidence-scheduled draft-length pruning
206
+ (single-user form). ⚠ Eq 6 + §3.2.1 shapes are design-doc-faithful readings
207
+ (paper PDF absent) — verify against the paper when available.
208
+ 4. ~~Rename `dflash`→`dspark`~~ **DONE 2026-07-06** (canonical variant +
209
+ central loader `src/spec/dspark/loader.ts`; legacy stamps still load).
45
210
 
46
211
  ## Key files
47
- `src/spec/dspark/{module,data,generate}-dflash.ts`, `{loss,sample}.ts`;
212
+ `src/spec/dspark/{module,data,generate}-dflash.ts`, `{loss,sample,calibration,loader}.ts`;
213
+ `src/spec/{source,serve-loop,dflash-source,assistant-source}.ts` (the serve seam);
48
214
  `src/model/gemma4.ts` (hiddenTap), `gemma4-base.ts` (trim bypass);
49
215
  `scripts/dspark-{regen,train,measure}-dflash.ts`, `dspark-dflash-smoke.ts`.
50
216
 
@@ -12,7 +12,7 @@ area reviews + full mechanisms): `reports/kernel-perf-review-2026-07-01.json`
12
12
  vendored MLX steel machinery, exact-by-default backward with autograd + FD
13
13
  parity — marred by decision drift: the PLAN-decided 1.35× filter win was
14
14
  never landed and the in-code comments argued three contradictory positions
15
- (comments fixed 2026-07-01; the FLIP itself is still backlog #1).
15
+ (comments fixed 2026-07-01; the FLIP LANDED 2026-07-02 — see backlog #1).
16
16
  - **Training flash-attention kernel**: faithful port with exemplary post-bug
17
17
  discipline, correctly opt-in — but it copies a naive oracle (~30× slower)
18
18
  and its tests covered none of the production configs (extended 2026-07-01:
@@ -47,35 +47,109 @@ evalSftLoss error swallowing · tokens_per_sec metric · inverted comments
47
47
  (gemma4.ts:298, flash-cce filter trio) · silent steel fallback (now warns;
48
48
  H%128 throw narrowed) · flash-attn test blindness.
49
49
 
50
- STILL OPEN: **FUSED_DECODE=1 inside whole-graph compiled decode** bakes the
51
- tile loop at trace-time N — q-cat adapters grow N per step under shapeless
52
- replay, so the newest KV rows are silently never attended (gemma4-base.ts
53
- quantizedSdpaTiled; deferred for file-ownership at fix time — guard the combo
54
- or make the tile loop trace-safe).
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.
55
77
 
56
78
  ## The ranked optimization backlog (task #12)
57
79
 
58
- 1. **[S] Land the coeff filter at eps~1e-5** (+ measure blockMax skip on real
59
- data) — 1.35× ORPO head backward, ALREADY MEASURED
60
- (flash-cce-filter-realdata.ts, PLAN.md "filter-on-real-data"); gates:
61
- E4B=1 rerun + teacher-forced grad fidelity + eps=0 byte-identical.
62
- 2. **[M] planSegments full-attention isolation** — e4b@8K ~17.5→10 GB;
63
- unblocks 8K training on the 24 GB M4 Pro.
64
- 3. **[S] Auto-dispatch the training head by M** — exact fused QM head for
65
- short-M (~1.9×: 481 ms vs 934 ms at e4b M=512), flash only when memory
66
- demands; both heads already share the fusedRespLogpMean interface.
67
- 4. **[S] Fused decode full-capacity KV buffers + activeN** — kills 6
68
- ensureRowContiguous copies/layer/step; 1-3% @8k, ~4× that at 32k.
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.
69
126
  5. **[M] Collapse per-token host syncs in the spec/DSpark loop** — ~2γ+1
70
127
  round-trips → ~2 per verify cycle; directly attacks DSpark's fixed draft
71
128
  overhead.
72
129
  6. **[M] Pipeline the batched decode loop + clearCache cadence** — folded into
73
130
  the batching-v2 wave (docs/design/batching-v2-plan.md).
74
131
  7. **[S] Enable the 26B perf kernel** (open PLAN Phase E box; oracle frozen).
75
- 8. **[S] Bound the SFT segmented head** — replace full-[M,V] responseOnlyCe in
76
- the segmented head vjp; ~2-4 GB peak on long-response e4b SFT.
77
- 9. **[S] Segmented-step overhead pass** — detachLeaf single-copy, one evalAll
78
- barrier/segment, memoize the CPM5 prefix block-sparse mask.
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).
79
153
  10. **[M] e4b prefill gap** — profile-first; NOTE: the decode look-again could
80
154
  NOT reproduce RESULTS.md's 304-vs-373 row on the M1 Max (no eval-DB
81
155
  backing) — re-bench on the M4 Pro before building anything.
@@ -85,8 +159,13 @@ or make the tile loop trace-safe).
85
159
  The review's framing assumed the megakernel post-mortem's "M=1 decode is at
86
160
  the floor". The follow-up investigation (docs/investigations/
87
161
  decode-roofline-lookagain.md) OVERTURNED that: only the 12B is at the wall
88
- (~92-93% of measured roofline); CPM5 58-64%, e4b 64-70%, 26B 60-62%, with the
89
- host JS graph build as the #1 recoverable term. Its ranked fixes (CPM5
90
- compiled-decode port; build-ahead graph overlap spike; 26B gather-qmm
91
- bandwidth; e4b dispatch batching) COMPOSE with the backlog above and should be
92
- sequenced first where they overlap.
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.
@@ -1,7 +1,7 @@
1
1
  # Build journal — mlx-bun Lab (web UI + native quantize/train/dataset + pi web chat)
2
2
 
3
3
  *Branch `web-ui-and-native-lab`, 2026-06-13/14. Visual summary:
4
- [archive/mlx-bun-lab-report.html](../../archive/mlx-bun-lab-report.html). Evidence
4
+ [archive/mlx-bun-lab-report.html](../../docs/archive/mlx-bun-lab-report.html). Evidence
5
5
  artifact: `benchmarks/lab-verification.json` (regenerable, gitignored — produced by
6
6
  `scripts/verify-lab.ts`).*
7
7
 
@@ -22,7 +22,7 @@ page. Compute is **native Bun/MLX (no Python)** per Josh's decision.
22
22
 
23
23
  2. **Gate the riskiest unknown first.** Native LoRA needs autograd across the
24
24
  FFI boundary. Rather than discover mid-build that it's infeasible, the first
25
- thing built was `spikes/phase-train-vag.ts`: a tiny LoRA-shaped loss
25
+ thing built was `lab/spikes/phase-train-vag.ts`: a tiny LoRA-shaped loss
26
26
  (`mean((x + (x@A)@B − y)²)`) differentiated by `mlx_value_and_grad` through a
27
27
  `bun:ffi` JSCallback closure, checked against central finite differences.
28
28
  **PASS (<1e-2), exactly 2 grads returned (base frozen).** Only then was the