mlx-bun 0.0.13 → 0.3.0

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 (427) hide show
  1. package/README.md +71 -578
  2. package/bin/mlx-bun.mjs +1 -1
  3. package/docs/README.md +91 -0
  4. package/docs/{design → archive}/hlg-sampling.md +3 -3
  5. package/docs/{design → archive/investigations}/audio-input-plan.md +4 -4
  6. package/docs/{design → archive/investigations}/colibri-glm52-port.md +1 -1
  7. package/docs/{design → archive/investigations}/diffusion-gemma-port.md +3 -3
  8. package/docs/{design → archive/investigations}/faithful-l1-consolidation.md +4 -4
  9. package/docs/{investigations → archive/investigations}/hlg-sampling-investigation.md +1 -1
  10. package/docs/{design → archive/investigations}/minicpm5-decode-megakernel.md +11 -11
  11. package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
  12. package/docs/archive/investigations/pareto-specialized-runtime-findings.md +92 -0
  13. package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
  14. package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
  15. package/docs/{planning → archive/planning}/chat-ui-vision.md +1 -1
  16. package/docs/{planning → archive/planning}/chunk-task-roadmap.md +1 -1
  17. package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +4 -4
  18. package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +1 -1
  19. package/docs/{planning → archive/planning}/release-notes-v0.0.9.md +1 -1
  20. package/docs/archive/planning/release-notes-v0.2.0.md +49 -0
  21. package/docs/{planning → archive/planning}/website-readme-pass-plan.md +1 -1
  22. package/docs/design/batching.md +569 -0
  23. package/docs/design/decode-speed-program.md +3178 -169
  24. package/docs/design/dreaming-nightly-pipeline.md +675 -282
  25. package/docs/design/generic-model-support.md +579 -334
  26. package/docs/design/kv-cache.md +680 -0
  27. package/docs/design/orpo-training.md +673 -1239
  28. package/docs/design/speculative-decoding.md +1627 -0
  29. package/docs/design/turboquant.md +2102 -0
  30. package/docs/design/unified-engine-frontier-plan.md +1831 -727
  31. package/docs/design/web-chat-redesign.md +666 -1083
  32. package/docs/planning/PRODUCT_ROADMAP.md +34 -1
  33. package/docs/planning/release-notes-v0.3.0.md +35 -0
  34. package/docs/planning/video-series.md +421 -0
  35. package/docs/reference/benchmarks.md +1162 -0
  36. package/docs/reference/cli.md +395 -149
  37. package/docs/reference/distribution.md +321 -181
  38. package/docs/reference/environment.md +348 -0
  39. package/docs/reference/glossary.md +39 -0
  40. package/docs/reference/library-api.md +580 -142
  41. package/docs/reference/memory.md +227 -64
  42. package/docs/reference/models.md +137 -6
  43. package/docs/reference/server-api.md +938 -422
  44. package/docs/reference/server-config.md +652 -320
  45. package/docs/reference/training.md +393 -254
  46. package/docs/reference/troubleshooting.md +166 -0
  47. package/package.json +10 -5
  48. package/src/admission-plan.ts +18 -0
  49. package/src/anthropic.ts +92 -3
  50. package/src/audio/decode.ts +2 -2
  51. package/src/audio/features.ts +2 -2
  52. package/src/backends/mlx/autoregressive.ts +108 -0
  53. package/src/backends/mlx/batch-group.ts +1646 -0
  54. package/src/backends/mlx/diffusion.ts +39 -0
  55. package/src/backends/mlx/draft-target.ts +47 -0
  56. package/src/backends/mlx/evaluation.ts +105 -0
  57. package/src/backends/mlx/fill-append.ts +45 -0
  58. package/src/backends/mlx/gateway-binding.ts +71 -0
  59. package/src/backends/mlx/graph.ts +38 -0
  60. package/src/backends/mlx/memory-client.ts +189 -0
  61. package/src/backends/mlx/memory-guard.ts +40 -0
  62. package/src/backends/mlx/methods.ts +90 -0
  63. package/src/backends/mlx/model-prompt.ts +164 -0
  64. package/src/backends/mlx/model-serving.ts +79 -0
  65. package/src/backends/mlx/prefill.ts +33 -0
  66. package/src/backends/mlx/request-policy.ts +20 -0
  67. package/src/backends/mlx/rollback.ts +51 -0
  68. package/src/backends/mlx/serial-executor.ts +273 -0
  69. package/src/backends/mlx/speculative.ts +114 -0
  70. package/src/backends/mlx/state-views.ts +26 -0
  71. package/src/backends/mlx/text-engine.ts +73 -0
  72. package/src/cli.ts +189 -61
  73. package/src/client.ts +66 -0
  74. package/src/config.ts +21 -4
  75. package/src/contracts/completion.ts +16 -0
  76. package/src/contracts/execution.ts +49 -0
  77. package/src/contracts/generation.ts +103 -0
  78. package/src/contracts/host.ts +7 -0
  79. package/src/contracts/jobs.ts +66 -0
  80. package/src/contracts/pi-provider.ts +30 -0
  81. package/src/contracts/pi.ts +259 -0
  82. package/src/contracts/resources.ts +18 -0
  83. package/src/contracts/scheduling.ts +27 -0
  84. package/src/contracts/tsconfig.json +14 -0
  85. package/src/diffusion/diffusion-generate.ts +218 -153
  86. package/src/embed.ts +1 -1
  87. package/src/engine/admission.ts +59 -0
  88. package/src/engine/cancellation.ts +38 -0
  89. package/src/engine/engine.ts +56 -0
  90. package/src/engine/execution-coordinator.ts +70 -0
  91. package/src/engine/execution-plan.ts +51 -0
  92. package/src/engine/resources.ts +42 -0
  93. package/src/engine/scheduler.ts +67 -0
  94. package/src/engine/session.ts +247 -0
  95. package/src/eval/README.md +44 -1
  96. package/src/eval/kl.ts +263 -23
  97. package/src/eval/reference-logits.ts +207 -0
  98. package/src/eval/runner.ts +10 -79
  99. package/src/eval/tasks/bfcl.ts +2 -1
  100. package/src/eval/tasks/gsm8k.ts +2 -1
  101. package/src/eval/tasks/hashhop.ts +2 -1
  102. package/src/eval/tasks/humaneval.ts +5 -4
  103. package/src/eval/tasks/ifeval.ts +2 -1
  104. package/src/eval/tasks/mmlu.ts +2 -1
  105. package/src/expert-io.ts +3 -1
  106. package/src/expert-offload.ts +3 -1
  107. package/src/fill/echo-index.ts +188 -0
  108. package/src/fill/fill-session.ts +452 -0
  109. package/src/fill/proposal.ts +68 -0
  110. package/src/fill/schema-rows.ts +506 -0
  111. package/src/fill/tool-boundary.ts +103 -0
  112. package/src/fit.ts +24 -43
  113. package/src/generate.ts +740 -337
  114. package/src/grammar.ts +14 -8
  115. package/src/harness-pi.ts +15 -34
  116. package/src/hub-rest.ts +4 -4
  117. package/src/index.ts +27 -0
  118. package/src/inference/denoising.ts +17 -0
  119. package/src/inference/graph.ts +33 -0
  120. package/src/inference/prefill.ts +39 -0
  121. package/src/inference/rollback.ts +7 -0
  122. package/src/jobs/db.ts +4 -0
  123. package/src/jobs/index.ts +2 -0
  124. package/src/jobs/job-entry.ts +15 -14
  125. package/src/jobs/runner.ts +98 -13
  126. package/src/jobs/task-client.ts +19 -0
  127. package/src/jobs/types.ts +2 -58
  128. package/src/kv-scheme.ts +199 -0
  129. package/src/kv-store.ts +459 -234
  130. package/src/lab/README.md +18 -0
  131. package/src/{assets → lab/curve}/curve-designer.html +2 -2
  132. package/src/{curve-sampler.ts → lab/curve/curve-sampler.ts} +2 -2
  133. package/src/{expert-trace.ts → lab/expert-trace/expert-trace.ts} +7 -5
  134. package/src/{model → lab/paged-kv}/paged-kv.ts +9 -6
  135. package/src/library.ts +40 -0
  136. package/src/lora.ts +18 -0
  137. package/src/media-fetch.ts +20 -4
  138. package/src/memory/article.ts +1 -1
  139. package/src/memory/chunk.ts +1 -1
  140. package/src/memory/model.ts +32 -170
  141. package/src/memory/prompts.ts +1 -1
  142. package/src/memory/rest.ts +1 -1
  143. package/src/memory/schedule.ts +1 -1
  144. package/src/memory/stages.ts +4 -1
  145. package/src/memory/vault.ts +3 -4
  146. package/src/mlx/array.ts +9 -10
  147. package/src/mlx/compile.ts +12 -2
  148. package/src/mlx/ffi.ts +66 -22
  149. package/src/mlx/materialize.ts +23 -0
  150. package/src/mlx/metal-kernel.ts +10 -11
  151. package/src/mlx/ops.ts +64 -19
  152. package/src/mlx/quantized-rows.ts +36 -0
  153. package/src/mlx/token-bitmask.ts +52 -0
  154. package/src/mlx/turboquant-kv-decode.ts +82 -0
  155. package/src/mlx/turboquant-kv-inverse.ts +122 -0
  156. package/src/mlx/turboquant-ops.ts +33 -3
  157. package/src/mlx/turboquant-tables.ts +2 -2
  158. package/src/model/batched-mask.ts +9 -3
  159. package/src/model/batched-quant.ts +2 -2
  160. package/src/model/batched-rotating-quant.ts +45 -99
  161. package/src/model/batched-rotating-state.ts +73 -0
  162. package/src/model/batched-rotating.ts +57 -106
  163. package/src/model/batched-row-storage.ts +127 -0
  164. package/src/model/compiled-decode.ts +4 -3
  165. package/src/model/diffusion-gemma.ts +8 -8
  166. package/src/model/factory.ts +87 -49
  167. package/src/model/fingerprint.ts +1 -1
  168. package/src/model/flash-attention.ts +3 -3
  169. package/src/model/gemma4-base.ts +187 -17
  170. package/src/model/gemma4.ts +6 -6
  171. package/src/model/generated/gemma4-12b.ts +52 -148
  172. package/src/model/generated/gemma4-26b.ts +34 -94
  173. package/src/model/generated/gemma4-e4b.ts +28 -76
  174. package/src/model/glm52-cache.ts +19 -9
  175. package/src/model/glm52-dsa.ts +13 -9
  176. package/src/model/glm52-memory.ts +28 -1
  177. package/src/model/implementation.ts +69 -0
  178. package/src/model/minicpm5.ts +7 -8
  179. package/src/model/profile.ts +443 -0
  180. package/src/model/qwen-append.ts +12 -0
  181. package/src/model/qwen-conv.ts +52 -0
  182. package/src/model/qwen3-delta.ts +98 -2
  183. package/src/model/qwen3-mrope.ts +252 -0
  184. package/src/model/qwen3_5-checkpoint.ts +164 -0
  185. package/src/model/qwen3_5.ts +336 -60
  186. package/src/model/support.ts +1 -1
  187. package/src/model/trellis-balanced-scatter.ts +51 -0
  188. package/src/model/trellis-linear.ts +672 -0
  189. package/src/model/trellis-shared-m.ts +151 -0
  190. package/src/model/trellis-shared-scatter.ts +144 -0
  191. package/src/model/trellis-splitk-prefill.ts +158 -0
  192. package/src/model/trellis-tiled-prefill.ts +136 -0
  193. package/src/model/trellis-vector-expand.ts +62 -0
  194. package/src/model/trellis-wide-prefill.ts +94 -0
  195. package/src/model/universal/dense.ts +1 -1
  196. package/src/model/universal/modules.ts +8 -1
  197. package/src/native/frame_extract.swift +140 -0
  198. package/src/native-pack.ts +15 -7
  199. package/src/pi-provider.ts +6 -64
  200. package/src/pi-web.ts +14 -320
  201. package/src/prompt-cache.ts +97 -54
  202. package/src/quantize/allocator.ts +1 -1
  203. package/src/quantize/atomic-output.ts +38 -0
  204. package/src/quantize/calibration.ts +2 -1
  205. package/src/quantize/config-writer.ts +16 -3
  206. package/src/quantize/index.ts +28 -1
  207. package/src/quantize/job.ts +11 -0
  208. package/src/quantize/quantizer.ts +265 -100
  209. package/src/quantize/rotate.ts +603 -0
  210. package/src/quantize/safetensors-writer.ts +79 -1
  211. package/src/quantize/sensitivity.ts +41 -59
  212. package/src/quantize/trellis.ts +586 -0
  213. package/src/quantize/weight-transform.ts +292 -0
  214. package/src/responses.ts +130 -3
  215. package/src/runtime-config.ts +81 -0
  216. package/src/sampler.ts +276 -4
  217. package/src/serve/admin-routes.ts +184 -0
  218. package/src/serve/aux-routes.ts +132 -0
  219. package/src/serve/batch-scheduler.ts +8 -1558
  220. package/src/serve/chat-request.ts +391 -0
  221. package/src/serve/chat-stage.ts +195 -0
  222. package/src/serve/checkpoint-identity.ts +64 -0
  223. package/src/serve/completion-executor.ts +354 -0
  224. package/src/serve/completion-sink.ts +157 -0
  225. package/src/serve/discovery-routes.ts +204 -0
  226. package/src/serve/generation-gateway.ts +211 -243
  227. package/src/serve/http.ts +159 -0
  228. package/src/serve/inference-request.ts +79 -0
  229. package/src/serve/isolate.ts +297 -105
  230. package/src/serve/kv-budget.ts +14 -0
  231. package/src/serve/lab-routes.ts +341 -0
  232. package/src/serve/lane-registry.ts +2 -1
  233. package/src/serve/model-admin-routes.ts +155 -0
  234. package/src/serve/model-binding.ts +43 -0
  235. package/src/serve/model-host.ts +512 -0
  236. package/src/serve/openai-wire.ts +103 -0
  237. package/src/serve/parent-application.ts +56 -0
  238. package/src/serve/pipeline.ts +33 -0
  239. package/src/serve/preparation.ts +33 -0
  240. package/src/serve/prompt-response-trace.ts +147 -0
  241. package/src/serve/request-plan.ts +162 -0
  242. package/src/serve/request-prep.ts +346 -0
  243. package/src/serve/response-lease.ts +24 -0
  244. package/src/serve/responses-client.ts +69 -0
  245. package/src/serve/session-completion-engine.ts +110 -0
  246. package/src/serve/session-search.ts +1 -1
  247. package/src/serve/static-routes.ts +101 -0
  248. package/src/serve/text-completion-stage.ts +104 -0
  249. package/src/serve/token-streams.ts +519 -0
  250. package/src/server.ts +523 -3551
  251. package/src/spec/assistant-source.ts +10 -44
  252. package/src/spec/deepspec-source.ts +6 -6
  253. package/src/spec/dflash-source.ts +9 -8
  254. package/src/spec/dspark/ab-stats.ts +3 -3
  255. package/src/spec/dspark/calibration.ts +2 -2
  256. package/src/spec/dspark/deepspec-module.ts +2 -2
  257. package/src/spec/dspark/generate-dflash.ts +2 -2
  258. package/src/spec/dspark/generate.ts +2 -2
  259. package/src/spec/dspark/loader.ts +1 -1
  260. package/src/spec/dspark/module-dflash.ts +5 -4
  261. package/src/spec/dspark/module.ts +3 -3
  262. package/src/spec/dspark/quantize-drafter.ts +1 -1
  263. package/src/spec/generate.ts +1 -1
  264. package/src/spec/glm52-mtp-source.ts +9 -1
  265. package/src/spec/ngram-source.ts +1 -1
  266. package/src/spec/prefix-state.ts +45 -0
  267. package/src/spec/qwen-mtp-source.ts +528 -0
  268. package/src/spec/serve-loop.ts +179 -213
  269. package/src/spec/source.ts +42 -10
  270. package/src/spec/two-model.ts +1 -1
  271. package/src/ssd-cache.ts +95 -24
  272. package/src/ssd-durability.ts +207 -0
  273. package/src/tool-call.ts +51 -14
  274. package/src/train/flash-cce.ts +14 -13
  275. package/src/train/forward.ts +38 -1
  276. package/src/train/loss.ts +3 -2
  277. package/src/train/prefix-shared.ts +5 -1
  278. package/src/train/segmented.ts +9 -6
  279. package/src/train/trainer.ts +9 -8
  280. package/src/vision/preprocess.ts +1 -1
  281. package/src/vision/prompt.ts +48 -2
  282. package/src/vision/qwen3vl-preprocess.ts +409 -0
  283. package/src/vision/qwen3vl-prompt.ts +196 -0
  284. package/src/vision/qwen3vl-tower.ts +555 -0
  285. package/src/vision/video-frames.ts +148 -0
  286. package/src/web/app.html +1 -1
  287. package/src/web/app.js +15 -6
  288. package/src/web/src/adapters-panel.ts +3 -3
  289. package/src/web/src/assistant.ts +2 -36
  290. package/src/web/src/chat.ts +16 -9
  291. package/src/web/src/composer.ts +4 -4
  292. package/src/web/src/hub.ts +1 -1
  293. package/src/web/src/markdown.ts +0 -0
  294. package/src/web/src/memory-panel.ts +1 -1
  295. package/src/web/src/model-picker.ts +1 -1
  296. package/src/web/src/palette.ts +1 -1
  297. package/src/web/src/protocol.ts +3 -39
  298. package/src/web/src/sessions.ts +1 -1
  299. package/src/web/src/ui-catalog.ts +4 -18
  300. package/src/web-assets.ts +47 -0
  301. package/src/weights.ts +134 -33
  302. package/docs/archive/mlx-bun-lab-report.html +0 -275
  303. package/docs/archive/wwdc-mlx-bun.html +0 -848
  304. package/docs/design/adapters-end-to-end.md +0 -205
  305. package/docs/design/batching-perf-path.md +0 -255
  306. package/docs/design/batching-v2-plan.md +0 -529
  307. package/docs/design/bucketing-stage.md +0 -197
  308. package/docs/design/compat-cli-surface-design.md +0 -230
  309. package/docs/design/docs-reorg-plan.md +0 -165
  310. package/docs/design/dspark-serving-program.md +0 -371
  311. package/docs/design/dspark-speculative-decoding.md +0 -268
  312. package/docs/design/grammar-spec-batching-integration.md +0 -301
  313. package/docs/design/memory-inference-path.md +0 -422
  314. package/docs/design/memory-synthesis.md +0 -287
  315. package/docs/design/memory-system.md +0 -237
  316. package/docs/design/mlx-lm-tool-parity-plan.md +0 -545
  317. package/docs/design/omlx-adoption-map.md +0 -98
  318. package/docs/design/orpo-dynamic-lambda.md +0 -220
  319. package/docs/design/orpo-future-enhancements.md +0 -340
  320. package/docs/design/paged-kv-cache.md +0 -214
  321. package/docs/design/parallel-slots.md +0 -567
  322. package/docs/design/parity-tier-dag.md +0 -161
  323. package/docs/design/repo-cleanup-plan.md +0 -189
  324. package/docs/design/runtime-isolation.md +0 -263
  325. package/docs/design/segmented-backward-training.md +0 -543
  326. package/docs/design/spec-decode-larger-targets.md +0 -229
  327. package/docs/design/ssd-kv-cold-tier.md +0 -288
  328. package/docs/design/structured-output.md +0 -484
  329. package/docs/design/tauri-desktop-app.md +0 -127
  330. package/docs/design/the-dreaming-master-plan.md +0 -445
  331. package/docs/design/turboquant-kv.md +0 -235
  332. package/docs/design/w4a16-compute-precision-spike.md +0 -232
  333. package/docs/design/web-chat-beat-matrix.md +0 -315
  334. package/docs/design/write-pipeline-entity.md +0 -468
  335. package/docs/investigations/curve-runs/bisector-route.json +0 -98
  336. package/docs/investigations/curve-runs/bisector-trace.json +0 -1393
  337. package/docs/investigations/curve-runs/curve-terrain.json +0 -1179
  338. package/docs/investigations/curve-runs/curve-terrain.svg +0 -42
  339. package/docs/investigations/hlg-ab.html +0 -34
  340. package/docs/investigations/hlg-explorer.html +0 -238
  341. package/docs/investigations/hlg-figs/cliff.svg +0 -1
  342. package/docs/investigations/hlg-figs/cross.svg +0 -1
  343. package/docs/investigations/hlg-figs/edges.svg +0 -1
  344. package/docs/investigations/hlg-figs/k20-grids.svg +0 -1
  345. package/docs/investigations/hlg-figs/k20-peaks.svg +0 -1
  346. package/docs/investigations/hlg-figs/lw-curve.svg +0 -1
  347. package/docs/investigations/hlg-figs/terrain.svg +0 -1
  348. package/docs/investigations/hlg-report.html +0 -262
  349. package/docs/investigations/hlg-runs/hlg-ab.json +0 -149
  350. package/docs/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
  351. package/docs/investigations/hlg-runs/hlg-blind.json +0 -107
  352. package/docs/investigations/hlg-runs/hlg-cliff.json +0 -197
  353. package/docs/investigations/hlg-runs/hlg-cross.json +0 -505
  354. package/docs/investigations/hlg-runs/hlg-edge.json +0 -388
  355. package/docs/investigations/hlg-runs/hlg-eval1.json +0 -25
  356. package/docs/investigations/hlg-runs/hlg-frontier.json +0 -86
  357. package/docs/investigations/hlg-runs/hlg-grid.json +0 -242
  358. package/docs/investigations/hlg-runs/hlg-grid2.json +0 -343
  359. package/docs/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
  360. package/docs/investigations/hlg-runs/hlg-map.json +0 -383
  361. package/docs/investigations/hlg-runs/run0-original.log +0 -159
  362. package/docs/investigations/hlg-runs/runA-recommended.log +0 -131
  363. package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
  364. package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
  365. package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
  366. package/docs/investigations/hlg-runs/runAD-ab-samples.log +0 -8
  367. package/docs/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
  368. package/docs/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
  369. package/docs/investigations/hlg-runs/runAG-eval-config2.log +0 -16
  370. package/docs/investigations/hlg-runs/runB-gain-variants.log +0 -192
  371. package/docs/investigations/hlg-runs/runC-pivots.log +0 -162
  372. package/docs/investigations/hlg-runs/runD-oetf.log +0 -132
  373. package/docs/investigations/hlg-runs/runE-eotf.log +0 -162
  374. package/docs/investigations/hlg-runs/runF-shaper-working.log +0 -86
  375. package/docs/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
  376. package/docs/investigations/hlg-runs/runH-wsweep.log +0 -176
  377. package/docs/investigations/hlg-runs/runI-asweep.log +0 -176
  378. package/docs/investigations/hlg-runs/runJ-knobmap.log +0 -49
  379. package/docs/investigations/hlg-runs/runK-frontier.log +0 -19
  380. package/docs/investigations/hlg-runs/runL-widemap.log +0 -65
  381. package/docs/investigations/hlg-runs/runM-frontier-emb.log +0 -15
  382. package/docs/investigations/hlg-runs/runN-verify.log +0 -116
  383. package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
  384. package/docs/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
  385. package/docs/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
  386. package/docs/investigations/hlg-runs/runR-blind-transcript.log +0 -79
  387. package/docs/investigations/hlg-runs/runS-cross-coherence.log +0 -33
  388. package/docs/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
  389. package/docs/investigations/hlg-runs/runU-cross-12b.log +0 -33
  390. package/docs/investigations/hlg-runs/runV-lw-e4b.log +0 -22
  391. package/docs/investigations/hlg-runs/runW-lw-12b.log +0 -22
  392. package/docs/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
  393. package/docs/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
  394. package/docs/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
  395. package/docs/planning/IDEAS.md +0 -69
  396. package/docs/reference/embedding.md +0 -197
  397. package/docs/reference/features-matrix.md +0 -100
  398. package/docs/reference/orpo-quickstart.md +0 -135
  399. package/src/flags.ts +0 -13
  400. /package/docs/{investigations → archive/investigations}/chunk-finetune-experiment.md +0 -0
  401. /package/docs/{investigations → archive/investigations}/colibri-oracle-pin.md +0 -0
  402. /package/docs/{investigations → archive/investigations}/curve-bisector-routing.md +0 -0
  403. /package/docs/{investigations → archive/investigations}/decode-roofline-lookagain.md +0 -0
  404. /package/docs/{investigations → archive/investigations}/dspark-handoff.md +0 -0
  405. /package/docs/{investigations → archive/investigations}/expert-offload-single-user-moe.md +0 -0
  406. /package/docs/{investigations → archive/investigations}/indexshare-performance-spike.md +0 -0
  407. /package/docs/{investigations → archive/investigations}/kernel-perf-review-2026-07.md +0 -0
  408. /package/docs/{investigations → archive/investigations}/lab-build-journal.md +0 -0
  409. /package/docs/{investigations → archive/investigations}/openwiki-evaluation.md +0 -0
  410. /package/docs/{investigations → archive/investigations}/orpo-base-uf-experiment-and-directions.md +0 -0
  411. /package/docs/{investigations → archive/investigations}/orpo-flash-cce-pin-leak.md +0 -0
  412. /package/docs/{investigations → archive/investigations}/orpo-uf-testing-handoff.md +0 -0
  413. /package/docs/{investigations → archive/investigations}/pi-builtin-investigation.md +0 -0
  414. /package/docs/{investigations → archive/investigations}/segmented-backward-handoff.md +0 -0
  415. /package/docs/{investigations → archive/investigations}/starter-model-port-handoff.md +0 -0
  416. /package/docs/{investigations → archive/investigations}/steel-flash-cce-handoff.md +0 -0
  417. /package/docs/{investigations → archive/investigations}/trainer-validation-experiment.md +0 -0
  418. /package/docs/{planning → archive/planning}/chat-ui-vision-memory-first.md +0 -0
  419. /package/docs/{planning → archive/planning}/journal.md +0 -0
  420. /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
  421. /package/docs/{planning → archive/planning}/release-notes-v0.0.10.md +0 -0
  422. /package/docs/{planning → archive/planning}/release-notes-v0.0.11.md +0 -0
  423. /package/docs/{planning → archive/planning}/release-notes-v0.0.12.md +0 -0
  424. /package/docs/{planning → archive/planning}/release-notes-v0.0.13.md +0 -0
  425. /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
  426. /package/{docs/investigations → src/serve/assets}/curve-terrain.html +0 -0
  427. /package/{docs/dag → src/serve/assets}/training-inference-map.html +0 -0
