mlx-bun 0.0.10 → 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 (173) 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 +58 -35
  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 +14 -6
  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 +3 -2
  27. package/docs/design/ssd-kv-cold-tier.md +130 -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/dspark-handoff.md +182 -16
  35. package/docs/investigations/lab-build-journal.md +2 -2
  36. package/docs/investigations/openwiki-evaluation.md +249 -0
  37. package/docs/planning/IDEAS.md +14 -0
  38. package/docs/planning/PRODUCT_ROADMAP.md +2 -3
  39. package/docs/planning/chat-ui-vision-memory-first.md +199 -0
  40. package/docs/planning/chat-ui-vision.md +50 -0
  41. package/docs/planning/memory-docs-and-dag-plan.md +15 -6
  42. package/docs/planning/release-notes-v0.0.11.md +239 -0
  43. package/docs/reference/cli.md +35 -16
  44. package/docs/reference/distribution.md +13 -6
  45. package/docs/reference/embedding.md +35 -14
  46. package/docs/reference/features-matrix.md +97 -0
  47. package/docs/reference/library-api.md +30 -9
  48. package/docs/reference/memory.md +26 -6
  49. package/docs/reference/models.md +11 -1
  50. package/docs/reference/orpo-quickstart.md +3 -4
  51. package/docs/reference/server-api.md +634 -22
  52. package/docs/reference/server-config.md +250 -121
  53. package/docs/reference/training.md +14 -15
  54. package/package.json +5 -3
  55. package/src/anthropic.ts +13 -0
  56. package/src/audio/conformer.ts +647 -0
  57. package/src/audio/decode.ts +133 -0
  58. package/src/audio/features.ts +202 -0
  59. package/src/audio/transcode.ts +67 -0
  60. package/src/cli.ts +356 -105
  61. package/src/config.ts +49 -2
  62. package/src/download.ts +40 -0
  63. package/src/eval/README.md +7 -6
  64. package/src/eval/kl.ts +59 -3
  65. package/src/eval/quality-db.ts +2 -2
  66. package/src/eval/runner.ts +3 -3
  67. package/src/expert-offload.ts +15 -2
  68. package/src/flags.ts +13 -0
  69. package/src/generate.ts +330 -24
  70. package/src/grammar.ts +585 -0
  71. package/src/hub-rest.ts +242 -0
  72. package/src/kv-store.ts +442 -88
  73. package/src/lora.ts +8 -0
  74. package/src/md-modules.d.ts +42 -0
  75. package/src/media-fetch.ts +284 -0
  76. package/src/memory/rest.ts +245 -0
  77. package/src/memory/vault.ts +74 -0
  78. package/src/mlx/array.ts +106 -19
  79. package/src/mlx/autograd.ts +2 -2
  80. package/src/mlx/ffi.ts +27 -3
  81. package/src/mlx/metal-kernel.ts +7 -4
  82. package/src/mlx/ops.ts +153 -23
  83. package/src/mlx/turboquant-ops.ts +831 -0
  84. package/src/mlx/turboquant-tables.ts +237 -0
  85. package/src/model/batched-mask.ts +62 -1
  86. package/src/model/batched-quant.ts +206 -0
  87. package/src/model/batched-rotating-quant.ts +346 -0
  88. package/src/model/batched-rotating.ts +46 -1
  89. package/src/model/compiled-decode.ts +4 -15
  90. package/src/model/factory.ts +10 -4
  91. package/src/model/gemma4-base.ts +363 -39
  92. package/src/model/gemma4.ts +122 -33
  93. package/src/model/generated/gemma4-12b.ts +9 -13
  94. package/src/model/generated/gemma4-26b.ts +10 -5
  95. package/src/model/generated/gemma4-e4b.ts +14 -9
  96. package/src/model/minicpm5.ts +58 -102
  97. package/src/model/paged-kv.ts +253 -0
  98. package/src/model/qwen3-delta.ts +76 -15
  99. package/src/model/qwen3-moe.ts +404 -0
  100. package/src/model/qwen3.ts +9 -8
  101. package/src/model/qwen3_5.ts +88 -23
  102. package/src/model/support.ts +12 -1
  103. package/src/model/universal/dense.ts +40 -5
  104. package/src/model/universal/rope.ts +36 -21
  105. package/src/pi-web.ts +1299 -82
  106. package/src/preflight.ts +27 -8
  107. package/src/prompt-cache.ts +293 -33
  108. package/src/quantize/quantizer.ts +16 -1
  109. package/src/registry.ts +57 -2
  110. package/src/serve/batch-scheduler.ts +965 -72
  111. package/src/serve/generation-gateway.ts +214 -11
  112. package/src/serve/isolate.ts +417 -0
  113. package/src/serve/lane-registry.ts +44 -0
  114. package/src/serve/session-search.ts +326 -0
  115. package/src/server.ts +1380 -135
  116. package/src/spec/assistant-source.ts +150 -0
  117. package/src/spec/deepspec-source.ts +143 -0
  118. package/src/spec/dflash-source.ts +124 -0
  119. package/src/spec/dspark/ab-stats.ts +136 -0
  120. package/src/spec/dspark/calibration.ts +112 -0
  121. package/src/spec/dspark/deepspec-module.ts +801 -0
  122. package/src/spec/dspark/generate-dflash.ts +72 -20
  123. package/src/spec/dspark/loader.ts +23 -0
  124. package/src/spec/dspark/module-dflash.ts +278 -36
  125. package/src/spec/dspark/quantize-drafter.ts +91 -0
  126. package/src/spec/generate.ts +59 -25
  127. package/src/spec/ngram-source.ts +132 -0
  128. package/src/spec/serve-loop.ts +536 -0
  129. package/src/spec/source.ts +93 -0
  130. package/src/spec/two-model.ts +171 -0
  131. package/src/ssd-cache.ts +107 -35
  132. package/src/tokenizer.ts +288 -2
  133. package/src/tool-approvals.ts +131 -0
  134. package/src/tool-call.ts +408 -20
  135. package/src/train/flash-cce.ts +2 -2
  136. package/src/train/job.ts +0 -2
  137. package/src/train/trainer.ts +1 -4
  138. package/src/vision/prompt.ts +217 -48
  139. package/src/web/app.html +1184 -1789
  140. package/src/web/app.js +5667 -0
  141. package/src/web/icon.svg +13 -0
  142. package/src/web/manifest.webmanifest +24 -0
  143. package/src/web/src/adapters-panel.ts +288 -0
  144. package/src/web/src/api.ts +57 -0
  145. package/src/web/src/assistant.ts +485 -0
  146. package/src/web/src/chat.ts +1050 -0
  147. package/src/web/src/composer.ts +959 -0
  148. package/src/web/src/dataset.ts +132 -0
  149. package/src/web/src/finetune.ts +267 -0
  150. package/src/web/src/hub.ts +428 -0
  151. package/src/web/src/main.ts +98 -0
  152. package/src/web/src/markdown.ts +0 -0
  153. package/src/web/src/memory-panel.ts +572 -0
  154. package/src/web/src/model-picker.ts +159 -0
  155. package/src/web/src/palette.ts +346 -0
  156. package/src/web/src/protocol.ts +62 -0
  157. package/src/web/src/quantize.ts +167 -0
  158. package/src/web/src/rag.ts +274 -0
  159. package/src/web/src/sessions.ts +469 -0
  160. package/src/web/src/shell.ts +804 -0
  161. package/src/web/src/status.ts +252 -0
  162. package/src/web/src/ui-catalog.ts +92 -0
  163. package/src/web/sw.js +71 -0
  164. package/src/web/vendor/README.md +61 -0
  165. package/src/web/vendor/hljs-LICENSE +29 -0
  166. package/src/web/vendor/hljs-theme.css +59 -0
  167. package/src/web/vendor/hljs.js +26 -0
  168. package/src/model/fused-decode-kernel.ts +0 -312
  169. package/src/model/fused-geglu-kernel.ts +0 -198
  170. package/src/model/fused-mlp-kernel.ts +0 -169
  171. package/src/model/fused-swiglu-kernel.ts +0 -240
  172. package/src/model/steel-linear-kernel.ts +0 -96
  173. package/src/status-page.html +0 -497
