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
package/src/kv-store.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import { cleanupFailure, disposeResources } from "./engine/resources";
1
2
  // KV-cache persistence: save prompt caches to disk, reload by streamed copy.
2
- // The serialization core of the SSD cold tier (docs/design/ssd-kv-cold-tier.md).
3
+ // The serialization core of the SSD cold tier (docs/design/kv-cache.md).
3
4
  //
4
5
  // File layout (every tensor PAGE-ALIGNED — the Phase 1 corollary: files
5
6
  // we write can be mmap'd and handed to the GPU without copies):
@@ -46,7 +47,7 @@ import { MlxArray } from "./mlx/array";
46
47
  import type { Dtype } from "./mlx/ffi";
47
48
  import * as ops from "./mlx/ops";
48
49
  import {
49
- type Cache, KVCache, RotatingKVCache,
50
+ type Cache, cacheSignature, KVCache, RotatingKVCache,
50
51
  QuantizedKVCache, RotatingQuantizedKVCache,
51
52
  TurboQuantKVCache, type TurboQuantTensor,
52
53
  } from "./model/gemma4-base";
@@ -121,9 +122,26 @@ export interface KvSaveMeta {
121
122
  ns?: string;
122
123
  /** sha256 of tokenizer.json: identical ids must mean identical text. */
123
124
  tokenizerHash?: string;
125
+ /** In-flight generation state. The serialized caches cover `tokens`;
126
+ * pendingToken was sampled from that state but has not been emitted. */
127
+ generationCheckpoint?: {
128
+ key: string;
129
+ cacheNs: string;
130
+ originalPromptTokens: number;
131
+ generatedTokens: number;
132
+ pendingToken: number;
133
+ /** Actual sampler seed, including a server-generated default. Required to
134
+ * continue the random stream exactly after a restart. */
135
+ seed: number;
136
+ /** True when `seed` was part of the client request rather than generated
137
+ * by the server. */
138
+ seedWasExplicit: boolean;
139
+ };
124
140
  }
125
141
 