@@ -0,0 +1,680 @@
1
+ ---
2
+ status: landed
3
+ axis: ON
4
+ canonical-for: kv-cache
5
+ plan-anchor: "Phase 18 — Concurrent / batched serving (slots) + parallel load benchmark `[~]` (2026-06-13)"
6
+ last-verified: 2026-08-23
7
+ ---
8
+
9
+ # KV cache — residency and layout
10
+
11
+ The one doc for where KV lives and how it is arranged: the cache classes
12
+ and their `signature()` capability model, the RAM prompt cache
13
+ (`PromptCache.take()`), the SSD cold tier and its durability boundary, the
14
+ optional paged block allocator, and how KV *schemes* plug into all of it.
15
+ The TurboQuant codec itself is documented in
16
+ [docs/design/turboquant.md](./turboquant.md); this doc only covers how its cache
17
+ class participates in residency.
18
+
19
+ Sources consolidated here: `docs/design/kv-cache.md`, `docs/design/kv-cache.md`, and
20
+ the cache-integration parts of `docs/design/turboquant.md`. Status and changelog
21
+ prose lives in PLAN.md; this file keeps mechanism, invariants, decisions,
22
+ and open items.
23
+
24
+ Code: `src/model/gemma4-base.ts` (Cache interface + the K/V cache family),
25
+ `src/model/batched-*.ts`, `src/lab/paged-kv/paged-kv.ts`, `src/prompt-cache.ts`,
26
+ `src/kv-store.ts`, `src/ssd-cache.ts`, `src/ssd-durability.ts`,
27
+ `src/kv-scheme.ts`, `src/generate.ts` (`maybeQuantizeKv` / `maybePageKv`),
28
+ `src/server.ts` (wiring, `/stats`, `POST /admin/cache/flush`),
29
+ `src/serve/generation-gateway.ts`, `src/serve/batch-scheduler.ts`.
30
+
31
+ ## 1. Layering
32
+
33
+ Residency is a three-tier stack, each tier keyed by the exact token prefix
34
+ its KV encodes:
35
+
36
+ | tier | owner | unit | key |
37
+ |---|---|---|---|
38
+ | live caches | one generation (serial lane or a batch row) | `Cache[]`, one per layer | — |
39
+ | RAM prompt cache | `PromptCache` (`src/prompt-cache.ts`) | whole prefix entry (tokens + `Cache[]`) | tokens + adapter ns |
40
+ | SSD cold tier | `SsdCacheStore` (`src/ssd-cache.ts`) over `src/kv-store.ts` files | one `.mlxkv` file per entry | tokens + ns + model/scheme/tokenizer fingerprint |
41
+
42
+ The prompt cache tiers over the SSD store *inside* `take()` (structural
43
+ `ColdTier` interface), so every consumer (serial lane, batch scheduler
44
+ joiners, `/admin/drain`) gets both tiers through the same `take()`/`put()`
45
+ calls. The default path does no persistence work on the token loop:
46
+ spill/persist happen at request boundaries, evictions, idle sweeps, and
47
+ explicit flushes. The opt-in `--generation-checkpoint N` path deliberately
48
+ pauses at coarse, safe decode boundaries to atomically persist an in-flight
49
+ generation (section 5.7). Paged KV
50
+ (section 6) is a *layout* choice for the live tier and, in v1, bypasses the
51
+ other two.
52
+
53
+ ## 2. Cache classes and the signature/capability model
54
+
55
+ ### 2.1 The `Cache` contract (`gemma4-base.ts`)
56
+
57
+ Every cache-owning layer holds one `Cache`:
58
+
59
+ - `updateAndFetch(k, v)` appends and returns the attended window;
60
+ `makeMask(N, windowSize)`; `state()` (arrays to eval at a prefill chunk
61
+ boundary); `dispose()`.
62
+ - `isTrimmable()` / `trim(n, bypass?)` — whether the last `n` tokens can be
63
+ dropped. This single predicate drives prefix reuse: an entry longer than
64
+ the matched prefix is usable only if every cache can trim.
65
+ - `signature?()` — stable storage identity ("`kv:plain`",
66
+ "`kv:quant:<bits>:<group>`", "`kv:rotating-plain`",
67
+ "`kv:rotating-quant:<bits>:<group>`", "`kv:turboquant:<k>:<v>`", "`ssm`",
68
+ "`kv:mla:target`", "`kv:mla:target:dsa`", "`kv:mla:mtp`"). Persistence
69
+ codecs (`kv-store.ts` `CACHE_CODECS`) and the scheduler's layer-kind map
70
+ dispatch on it. `cacheSignature(cache)` returns `"unknown"` when the
71
+ method is absent.
72
+ - `bytesPerToken?()` — physical growth per token per row (0 for recurrent
73
+ state); `stateNeedsDispose` (TurboQuant's `state()` returns fresh views
74
+ the caller must dispose); `ropeOffsetArr` (per-row RoPE positions for
75
+ batched/compiled paths); `specRound*` (speculative rollback for
76
+ untrimmable recurrent caches).
77
+
78
+ Capability interfaces sit beside the signature: `RowBatchCache`
79
+ (`batchSize`/`filterRows`/`extractRow`) and `BatchableCache` (caches that
80
+ own their own dynamic-row batching — GLM's MLA/DSA state). Type guards
81
+ (`isPlainKvCache`, `isRotatingPlainCache`, …) are signature-based;
82
+ `isRowBatchCache`/`isBatchableCache` are structural.
83
+
84
+ ### 2.2 The family
85
+
86
+ | class | file | signature | trimmable | notes |
87
+ |---|---|---|---|---|
88
+ | `KVCache` | gemma4-base.ts | `kv:plain` | yes | mlx-lm port; grows in `STEP = 256` slabs, in-place `sliceUpdate` |
89
+ | `QuantizedKVCache` | gemma4-base.ts | `kv:quant:b:g` | yes | affine triples (packed/scales/biases), groups along head_dim |
90
+ | `RotatingKVCache` | gemma4-base.ts | `kv:rotating-plain` | **only while `offset < maxSize`** (pre-wrap) | sliding-window ring |
91
+ | `RotatingQuantizedKVCache` | gemma4-base.ts | `kv:rotating-quant:b:g` | pre-wrap | quantized ring |
92
+ | `TurboQuantKVCache` | gemma4-base.ts | `kv:turboquant:k:v` | yes | not a `KVCache` subclass — deliberately fails every `instanceof` gate (monolith fallback, solo-only); see docs/design/turboquant.md |
93
+ | `SSMCache` | qwen3-delta.ts | `ssm` | **no** (recurrent) | `bytesPerToken()` = 0; per-row `offsets` in the batch lane |
94
+ | `Glm52Cache` / `MLACache` | glm52-cache.ts | `kv:mla:*` | yes | `BatchableCache`; compressed latent + rope (+ DSA index) |
95
+ | `PagedKVCache` | paged-kv.ts | **none** | yes | block pool + gather; section 6 |
96
+ | `BatchedRotatingCache` | batched-rotating.ts | `kv:rotating-plain` | pre-wrap | `RowBatchCache`; batched ring, host-side scalar ring state |
97
+ | `BatchedRotatingQuantCache` | batched-rotating-quant.ts | inherited (`extends RotatingQuantizedKVCache`) | pre-wrap | batched quantized ring |
98
+ | `BatchedDecodeMaskCache` | batched-mask.ts | **none** | delegates | per-step wrapper over a B-generic inner (padding mask + per-row RoPE) |
99
+ | `BatchedQuantDecodeMaskCache` | batched-quant.ts | inherited (`extends QuantizedKVCache`) | delegates | quantized twin of the wrapper; subclassing keeps generated forwards' `instanceof` guards happy |
100
+ | compiled-decode `Trace*` | compiled-decode.ts | inherited | — | trace adapters over the four compiled-supported classes |
101
+ | training caches (`TrainingCache`, `TrainingSSMCache`, `BatchedMaskCache`, `PrefixSharedCache`, `Gemma4PrefixSharedCache`, `Gemma4PrefixSharedSegCache`) | src/train/* | **none** | — | never admitted, cloned, or persisted; the interface comment explicitly allows omission here |
102
+
103
+ Invariant: **quantization groups run along head_dim, never the token
104
+ axis**, so token-axis slicing (trim, `cloneKvCaches` live views, batch row
105
+ surgery, persistence slicing to `offset`) is byte-safe for every quantized
106
+ kind. Every cache kind persisted by kv-store has a `restoreState(...)`.
107
+
108
+ ### 2.3 Batching gates
109
+
110
+ Two independent checks decide whether a model/scheme can batch
111
+ (`generation-gateway.ts`):
112
+
113
+ - `#modelCachesBatchable()` — every fresh `makeCache()` entry is a
114
+ `KVCache`, `RotatingKVCache`, `BatchableCache`, or (unless
115
+ `MLX_BUN_BATCH_SSM=0`) `SSMCache`. Novel classes (TurboQuant, paged) fail
116
+ this by construction.
117
+ - `#kvBatchable()` — `KvScheme.batchable(config, canConvert)`: only the
118
+ per-layer `affine-config` scheme batches, and only when every configured
119
+ layer is a plain or rotating cache that can convert. Uniform `kvBits`
120
+ and TurboQuant stay serial; a scheme-less gateway must never silently
121
+ drop quantization (`kv-scheme.ts` makes the capability probe mandatory
122
+ for quantized schemes).
123
+
124
+ The scheduler then maps each layer to a kind ("owned-batch" / "rot" /
125
+ "ssm" / "full") from the prototype caches, and joins/filters/extracts by
126
+ that kind.
127
+
128
+ ### 2.4 Known design gap: optional `signature()` with an `"unknown"` fallback
129
+
130
+ `Cache.signature` is optional and `cacheSignature()` substitutes
131
+ `"unknown"`. That makes an unsigned class *route* rather than *fail*: any
132
+ signature-based conjunct silently evaluates false. This is exactly how the
133
+ 2026-08-22 agg×4 regression happened (PLAN.md "agg×4 regression —
134
+ root-caused and fixed", commit `443f333`; the BatchedRotatingCache fix
135
+ PRs #42/#43): the scheduler's rot-merge required
136
+ `isRowBatchCache(prev) && isRotatingPlainCache(prev)`, `BatchedRotatingCache`
137
+ had no `signature()`, the running batch's ring was dropped from the join,
138
+ and the next full-B decode step crashed in the grow-path concatenate. The
139
+ quantized twin escaped only because it inherits a real signature.
140
+
141
+ What holds today (verified in src, 2026-08-23):
142
+
143
+ - `BatchedRotatingCache` now returns `"kv:rotating-plain"`
144
+ (`batched-rotating.ts`), and the scheduler's rot branch routes by
145
+ capability alone (`isRowBatchCache`). The comment at
146
+ `batch-scheduler.ts` ~line 1007 still says the class has *no* override —
147
+ that comment is stale; the guard order it describes (capability first)
148
+ is still the rule.
149
+ - Still unsigned: `PagedKVCache` and `BatchedDecodeMaskCache` in the
150
+ serving tree, plus the six training-only caches listed above. Neither
151
+ serving class is currently reachable by a signature-dispatched path
152
+ (paged bypasses prompt-cache/kv-store/batching; the mask wrapper is
153
+ rebuilt per step and the scheduler dispatches on the *inner* caches),
154
+ so today the gap is latent, not live.
155
+ - Rule going forward (PLAN.md lesson): **"unknown" is a bug, not a
156
+ route.** Any new `Cache` that can reach the scheduler, `cloneKvCaches`,
157
+ or kv-store must declare a signature; kv-store already throws on an
158
+ unknown signature rather than guessing. Making `signature()` required on
159
+ the interface (with training adapters returning an explicit
160
+ `"training"`) is the open hardening item.
161
+
162
+ ## 3. KV schemes — how they plug into the cache (`src/kv-scheme.ts`)
163
+
164
+ `KvScheme` is the immutable declaration of *what bytes a token costs and
165
+ which cache class holds it*: kind `bf16` | `affine-uniform` |
166
+ `affine-config` | `turbo`, resolved once at server start by
167
+ `resolveKvScheme` (TurboQuant wins if set; `--kv-quant off|<bits>|config`
168
+ otherwise; a missing `kv_config.json` under `config` is bf16 or an error
169
+ per caller). Its roles in residency:
170
+
171
+ - **Conversion chokepoint.** `maybeQuantizeKv` (`generate.ts`) converts
172
+ populated plain/rotating caches in place after the first prefill
173
+ (`offset === 0` caches are skipped — the optiq mixed-KV convention; the
174
+ affine start threshold is `quantizedKvStart`, which `resolveKvScheme`
175
+ pins to 0 for every server scheme — the `generate.ts` fallback of 5000
176
+ for uniform bits only reaches library callers who pass `kvBits` without
177
+ a threshold) and evaluates layer by layer so the bf16 source frees
178
+ before the next layer converts. TurboQuant converts
179
+ full-attention `KVCache` layers only; rotating layers stay bf16 with a
180
+ one-time warning.
181
+ - **Byte accounting.** `kvBytesAt`/`kvGeometry` bill per layer type (full
182
+ / sliding capped at the window / linear state) with
183
+ `bits/8 + 4/groupSize` bytes per affine element. `fitOptions` bills
184
+ TurboQuant as bf16 on purpose (conservative until its packed layout
185
+ exposes a stable projector). The scheduler's KV-budget admission uses
186
+ the same seam, and `BatchableCache.projectedBytes` for owned-batch
187
+ caches.
188
+ - **Cache compatibility key.** `cacheKey` (`bf16` | `kv<bits>` | `config`
189
+ | `turbo-k<k>v<v>`) is folded into the SSD store's fingerprint
190
+ (`${configFingerprint(config)}-${schemeKey}`, `server.ts`), so restored
191
+ files always match what serving would have produced. `configFingerprint`
192
+ itself covers the per-layer `kv_config.json` scheme but not CLI-only
193
+ levers (uniform bits, TurboQuant) — the scheme key is the anti-collision
194
+ mechanism for those.
195
+ - **Batch placement.** `batchable()` as in 2.3.
196
+
197
+ Uniform 4/8-bit affine KV is bit-exact L1 (unfused SDPA == mlx-lm
198
+ `base.py`); the per-layer config scheme is the L2/optiq composition, and
199
+ TurboQuant is a third, oracle-backed codec (vllm-metal). The scheme axis
200
+ (which bits where) is orthogonal to the residency axis (which tier holds
201
+ them); nothing in this doc changes numerics.
202
+
203
+ ## 4. RAM tier — `PromptCache` (`src/prompt-cache.ts`)
204
+
205
+ Byte-capped LRU of whole prefix entries (`--prompt-cache <GB>`, default
206
+ 8 GB; `0` disables). The mlx-lm lesson that motivated it — a count-capped
207
+ cache of multi-GB entries is an OOM footgun — still stands even though
208
+ upstream is byte-capped now.
209
+
210
+ **`take(prompt, ns)` — prefix sharing.** Finds the entry with the longest
211
+ usable common prefix (usable = common prefix capped at `prompt.length - 1`,
212
+ so at least one token is forwarded; an entry longer than the match must be
213
+ trimmable). Trimmable entries are cloned zero-copy (`cloneKvCaches`: slice
214
+ views of live arrays), trimmed, and returned with a ref-counted `retain`
215
+ share; the donor stays in place, so N agents can share one prefill. An exact
216
+ hit on a Qwen hybrid entry containing recurrent SSM state transfers the donor
217
+ itself to bound resident snapshots; the request puts its extended state back
218
+ on completion. With an SSD tier, `take` persists that exact boundary before
219
+ transferring ownership. An already-durable prefix needs only an index check.
220
+ The extended recurrent state cannot trim back to the consumed boundary, so
221
+ keeping its SSD copy preserves later exact prompt repeats. A failed required
222
+ store leaves the donor and backing owned by the cache and refuses the transfer.
223
+
224
+ **Tier order inside `take()`.** The cold tier is consulted with
225
+ `find(prompt, ns)` (index-only, no I/O) and wins only with a *strictly*
226
+ longer usable prefix; then `restore(handle)` materializes it and the
227
+ divergent tail is trimmed. A cold entry that would need a trim but is
228
+ untrimmable (wrapped ring, SSM) is disposed loudly and the RAM candidate
229
+ (or a fresh prefill) serves. `hits`/`misses` count RAM candidacy only.
230
+
231
+ **`put(tokens, caches, ns, retain)` — supersession + eviction.** Exact
232
+ duplicates (same tokens) are replaced regardless of trimmability. Strict
233
+ prefix-ancestors in the same ns are superseded without spill — but only
234
+ when the *new* entry is fully trimmable; an untrimmable new entry (wrapped
235
+ ring) can only serve exact-length matches, so shorter ancestors (the
236
+ prompt-boundary snapshot) must survive. Then LRU-evict until under cap,
237
+ spilling each evictee to the cold tier first. An entry larger than the cap
238
+ is spilled and disposed, never stored. `onPut` fires after every
239
+ successful put — the server hangs the write-behind scheduler there so both
240
+ lanes' entries persist.
241
+
242
+ **Prompt-boundary snapshot.** Serial lane (`server.ts`) and batch
243
+ scheduler (`snapshotAt`) both clone-and-put a trim-free *prompt-only*
244
+ entry at the template probe's stable boundary during a substantial cold
245
+ prefill. Rationale: the prompt+generation entry put at completion is
246
+ untrimmable past the sliding window (wrapped rings) and under mid-group
247
+ quantization, so any decode→encode drift in the reply the client sends
248
+ back would turn the next turn into a total miss; a prompt-only entry is
249
+ always an exact prefix of the next rendering.
250
+
251
+ **Namespaces.** `ns` = the adapter spec (`adapters.join("+")`, `""` = base).
252
+ KV computed under one adapter never seeds another's prefill.
253
+
254
+ **Bypasses.** Media (vision/audio) requests skip the cache (soft tokens are
255
+ identical placeholder ids → false hits), as do paged-KV requests (v1) and
256
+ the speculative lane (v1). Merged batch rows are not re-put on finish;
257
+ never-merged lone rows put their adopted caches back zero-copy, and
258
+ merged rows are extracted per row (`#extractAndPut`) before `filter()`
259
+ mutates the batched inners.
260
+
261
+ **Idle demotion (`demoteIdle`).** Entries unused past a threshold spill to
262
+ the cold tier and free their GPU arrays; the prefix stays reachable via
263
+ restore. No-op without a cold tier (demotion with nowhere to demote to is
264
+ data loss). `/admin/drain` (unix-socket engine children only) is
265
+ `demoteIdle(0)` under the gateway lock — the model pool's lossless
266
+ pre-eviction step.
267
+
268
+ **Spill contract (`SpillSink`).** Preferred `spillOwned`: the cache hands
269
+ the sink zero-copy clones + copied tokens made *before* the donor is
270
+ disposed, and the sink owns their disposal on every settle path. A sink
271
+ that throws synchronously has not taken ownership; the spill degrades to
272
+ a no-op and eviction never unwinds. `spillSync` (live entry, synchronous)
273
+ remains for tests and simple embedders.
274
+
275
+ Known numerics property (not a residency defect): continuing from a
276
+ restored or reused prefix produces the same output as the RAM tier's own
277
+ cache hit, but a bf16 prefix-reuse continuation can differ from a fresh
278
+ full prefill (GEMV-vs-GEMM reduction order). Both tiers share it.
279
+
280
+ ## 5. SSD cold tier
281
+
282
+ ### 5.1 Decisions
283
+
284
+ - **D1 — Whole-prefix-entry spill, not paged blocks.** Single-user serving
285
+ with a prefix-entry cache that grows per conversation. Per-block hashing
286
+ and save-queue bookkeeping during decode is where oMLX's measured ~20%
287
+ steady-state tax came from; ours does zero work on the token loop
288
+ (prefix tokens are the key). Partial-prefix reuse works without paging:
289
+ restore + `trim()`. The economics that justify the tier are
290
+ SSD-vs-recompute, not SSD-vs-RAM: at 12B prefill rates a 30k agent
291
+ context is minutes of GPU compute against a ~1 s NVMe restore.
292
+ - **D2 — One store, two tiers.** The cold tier is bound *into*
293
+ `PromptCache` (section 4); restart persistence comes from a debounced
294
+ write-behind snapshot after each put, not only from spill-on-evict.
295
+ - **D3 — Own page-aligned format, not safetensors.** `src/kv-store.ts`
296
+ writes `MLXBUNKV2\n`-magic files whose current layout is the **v3
297
+ streaming writer**: header sized up front with fixed-width tensor
298
+ hashes, tensors materialized/hashed/written one at a time at 16 KiB
299
+ aligned offsets, header patched last (peak host transient = one
300
+ tensor). Header carries `formatVersion`, `modelId`,
301
+ `configFingerprint`, `ns`, `tokenizerHash`, `createdAt`, `tokens`, and
302
+ per-cache `{kind, offset, idx?, maxSize?, groupSize?, bits?, kBits?,
303
+ vBits?, headDim?, GLM geometry, tensors[]}`. Kinds: `kv`, `rotating`,
304
+ `qkv`, `rotating-qkv`, `ssm`, `turboquant`, `mla`, `mla-dsa`, `mtp-mla`.
305
+ Quantized buffers are sliced to `offset` (no 256-step slack persisted).
306
+ Header hash always verified; per-tensor hashes verified only under
307
+ `--ssd-cache-verify` (eager reads roughly double restore cost). Writes
308
+ are atomic (tmp + fsync + rename). Older v1/v2 files read as
309
+ unsupported and are deleted-and-regenerated (machine-local, cheap).
310
+ - **D4 — Files are the database.**
311
+ `<dir>/<configFingerprint>/<nsHash>/<uuid>.mlxkv`. Startup recovery is a
312
+ header-only scan of *our* fingerprint dir into an in-memory
313
+ `SsdIndexEntry[]` (`{path, ns, tokens, bytes, mtimeMs, trimmable}`);
314
+ foreign fingerprint dirs are ignored, never deleted. LRU = file mtime
315
+ (`utimes` on hit); the byte cap is enforced at write time by oldest-mtime
316
+ eviction; corrupt headers and metadata mismatches are unlinked; `.tmp`
317
+ orphans reaped; disk-full is a warn-once soft-fail. Every failure path
318
+ degrades to "no hit" / "not stored" — the tier can never take serving
319
+ down.
320
+ - **Numerical compatibility.** The serving fingerprint also includes a hash
321
+ of `ModelServingBinding.stateCompatibility`. The MLX backend supplies its
322
+ actual runtime version and GPU architecture at binding creation. The same
323
+ artifact produces different cache tensors in the recorded 0.31.2/0.32.2
324
+ RTN4 comparisons, so graph geometry and KV format alone cannot qualify
325
+ automatic reuse. A runtime/architecture change selects another directory;
326
+ previous snapshots remain intact. The binding owns this identity, keeping
327
+ backend details out of request parsing and portable model configuration.
328
+ A replacement-binding restart regression verifies matching reuse, isolation
329
+ and recovery of the original snapshot. Evidence:
330
+ `reports/qwen38-rd/mlx-upgrade-0.32.2/cross-runtime-cache-numerics-review.json`.
331
+ The real RTN4 HTTP gate also passes all eight responses across four
332
+ internal-SSD server starts. Its first requests reuse 0/352/0/352 tokens
333
+ under default/default/alternate/default identities; every second request
334
+ reuses 352. The alternate tag changes metadata only. All response content,
335
+ tool calls and usage counts other than cached tokens match. Exit allocation
336
+ includes a retained prompt cache with different fresh/restored capacity,
337
+ so this is not a post-disposal memory comparison. Evidence:
338
+ `reports/qwen38-rd/runtime-state-http-review.json`.
339
+ - **D5 — Flags and interactions.** `--ssd-cache <dir>` (off unless set),
340
+ `--ssd-cache-max <GB>` (optional; `0` or unset = unlimited), `--ssd-cache-verify`,
341
+ `--ssd-demote-idle <sec>` (default 300 with the tier on; `0` disables).
342
+ Requires the RAM tier (`--prompt-cache 0` + `--ssd-cache` is a startup
343
+ error). Sub-flags without `--ssd-cache` warn and are ignored.
344
+ `--memory-budget` is unchanged (admission already assumes worst-case
345
+ bf16). `--paged-kv` warns that the tier sees nothing (section 6).
346
+ - **D6 — In-flight resume is explicit.** `--generation-checkpoint <tokens>`
347
+ requires `--ssd-cache` and `--batch 1`. It is off by default because each
348
+ checkpoint synchronizes and writes the live KV, trading decode speed and
349
+ SSD writes for bounded lost work. An identical request resumes; a changed
350
+ prompt or sampling policy starts normally.
351
+
352
+ ### 5.2 Restore is a streamed copy (supersedes the original zero-copy plan)
353
+
354
+ The first design restored by COW mmap + `fromPointer` and unmapped in the
355
+ entry's `retain`. Two things killed it: the 2026-07-06 rule that mlx
356
+ buffer destructors must never call into JS removed the unmap signal, so
357
+ every restore became a process-lifetime mapping leak; and exactly-sized
358
+ restored buffers made the first post-restore decode step concat-copy the
359
+ whole entry anyway. `loadKvCache` now copies each tensor out of a
360
+ read-only mmap into an mlx-owned leaf (`fromBytesCopy`), drops the clean
361
+ file pages (`MADV_DONTNEED`; offsets are 16 KiB = arm64 page size),
362
+ unmaps before returning, and lands plain-KV tensors in STEP-rounded
363
+ capacity with slack so the first step updates in place. Peak host
364
+ transient = live entry + one tensor; nothing outlives the call. The
365
+ `retain` thunk stays in the entry contract as a no-op so callers' dispose
366
+ ordering is unchanged (the server's `coldTier.restore` returns
367
+ `() => {}`). Comments in `prompt-cache.ts` that still say "zero-copy
368
+ mmap" describe the superseded mechanism.
369
+
370
+ The write side is the mirror: hashing and writing from a zero-copy view
371
+ of the contiguous mlx buffer (`rawBytesView`), no JS-heap copy at any
372
+ point.
373
+
374
+ ### 5.3 `find()` returns partial matches, gated on trimmability
375
+
376
+ `SsdCacheStore.find` returns the longest usable prefix (capped at
377
+ `prompt.length - 1`) and *skips* entries that would need a trim but are
378
+ untrimmable (`trimmable` is derived from the header kinds + state at
379
+ store/scan time via `cacheHeadersTrimmable`). Before this gate the big
380
+ untrimmable `[prompt+gen]` file always outranked the usable boundary
381
+ snapshot, got restored, and was thrown away — the 2026-07-06 "restart
382
+ restores 0 tokens" defect and its silent restore-then-re-prefill cost.
383
+ Store-side supersession mirrors `PromptCache.put`: exact duplicates
384
+ replaced; prefix-ancestors superseded only when the new entry is
385
+ trimmable.
386
+
387
+ ### 5.4 Write-behind scheduling contract: own each tensor step
388
+
389
+ Every per-tensor flush step is real GPU-stream and JS-thread work
390
+ (`ops.contiguous` on the decode stream, a synchronous eval for
391
+ `rawBytesView`, a synchronous multi-MB `writeSync`). Interleaving those
392
+ between decode tokens taxed decode at long context, so:
393
+
394
+ - `saveKvCacheAsync` / `SsdCacheStore.storeAsync` accept a per-step runner.
395
+ The server passes `gateway.runExclusive`, so the idle decision and the
396
+ blocking MLX readback are one atomic engine turn. The earlier
397
+ `gateway.onIdle()` check had a check/use race: a request could begin after
398
+ the check and overlap the next tensor sync. A request arriving mid-flush
399
+ previously could run between tensor writes. Serial generation now first
400
+ drains the durability coordinator outside the engine lock, so pending
401
+ snapshot clones release their buffers before the next prefill.
402
+ - `GenerationGateway.busy` covers both lanes (serial mutex held/awaited or
403
+ batch rows active/pending).
404
+ - `MLX_BUN_SSD_WRITEBEHIND=0` disables write-behind snapshots entirely
405
+ (kill switch + paired-A/B lever; restart survival then degrades to
406
+ spill-on-evict).
407
+
408
+ Serial requests may wait for outstanding snapshots to finish. The drain
409
+ runs before acquiring the generation lock because the writer needs that
410
+ same lock. Continuous batching retains per-tensor scheduling.
411
+
412
+ The RAM cache's configured byte cap is only an upper bound. Serial serving
413
+ checks MLX's live allocation count against the smaller of admission's usable
414
+ memory and Metal's recommended working set, reserving prefill workspace and
415
+ the prompt's KV footprint for growth/copy-on-write overlap. Pressure relief
416
+ persists LRU entries synchronously before disposing them, avoiding deferred
417
+ spill clones that would keep the memory resident. The selected request cache
418
+ remains owned by the request; older entries remain reusable from SSD. Qwen
419
+ checks between prefill layers, and serial decode checks every 256 tokens.
420
+ If no reclaimable entries remain, the guard raises a request error instead
421
+ of intentionally proceeding over that budget. In-flight checkpoints survive.
422
+
423
+ ### 5.5 Bounded retention: `SpillQueue` (`kv-store.ts`)
424
+
425
+ All three producers — eviction spills, idle demotions, write-behind
426
+ snapshots — go through one serial, byte-capped queue. Pending clones pin
427
+ their entries' GPU buffers until the generation-locked flush gets a turn; without
428
+ a cap, sustained traffic (gate starved, evictions ongoing) made resident
429
+ memory = prompt-cache cap + every queued clone. Policy: cap default 2 GiB
430
+ (`MLX_BUN_SSD_SPILL_QUEUE_GB`; `0` keeps only the newest + in-flight
431
+ clone); over cap the *oldest* not-in-flight item drops and its clones are
432
+ disposed immediately; the item just enqueued is never its own victim
433
+ (soft cap). A dropped spill is a future cache miss, never a wrong result.
434
+ Clones are disposed on every settle path (fulfil, reject, drop). The
435
+ snapshot timer never holds batch admission: while rows are active it
436
+ re-arms instead of grabbing the exclusive (entries are immutable; a late
437
+ snapshot is equally valid).
438
+
439
+ ### 5.6 Durability boundary (`src/ssd-durability.ts`)
440
+
441
+ The 2026-08-22 finding: a debounce timer (1 s), a busy re-arm (5 s), and a
442
+ serial queue meant a SIGTERM shortly after a long request could outrun
443
+ the write — the server implied durability it did not have.
444
+ `SsdDurabilityCoordinator` turns puts into a state machine:
445
+
446
+ - `schedule(tokens, ns)` records a *dirty* key that includes the exact
447
+ token sequence (two same-ns, same-length conversations cannot cancel
448
+ each other) and arms the debounce.
449
+ - An attempt checks SSD coverage under `gateway.runExclusive` before cloning.
450
+ Otherwise it snapshots the RAM entry (`findExact` + `cloneKvCaches`) and
451
+ enqueues it; a busy gateway re-arms;
452
+ a dropped or failed store leaves the key dirty so it is retryable.
453
+ A snapshot that vanished from RAM is "stored" only if the SSD index
454
+ already covers the prefix (`hasDurablePrefix`), else "missing" and still
455
+ dirty. A second flush cannot clear that failure without durable coverage.
456
+ - `flush()` cancels timers, awaits in-flight attempts and `drain()`, then
457
+ forces each dirty record version once, one at a time (so the queue cap
458
+ cannot drop a boundary snapshot while a large final snapshot is in flight).
459
+ A replacement scheduled during a write receives its own attempt. After
460
+ all writes settle, the coordinator checks missing ancestors against the
461
+ committed SSD index again: a later trimmable descendant may now cover a
462
+ superseded RAM prefix. Uncovered or foreign-namespace prefixes stay dirty;
463
+ this adds no duplicate snapshot write. Result:
464
+ `durable` is true only when nothing is pending, nothing dropped or
465
+ failed during this flush, and no missing prefix remains uncovered.
466
+
467
+ Surfaces: `POST /admin/cache/flush` returns 200 at the boundary (503
468
+ otherwise) with pending/dropped/failed/entry-count/longest-prefix
469
+ evidence; `mlx-bun serve` handles SIGINT/SIGTERM by stopping admission,
470
+ draining active requests, and awaiting the same flush, bounded by
471
+ `MLX_BUN_SHUTDOWN_TIMEOUT_MS` (default 120000). `/stats.ssd_cache` exposes
472
+ `entries`, `bytes`, `max_bytes`, `restores`, `spills`, `restore_ms_last`,
473
+ `demotions`, `pending_snapshots` (includes debounce/re-arm timers),
474
+ `pending_spills`, `pending_spill_bytes`, `dropped_spills`,
475
+ `failed_spills`, `longest_durable_prefix_tokens`. The standard restart
476
+ benchmark calls the endpoint instead of sleeping.
477
+
478
+ ### 5.7 In-flight generation checkpoints
479
+
480
+ At every configured token interval, `generate()` reaches a boundary where
481
+ the live caches cover every emitted token and the next token has already been
482
+ sampled but not emitted. The SSD entry stores both pieces atomically:
483
+
484
+ - the normal token list and cache tensors encode the emitted prefix;
485
+ - `generationCheckpoint` in the KV header records the request key, original
486
+ prompt length, emitted-token count, adapter namespace, and pending token.
487
+
488
+ Saving the pending token is what makes the mechanism work for Qwen hybrid
489
+ models: recurrent `SSMCache` state cannot trim one token to recompute logits.
490
+ After restart, an identical request restores the full cache, replays the saved
491
+ tokens through the normal completion sink so the client receives one ordinary
492
+ assistant response, then continues directly from the pending token with the
493
+ original sampler step and history. The request key includes rendered prompt
494
+ IDs and every sampling/KV policy field. Grammar, token-fill, media, paged KV,
495
+ and logprob requests are not checkpointed in v1 because they carry additional
496
+ state or response data that cannot yet be reconstructed by token replay.
497
+
498
+ Only the newest successful checkpoint for a request is retained. The old file
499
+ remains valid until the new file's fsync+rename completes; a crash mid-write
500
+ therefore loses at most one interval. Normal completion removes the checkpoint.
501
+ Client cancellation throws through the serial token callback and preserves the
502
+ last checkpoint, so terminating a long-lived client does not erase its restart
503
+ point.
504
+
505
+ ### 5.8 Invalidation
506
+
507
+ Compatibility key = `configFingerprint(config)` + `KvScheme.cacheKey` +
508
+ tokenizer hash (`Bun.hash(tokenizer.json)`) + adapter ns; all enforced on
509
+ scan and load (`KvLoadExpect`). Chat-template drift degrades to a shorter
510
+ matched prefix (safe). GLM compressed caches additionally validate
511
+ geometry (`kvLoraRank`, `ropeHeadDim`, `dsaHeadDim`, `maxTokens`) against
512
+ the model's prototype cache before opening the tensor mmap.
513
+
514
+ ### 5.9 Measured (kept with labels)
515
+
516
+ - 2026-07-02, M1 Max, MiniCPM5, 13.7k-token prefix, `kv_config` quant:
517
+ restart TTFT 12,083 ms (full prefill) → 236 ms restored; steady-state
518
+ decode 192.0 vs 192.3 tok/s (flag off/on). Restored + trimmed
519
+ continuation identical to the RAM tier's cache-hit output.
520
+ - 2026-07-07, streamed-copy restore, 512 MB synthetic entry: mlx peak
521
+ during restore + first step = live entry + one tensor (552 vs 520 MB
522
+ active); `vmmap` shows no `.mlxkv` mapping survives; 12B real-model cold
523
+ cache-load → first token 277 ms.
524
+ - 2026-07-07, e4b 9.5k-token entry: repeat decode 37.9 tok/s with the
525
+ flush overlapping vs 47.1 cold; flat 44–46 across three samples after
526
+ the idle gate.
527
+ - 2026-08-22, unified e4b, dirty machine (correctness evidence only):
528
+ 4,026-token prompt flushed in 408 ms with `durable=true`, then 4,025
529
+ cached tokens restored after SIGTERM + respawn.
530
+
531
+ ### 5.9 Open items
532
+
533
+ - Full clean 16k e4b / 12B / Qwen3.8 restart matrix (batch 8 and
534
+ `--batch 1`, `cached_tokens ≥ prompt_tokens - 1`) is the acceptance run
535
+ for the durability fix; only the dirty-machine smoke has run.
536
+ - P4 hardening extras never closed: kill-during-write crash test; a
537
+ `THIRD_PARTY_LICENSES.md` note if any oMLX-derived code (not just ideas)
538
+ ever lands.
539
+ - Block-granular dedup for multi-user shared prefixes stays out of scope
540
+ (D1); revisit only with the paged follow-ups in 6.4.
541
+ - Stale comments: `prompt-cache.ts` still describes restore as zero-copy
542
+ mmap (5.2).
543
+
544
+ ## 6. Optional paged KV (`src/lab/paged-kv/paged-kv.ts`, `--paged-kv`)
545
+
546
+ Default off; with the flag unset no paged code executes and the plain
547
+ `KVCache`/`RotatingKVCache` path is byte-identical.
548
+
549
+ ### 6.1 What it buys here, honestly
550
+
551
+ vLLM's PagedAttention solves contiguous worst-case per-sequence
552
+ reservation and allocator fragmentation for a multi-tenant server.
553
+ mlx-bun is one process on unified memory with byte-budget admission
554
+ already shipped, so what transfers is: (a) padded-batch waste removal —
555
+ today's batched buffer width tracks the longest live row
556
+ (`mergeKVRows`/`extendKVRows`), so a short row cohabiting with a long one
557
+ pays the long one's KV; and (b) block-level copy-on-write prefix sharing.
558
+ Neither is in v1. **At batch=1 paging buys nothing** — gather-before-attend
559
+ is bandwidth tax on a bandwidth-bound decode. v1 ships the
560
+ correctness-proven block-manager abstraction at serial scope as
561
+ scaffolding for the follow-ups, not as a speed win.
562
+
563
+ This is neither of the two paging rejections already on record:
564
+ paged KV as a *prompt-cache substitute* (rejected 2026-07-07 — `take()`'s
565
+ zero-copy clones already share physically) and paged *blocks* as the SSD
566
+ spill granularity (D1). It is the rung-3 allocation abstraction from
567
+ docs/design/batching.md.
568
+
569
+ No external oracle exists (mlx-lm's `cache.py` has no paged cache); the
570
+ gate is mlx-bun's own plain `KVCache`, valid because the claim is storage
571
+ equivalence (same bytes, different arrangement), so the bar is bit-exact
572
+ (tol 0): `tests/paged-kv.test.ts` (model-free: block-boundary crossing,
573
+ free-list reuse after trim, typed exhaustion, gathered fetch vs a plain
574
+ reference, dispose) and `tests/paged-kv-parity.test.ts` (weights-gated
575
+ greedy trajectory paged-on vs paged-off).
576
+
577
+ ### 6.2 Mechanism
578
+
579
+ - **`BlockPool`** — per-layer arena: K and V pool tensors
580
+ `[numBlocks, H_kv, blockSize, headDim]` (V may differ in head dim),
581
+ `ops.zeros`-allocated so they are mlx-owned end to end (no host-pointer
582
+ alignment/dtor hazards). LIFO free list; `alloc()` throws the typed
583
+ `PagedPoolExhausted`; writes are `ops.sliceUpdate` at
584
+ `[block, 0, within, 0]` — the incoming `[1,H,l,D]` piece matches the
585
+ destination slice directly.
586
+ - **`PagedKVCache implements Cache`** — standalone (not a `KVCache`
587
+ subclass, the TurboQuant reasoning: every `instanceof` gate excludes it
588
+ and routes to the monolith forward). `updateAndFetch` splits the
589
+ incoming `L` along block boundaries, allocates tail blocks as reached,
590
+ then gathers occupied blocks with `ops.takeAxis(pool, blockTable, 0)` →
591
+ transpose `[1,0,2,3]` → reshape `[1,H,nb·bs,D]` → slice to `offset`. Only
592
+ existing bound ops — no `mlx_gather` FFI binding was needed. `makeMask`
593
+ is `KVCache.makeMask`'s logic; `trim(n)` rewinds `offset` and frees
594
+ now-unoccupied tail blocks (stale bytes past `offset` are never read —
595
+ the `KVCache` padding invariant, block-shaped). Block table is a
596
+ host-side `number[]`; all intermediates are bound and disposed.
597
+ - Pool lazily allocates on the first write (head count/dims/dtype from
598
+ the first k/v pair) and is sized once from `capacityTokens` =
599
+ prompt + `max_tokens`, so exhaustion is unreachable absent an
600
+ accounting bug — it exists as a tripwire, never silent truncation.
601
+ - **Wiring** — `maybePageKv(cache, options, capacityTokens)`
602
+ (`generate.ts`) mirrors `maybeQuantizeKv`'s post-construction in-place
603
+ swap but runs *once before prefill*: paging changes layout, not
604
+ arithmetic, so there is no "convert when populated" trigger. Only fresh
605
+ (`offset === 0`) plain `KVCache` entries are replaced; rotating layers
606
+ keep their scheme — mixed paged-full + rotating-sliding is the supported
607
+ shape. Default block size 256 = `KVCache.STEP`, so v1's growth
608
+ granularity is a permutation of today's into reusable slots, not a new
609
+ tuning axis.
610
+
611
+ ### 6.3 Gates (explicit refusals, never silent downgrades)
612
+
613
+ - Startup (`server.ts`): `--paged-kv` with `--batch N>1`, any `--kv-quant`
614
+ (affine or TurboQuant), `--draft-model`, or a non-`gemma4*` model exits
615
+ with a clear message; `--paged-kv-block-size` must be a positive
616
+ integer; `--ssd-cache` alongside it warns that the tier sees nothing.
617
+ With no explicit `--batch`, the CLI pins `--batch 1` (default is 8) and
618
+ says so. Env `MLX_BUN_PAGED_KV=1` is equivalent to the flag.
619
+ - Request scope: media (vision/audio) and LoRA-adapter requests strip the
620
+ flag and run the plain cache path (v1 non-goal cells, never a 400). One
621
+ effective `pagedKv` value per request keeps the strip and the
622
+ prompt-cache bypass coherent.
623
+ - Serial lane: paged requests skip `take`/`put` and the boundary
624
+ snapshot; caches are disposed on completion. Spec eligibility also
625
+ excludes `options.pagedKv` (belt on top of the startup refusal).
626
+ - Compiled decode: `CompiledDecode.supports()` excludes `PagedKVCache`
627
+ automatically (not one of the four supported classes) — a
628
+ data-dependent block-list length is the shape shapeless replay already
629
+ broke on.
630
+ - Batch lane: unreachable by construction (startup pins serial); the
631
+ `RequestShape` gate to add when batched paging lands is noted here.
632
+
633
+ ### 6.4 Follow-ups (dependency order) and open items
634
+
635
+ 1. Batched integration — `PagedKVCache` into the scheduler's inner union;
636
+ block-count admission replacing byte projection; join/filter/extract
637
+ allocating and freeing blocks instead of pad+concat. This is where the
638
+ padded-batch-waste win lives. Tracked under the plan anchor (Phase 18
639
+ S3+).
640
+ 2. Block-level CoW prefix sharing — refcounted block table, fork on
641
+ divergent write; extends `PromptCache`'s entry-level ref-counting down
642
+ to blocks.
643
+ 3. Quantized paged blocks — dtype-parametric block descriptor sharing one
644
+ block-index space.
645
+ 4. Fused paged-attention Metal kernel (optiq's `sdpa_2pass_paged` is the
646
+ port source) — kills the per-step gather copy.
647
+ - Perf disclosure: paged-on vs paged-off decode/prefill on the quiet
648
+ reference machine has not been recorded in `docs/reference/benchmarks.md`
649
+ (expected: a small decode regression at batch=1, to be reported plainly).
650
+ - `PagedKVCache` has no `signature()` (2.4) and no kv-store codec; both
651
+ are prerequisites for any prompt-cache or SSD integration.
652
+
653
+ ## History
654
+
655
+ - 2026-06-14 — `BatchedRotatingCache` (sliding-window batching) landed.
656
+ - 2026-07-02 — SSD cold tier P1–P3 landed: kv-store v2 (all cache kinds,
657
+ invalidation metadata, atomic writes), `SsdCacheStore`, tier
658
+ integration, flags.
659
+ - 2026-07-04 — prompt-boundary snapshot (multi-turn agent reuse fix).
660
+ - 2026-07-05 — non-consuming prefix sharing in `take()`; Layer-0 tiering
661
+ moved inside `PromptCache.take()` (both lanes); idle demotion
662
+ (`--ssd-demote-idle`).
663
+ - 2026-07-06 — TurboQuant v1 cache class + kv-store `turboquant` kind;
664
+ RAM cap default 2 GB → 8 GB; non-blocking `spillOwned` spills; `find()`
665
+ trimmability gate (restart-0 defect).
666
+ - 2026-07-07 — streamed-copy restore + zero-copy write view (A7 RSS);
667
+ idle-gated write-behind (`MLX_BUN_SSD_WRITEBEHIND`); bounded
668
+ `SpillQueue` (`MLX_BUN_SSD_SPILL_QUEUE_GB`); admission-safe snapshot
669
+ timer.
670
+ - Phase 21 (closed 2026-08-17) — GLM `mla` / `mla-dsa` / `mtp-mla`
671
+ kv-store kinds.
672
+ - 2026-08-21 — `KvScheme` seam: immutable scheme drives conversion,
673
+ admission billing, SSD key, and batch placement.
674
+ - 2026-08-22 — agg×4 regression root-caused to the unsigned
675
+ `BatchedRotatingCache` (capability-only routing fix, signature added);
676
+ `SsdDurabilityCoordinator`, `POST /admin/cache/flush`, graceful
677
+ SIGINT/SIGTERM flush.
678
+ - Paged KV v1 (serial, Gemma4, bf16, gather-then-SDPA) landed with
679
+ bit-exact gates; flag rows in cli.md / server-config.md (landing date
680
+ not recorded in the source doc, which still reads "in progress").