@@ -0,0 +1,189 @@
1
+ # Repo cleanup plan
2
+
3
+ Status: PLANNED (2026-07-02, written post-v0.0.10). The audit behind it:
4
+ `.git` was 179 MB, driven by 497 machine-specific golden `.bin` blobs
5
+ (~290 MB working tree, every regen compounding history); root has ~30
6
+ entries of mixed tracked/untracked provenance; `repro/` was undocumented.
7
+ Already done in v0.0.10: `.bin` goldens untracked + gitignored +
8
+ `goldens/README.md` policy; CLAUDE.md root-layout map. This plan is the
9
+ rest, phased so each step is safe, reversible (except C1, which is the
10
+ point), and independently shippable.
11
+
12
+ Principle: the hygiene discipline IS the product edge — every phase ends
13
+ with a guardrail so the mess can't re-form, not just a one-time sweep.
14
+
15
+ ## What can actually be lost? Nothing. (Read this before re-litigating.)
16
+
17
+ Every binary this plan untracks or purges survives in MULTIPLE independent
18
+ places, so "we might lose the goldens/fixtures" is not a reason to stall:
19
+
20
+ 1. **Both dev laptops' working trees** — `git rm --cached` and `.gitignore`
21
+ never touch disk; the files sit exactly where they were.
22
+ 2. **Git history** — until Phase C runs, every previously-committed byte is
23
+ still in `.git` and recoverable with `git show <rev>:<path>`.
24
+ 3. **The Phase-C mirror backup** — `scripts/history-rewrite.sh` refuses to
25
+ rewrite before writing `~/mlx-bun-history-backup-<date>.tar` (a full
26
+ pre-rewrite mirror, kept forever). Even after the purge, every historical
27
+ blob is one `tar -x` away.
28
+ 4. **Regen scripts** — goldens regenerate from `scripts/regen-*` + the
29
+ oracle venv; the universal-rope fixtures from their gen script.
30
+ 5. **Pinned-hash fetch** — the two LoRA adapter fixtures are sha256-pinned
31
+ in `scripts/fetch-test-fixtures.sh`, sourced from the `test-fixtures-v1`
32
+ GitHub release (one command to create — see the script header) or
33
+ either laptop.
34
+
35
+ And no test can break: every suite that reads a binary fixture gates on
36
+ FILE PRESENCE and skips cleanly when it's absent (goldens via
37
+ tests/goldens.ts, LoRA via `haveAdapters`, universal-rope via its own
38
+ check). Untracked ≠ deleted ≠ unrunnable.
39
+
40
+ ## Phase A — root consolidation (30 min, zero risk) — DONE 2026-07-02
41
+
42
+ - `[x]` **A1. `lab/`**: `git mv repro lab/repro && git mv spikes lab/spikes`.
43
+ One home for "code that proves a point but isn't the product": upstream
44
+ bug reproductions and feasibility probes. Update the CLAUDE.md layout map
45
+ + the two docs that reference `repro/` (grep first: `grep -rn "repro/"
46
+ docs/ *.md`).
47
+ - Also updated every path reference (CLAUDE.md, PLAN.md, PLAN-archive.md,
48
+ docs/investigations/lab-build-journal.md, docs/design/docs-reorg-plan.md,
49
+ and the `repro/`-pointing comments in src/mlx/{ffi,array,autograd,ops}.ts
50
+ + tests/{ffi-jit,train-autograd}.test.ts + the repro's own ISSUE.md).
51
+ - `[x]` **A2. root strays** (all untracked; no git surgery): delete
52
+ `train.log`; move `benchmarks-e4b-flag-matrix-2026-06-15.json` into
53
+ `reports/`; confirm `.DS_Store` is gitignored globally.
54
+ - `train.log` deleted; benchmark json moved to `reports/`; `.DS_Store`
55
+ already globally gitignored (`.gitignore`).
56
+ - `[x]` **A3. `archive/` → `docs/archive/`**: the two frozen HTML reports
57
+ are docs, not a root concern. `git mv`, update CLAUDE.md map.
58
+ - Exit: root = code (`src tests scripts`), product surfaces (`docs website
59
+ packaging bin extensions`), fixtures (`goldens fixtures lab`), meta
60
+ (README/PLAN/STATUS/CLAUDE + configs), and gitignored working dirs. Every
61
+ tracked root entry appears in the CLAUDE.md layout map.
62
+
63
+ ## Phase B — tracked-content policy + guardrail (1 h) — DONE 2026-07-02 (B2 closed same day)
64
+
65
+ - `[x]` **B1. Binary-in-git gate**: `scripts/check-hygiene.ts` — FAILS on
66
+ any tracked file that is (a) >1 MB, or (b) matches `*.bin|*.safetensors|
67
+ *.dylib|*.gguf|*.metallib` — outside an explicit, size-capped, justified
68
+ allowlist. Wired into `scripts/test.sh` (runs before tests) AND CI
69
+ (`.github/workflows/ci.yml` `hygiene` job, gates typecheck). Supports
70
+ `--staged` for pre-commit use. This is the "can't re-form" guarantee.
71
+ - `[x]` **B2. fixtures/ audit**: 13 MB, 2 tracked `.safetensors`.
72
+ Decision per file:
73
+ - `fixtures/adapters/{upper,french}/adapters.safetensors` (6.6 MB each):
74
+ KEEP tracked, on the gate's explicit allowlist (cap 7 MB). They are
75
+ stable one-time-trained LoRA adapter INPUTS (trained 2026-06-10, never
76
+ churned) for the opt-in `MLX_BUN_TEST_LORA` hot-swap test, which also
77
+ requires the e4b base. The tracked `adapter_config.json` + `data-*/`
78
+ jsonl fully describe how to reproduce them; a bit-exact regen script
79
+ (trainer-of-record: optiq `lora train` vs mlx-lm — the saved config is
80
+ ambiguous) is a Josh task to confirm + smoke. NOT untracked now because
81
+ an untested untrack would break the lora hot-swap parity test, and these
82
+ do not churn (the goldens-bloat problem was *churning* artifacts). This
83
+ is a deliberate allowlist exception, not the default `fixtures/ ≤ 2 MB`
84
+ policy.
85
+ - **CLOSED later the same day**: untracked + gitignored after pinning the
86
+ bytes by sha256 in `scripts/fetch-test-fixtures.sh` (fetch from the
87
+ `test-fixtures-v1` release / either laptop / pre-rewrite history; the
88
+ gated LoRA test skips cleanly when absent — verified). The regen-
89
+ ambiguity concern was real but orthogonal: preservation is by pinned
90
+ original bytes, not by retraining. Allowlist entry removed; gate green
91
+ with ZERO multi-MB binaries tracked.
92
+ - `[x]` **B3. tests/ binary audit**: 27 tracked `.bin` under
93
+ `tests/fixtures/universal-rope/` (4–8 KB each, ~108 KB total). All are
94
+ model-free CI-load-bearing bit-exact oracle fixtures (used by
95
+ `tests/universal-rope.test.ts`, which skips cleanly if absent),
96
+ regenerated by `scripts/gen-universal-rope-fixtures.py`. STAY, on the
97
+ allowlist (cap 16 KB each). Also allowlisted: `tests/fixtures/qwen-delta-golden.json`
98
+ (1.08 MB text JSON golden, model-free, regen script exists).
99
+ - Exit: `git ls-files | grep -E '\.(bin|safetensors|dylib)$'` returns only
100
+ allowlisted entries; the gate enforces it forever.
101
+
102
+ ## Phase C — history rewrite (the 179 MB) — DONE 2026-07-02 (Josh go)
103
+
104
+ Executed 2026-07-02 on Josh's explicit go. Result: `.git` **182 MB →
105
+ 20 MB**. Notes on what actually ran (vs the sketch below):
106
+
107
+ - C1: dry-run `--analyze` confirmed goldens `.bin` = the entire bloat.
108
+ Actual invocation, narrower than sketched (no size threshold needed):
109
+ `git filter-repo --invert-paths --path-glob 'goldens/*.bin'
110
+ --path-glob 'goldens/*/*.bin'` — both globs needed because git
111
+ pathspec `**` doesn't span the flat + one-deep layout; covers all 497
112
+ historical blobs. Verified pre-push: 343 commits in/out, HEAD tree
113
+ bit-identical to original, zero `.bin` left anywhere in goldens
114
+ history, tags remapped.
115
+ - C2: force-pushed main + branches + tags. Mirror backup at
116
+ `~/mlx-bun-mirror-backup-2026-07-02.tar.gz` (143 MB) — delete once
117
+ both boxes verified. M1 Max done: fetch --force + reset --hard,
118
+ surviving branches remapped, stale agent-session worktrees/branches
119
+ deleted (Josh: only main matters), stray `refs/codex`/`refs/original`
120
+ pruned, reflog expired, gc'd to 20 MB. **M4 Pro remaining (Josh)**:
121
+ `git fetch origin --tags --force --prune && git reset --hard
122
+ origin/main` (or fresh clone).
123
+ - C3: `v0.0.10` resolves; release assets untouched (they live outside
124
+ git history); hygiene gate green; regen path proven live —
125
+ `bun scripts/regen-fused-sdpa-goldens.ts` rewrote
126
+ `goldens/apple-m1-max/fused-sdpa.{json,bin}`. The full suite could not
127
+ be run from the executing session (tooling restriction); run it once
128
+ per box as the final check.
129
+
130
+ **Now ONE COMMAND**: `scripts/history-rewrite.sh` (dry-run by default,
131
+ `MLX_BUN_REWRITE_GO=1` to execute) — it enforces every precondition below,
132
+ writes the permanent mirror backup first, purges goldens `.bin` + the
133
+ adapter safetensors from history, force-pushes, and prints the re-clone
134
+ steps for the other laptop.
135
+
136
+ One-time, destructive-by-design, ~15 min total. Preconditions: BOTH
137
+ laptops fully pushed (verify `git log origin/main..HEAD` empty on each);
138
+ no open PRs; a fresh `git clone --mirror` tarball kept as belt-and-
139
+ suspenders until both machines are re-cloned.
140
+
141
+ - `[x]` **C1. rewrite**: `git filter-repo --strip-blobs-bigger-than 200K
142
+ --path-glob 'goldens/**/*.bin' --invert-paths` (exact invocation to be
143
+ dry-run first with `--analyze`; target: drop all historical goldens
144
+ `.bin` while keeping every text file's history intact).
145
+ - `[x]` **C2. force-push** `main` (+ tags), re-clone on BOTH laptops
146
+ (M1 Max + M4 Pro), re-link local-only assets (nothing tracked is lost —
147
+ the blobs being dropped are regenerable and already untracked at HEAD).
148
+ - `[x]` **C3. verify**: `du -sh .git` (expect ~15–25 MB), `bash
149
+ scripts/test.sh` green, one `scripts/regen-*` smoke to prove the regen
150
+ path, tag `v0.0.10` still resolves + release assets untouched (GitHub
151
+ releases store assets outside git history).
152
+ - NOT doing LFS: 290 MB of churning machine-specific artifacts is exactly
153
+ what LFS quotas punish, and the regen scripts make the artifacts
154
+ reproducible — a fixture server is overkill for a two-laptop project.
155
+ - Exit: fresh clone < 30 MB ✓ (20 MB); M1 Max on rewritten history ✓;
156
+ M4 Pro pending its one-line reset (see notes above).
157
+
158
+ ## Phase D — docs debt (follow-the-map, 1–2 h, independent) — gate DONE 2026-07-02
159
+
160
+ - `[~]` **D1.** PLAN.md is 139 KB — move closed phases older than Phase 17
161
+ into PLAN-archive.md (the established pattern; keep PLAN.md scannable).
162
+ - **Finding (2026-07-02):** no candidates currently. Every phase <17 in
163
+ PLAN.md (6, 7, 12, 13, 14, 15, 16) is marked `[~]` or `[ ]` — none are
164
+ `[x]`/CLOSED, and the established archive pattern archives only
165
+ `[x]`-marked phases. Phases 6 and 16 have *all* their checkboxes
166
+ checked but carry a deliberate `[~]`; reclassifying them to closed +
167
+ archiving is a Josh state-call (they may have ongoing narrative
168
+ threads). The closed `oMLX` phase (2026-07-02) and Phase 19 are
169
+ *newer* than 17, so they stay in PLAN.md as recent history per the
170
+ criterion. Revisit D1 when a <17 phase is confirmed closed.
171
+ - `[x]` **D2.** `docs/investigations/curve-runs/` + two untracked HTML
172
+ strays in docs/investigations: gitignore or move under `reports/`.
173
+ - Already satisfied: `docs/investigations/curve-runs/` is gitignored
174
+ (`.gitignore`), and the four HTML strays (`curve-terrain.html`,
175
+ `hlg-ab.html`, `hlg-report.html`, `hlg-explorer.html`) are gitignored
176
+ via `docs/investigations/*.html`. No action needed.
177
+ - `[x]` **D3.** The CLAUDE.md doc-map lists are hand-maintained; add a
178
+ `scripts/check-hygiene.ts` assertion that every `docs/**/*.md` appears in
179
+ the map (same guardrail spirit as B1).
180
+ - Implemented in `check-hygiene.ts` (`docs-map` check). First run caught
181
+ 10 drifted docs (3 design, 1 investigation, 6 planning) — all added to
182
+ the CLAUDE.md map. Gate now green; CI enforces it on every push/PR.
183
+
184
+ ## Ordering & ownership
185
+
186
+ A and D anytime (safe, agent-executable solo) — DONE 2026-07-02. B gate
187
+ DONE 2026-07-02 (B2 adapter-untrack deferred to Josh — see B2 finding).
188
+ C executed 2026-07-02 on Josh's explicit go — only remaining tail is the
189
+ M4 Pro reset + deleting the backup tarball once both boxes are green.
@@ -1,9 +1,25 @@
1
1
  # Runtime isolation — the AI may crash, the UI never may
2
2
 
3
- Status: **design** (not started — deferred future work; no scheduled PLAN.md phase)
3
+ Status: **P1 LANDED 2026-07-05** (opt-in `--isolate`; default flip after
4
+ bake). Phase 1 (yields) fully landed same day (the gateway onToken hop
5
+ existed; prefill chunk yields added). **ARCHITECTURE DECISION (2026-07-05,
6
+ deviates from the IPC sketch below):** the engine child is the ENTIRE
7
+ existing server on a unix domain socket; the parent is a thin reverse
8
+ proxy (src/serve/isolate.ts). Rationale: the gateway-as-IPC-client cut
9
+ would serialize grammar controllers (WASM), vision embeddings (GPU
10
+ arrays), sampler closures, and prompt-cache hooks over a message protocol
11
+ — weeks of re-plumbing; HTTP-over-UDS reuses the whole serving stack
12
+ verbatim, streams SSE for free, and cancellation is abort propagation.
13
+ The inter-process API is the existing /v1 surface — no second protocol.
14
+ **Measured (M1 Max, cpm5, paired)**: proxied 197.2 vs direct 197.9 tok/s
15
+ (−0.4%, noise), TTFT +2 ms, SSE per-token granularity preserved (96 reads
16
+ / 96 tokens), parent answers in 0.6 ms mid-decode (criterion: <50 ms),
17
+ child kill → 502 + respawn (gated model-free + E2E:
18
+ tests/isolate-proxy.test.ts, tests/isolate-e2e.test.ts). Not proxied yet:
19
+ /ws/chat (501 + pointer). P2 = child-per-model pool (multi-model switch by
20
+ spawn-overlap); P3 = GPU-lease unification. The original design follows.
4
21
  Owner: serving layer / runtime
5
- Default today: inference runs **in-process**, on the server's event-loop
6
- thread. This doc proposes moving it to a **subprocess**.
22
+ Default today: inference runs in-process unless `--isolate` is passed.
7
23
 
8
24
  ## Goal
9
25
 
@@ -249,8 +249,9 @@ returns the split indices + which donor K/V to save, computed from the above.
249
249
  - The chunk data needs ≥4096 (median 4092; 0% ≤2048; 98% ≤8192). Success = e4b
250
250
  trains at 8192 ≤ ~10 GB AND the chunk eval (`scripts/chunk-eval.ts`) shows a
251
251
  quality delta on e4b like MiniCPM5's.
252
- - Training requires `MLX_BUN_PERF_KERNEL=0 MLX_BUN_FUSED_GELU=0` (those fused
253
- CustomKernels have no vjp).
252
+ - Training needs no env-flag sanitization (the no-vjp fused CustomKernels that
253
+ once required `MLX_BUN_PERF_KERNEL=0 MLX_BUN_FUSED_GELU=0` were deleted
254
+ 2026-07-05; see docs/design/unified-engine-frontier-plan.md §6).
254
255
  - MLX `Date.now()`/random caveats don't apply here; standard determinism via seed.
255
256
 
256
257
  ## 7. Key files
@@ -1,6 +1,22 @@
1
1
  # SSD cold tier for the prompt/KV cache
2
2
 
3
3
  Status: **LANDED P1–P3 (2026-07-02, same day)** — P4 hardening extras remain.
4
+ **Layer-0 upgrade (2026-07-05, unified-engine plan):** D2 finished properly —
5
+ the two-tier take/restore/trim dance moved INSIDE `PromptCache.take()`
6
+ (structural `ColdTier` interface; the server binds SsdCacheStore + model into
7
+ it), so EVERY consumer tiers automatically: the serial lane's hand-rolled
8
+ block in runGeneration is gone, and the BATCH SCHEDULER restores prefixes
9
+ from disk at admission (gated end-to-end: tests/batch-scheduler.test.ts
10
+ "SSD tier through the batch lane"). `PromptCache.onPut` drives the debounced
11
+ write-behind snapshot for both lanes' puts. NEW: **idle demotion**
12
+ (`--ssd-demote-idle`, default 300 s with the tier on) — entries unused past
13
+ the threshold spill to disk and free their GPU arrays; RAM drains between
14
+ agent bursts while prefixes stay reachable via zero-copy restore. The sweep
15
+ runs only when the gateway is fully idle (a runExclusive would otherwise
16
+ drain a live batch). Rationale (Josh): prefill is the cost being avoided —
17
+ 12B prefill is 168 tok/s, so a 30k agent context is ~3 min of GPU compute vs
18
+ a ~1 s NVMe restore; SSD-vs-RAM (40×) is the wrong comparison, SSD-vs-
19
+ recompute (50–180×, growing with context) is the real one.
4
20
 
5
21
  **Measured (M1 Max, MiniCPM5, 13.7k-token prefix, kv_config quant):**
6
22
  - Restart TTFT: **12,083 ms (full prefill) → 236 ms** restored — 2% of full
@@ -156,3 +172,117 @@ Total ~7–10 working days.
156
172
  discipline) before making footprint claims.
157
173
  - **Stale-token poisoning**: `configFingerprint` + `tokenizerHash` + ns;
158
174
  chat-template drift degrades to a shorter matched prefix (safe).
175
+
176
+ ## Addendum 2026-07-07 — A7 closure: bounded memory on BOTH sides of the file
177
+
178
+ The 07-06/07 serve benches flagged elevated ctx/restart-leg RSS on
179
+ --ssd-cache arms ("A7"). Root-caused in three parts; the first two were
180
+ real defects, both fixed in `src/kv-store.ts`:
181
+
182
+ - **Write side** (residual after the v3 streaming writer of 4f9bad9): the
183
+ per-tensor `rawBytes()` readback ended in a JS-heap `.slice()`; under GC
184
+ lag the dead copies outlive the flush loop, re-spiking RSS toward the
185
+ whole entry. The writer now hashes and writes from a ZERO-COPY view of
186
+ the contiguous mlx buffer (`MlxArray.rawBytesView`) — no JS-heap copy
187
+ exists at any point; save-transient mlx memory measured 0 bytes for
188
+ contiguous sources (one tensor otherwise).
189
+ - **Restore side** (never covered by A7's original scope): the zero-copy
190
+ restore (COW mmap + `fromPointer`) became a per-restore PROCESS-LIFETIME
191
+ mapping leak once the 2026-07-06 FFI-dtor fix removed the unmap signal,
192
+ and the exactly-offset-sized restored buffers made the first
193
+ post-restore decode step concat-copy the entire entry into fresh mlx
194
+ memory. Restore is now a **streamed copy** — each tensor is copied into
195
+ an mlx-owned leaf (`fromBytesCopy`, no dtor contract), its clean file
196
+ pages dropped (`MADV_DONTNEED`; tensor offsets are 16 KiB-aligned = the
197
+ arm64 page size), the mapping unmapped before `loadKvCache` returns, and
198
+ plain-KV tensors land in STEP-rounded capacity with ≥1 token of slack so
199
+ the first step updates in place. Measured (512 MB synthetic entry): mlx
200
+ peak during restore+first-step = live entry + ONE tensor (552 vs 520 MB
201
+ active); `vmmap` confirms no `.mlxkv` mapping survives; 12B real-model
202
+ cold cache-load → first token 277 ms (parity with the old zero-copy
203
+ ~240 ms claim — the old path paid the same whole-entry copy on the first
204
+ decode step anyway). Byte identity of the copy-restore is pinned for all
205
+ five cache kinds (save → load(verify) → re-save hash-identical;
206
+ tests/kv-store.test.ts "copy-restore byte identity") plus the real-model
207
+ bf16/quantized/SSM continuation suites.
208
+ - **What was NOT a defect**: most of the benched leg delta is `ps` RSS
209
+ ACCOUNTING — the write-behind's hash+write CPU-touches the live KV
210
+ entry, which makes those already-allocated unified-memory pages visible
211
+ to `ps` (GPU-written buffers and python-arm KV never show). The bench
212
+ report now footnotes this instead of promising a pending fix.
213
+
214
+ Supersedes the D2 line "restored entry carries its MmapFile, unmapped on
215
+ entry dispose" and the D1 "restore = one zero-copy mmap with lazy page
216
+ fault-in" mechanics above — the economics (SSD-vs-recompute) and the
217
+ zero-work-on-token-loop property are unchanged.
218
+
219
+ ## Addendum 2026-07-07 — write-behind scheduling contract: flush only while idle
220
+
221
+ The 07-07 serve bench showed decode@ctx losing to mlx-lm on the bun arms
222
+ (e4b −9.3%, 12B −3.9%) while short-ctx decode won on every model — and the
223
+ mixed arm (4× smaller flush bytes) BEAT the bf16 arms at decode@ctx. Root
224
+ cause: "non-blocking" write-behind was only non-blocking at the event-loop
225
+ level. Every per-tensor flush step is (1) `ops.contiguous` — a kernel on
226
+ the SAME GPU stream decode uses — then (2) `rawBytesView()` → a synchronous
227
+ `mlx_array_eval` that blocks the JS thread until the stream drains, then
228
+ (3) a synchronous multi-MB `writeSync`. The `setImmediate` pacing
229
+ interleaved those slices exactly between decode tokens, so a ~16k entry's
230
+ flush overlapping the bench's cached ctx repeats taxed their 64-token
231
+ decode windows (reproduced standalone: e4b 9.5k-token entry, repeat decode
232
+ 37.9 vs cold 47.1 tok/s; flat 44–46 across all three samples post-fix).
233
+ mlx-lm runs no equivalent background work — the gap was pure self-inflicted
234
+ contention.
235
+
236
+ **The contract now:** the flush only progresses while the engine is idle.
237
+ - `GenerationGateway.busy` covers BOTH lanes (mutex held/awaited — the
238
+ serial lane shows zero rows — OR batch rows active/pending);
239
+ `gateway.onIdle()` is the poll-based waiter (`generation-gateway.ts`).
240
+ - `saveKvCacheAsync`/`SsdCacheStore.storeAsync` accept a per-step
241
+ `waitTurn` gate awaited before EVERY tensor step (including the first),
242
+ so a request arriving MID-flush pauses the remaining tensors until idle
243
+ again; the server passes `() => gateway.onIdle()` at both chain sites
244
+ (write-behind snapshots AND eviction/demotion spills). Gate failures are
245
+ swallowed — scheduling advice must never corrupt the write path.
246
+ - `MLX_BUN_SSD_WRITEBEHIND=0` disables write-behind snapshots entirely
247
+ (kill switch + paired-A/B lever; restart survival then degrades to
248
+ spill-on-evict only).
249
+
250
+ Accepted tradeoffs: durability waits for a quiet moment (single-user
251
+ serving quiesces constantly; sustained hammering defers the flush AND the
252
+ spill clones' GPU-memory release, bounded by the serial chain); one
253
+ in-flight tensor step (~10–15 MB) can still land ahead of a just-arrived
254
+ request; and the restart window tightens — the flush now runs in idle gaps
255
+ instead of overlapping decodes, so the bench's 2.5 s pre-kill beat must
256
+ absorb it (verified surviving: e4b 9.5k entry, restart cached=9575 with
257
+ the gate on).
258
+
259
+ ## Addendum 2026-07-07 (later) — bounded retention + admission-safe snapshots
260
+
261
+ The post-merge review found two operational holes in the contract above:
262
+
263
+ 1. **Snapshot timer could stall batch admission.** `scheduleSsdSnapshot`'s
264
+ debounced timer fired `gateway.runExclusive` unconditionally; the
265
+ exclusive registers a serial WAITER, which holds batch admission until
266
+ every running row drains naturally — a background durability timer
267
+ periodically freezing admission under `--batch N` sustained load. Fixed
268
+ with the same guard `demoteIdle` always had: the timer checks
269
+ `activeRows/pendingRows` first and RE-ARMS (5 s, unref'd) while busy —
270
+ entries are immutable, a late snapshot is equally valid.
271
+ 2. **Starved-gate clone retention was unbounded.** Every eviction/demotion/
272
+ snapshot chained GPU-pinning zero-copy clones onto a bare serial promise
273
+ chain, and every flush step awaits `onIdle()` — under continuous traffic
274
+ the chain starves and resident memory = prompt-cache cap + every queued
275
+ clone, defeating the byte cap exactly under the load causing evictions.
276
+ Now all three producers go through **`SpillQueue`** (kv-store.ts):
277
+ queued bytes are capped (default 2 GB = a quarter of the 8 GB RAM-cache
278
+ default; `MLX_BUN_SSD_SPILL_QUEUE_GB` overrides) — over cap the OLDEST
279
+ not-mid-store item drops with its clones disposed immediately. A dropped
280
+ spill is a future cache miss, never a wrong result: the contention-free
281
+ alternative to letting the flush cut into decode. `/stats.ssd_cache`
282
+ exposes `pending_spills` / `pending_spill_bytes` / `dropped_spills`.
283
+
284
+ Explicitly accepted (documented, not built): **no shutdown flush** — a
285
+ SIGINT/SIGTERM during (or within the debounce after) traffic loses queued
286
+ write-behind entries; restart survival degrades to whatever already
287
+ flushed. This is a best-effort cache tier; the cost of a lost entry is one
288
+ re-prefill. `SpillQueue.drain()` exists if a shutdown hook is ever wanted.