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