126
142
  export interface KvFileHeader extends KvSaveMeta {
143
+ /** Absent in earlier v3 files, which used mlx-cache-v3. */
144
+ codecProvider?: string;
127
145
  formatVersion: 3;
128
146
  createdAt: number;
129
147
  tokens: number[];
@@ -142,7 +160,366 @@ const alignUp = (n: number) => Math.ceil(n / ALIGN) * ALIGN;
142
160
  * as-laid-out with ringIdx. Slice handles are lazy graph nodes (no GPU
143
161
  * materialization until rawBytes at write time). */
144
162
  interface TensorSource { arr: MlxArray; disposeAfter: boolean }
145
- function snapshotCache(c: Cache, dataOffset: number): { entry: CacheHeaderEntry; sources: TensorSource[]; next: number } {
163
+
164
+ export interface SnapshotContext {
165
+ slots: TensorSlot[];
166
+ push(a: MlxArray, disposeAfter: boolean): void;
167
+ liveSlice(a: MlxArray, upTo: number): MlxArray;
168
+ liveMlaSlice(a: MlxArray, upTo: number): MlxArray;
169
+ pushTriple(t: ops.QuantizedTensor, upTo: number | null): void;
170
+ }
171
+
172
+ export interface CloneContext {
173
+ view(a: MlxArray): MlxArray;
174
+ liveView(a: MlxArray, upTo: number): MlxArray;
175
+ mlaView(a: MlxArray, upTo: number): MlxArray;
176
+ tripleView(t: ops.QuantizedTensor, upTo: number | null): ops.QuantizedTensor;
177
+ }
178
+
179
+ export interface LoadContext {
180
+ path: string;
181
+ arr(slot: TensorSlot): MlxArray;
182
+ grownArr(slot: TensorSlot, offset: number): MlxArray;
183
+ triple(slots: TensorSlot[], at: number): ops.QuantizedTensor;
184
+ }
185
+
186
+ export interface CacheCodec {
187
+ matches(cache: Cache): boolean;
188
+ snapshot(cache: Cache, context: SnapshotContext): CacheHeaderEntry;
189
+ clone(cache: Cache, context: CloneContext): Cache;
190
+ load(entry: CacheHeaderEntry, context: LoadContext): Cache;
191
+ headerTrimmable(entry: CacheHeaderEntry): boolean;
192
+ }
193
+
194
+ const requireMlaState = (cache: Glm52Cache, operation: string): void => {
195
+ if (!cache.latent || !cache.rope || cache.batchSize === null || cache.offset === 0)
196
+ throw new Error(`cannot ${operation} an empty GLM compressed cache`);
197
+ if (cache.dsa && (!cache.dsa.data || cache.dsa.offset !== cache.offset))
198
+ throw new Error(`cannot ${operation} misaligned GLM DSA state`);
199
+ };
200
+
201
+ const snapshotMla = (
202
+ kind: Extract<CacheKind, "mla" | "mla-dsa" | "mtp-mla">,
203
+ cache: Cache,
204
+ context: SnapshotContext,
205
+ ): CacheHeaderEntry => {
206
+ const c = cache as Glm52Cache;
207
+ requireMlaState(c, "persist");
208
+ context.push(context.liveMlaSlice(c.latent!, c.offset), true);
209
+ context.push(context.liveMlaSlice(c.rope!, c.offset), true);
210
+ if (c.dsa) context.push(context.liveMlaSlice(c.dsa.data!, c.offset), true);
211
+ return {
212
+ kind,
213
+ offset: c.offset,
214
+ kvLoraRank: c.kvLoraRank,
215
+ ropeHeadDim: c.ropeHeadDim,
216
+ ...(c.dsa ? { dsaHeadDim: c.dsa.headDim } : {}),
217
+ maxTokens: c.maxTokens,
218
+ tensors: context.slots,
219
+ };
220
+ };
221
+
222
+ const cloneMla = (cache: Cache, context: CloneContext): Cache => {
223
+ const c = cache as Glm52Cache;
224
+ requireMlaState(c, "clone");
225
+ const clone = new MLACache({
226
+ kvLoraRank: c.kvLoraRank,
227
+ ropeHeadDim: c.ropeHeadDim,
228
+ ...(c.dsa ? { dsa: { headDim: c.dsa.headDim } } : {}),
229
+ maxTokens: c.maxTokens,
230
+ role: c.role,
231
+ });
232
+ clone.restoreCompressedState(
233
+ context.mlaView(c.latent!, c.offset),
234
+ context.mlaView(c.rope!, c.offset),
235
+ c.dsa ? context.mlaView(c.dsa.data!, c.offset) : null,
236
+ c.offset,
237
+ );
238
+ return clone;
239
+ };
240
+
241
+ const loadMla = (entry: CacheHeaderEntry, context: LoadContext): Cache => {
242
+ const hasDsa = entry.kind === "mla-dsa";
243
+ if (entry.kind === "mtp-mla" && entry.dsaHeadDim !== undefined)
244
+ throw new Error(`${context.path}: native MTP cache cannot contain DSA state`);
245
+ const cache = new MLACache({
246
+ kvLoraRank: entry.kvLoraRank!,
247
+ ropeHeadDim: entry.ropeHeadDim!,
248
+ ...(hasDsa ? { dsa: { headDim: entry.dsaHeadDim! } } : {}),
249
+ maxTokens: entry.maxTokens,
250
+ role: entry.kind === "mtp-mla" ? "mtp" : "target",
251
+ });
252
+ cache.restoreCompressedState(
253
+ context.arr(entry.tensors[0]!),
254
+ context.arr(entry.tensors[1]!),
255
+ hasDsa ? context.arr(entry.tensors[2]!) : null,
256
+ entry.offset,
257
+ );
258
+ return cache;
259
+ };
260
+
261
+ /** One authoritative persistence algebra. Adding a cache storage kind must
262
+ * define all four operations here; save/clone/load and SSD eligibility
263
+ * therefore cannot grow independent instanceof/switch chains. */
264
+ const CACHE_CODECS = {
265
+ kv: {
266
+ matches: (cache) => cacheSignature(cache) === "kv:plain",
267
+ snapshot: (cache, context) => {
268
+ const c = cache as KVCache;
269
+ if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
270
+ context.push(context.liveSlice(c.keys, c.offset), true);
271
+ context.push(context.liveSlice(c.values, c.offset), true);
272
+ return { kind: "kv", offset: c.offset, tensors: context.slots };
273
+ },
274
+ clone: (cache, context) => {
275
+ const c = cache as KVCache;
276
+ if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
277
+ const clone = new KVCache();
278
+ clone.restoreState(
279
+ context.liveView(c.keys, c.offset),
280
+ context.liveView(c.values, c.offset),
281
+ c.offset,
282
+ );
283
+ return clone;
284
+ },
285
+ load: (entry, context) => {
286
+ const cache = new KVCache();
287
+ cache.restoreState(
288
+ context.grownArr(entry.tensors[0]!, entry.offset),
289
+ context.grownArr(entry.tensors[1]!, entry.offset),
290
+ entry.offset,
291
+ );
292
+ return cache;
293
+ },
294
+ headerTrimmable: () => true,
295
+ },
296
+ rotating: {
297
+ matches: (cache) => cacheSignature(cache) === "kv:rotating-plain",
298
+ snapshot: (cache, context) => {
299
+ const c = cache as RotatingKVCache;
300
+ if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
301
+ context.push(c.keys, false);
302
+ context.push(c.values, false);
303
+ return { kind: "rotating", offset: c.offset, idx: c.ringIdx,
304
+ maxSize: c.maxSize, tensors: context.slots };
305
+ },
306
+ clone: (cache, context) => {
307
+ const c = cache as RotatingKVCache;
308
+ if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
309
+ const clone = new RotatingKVCache(c.maxSize);
310
+ clone.restoreState(context.view(c.keys), context.view(c.values), c.offset, c.ringIdx);
311
+ return clone;
312
+ },
313
+ load: (entry, context) => {
314
+ const cache = new RotatingKVCache(entry.maxSize!);
315
+ cache.restoreState(
316
+ context.arr(entry.tensors[0]!), context.arr(entry.tensors[1]!),
317
+ entry.offset, entry.idx!,
318
+ );
319
+ return cache;
320
+ },
321
+ headerTrimmable: (entry) => entry.offset < (entry.maxSize ?? 0),
322
+ },
323
+ qkv: {
324
+ matches: (cache) => cacheSignature(cache).startsWith("kv:quant:"),
325
+ snapshot: (cache, context) => {
326
+ const c = cache as QuantizedKVCache;
327
+ if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
328
+ context.pushTriple(c.keys, c.offset);
329
+ context.pushTriple(c.values, c.offset);
330
+ return { kind: "qkv", offset: c.offset, groupSize: c.groupSize,
331
+ bits: c.bits, tensors: context.slots };
332
+ },
333
+ clone: (cache, context) => {
334
+ const c = cache as QuantizedKVCache;
335
+ if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
336
+ const clone = new QuantizedKVCache(c.groupSize, c.bits);
337
+ clone.restoreState(
338
+ context.tripleView(c.keys, c.offset),
339
+ context.tripleView(c.values, c.offset),
340
+ c.offset,
341
+ );
342
+ return clone;
343
+ },
344
+ load: (entry, context) => {
345
+ const cache = new QuantizedKVCache(entry.groupSize!, entry.bits!);
346
+ cache.restoreState(
347
+ context.triple(entry.tensors, 0), context.triple(entry.tensors, 3), entry.offset,
348
+ );
349
+ return cache;
350
+ },
351
+ headerTrimmable: () => true,
352
+ },
353
+ "rotating-qkv": {
354
+ matches: (cache) => cacheSignature(cache).startsWith("kv:rotating-quant:"),
355
+ snapshot: (cache, context) => {
356
+ const c = cache as RotatingQuantizedKVCache;
357
+ if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
358
+ context.pushTriple(c.keys, null);
359
+ context.pushTriple(c.values, null);
360
+ return { kind: "rotating-qkv", offset: c.offset, idx: c.ringIdx,
361
+ maxSize: c.maxSize, groupSize: c.groupSize, bits: c.bits,
362
+ tensors: context.slots };
363
+ },
364
+ clone: (cache, context) => {
365
+ const c = cache as RotatingQuantizedKVCache;
366
+ if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
367
+ const clone = new RotatingQuantizedKVCache(c.maxSize, c.groupSize, c.bits);
368
+ clone.restoreState(
369
+ context.tripleView(c.keys, null), context.tripleView(c.values, null),
370
+ c.offset, c.ringIdx,
371
+ );
372
+ return clone;
373
+ },
374
+ load: (entry, context) => {
375
+ const cache = new RotatingQuantizedKVCache(
376
+ entry.maxSize!, entry.groupSize!, entry.bits!,
377
+ );
378
+ cache.restoreState(
379
+ context.triple(entry.tensors, 0), context.triple(entry.tensors, 3),
380
+ entry.offset, entry.idx!,
381
+ );
382
+ return cache;
383
+ },
384
+ headerTrimmable: (entry) => entry.offset < (entry.maxSize ?? 0),
385
+ },
386
+ ssm: {
387
+ matches: (cache) => cacheSignature(cache) === "ssm",
388
+ snapshot: (cache, context) => {
389
+ const c = cache as SSMCache;
390
+ if (!c.conv || !c.recurrent) throw new Error("cannot persist an empty cache");
391
+ context.push(c.conv, false);
392
+ context.push(c.recurrent, false);
393
+ return { kind: "ssm", offset: c.offset, tensors: context.slots };
394
+ },
395
+ clone: (cache, context) => {
396
+ const c = cache as SSMCache;
397
+ if (!c.conv || !c.recurrent) throw new Error("cannot clone an empty cache");
398
+ const clone = new SSMCache();
399
+ clone.conv = context.view(c.conv);
400
+ clone.recurrent = context.view(c.recurrent);
401
+ clone.offset = c.offset;
402
+ return clone;
403
+ },
404
+ load: (entry, context) => {
405
+ const cache = new SSMCache();
406
+ cache.conv = context.arr(entry.tensors[0]!);
407
+ cache.recurrent = context.arr(entry.tensors[1]!);
408
+ cache.offset = entry.offset;
409
+ return cache;
410
+ },
411
+ headerTrimmable: () => false,
412
+ },
413
+ turboquant: {
414
+ matches: (cache) => cacheSignature(cache).startsWith("kv:turboquant:"),
415
+ snapshot: (cache, context) => {
416
+ const c = cache as TurboQuantKVCache;
417
+ const state = c.state();
418
+ if (state.length === 0) throw new Error("cannot persist an empty cache");
419
+ for (const array of state) context.push(array, true);
420
+ return { kind: "turboquant", offset: c.offset, kBits: c.kBits,
421
+ vBits: c.vBits, headDim: c.headDim ?? 0, tensors: context.slots };
422
+ },
423
+ clone: (cache, context) => {
424
+ const c = cache as TurboQuantKVCache;
425
+ const state = c.state();
426
+ if (state.length === 0) throw new Error("cannot clone an empty cache");
427
+ try {
428
+ const [kIdx, kScales, kZeros, vPacked, vScales] = state;
429
+ const clone = new TurboQuantKVCache(c.kBits, c.vBits);
430
+ clone.restoreState({
431
+ kIdx: context.view(kIdx!),
432
+ kScales: context.view(kScales!),
433
+ kZeros: context.view(kZeros!),
434
+ vPacked: context.view(vPacked!),
435
+ vScales: context.view(vScales!),
436
+ }, c.offset, c.headDim!);
437
+ return clone;
438
+ } finally {
439
+ for (const array of state) array.dispose();
440
+ }
441
+ },
442
+ load: (entry, context) => {
443
+ const cache = new TurboQuantKVCache(entry.kBits!, entry.vBits!);
444
+ const tensors: TurboQuantTensor = {
445
+ kIdx: context.arr(entry.tensors[0]!),
446
+ kScales: context.arr(entry.tensors[1]!),
447
+ kZeros: context.arr(entry.tensors[2]!),
448
+ vPacked: context.arr(entry.tensors[3]!),
449
+ vScales: context.arr(entry.tensors[4]!),
450
+ };
451
+ cache.restoreState(tensors, entry.offset, entry.headDim!);
452
+ return cache;
453
+ },
454
+ headerTrimmable: () => true,
455
+ },
456
+ mla: {
457
+ matches: (cache) => cacheSignature(cache) === "kv:mla:target",
458
+ snapshot: (cache, context) => snapshotMla("mla", cache, context),
459
+ clone: cloneMla,
460
+ load: loadMla,
461
+ headerTrimmable: () => true,
462
+ },
463
+ "mla-dsa": {
464
+ matches: (cache) => cacheSignature(cache) === "kv:mla:target:dsa",
465
+ snapshot: (cache, context) => snapshotMla("mla-dsa", cache, context),
466
+ clone: cloneMla,
467
+ load: loadMla,
468
+ headerTrimmable: () => true,
469
+ },
470
+ "mtp-mla": {
471
+ matches: (cache) => cacheSignature(cache) === "kv:mla:mtp",
472
+ snapshot: (cache, context) => snapshotMla("mtp-mla", cache, context),
473
+ clone: cloneMla,
474
+ load: loadMla,
475
+ headerTrimmable: () => true,
476
+ },
477
+ } satisfies Record<CacheKind, CacheCodec>;
478
+
479
+ /** Codecs belong to the loaded backend binding. The persisted ID is checked
480
+ * before allocating state; codecs are engine code, never loaded from a file. */
481
+ export interface CacheCodecProvider {
482
+ readonly id: string;
483
+ forCache(cache: Cache): CacheCodec;
484
+ forHeader(entry: CacheHeaderEntry): CacheCodec;
485
+ }
486
+
487
+ export function createCacheCodecProvider(
488
+ id: string, entries: Readonly<Partial<Record<CacheKind, CacheCodec>>>,
489
+ ): CacheCodecProvider {
490
+ if (!id.trim()) throw new Error("cache codec provider needs an identity");
491
+ const table = new Map(Object.entries(entries).map(([kind, codec]) => [kind, Object.freeze({
492
+ matches: codec.matches.bind(codec), snapshot: codec.snapshot.bind(codec), clone: codec.clone.bind(codec),
493
+ load: codec.load.bind(codec), headerTrimmable: codec.headerTrimmable.bind(codec),
494
+ })]));
495
+ const candidates = [...table.values()];
496
+ const selected = new WeakMap<Cache, CacheCodec>();
497
+ return Object.freeze({
498
+ id,
499
+ forCache(cache: Cache) {
500
+ const bound = selected.get(cache);
501
+ if (bound) return bound;
502
+ const matches = candidates.filter((codec) => codec.matches(cache));
503
+ if (matches.length !== 1)
504
+ throw new Error(`cache codec provider ${id}: ${matches.length} matches for ${cacheSignature(cache)}`);
505
+ selected.set(cache, matches[0]!);
506
+ return matches[0]!;
507
+ },
508
+ forHeader(entry: CacheHeaderEntry) {
509
+ const codec = table.get(entry.kind);
510
+ if (!codec) throw new Error(`cache codec provider ${id}: unsupported cache kind ${entry.kind}`);
511
+ return codec;
512
+ },
513
+ });
514
+ }
515
+
516
+ export const legacyCacheCodecs = createCacheCodecProvider("mlx-cache-v3", CACHE_CODECS);
517
+
518
+ export const cacheHeadersTrimmable = (
519
+ caches: readonly CacheHeaderEntry[], codecs: CacheCodecProvider = legacyCacheCodecs,
520
+ ): boolean => caches.every((entry) => codecs.forHeader(entry).headerTrimmable(entry));
521
+
522
+ function snapshotCache(c: Cache, dataOffset: number, codecs: CacheCodecProvider): { entry: CacheHeaderEntry; sources: TensorSource[]; next: number } {
146
523
  const slots: TensorSlot[] = [];
147
524
  const sources: TensorSource[] = [];
148
525
  let cursor = dataOffset;
@@ -168,65 +545,13 @@ function snapshotCache(c: Cache, dataOffset: number): { entry: CacheHeaderEntry;
168
545
  }
169
546
  };
170
547
 
171
- let entry: CacheHeaderEntry;
172
- if (c instanceof Glm52Cache) {
173
- if (!c.latent || !c.rope || c.batchSize === null || c.offset === 0)
174
- throw new Error("cannot persist an empty GLM compressed cache");
175
- push(liveMlaSlice(c.latent, c.offset), true);
176
- push(liveMlaSlice(c.rope, c.offset), true);
177
- if (c.dsa) {
178
- if (!c.dsa.data || c.dsa.offset !== c.offset)
179
- throw new Error("cannot persist misaligned GLM DSA state");
180
- push(liveMlaSlice(c.dsa.data, c.offset), true);
181
- }
182
- entry = {
183
- kind: c.role === "mtp" ? "mtp-mla" : c.dsa ? "mla-dsa" : "mla",
184
- offset: c.offset,
185
- kvLoraRank: c.kvLoraRank,
186
- ropeHeadDim: c.ropeHeadDim,
187
- ...(c.dsa ? { dsaHeadDim: c.dsa.headDim } : {}),
188
- maxTokens: c.maxTokens,
189
- tensors: slots,
190
- };
191
- } else if (c instanceof TurboQuantKVCache) {
192
- const t = c.state();
193
- if (t.length === 0) throw new Error("cannot persist an empty cache");
194
- const [kIdx, kScales, kZeros, vPacked, vScales] = t as [MlxArray, MlxArray, MlxArray, MlxArray, MlxArray];
195
- // state() already trims to offset — dispose these lazy slice views
196
- // once written (same disposeAfter contract as liveSlice elsewhere).
197
- for (const a of [kIdx, kScales, kZeros, vPacked, vScales]) push(a, true);
198
- entry = { kind: "turboquant", offset: c.offset, kBits: c.kBits, vBits: c.vBits,
199
- headDim: c.headDim ?? 0, tensors: slots };
200
- } else if (c instanceof RotatingQuantizedKVCache) {
201
- if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
202
- pushTriple(c.keys, null);
203
- pushTriple(c.values, null);
204
- entry = { kind: "rotating-qkv", offset: c.offset, idx: c.ringIdx, maxSize: c.maxSize,
205
- groupSize: c.groupSize, bits: c.bits, tensors: slots };
206
- } else if (c instanceof QuantizedKVCache) {
207
- if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
208
- pushTriple(c.keys, c.offset);
209
- pushTriple(c.values, c.offset);
210
- entry = { kind: "qkv", offset: c.offset, groupSize: c.groupSize, bits: c.bits, tensors: slots };
211
- } else if (c instanceof RotatingKVCache) {
212
- if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
213
- push(c.keys, false);
214
- push(c.values, false);
215
- entry = { kind: "rotating", offset: c.offset, idx: c.ringIdx, maxSize: c.maxSize, tensors: slots };
216
- } else if (c instanceof SSMCache) {
217
- if (!c.conv || !c.recurrent) throw new Error("cannot persist an empty cache");
218
- push(c.conv, false);
219
- push(c.recurrent, false);
220
- entry = { kind: "ssm", offset: c.offset, tensors: slots };
221
- } else if (c instanceof KVCache) {
222
- if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
223
- push(liveSlice(c.keys, c.offset), true);
224
- push(liveSlice(c.values, c.offset), true);
225
- entry = { kind: "kv", offset: c.offset, tensors: slots };
226
- } else {
227
- throw new Error("unknown cache type");
548
+ try {
549
+ const entry = codecs.forCache(c).snapshot(c, { slots, push, liveSlice, liveMlaSlice, pushTriple });
550
+ return { entry, sources, next: cursor };
551
+ } catch (error) {
552
+ for (const source of sources) if (source.disposeAfter) source.arr.dispose();
553
+ throw error;
228
554
  }
229
- return { entry, sources, next: cursor };
230
555
  }
231
556
 
232
557
  /** Persist `caches` (+ the exact token prefix they encode) to `path`.
@@ -243,15 +568,17 @@ function snapshotCache(c: Cache, dataOffset: number): { entry: CacheHeaderEntry;
243
568
  * called while the caches hold EXACTLY the state to snapshot (post-wrap
244
569
  * rings cannot be rewound later). Same per-kind dispatch as
245
570
  * snapshotCache/loadKvCache. */
246
- export function cloneKvCaches(caches: Cache[]): Cache[] {
247
- const view = (a: MlxArray): MlxArray => a.slice(a.shape.map(() => 0), [...a.shape]);
571
+ export function cloneKvCaches(caches: Cache[], codecs: CacheCodecProvider = legacyCacheCodecs): Cache[] {
572
+ const pending: MlxArray[] = [];
573
+ const hold = (array: MlxArray): MlxArray => { pending.push(array); return array; };
574
+ const view = (a: MlxArray): MlxArray => hold(a.slice(a.shape.map(() => 0), [...a.shape]));
248
575
  const liveView = (a: MlxArray, upTo: number): MlxArray => {
249
576
  const [B, H, , D] = a.shape as [number, number, number, number];
250
- return a.slice([0, 0, 0, 0], [B, H, upTo, D]);
577
+ return hold(a.slice([0, 0, 0, 0], [B, H, upTo, D]));
251
578
  };
252
579
  const mlaView = (a: MlxArray, upTo: number): MlxArray => {
253
580
  const [B, , D] = a.shape as [number, number, number];
254
- return a.slice([0, 0, 0], [B, upTo, D]);
581
+ return hold(a.slice([0, 0, 0], [B, upTo, D]));
255
582
  };
256
583
  const tripleView = (t: ops.QuantizedTensor, upTo: number | null): ops.QuantizedTensor => ({
257
584
  packed: upTo === null ? view(t.packed) : liveView(t.packed, upTo),
@@ -261,69 +588,11 @@ export function cloneKvCaches(caches: Cache[]): Cache[] {
261
588
  const out: Cache[] = [];
262
589
  try {
263
590
  for (const c of caches) {
264
- if (c instanceof Glm52Cache) {
265
- if (!c.latent || !c.rope || c.batchSize === null || c.offset === 0)
266
- throw new Error("cannot clone an empty GLM compressed cache");
267
- const n = new MLACache({
268
- kvLoraRank: c.kvLoraRank,
269
- ropeHeadDim: c.ropeHeadDim,
270
- ...(c.dsa ? { dsa: { headDim: c.dsa.headDim } } : {}),
271
- maxTokens: c.maxTokens,
272
- role: c.role,
273
- });
274
- n.restoreCompressedState(
275
- mlaView(c.latent, c.offset),
276
- mlaView(c.rope, c.offset),
277
- c.dsa ? mlaView(c.dsa.data!, c.offset) : null,
278
- c.offset,
279
- );
280
- out.push(n);
281
- } else if (c instanceof TurboQuantKVCache) {
282
- const state = c.state();
283
- if (state.length === 0) throw new Error("cannot clone an empty cache");
284
- const [kIdx, kScales, kZeros, vPacked, vScales] = state;
285
- const n = new TurboQuantKVCache(c.kBits, c.vBits);
286
- n.restoreState(
287
- { kIdx: view(kIdx!), kScales: view(kScales!), kZeros: view(kZeros!),
288
- vPacked: view(vPacked!), vScales: view(vScales!) },
289
- c.offset, c.headDim!,
290
- );
291
- for (const a of state) a.dispose();
292
- out.push(n);
293
- } else if (c instanceof RotatingQuantizedKVCache) {
294
- if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
295
- const n = new RotatingQuantizedKVCache(c.maxSize, c.groupSize, c.bits);
296
- n.restoreState(tripleView(c.keys, null), tripleView(c.values, null), c.offset, c.ringIdx);
297
- out.push(n);
298
- } else if (c instanceof QuantizedKVCache) {
299
- if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
300
- const n = new QuantizedKVCache(c.groupSize, c.bits);
301
- n.restoreState(tripleView(c.keys, c.offset), tripleView(c.values, c.offset), c.offset);
302
- out.push(n);
303
- } else if (c instanceof RotatingKVCache) {
304
- if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
305
- const n = new RotatingKVCache(c.maxSize);
306
- n.restoreState(view(c.keys), view(c.values), c.offset, c.ringIdx);
307
- out.push(n);
308
- } else if (c instanceof SSMCache) {
309
- if (!c.conv || !c.recurrent) throw new Error("cannot clone an empty cache");
310
- const n = new SSMCache();
311
- n.conv = view(c.conv);
312
- n.recurrent = view(c.recurrent);
313
- n.offset = c.offset;
314
- out.push(n);
315
- } else if (c instanceof KVCache) {
316
- if (!c.keys || !c.values) throw new Error("cannot clone an empty cache");
317
- const n = new KVCache();
318
- n.restoreState(liveView(c.keys, c.offset), liveView(c.values, c.offset), c.offset);
319
- out.push(n);
320
- } else {
321
- throw new Error("cloneKvCaches: unknown cache type");
322
- }
591
+ out.push(codecs.forCache(c).clone(c, { view, liveView, mlaView, tripleView }));
592
+ pending.length = 0; // transferred to the returned cache
323
593
  }
324
594
  } catch (err) {
325
- for (const c of out) c.dispose();
326
- throw err;
595
+ cleanupFailure(err, () => disposeResources([...pending, ...out]));
327
596
  }
328
597
  return out;
329
598
  }
@@ -333,14 +602,14 @@ export function cloneKvCaches(caches: Cache[]): Cache[] {
333
602
  * between yields so the EVENT LOOP KEEPS SERVING while a multi-hundred-MB
334
603
  * entry flushes (the write-behind persistence contract: durability never
335
604
  * blocks a request). try/finally still runs on early generator close. */
336
- function* saveKvCacheSteps(path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta): Generator<void, void, void> {
605
+ function* saveKvCacheSteps(path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta, codecs: CacheCodecProvider): Generator<void, void, void> {
337
606
  // Plan pass: header entries + lazy tensor sources, NO bytes materialized.
338
607
  const entries: CacheHeaderEntry[] = [];
339
608
  const sources: TensorSource[] = [];
340
609
  let dataOffset = 0; // relative; rebased after header is sized
341
610
  try {
342
611
  for (const c of caches) {
343
- const s = snapshotCache(c, dataOffset);
612
+ const s = snapshotCache(c, dataOffset, codecs);
344
613
  entries.push(s.entry);
345
614
  sources.push(...s.sources);
346
615
  dataOffset = s.next;
@@ -349,7 +618,7 @@ function* saveKvCacheSteps(path: string, tokens: number[], caches: Cache[], meta
349
618
  // Header size is final NOW: hashes are fixed-width placeholders that get
350
619
  // patched in place (same byte length) after the data pass computes them.
351
620
  const header: KvFileHeader = {
352
- formatVersion: 3, createdAt: Date.now(), ...meta, tokens, caches: entries,
621
+ formatVersion: 3, createdAt: Date.now(), ...meta, codecProvider: codecs.id, tokens, caches: entries,
353
622
  };
354
623
  const headerLen = new TextEncoder().encode(JSON.stringify(header)).length;
355
624
  const dataStart = alignUp(PREFIX_LEN + headerLen);
@@ -414,32 +683,34 @@ function* saveKvCacheSteps(path: string, tokens: number[], caches: Cache[], meta
414
683
  }
415
684
  }
416
685
 
417
- export function saveKvCache(path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta = {}): void {
418
- for (const _ of saveKvCacheSteps(path, tokens, caches, meta)) { /* drain */ }
686
+ export function saveKvCache(path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta = {}, codecs: CacheCodecProvider = legacyCacheCodecs): void {
687
+ for (const _ of saveKvCacheSteps(path, tokens, caches, meta, codecs)) { /* drain */ }
419
688
  }
420
689
 
421
690
  /** Non-blocking variant: yields the event loop after every tensor write so
422
691
  * serving interleaves with the flush. Caller owns `caches` lifetime for
423
692
  * the duration (pass zero-copy clones, dispose after).
424
693
  *
425
- * `waitTurn` (optional) is awaited BEFORE every step — including the first —
426
- * so the caller can gate the flush's schedule (the server passes the
427
- * gateway's onIdle: each tensor step is a blocking GPU sync on the decode
428
- * stream + a synchronous multi-MB writeSync, and the old unconditional
429
- * setImmediate pacing interleaved those slices exactly between decode
430
- * tokens — the 2026-07-07 decode@ctx contamination. A request arriving
431
- * MID-flush pauses the remaining tensors until idle again). Gate failures
432
- * are swallowed: scheduling advice must never corrupt the write path
433
- * (an early generator close inside the fd-open section would leak the fd). */
694
+ * `runStep` (optional) runs each blocking tensor step inside the caller's
695
+ * exclusion domain. Waiting for an idle observation is not sufficient: a
696
+ * request can acquire the engine between that check and `steps.next()`,
697
+ * putting rawBytesView's GPU sync on the decode stream during prefill. */
434
698
  export async function saveKvCacheAsync(
435
699
  path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta = {},
436
- waitTurn?: () => Promise<void>,
700
+ runStep?: <T>(step: () => T) => Promise<T>, codecs: CacheCodecProvider = legacyCacheCodecs,
437
701
  ): Promise<void> {
438
- const steps = saveKvCacheSteps(path, tokens, caches, meta);
439
- while (true) {
440
- if (waitTurn) await waitTurn().catch(() => {});
441
- if (steps.next().done) break;
442
- await new Promise<void>((r) => setImmediate(r));
702
+ const steps = saveKvCacheSteps(path, tokens, caches, meta, codecs);
703
+ try {
704
+ while (true) {
705
+ const next = runStep ? await runStep(() => steps.next()) : steps.next();
706
+ if (next.done) break;
707
+ await new Promise<void>((r) => setImmediate(r));
708
+ }
709
+ } catch (error) {
710
+ // A rejected execution lease can leave the writer suspended with an
711
+ // open fd. Throw through it so its existing failure cleanup runs.
712
+ steps.throw(error);
713
+ throw error;
443
714
  }
444
715
  }
445
716
 
@@ -454,7 +725,7 @@ export interface SpillItem {
454
725
  /** Bounded write-behind queue (2026-07-07 post-merge review fix).
455
726
  *
456
727
  * Pending spill/snapshot clones pin their entries' GPU buffers until the
457
- * idle-gated flush gets a turn; the old bare promise chain queued them
728
+ * generation-locked flush gets a turn; the old bare promise chain queued them
458
729
  * WITHOUT BOUND, so under sustained traffic (gate starved, evictions
459
730
  * ongoing) resident memory = prompt-cache cap + every queued clone —
460
731
  * allocator pressure exactly under the load that caused the evictions.
@@ -468,12 +739,17 @@ export interface SpillItem {
468
739
  * Items run strictly serially in enqueue order via an internal chain;
469
740
  * store failures are swallowed (cold tier is best-effort) but clones
470
741
  * are disposed on every settle path. */
471
- type SpillRec = SpillItem & { bytes: number; dropped: boolean };
742
+ type SpillRec = SpillItem & {
743
+ bytes: number;
744
+ dropped: boolean;
745
+ settle: (stored: boolean) => void;
746
+ };
472
747
 
473
748
  export class SpillQueue {
474
749
  #queue: SpillRec[] = [];
475
750
  #bytes = 0;
476
751
  #dropped = 0;
752
+ #failed = 0;
477
753
  #inFlight: object | null = null;
478
754
  #chain: Promise<void> = Promise.resolve();
479
755
 
@@ -492,9 +768,18 @@ export class SpillQueue {
492
768
  get pendingCount(): number { return this.#queue.length; }
493
769
  /** Spills dropped by the cap since start (each = one future cache miss). */
494
770
  get droppedCount(): number { return this.#dropped; }
771
+ /** Store calls that threw or returned false. */
772
+ get failedCount(): number { return this.#failed; }
495
773
 
496
- enqueue(item: SpillItem): void {
497
- const rec = { ...item, bytes: this.bytesOf(item.caches), dropped: false };
774
+ enqueue(item: SpillItem): Promise<boolean> {
775
+ let settle!: (stored: boolean) => void;
776
+ const result = new Promise<boolean>((resolve) => { settle = resolve; });
777
+ const rec: SpillRec = {
778
+ ...item,
779
+ bytes: this.bytesOf(item.caches),
780
+ dropped: false,
781
+ settle,
782
+ };
498
783
  this.#queue.push(rec);
499
784
  this.#bytes += rec.bytes;
500
785
  while (this.#bytes > this.capBytes) {
@@ -508,24 +793,30 @@ export class SpillQueue {
508
793
  if (rec.dropped) return; // clones already disposed at drop time
509
794
  this.#inFlight = rec;
510
795
  try {
511
- await this.store(rec);
796
+ const stored = await this.store(rec);
797
+ if (stored === false) this.#failed++;
798
+ rec.settle(stored !== false);
512
799
  } catch {
513
800
  // best-effort tier — the entry simply won't restore
801
+ this.#failed++;
802
+ rec.settle(false);
514
803
  } finally {
515
804
  this.#inFlight = null;
516
805
  this.#remove(rec);
517
806
  this.disposeClones(rec.caches);
518
807
  }
519
808
  });
809
+ return result;
520
810
  }
521
811
 
522
812
  /** Settles when everything enqueued so far has flushed or dropped
523
- * (tests; a future shutdown hook would await this). */
813
+ * (tests and graceful shutdown). */
524
814
  drain(): Promise<void> { return this.#chain; }
525
815
 
526
816
  #drop(rec: SpillRec): void {
527
817
  rec.dropped = true;
528
818
  this.#dropped++;
819
+ rec.settle(false);
529
820
  this.#remove(rec);
530
821
  this.disposeClones(rec.caches);
531
822
  }
@@ -592,24 +883,26 @@ function validateGlm52Prototype(
592
883
  ): void {
593
884
  const glmKind = entry.kind === "mla" || entry.kind === "mla-dsa" ||
594
885
  entry.kind === "mtp-mla";
595
- if (!(prototype instanceof Glm52Cache)) {
886
+ const prototypeSignature = cacheSignature(prototype);
887
+ if (!prototypeSignature.startsWith("kv:mla:")) {
596
888
  if (glmKind)
597
889
  throw new Error(`${path}: GLM cache kind ${entry.kind} does not match model cache`);
598
890
  return;
599
891
  }
600
- const expectedKind: CacheKind = prototype.role === "mtp"
892
+ const glmPrototype = prototype as Glm52Cache;
893
+ const expectedKind: CacheKind = glmPrototype.role === "mtp"
601
894
  ? "mtp-mla"
602
- : prototype.dsa ? "mla-dsa" : "mla";
895
+ : glmPrototype.dsa ? "mla-dsa" : "mla";
603
896
  if (entry.kind !== expectedKind) {
604
897
  throw new Error(
605
898
  `${path}: cache kind ${entry.kind} != model cache kind ${expectedKind}`,
606
899
  );
607
900
  }
608
901
  for (const [field, actual, expected] of [
609
- ["kvLoraRank", entry.kvLoraRank, prototype.kvLoraRank],
610
- ["ropeHeadDim", entry.ropeHeadDim, prototype.ropeHeadDim],
611
- ["dsaHeadDim", entry.dsaHeadDim, prototype.dsa?.headDim],
612
- ["maxTokens", entry.maxTokens, prototype.maxTokens],
902
+ ["kvLoraRank", entry.kvLoraRank, glmPrototype.kvLoraRank],
903
+ ["ropeHeadDim", entry.ropeHeadDim, glmPrototype.ropeHeadDim],
904
+ ["dsaHeadDim", entry.dsaHeadDim, glmPrototype.dsa?.headDim],
905
+ ["maxTokens", entry.maxTokens, glmPrototype.maxTokens],
613
906
  ] as const) {
614
907
  if (actual !== expected) {
615
908
  throw new Error(
@@ -617,7 +910,7 @@ function validateGlm52Prototype(
617
910
  );
618
911
  }
619
912
  }
620
- const expectedTensors = prototype.dsa ? 3 : 2;
913
+ const expectedTensors = glmPrototype.dsa ? 3 : 2;
621
914
  if (entry.tensors.length !== expectedTensors) {
622
915
  throw new Error(
623
916
  `${path}: ${entry.kind} has ${entry.tensors.length} tensors, ` +
@@ -670,9 +963,12 @@ function withStepCapacity(a: MlxArray, offset: number): MlxArray {
670
963
  export function loadKvCache(
671
964
  path: string,
672
965
  model: { makeCache(): Cache[] },
673
- expect: KvLoadExpect = {},
966
+ expect: KvLoadExpect = {}, codecs: CacheCodecProvider = legacyCacheCodecs,
674
967
  ): LoadedKvCache {
675
968
  const header = readKvHeader(path);
969
+ if ((header.codecProvider ?? legacyCacheCodecs.id) !== codecs.id)
970
+ throw new Error(`${path}: cache codec provider mismatch`);
971
+ for (const entry of header.caches) codecs.forHeader(entry);
676
972
  for (const key of [
677
973
  "modelId",
678
974
  "configFingerprint",
@@ -732,79 +1028,8 @@ export function loadKvCache(
732
1028
  const caches: Cache[] = [];
733
1029
  try {
734
1030
  for (const e of header.caches) {
735
- const t = e.tensors;
736
- switch (e.kind) {
737
- case "kv": {
738
- const c = new KVCache();
739
- c.restoreState(
740
- grownArr(t[0]!, e.offset),
741
- grownArr(t[1]!, e.offset),
742
- e.offset,
743
- );
744
- caches.push(c);
745
- break;
746
- }
747
- case "rotating": {
748
- const c = new RotatingKVCache(e.maxSize!);
749
- c.restoreState(arr(t[0]!), arr(t[1]!), e.offset, e.idx!);
750
- caches.push(c);
751
- break;
752
- }
753
- case "qkv": {
754
- const c = new QuantizedKVCache(e.groupSize!, e.bits!);
755
- c.restoreState(triple(t, 0), triple(t, 3), e.offset);
756
- caches.push(c);
757
- break;
758
- }
759
- case "rotating-qkv": {
760
- const c = new RotatingQuantizedKVCache(e.maxSize!, e.groupSize!, e.bits!);
761
- c.restoreState(triple(t, 0), triple(t, 3), e.offset, e.idx!);
762
- caches.push(c);
763
- break;
764
- }
765
- case "ssm": {
766
- const c = new SSMCache();
767
- c.conv = arr(t[0]!);
768
- c.recurrent = arr(t[1]!);
769
- c.offset = e.offset;
770
- caches.push(c);
771
- break;
772
- }
773
- case "turboquant": {
774
- const c = new TurboQuantKVCache(e.kBits!, e.vBits!);
775
- const kv: TurboQuantTensor = {
776
- kIdx: arr(t[0]!), kScales: arr(t[1]!), kZeros: arr(t[2]!),
777
- vPacked: arr(t[3]!), vScales: arr(t[4]!),
778
- };
779
- c.restoreState(kv, e.offset, e.headDim!);
780
- caches.push(c);
781
- break;
782
- }
783
- case "mla":
784
- case "mla-dsa":
785
- case "mtp-mla": {
786
- const hasDsa = e.kind === "mla-dsa";
787
- if (e.kind === "mtp-mla" && e.dsaHeadDim !== undefined)
788
- throw new Error(`${path}: native MTP cache cannot contain DSA state`);
789
- const c = new MLACache({
790
- kvLoraRank: e.kvLoraRank!,
791
- ropeHeadDim: e.ropeHeadDim!,
792
- ...(hasDsa ? { dsa: { headDim: e.dsaHeadDim! } } : {}),
793
- maxTokens: e.maxTokens,
794
- role: e.kind === "mtp-mla" ? "mtp" : "target",
795
- });
796
- c.restoreCompressedState(
797
- arr(t[0]!),
798
- arr(t[1]!),
799
- hasDsa ? arr(t[2]!) : null,
800
- e.offset,
801
- );
802
- caches.push(c);
803
- break;
804
- }
805
- default:
806
- throw new Error(`${path}: unknown cache kind ${(e as { kind: string }).kind}`);
807
- }
1031
+ const codec = codecs.forHeader(e);
1032
+ caches.push(codec.load(e, { path, arr, grownArr, triple }));
808
1033
  // This entry's tensors are now owned by its cache — stop tracking them
809
1034
  // (the catch must not double-free through both pending AND caches).
810
1035
  pending.length = 0;