@mlx-bun/inference 0.0.0-stage → 1.0.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 (467) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1475 -2
  3. package/THIRD_PARTY_NOTICES.md +3404 -0
  4. package/dist/native/libmlx_bun_ane.dylib +0 -0
  5. package/dist/native/libmlx_bun_expert_io.dylib +0 -0
  6. package/dist/native/mlx-bun-frame-extract +0 -0
  7. package/examples/fit-model.ts +25 -0
  8. package/examples/qwen3-forward.ts +42 -0
  9. package/examples/qwen3-generate.ts +28 -0
  10. package/examples/trellis-expand.ts +23 -0
  11. package/package.json +129 -4
  12. package/src/adapters/available.ts +110 -0
  13. package/src/adapters/index.ts +3 -0
  14. package/src/adapters/manager.ts +316 -0
  15. package/src/artifacts/auxiliary-files.ts +45 -0
  16. package/src/artifacts/config-dialects.ts +94 -0
  17. package/src/artifacts/config.ts +341 -0
  18. package/src/artifacts/draft-vocabulary.ts +30 -0
  19. package/src/artifacts/expert-offload-build.ts +75 -0
  20. package/src/artifacts/expert-offload.ts +89 -0
  21. package/src/artifacts/experts/index.ts +3 -0
  22. package/src/artifacts/experts/io.ts +386 -0
  23. package/src/artifacts/experts/residency.ts +1134 -0
  24. package/src/artifacts/experts/usage.ts +415 -0
  25. package/src/artifacts/fingerprint.ts +44 -0
  26. package/src/artifacts/glm52/container.ts +305 -0
  27. package/src/artifacts/glm52/expert-layout.ts +249 -0
  28. package/src/artifacts/glm52/index.ts +7 -0
  29. package/src/artifacts/glm52/kernel-layout.ts +124 -0
  30. package/src/artifacts/glm52/layout.ts +152 -0
  31. package/src/artifacts/glm52/memory.ts +500 -0
  32. package/src/artifacts/glm52/resident-weights.ts +135 -0
  33. package/src/artifacts/glm52/weights.ts +503 -0
  34. package/src/artifacts/glm52-config.ts +262 -0
  35. package/src/artifacts/identity.ts +94 -0
  36. package/src/artifacts/index.ts +15 -0
  37. package/src/artifacts/mmap.ts +90 -0
  38. package/src/artifacts/safetensors-writer.ts +249 -0
  39. package/src/artifacts/safetensors.ts +164 -0
  40. package/src/artifacts/turboquant-fold.ts +42 -0
  41. package/src/artifacts/weight-audit.ts +43 -0
  42. package/src/artifacts/weights.ts +255 -0
  43. package/src/contracts/index.ts +2 -0
  44. package/src/contracts/mlx/attention.ts +8 -0
  45. package/src/contracts/mlx/cache.ts +510 -0
  46. package/src/contracts/mlx/checkpoint.ts +10 -0
  47. package/src/contracts/mlx/draft-checkpoint.ts +14 -0
  48. package/src/contracts/mlx/draft-projection.ts +27 -0
  49. package/src/contracts/mlx/draft-target.ts +84 -0
  50. package/src/contracts/mlx/drafter.ts +176 -0
  51. package/src/contracts/mlx/forward-work.ts +23 -0
  52. package/src/contracts/mlx/glm52-weights.ts +25 -0
  53. package/src/contracts/mlx/graph.ts +309 -0
  54. package/src/contracts/mlx/head.ts +6 -0
  55. package/src/contracts/mlx/index.ts +15 -0
  56. package/src/contracts/mlx/kv-codec.ts +9 -0
  57. package/src/contracts/mlx/media.ts +113 -0
  58. package/src/contracts/mlx/positions.ts +16 -0
  59. package/src/contracts/mlx/row-table.ts +2 -0
  60. package/src/contracts/mlx/trainable.ts +103 -0
  61. package/src/contracts/mlx/whisper.ts +96 -0
  62. package/src/contracts/portable/composition.ts +106 -0
  63. package/src/contracts/portable/denoising.ts +17 -0
  64. package/src/contracts/portable/execution.ts +79 -0
  65. package/src/contracts/portable/generation.ts +103 -0
  66. package/src/contracts/portable/graph.ts +80 -0
  67. package/src/contracts/portable/index.ts +15 -0
  68. package/src/contracts/portable/memory-plan.ts +26 -0
  69. package/src/contracts/portable/object-cache.ts +8 -0
  70. package/src/contracts/portable/prefill.ts +63 -0
  71. package/src/contracts/portable/prefix-cache.ts +33 -0
  72. package/src/contracts/portable/prompt.ts +2 -0
  73. package/src/contracts/portable/resources.ts +25 -0
  74. package/src/contracts/portable/rollback.ts +7 -0
  75. package/src/contracts/portable/sampling.ts +44 -0
  76. package/src/contracts/portable/scheduling.ts +54 -0
  77. package/src/contracts/portable/token-delivery.ts +4 -0
  78. package/src/contracts/portable/tsconfig.json +14 -0
  79. package/src/embeddings/index.ts +1 -0
  80. package/src/embeddings/text.ts +85 -0
  81. package/src/execution/admission.ts +59 -0
  82. package/src/execution/batch-group.ts +1240 -0
  83. package/src/execution/batch-types.ts +225 -0
  84. package/src/execution/cancellation.ts +38 -0
  85. package/src/execution/continuation-request.ts +45 -0
  86. package/src/execution/continuation-types.ts +10 -0
  87. package/src/execution/coordinator.ts +70 -0
  88. package/src/execution/denoising-group.ts +180 -0
  89. package/src/execution/engine.ts +56 -0
  90. package/src/execution/fill-group.ts +505 -0
  91. package/src/execution/fit.ts +184 -0
  92. package/src/execution/gateway-binding.ts +178 -0
  93. package/src/execution/grammar-group.ts +224 -0
  94. package/src/execution/index.ts +18 -0
  95. package/src/execution/methods.ts +75 -0
  96. package/src/execution/mixed-iteration.ts +96 -0
  97. package/src/execution/plan.ts +65 -0
  98. package/src/execution/prefill-cohort.ts +100 -0
  99. package/src/execution/prefill-rows.ts +263 -0
  100. package/src/execution/request-policy.ts +19 -0
  101. package/src/execution/row-sampling.ts +49 -0
  102. package/src/execution/scheduler.ts +92 -0
  103. package/src/execution/session.ts +247 -0
  104. package/src/execution/speculative-group.ts +797 -0
  105. package/src/execution/tasks.ts +44 -0
  106. package/src/generation/autoregressive.ts +840 -0
  107. package/src/generation/bindings/autoregressive.ts +104 -0
  108. package/src/generation/bindings/denoising.ts +60 -0
  109. package/src/generation/bindings/prefill-policy.ts +36 -0
  110. package/src/generation/checkpoint-identity.ts +60 -0
  111. package/src/generation/denoising.ts +85 -0
  112. package/src/generation/diffusion.ts +583 -0
  113. package/src/generation/fill/echo-index.ts +188 -0
  114. package/src/generation/fill/index.ts +3 -0
  115. package/src/generation/fill/proposal.ts +69 -0
  116. package/src/generation/fill/schema-rows.ts +506 -0
  117. package/src/generation/fill/session.ts +464 -0
  118. package/src/generation/fill/tool-boundary.ts +103 -0
  119. package/src/generation/fill-append.ts +58 -0
  120. package/src/generation/generate.ts +44 -0
  121. package/src/generation/grammar-step.ts +39 -0
  122. package/src/generation/index.ts +14 -0
  123. package/src/generation/phase-dispatch.ts +115 -0
  124. package/src/generation/prefill.ts +33 -0
  125. package/src/generation/result.ts +49 -0
  126. package/src/generation/scopes.ts +157 -0
  127. package/src/generation/speculative/acceptance.ts +41 -0
  128. package/src/generation/speculative/adaptive-draft-gate.ts +166 -0
  129. package/src/generation/speculative/bindings/assistant-rows.ts +102 -0
  130. package/src/generation/speculative/bindings/autoregressive-rows.ts +124 -0
  131. package/src/generation/speculative/bindings/binding.ts +106 -0
  132. package/src/generation/speculative/bindings/deepspec-rows.ts +33 -0
  133. package/src/generation/speculative/bindings/draft-target.ts +10 -0
  134. package/src/generation/speculative/bindings/glm52-mtp-rows.ts +128 -0
  135. package/src/generation/speculative/bindings/projected-context-rows.ts +78 -0
  136. package/src/generation/speculative/bindings/projected-draft-rows.ts +95 -0
  137. package/src/generation/speculative/bindings/qwen-mtp-rows.ts +336 -0
  138. package/src/generation/speculative/bindings/two-model-rows.ts +63 -0
  139. package/src/generation/speculative/cache-identity.ts +16 -0
  140. package/src/generation/speculative/draft-kind.ts +149 -0
  141. package/src/generation/speculative/draft-policy.ts +41 -0
  142. package/src/generation/speculative/draft-registry.ts +118 -0
  143. package/src/generation/speculative/draft-vocab.ts +110 -0
  144. package/src/generation/speculative/index.ts +17 -0
  145. package/src/generation/speculative/output.ts +49 -0
  146. package/src/generation/speculative/round.ts +307 -0
  147. package/src/generation/speculative/source.ts +144 -0
  148. package/src/generation/speculative/sources/assistant-source.ts +64 -0
  149. package/src/generation/speculative/sources/deepspec-source.ts +70 -0
  150. package/src/generation/speculative/sources/dflash-source.ts +84 -0
  151. package/src/generation/speculative/sources/dflash2-source.ts +86 -0
  152. package/src/generation/speculative/sources/glm52-mtp-source.ts +66 -0
  153. package/src/generation/speculative/sources/ngram-source.ts +165 -0
  154. package/src/generation/speculative/sources/qwen-mtp-source.ts +154 -0
  155. package/src/generation/speculative/sources/two-model.ts +75 -0
  156. package/src/generation/types.ts +194 -0
  157. package/src/index.ts +20 -0
  158. package/src/input/audio/audiotoolbox.ts +112 -0
  159. package/src/input/audio/decode.ts +133 -0
  160. package/src/input/audio/features.ts +202 -0
  161. package/src/input/audio/index.ts +6 -0
  162. package/src/input/audio/transcode.ts +67 -0
  163. package/src/input/audio/whisper-mel.ts +185 -0
  164. package/src/input/audio/whisper-tokenizer.ts +238 -0
  165. package/src/input/chat-template.ts +389 -0
  166. package/src/input/index.ts +6 -0
  167. package/src/input/media-fetch.ts +439 -0
  168. package/src/input/packed-mixed.ts +43 -0
  169. package/src/input/special-tokens.ts +75 -0
  170. package/src/input/token-groups.ts +49 -0
  171. package/src/input/tokenizer.ts +336 -0
  172. package/src/input/tool-call.ts +689 -0
  173. package/src/input/vision/index.ts +3 -0
  174. package/src/input/vision/preprocess.ts +247 -0
  175. package/src/input/vision/prompt.ts +398 -0
  176. package/src/input/vision/qwen3vl-preprocess.ts +409 -0
  177. package/src/input/vision/qwen3vl-prompt.ts +237 -0
  178. package/src/input/vision/video-frames.ts +100 -0
  179. package/src/kernels/ane/fill.ts +101 -0
  180. package/src/kernels/ane/linear.ts +416 -0
  181. package/src/kernels/attention/flash.ts +694 -0
  182. package/src/kernels/attention/kvq4-decode.ts +155 -0
  183. package/src/kernels/attention/masks.ts +40 -0
  184. package/src/kernels/attention/multi-query.ts +193 -0
  185. package/src/kernels/attention/paged.ts +130 -0
  186. package/src/kernels/delta/gated.ts +306 -0
  187. package/src/kernels/delta/index.ts +3 -0
  188. package/src/kernels/delta/silu-rms-heads.ts +69 -0
  189. package/src/kernels/delta/state.ts +74 -0
  190. package/src/kernels/gated-rms-norm.ts +64 -0
  191. package/src/kernels/glm52/layout.ts +37 -0
  192. package/src/kernels/glm52/streamed-experts.ts +360 -0
  193. package/src/kernels/logits.ts +38 -0
  194. package/src/kernels/quantization/affine-row.ts +154 -0
  195. package/src/kernels/quantization/affine-rows.ts +36 -0
  196. package/src/kernels/quantization/affine-split-mma.ts +144 -0
  197. package/src/kernels/quantization/affine3-mma.ts +117 -0
  198. package/src/kernels/quantization/affine3-rows.ts +103 -0
  199. package/src/kernels/quantization/affine4-mma.ts +113 -0
  200. package/src/kernels/quantization/affine4-rows.ts +102 -0
  201. package/src/kernels/residual-rms-norm.ts +65 -0
  202. package/src/kernels/sampling/index.ts +2 -0
  203. package/src/kernels/sampling/normalized-argmax.ts +116 -0
  204. package/src/kernels/sampling/token-bitmask.ts +52 -0
  205. package/src/kernels/softcap.ts +35 -0
  206. package/src/kernels/trellis/codebook.ts +123 -0
  207. package/src/kernels/trellis/down-codebook.ts +184 -0
  208. package/src/kernels/trellis/down-factored.ts +180 -0
  209. package/src/kernels/trellis/down-k3-interleaved-factored.ts +146 -0
  210. package/src/kernels/trellis/down-mma-wide.ts +195 -0
  211. package/src/kernels/trellis/down-mma.ts +145 -0
  212. package/src/kernels/trellis/expand.ts +42 -0
  213. package/src/kernels/trellis/gate-up-codebook-mma.ts +141 -0
  214. package/src/kernels/trellis/gate-up-codebook.ts +137 -0
  215. package/src/kernels/trellis/gate-up-factored.ts +197 -0
  216. package/src/kernels/trellis/gate-up-mma-wide.ts +147 -0
  217. package/src/kernels/trellis/gate-up-mma.ts +148 -0
  218. package/src/kernels/trellis/gate-up.ts +248 -0
  219. package/src/kernels/trellis/geometry.ts +22 -0
  220. package/src/kernels/trellis/index.ts +103 -0
  221. package/src/kernels/trellis/launch.ts +12 -0
  222. package/src/kernels/trellis/mixed-gate-up-factored.ts +119 -0
  223. package/src/kernels/trellis/mixed-gate-up.ts +146 -0
  224. package/src/kernels/trellis/qualified.ts +34 -0
  225. package/src/kernels/trellis/reduce.ts +150 -0
  226. package/src/kernels/trellis/scatter.ts +407 -0
  227. package/src/kernels/trellis/splitk-prefill.ts +152 -0
  228. package/src/kernels/trellis/tiled-prefill.ts +131 -0
  229. package/src/kernels/trellis/vector-expand.ts +58 -0
  230. package/src/kernels/trellis/wide-prefill.ts +89 -0
  231. package/src/kernels/turboquant/decode.ts +82 -0
  232. package/src/kernels/turboquant/index.ts +5 -0
  233. package/src/kernels/turboquant/inverse.ts +122 -0
  234. package/src/kernels/turboquant/ops.ts +860 -0
  235. package/src/kernels/turboquant/signs.ts +130 -0
  236. package/src/kernels/turboquant/tables.ts +124 -0
  237. package/src/layers/affine-verify-linear.ts +75 -0
  238. package/src/layers/ane-prefill-split.ts +283 -0
  239. package/src/layers/compiled-normalization.ts +41 -0
  240. package/src/layers/delta-heads.ts +23 -0
  241. package/src/layers/dense-embedding.ts +29 -0
  242. package/src/layers/dense-linear.ts +32 -0
  243. package/src/layers/dequantized-linear.ts +72 -0
  244. package/src/layers/folded-attention.ts +42 -0
  245. package/src/layers/geglu.ts +130 -0
  246. package/src/layers/helpers.ts +7 -0
  247. package/src/layers/index.ts +36 -0
  248. package/src/layers/kv4-decode-attention.ts +15 -0
  249. package/src/layers/layer-norm.ts +24 -0
  250. package/src/layers/loading.ts +92 -0
  251. package/src/layers/lora.ts +150 -0
  252. package/src/layers/normalization.ts +9 -0
  253. package/src/layers/prefix-layout.ts +33 -0
  254. package/src/layers/quantized-append-attention.ts +32 -0
  255. package/src/layers/quantized-attention.ts +353 -0
  256. package/src/layers/quantized-embedding.ts +39 -0
  257. package/src/layers/quantized-linear.ts +137 -0
  258. package/src/layers/quantized-switch-linear.ts +37 -0
  259. package/src/layers/qwen-append.ts +12 -0
  260. package/src/layers/qwen-mrope.ts +238 -0
  261. package/src/layers/rope-2d.ts +110 -0
  262. package/src/layers/rope.ts +389 -0
  263. package/src/layers/swiglu.ts +23 -0
  264. package/src/layers/trellis-codebook-down.ts +52 -0
  265. package/src/layers/trellis-codebook-gate-up.ts +42 -0
  266. package/src/layers/trellis-down.ts +117 -0
  267. package/src/layers/trellis-gate-up.ts +71 -0
  268. package/src/layers/trellis-linear.ts +211 -0
  269. package/src/layers/trellis-prompt.ts +102 -0
  270. package/src/layers/trellis-reduce-gate-up.ts +73 -0
  271. package/src/layers/trellis-scatter-down.ts +98 -0
  272. package/src/models/ane-install.ts +74 -0
  273. package/src/models/audio/conformer.ts +656 -0
  274. package/src/models/cache-composition.ts +44 -0
  275. package/src/models/capabilities.ts +89 -0
  276. package/src/models/chat-template.ts +10 -0
  277. package/src/models/composition.ts +107 -0
  278. package/src/models/diffusion-gemma/experts.ts +59 -0
  279. package/src/models/diffusion-gemma/family.ts +21 -0
  280. package/src/models/diffusion-gemma/model.ts +900 -0
  281. package/src/models/diffusion-gemma/vision.ts +549 -0
  282. package/src/models/drafter-loaders.ts +49 -0
  283. package/src/models/drafters.ts +39 -0
  284. package/src/models/factory.ts +351 -0
  285. package/src/models/families.ts +70 -0
  286. package/src/models/family.ts +125 -0
  287. package/src/models/gemma4/assistant-target.ts +46 -0
  288. package/src/models/gemma4/assistant.ts +327 -0
  289. package/src/models/gemma4/blocks.ts +755 -0
  290. package/src/models/gemma4/compiled-decode.ts +452 -0
  291. package/src/models/gemma4/family.ts +39 -0
  292. package/src/models/gemma4/gemma4-12b.ts +677 -0
  293. package/src/models/gemma4/gemma4-26b.ts +544 -0
  294. package/src/models/gemma4/gemma4-e4b.ts +599 -0
  295. package/src/models/gemma4/graphs.ts +147 -0
  296. package/src/models/gemma4/media.ts +88 -0
  297. package/src/models/gemma4/model.ts +860 -0
  298. package/src/models/gemma4/serving-blocks.ts +72 -0
  299. package/src/models/gemma4/trainable.ts +186 -0
  300. package/src/models/gemma4/written-blocks.ts +232 -0
  301. package/src/models/gemma4/written-forward.ts +159 -0
  302. package/src/models/gemma4/written-input.ts +29 -0
  303. package/src/models/glm52/atlas.ts +507 -0
  304. package/src/models/glm52/coupling.ts +388 -0
  305. package/src/models/glm52/dsa.ts +601 -0
  306. package/src/models/glm52/family.ts +20 -0
  307. package/src/models/glm52/mla.ts +812 -0
  308. package/src/models/glm52/model.ts +1127 -0
  309. package/src/models/glm52/moe.ts +223 -0
  310. package/src/models/glm52/mtp.ts +60 -0
  311. package/src/models/glm52/pilot.ts +537 -0
  312. package/src/models/glm52/reference.ts +445 -0
  313. package/src/models/glm52/residency.ts +557 -0
  314. package/src/models/glm52/streamed-experts.ts +447 -0
  315. package/src/models/graph.ts +59 -0
  316. package/src/models/implementation.ts +230 -0
  317. package/src/models/index.ts +15 -0
  318. package/src/models/media-input.ts +15 -0
  319. package/src/models/memory-plan.ts +39 -0
  320. package/src/models/minicpm5/family.ts +29 -0
  321. package/src/models/minicpm5/model.ts +394 -0
  322. package/src/models/minicpm5/trainable.ts +90 -0
  323. package/src/models/profile.ts +337 -0
  324. package/src/models/qwen/blocks.ts +658 -0
  325. package/src/models/qwen/checkpoint.ts +190 -0
  326. package/src/models/qwen/families.ts +57 -0
  327. package/src/models/qwen/media-input.ts +161 -0
  328. package/src/models/qwen/mtp.ts +101 -0
  329. package/src/models/qwen/qwen3-moe.ts +355 -0
  330. package/src/models/qwen/qwen3.ts +323 -0
  331. package/src/models/qwen/qwen38-27b-trellis-m1max.ts +1532 -0
  332. package/src/models/qwen/qwen38-27b-trellis-m4pro.ts +1092 -0
  333. package/src/models/qwen/qwen3_5.ts +433 -0
  334. package/src/models/qwen/written-layers.ts +285 -0
  335. package/src/models/row-table.ts +1 -0
  336. package/src/models/runtime.ts +73 -0
  337. package/src/models/speculative/deepspec-artifact.ts +10 -0
  338. package/src/models/speculative/deepspec.ts +744 -0
  339. package/src/models/speculative/dflash.ts +553 -0
  340. package/src/models/speculative/dflash2-artifact.ts +9 -0
  341. package/src/models/speculative/dflash2-operations.ts +1 -0
  342. package/src/models/speculative/dflash2.ts +576 -0
  343. package/src/models/speculative/loader.ts +29 -0
  344. package/src/models/support.ts +79 -0
  345. package/src/models/universal/archs.ts +365 -0
  346. package/src/models/universal/blocks.ts +479 -0
  347. package/src/models/universal/dense.ts +255 -0
  348. package/src/models/universal/family.ts +17 -0
  349. package/src/models/vision/qwen3vl.ts +566 -0
  350. package/src/models/vision/siglip.ts +520 -0
  351. package/src/models/vision/unified.ts +176 -0
  352. package/src/models/whisper/family.ts +19 -0
  353. package/src/models/whisper/fast.ts +734 -0
  354. package/src/models/whisper/model.ts +430 -0
  355. package/src/runtime/compiled-trace.ts +16 -0
  356. package/src/runtime/config.ts +77 -0
  357. package/src/runtime/expert-trace.ts +71 -0
  358. package/src/runtime/graph-forward.ts +65 -0
  359. package/src/runtime/integers.ts +6 -0
  360. package/src/runtime/native.ts +16 -0
  361. package/src/runtime/resources.ts +53 -0
  362. package/src/runtime/row-operations.ts +42 -0
  363. package/src/runtime/trace.ts +161 -0
  364. package/src/sampling/curve.ts +122 -0
  365. package/src/sampling/draft.ts +55 -0
  366. package/src/sampling/extras.ts +45 -0
  367. package/src/sampling/filters.ts +134 -0
  368. package/src/sampling/grammar.ts +630 -0
  369. package/src/sampling/hlg.ts +290 -0
  370. package/src/sampling/index.ts +9 -0
  371. package/src/sampling/processors.ts +142 -0
  372. package/src/sampling/sampler.ts +120 -0
  373. package/src/sampling/step.ts +343 -0
  374. package/src/sampling/types.ts +161 -0
  375. package/src/sampling/window-filters.ts +46 -0
  376. package/src/scoring/decisions.ts +96 -0
  377. package/src/scoring/full-sequence.ts +103 -0
  378. package/src/scoring/index.ts +9 -0
  379. package/src/scoring/kl.ts +29 -0
  380. package/src/scoring/perplexity.ts +165 -0
  381. package/src/scoring/response-logps.ts +65 -0
  382. package/src/state/affine-attention.ts +180 -0
  383. package/src/state/attachment.ts +68 -0
  384. package/src/state/attention-read.ts +215 -0
  385. package/src/state/batched-kv.ts +294 -0
  386. package/src/state/batched-mask.ts +445 -0
  387. package/src/state/batched-quant.ts +389 -0
  388. package/src/state/batched-quantized-kv.ts +209 -0
  389. package/src/state/batched-rotating-quant.ts +457 -0
  390. package/src/state/batched-rotating-state.ts +149 -0
  391. package/src/state/batched-rotating.ts +430 -0
  392. package/src/state/batched-row-storage.ts +179 -0
  393. package/src/state/batched-ssm.ts +54 -0
  394. package/src/state/batched-turboquant-kv.ts +144 -0
  395. package/src/state/bf16-first-kv.ts +663 -0
  396. package/src/state/cache-service.ts +592 -0
  397. package/src/state/capabilities.ts +65 -0
  398. package/src/state/checkpoint.ts +46 -0
  399. package/src/state/continuation-persistence.ts +126 -0
  400. package/src/state/decode-step-inputs.ts +31 -0
  401. package/src/state/decoded-kv-donor.ts +12 -0
  402. package/src/state/delayed-quantized-kv.ts +141 -0
  403. package/src/state/delayed-rotating-quantized-kv.ts +178 -0
  404. package/src/state/delayed-turboquant-kv.ts +123 -0
  405. package/src/state/durability.ts +214 -0
  406. package/src/state/encoder-cache.ts +26 -0
  407. package/src/state/full-kv-row-append.ts +33 -0
  408. package/src/state/full-kv-row-donor.ts +54 -0
  409. package/src/state/full-prefill-padding.ts +56 -0
  410. package/src/state/full-prefill-row.ts +112 -0
  411. package/src/state/full-transitioning-kv-rows.ts +70 -0
  412. package/src/state/glm52-cache.ts +783 -0
  413. package/src/state/grouped-heads-kv4-cache.ts +26 -0
  414. package/src/state/index.ts +38 -0
  415. package/src/state/kv-attention-view.ts +77 -0
  416. package/src/state/kv-budget.ts +14 -0
  417. package/src/state/kv-scheme.ts +315 -0
  418. package/src/state/kv-storage.ts +57 -0
  419. package/src/state/kv-tensor-rows.ts +212 -0
  420. package/src/state/kv.ts +324 -0
  421. package/src/state/kv4-head256-cache.ts +46 -0
  422. package/src/state/latent-legos.ts +895 -0
  423. package/src/state/layout.ts +277 -0
  424. package/src/state/leases.ts +26 -0
  425. package/src/state/paged/cache.ts +689 -0
  426. package/src/state/paged/codec.ts +36 -0
  427. package/src/state/paged/rows.ts +223 -0
  428. package/src/state/persistence-types.ts +173 -0
  429. package/src/state/persistence-worker.ts +83 -0
  430. package/src/state/persistence.ts +1036 -0
  431. package/src/state/persistence.worker.d.ts +3 -0
  432. package/src/state/persistence.worker.js +264 -0
  433. package/src/state/prefix-cache.ts +685 -0
  434. package/src/state/quantized-kv.ts +369 -0
  435. package/src/state/quantized-tensor.ts +26 -0
  436. package/src/state/request-policy.ts +48 -0
  437. package/src/state/retention-policy.ts +37 -0
  438. package/src/state/rollback.ts +131 -0
  439. package/src/state/rotating-kv-donor.ts +35 -0
  440. package/src/state/rotating-kv-layout.ts +341 -0
  441. package/src/state/rotating-kv.ts +529 -0
  442. package/src/state/rotating-quantized-kv.ts +616 -0
  443. package/src/state/rotating-row-transaction.ts +35 -0
  444. package/src/state/rows.ts +104 -0
  445. package/src/state/softcap-attention.ts +118 -0
  446. package/src/state/speculative/glm52-mtp-state.ts +38 -0
  447. package/src/state/speculative/qwen-mtp-state.ts +34 -0
  448. package/src/state/speculative-rotating-kv.ts +113 -0
  449. package/src/state/ssd-cache.ts +434 -0
  450. package/src/state/ssm-prefill-padding.ts +39 -0
  451. package/src/state/ssm.ts +422 -0
  452. package/src/state/target-layout.ts +7 -0
  453. package/src/state/tiered-prefix-cache.ts +45 -0
  454. package/src/state/training-cache.ts +300 -0
  455. package/src/state/transitioning-kv-rows.ts +267 -0
  456. package/src/state/turboquant-attention.ts +97 -0
  457. package/src/state/turboquant-codec.ts +90 -0
  458. package/src/state/turboquant-kv.ts +442 -0
  459. package/src/state/views.ts +26 -0
  460. package/src/transcription/index.ts +3 -0
  461. package/src/transcription/silero-vad.ts +379 -0
  462. package/src/transcription/whisper/decode.ts +773 -0
  463. package/src/transcription/whisper/format.ts +74 -0
  464. package/src/transcription/whisper/index.ts +4 -0
  465. package/src/transcription/whisper/timing.ts +274 -0
  466. package/src/transcription/whisper/transcribe.ts +394 -0
  467. package/src/transcription/whisper/types.ts +22 -0
@@ -0,0 +1,1036 @@
1
+ import { type CheckpointAttachment } from "../contracts/mlx/checkpoint";
2
+ import { cleanupFailure,disposeResources } from "../runtime/resources";
3
+ import { attachmentBytes,disposeAttachments } from "./checkpoint";
4
+ // KV-cache persistence: save prompt caches to disk, reload by streamed copy.
5
+ // The serialization core of the SSD cold tier (`02d723a:docs/design/kv-cache.md` §5).
6
+ //
7
+ // File layout (every tensor PAGE-ALIGNED — files
8
+ // we write can be mmap'd and handed to the GPU without copies):
9
+ // [magic "MLXBUNKV2\n"][u32 LE header length][u32 LE dataStart]
10
+ // [u64 LE header hash][JSON header][padding]
11
+ // [tensor data at 16 KiB-aligned offsets]
12
+ // Header: { formatVersion, modelId, configFingerprint, ns, tokenizerHash,
13
+ // createdAt, tokens, caches: [{ kind, offset, idx?, maxSize?,
14
+ // groupSize?, bits?, kBits?, vBits?, headDim?,
15
+ // tensors: [{ off, bytes, shape, dtype, hash }] }] }
16
+ //
17
+ // v2 over v1: quantized cache kinds (kv_config quantization — v1 could not
18
+ // persist it; NOTE bf16 is the serving default since the 2026-07-05 L1
19
+ // decision, quantized KV is opt-in), SSM kind (Qwen3.5 hybrid),
20
+ // invalidation metadata (configFingerprint covers the kv-quant scheme;
21
+ // tokenizerHash guards vocab drift; ns = adapter spec), per-tensor hashes
22
+ // (verified opt-in — the hash pass roughly doubles restore reads),
23
+ // a header hash (always verified — cheap), and atomic tmp+fsync+rename
24
+ // writes. v1 files are not migrated: nothing shipped wrote them (the
25
+ // format was test/experiment-only) — they read as "not a v2 file" and the
26
+ // SSD tier deletes-and-regenerates.
27
+ //
28
+ // v3 over v2: fixed-width (zero-padded 16-hex) tensor hashes, which let the
29
+ // writer STREAM — header sized up front, tensors materialized/hashed/written
30
+ // one at a time, header patched in last (peak host transient = one tensor,
31
+ // not the whole entry). Same magic; v2 files read as unsupported and the
32
+ // SSD tier deletes-and-regenerates (machine-local, cheap).
33
+ //
34
+ // Reload (2026-07-07, A7-restore) is a STREAMED COPY — the read-side twin
35
+ // of the v3 streaming writer: each tensor is copied out of a read-only mmap
36
+ // into an mlx-owned leaf (MlxArray.fromBytesCopy), its clean file pages
37
+ // dropped (MADV_DONTNEED) right after, and the mapping unmapped before
38
+ // loadKvCache returns. Peak host transient = the live entry + one tensor,
39
+ // and NOTHING outlives the call: the old zero-copy wrap (COW mmap +
40
+ // fromPointer) required pinning every restore mapping for the PROCESS
41
+ // lifetime once the FFI-dtor fix removed the unmap signal (2026-07-06) —
42
+ // one leaked full-entry mapping per restore — and the exactly-offset-sized
43
+ // restored buffers made the first post-restore decode step concat-copy the
44
+ // whole entry again.
45
+
46
+ import { MlxArray } from "@mlx-bun/mlx/array";
47
+ import type { Dtype } from "@mlx-bun/mlx/ffi";
48
+ import { HostBuffer } from "@mlx-bun/mlx/host-buffer";
49
+ import * as ops from "@mlx-bun/mlx/ops";
50
+ import { createHash } from "node:crypto";
51
+ import { closeSync,fsyncSync,openSync,readFileSync,readSync,renameSync,rmSync,writeSync } from "node:fs";
52
+ import { dirname,join } from "node:path";
53
+ import { MADV_DONTNEED,MmapFile } from "../artifacts/mmap";
54
+ import { type Cache } from "../contracts/mlx/cache";
55
+ import { cacheSignature } from "./capabilities";
56
+ import { MLACache } from "./glm52-cache";
57
+ import { KVCache } from "./kv";
58
+ import { pagedCacheCodec } from "./paged/codec";
59
+ import { kvReader,kvWriter,type KvWriteRequest } from "./persistence-worker";
60
+ import { QuantizedKVCache } from "./quantized-kv";
61
+ import type { AffineKernelSet } from "./affine-attention";
62
+ import { RotatingKVCache } from "./rotating-kv";
63
+ import { RotatingQuantizedKVCache } from "./rotating-quantized-kv";
64
+ import { SSMCache } from "./ssm";
65
+ import { type TurboQuantTensor } from "./turboquant-codec";
66
+ import { TurboQuantKVCache } from "./turboquant-kv";
67
+
68
+ const MAGIC = "MLXBUNKV2\n";
69
+ const ALIGN = 16384;
70
+ /** magic + u32 headerLen + u32 dataStart + u64 headerHash */
71
+ const PREFIX_LEN = MAGIC.length + 4 + 4 + 8;
72
+
73
+ // Fixed-width (16 hex chars): the v3 streaming writer sizes the header
74
+ // BEFORE materializing any tensor bytes, so hash strings must not change
75
+ // the header's byte length when the real values replace the placeholders.
76
+ const hash64 = (bytes: Uint8Array): string => Bun.hash(bytes).toString(16).padStart(16, "0");
77
+
78
+ const alignUp = (n: number) => Math.ceil(n / ALIGN) * ALIGN;
79
+
80
+ /** Snapshot one cache into header entry + LAZY tensor sources (v3 streaming
81
+ * writer: sizes/shapes are metadata, bytes materialize ONE tensor at a
82
+ * time at write — the old all-blobs-up-front path doubled RSS by the whole
83
+ * entry, ~390 MB for a 16k cpm5 entry, on every write-behind). Quantized
84
+ * full caches are sliced to the live [.., :offset, :] region (token axis —
85
+ * quantization groups run along the FEATURE dim, so a sequence-axis cut
86
+ * never splits a group); rotating caches persist the whole ring
87
+ * as-laid-out with ringIdx. Slice handles are lazy graph nodes (no GPU
88
+ * materialization until rawBytes at write time). */
89
+ interface TensorSource { arr: MlxArray; disposeAfter: boolean }
90
+
91
+ const requireMlaState = (cache: MLACache, operation: string): void => {
92
+ if (!cache.latent || !cache.rope || cache.batchSize === null || cache.offset === 0)
93
+ throw new Error(`cannot ${operation} an empty GLM compressed cache`);
94
+ if (cache.dsa && (!cache.dsa.data || cache.dsa.offset !== cache.offset))
95
+ throw new Error(`cannot ${operation} misaligned GLM DSA state`);
96
+ };
97
+
98
+ const snapshotMla = (
99
+ kind: Extract<CacheKind, "mla" | "mla-dsa" | "mtp-mla">,
100
+ cache: Cache,
101
+ context: SnapshotContext,
102
+ ): CacheHeaderEntry => {
103
+ const c = cache as MLACache;
104
+ requireMlaState(c, "persist");
105
+ context.push(context.liveMlaSlice(c.latent!, c.offset), true);
106
+ context.push(context.liveMlaSlice(c.rope!, c.offset), true);
107
+ if (c.dsa) context.push(context.liveMlaSlice(c.dsa.data!, c.offset), true);
108
+ return {
109
+ kind,
110
+ offset: c.offset,
111
+ kvLoraRank: c.kvLoraRank,
112
+ ropeHeadDim: c.ropeHeadDim,
113
+ ...(c.dsa ? { dsaHeadDim: c.dsa.headDim } : {}),
114
+ maxTokens: c.maxTokens,
115
+ tensors: context.slots,
116
+ };
117
+ };
118
+
119
+ const cloneMla = (cache: Cache, context: CloneContext): Cache => {
120
+ const c = cache as MLACache;
121
+ requireMlaState(c, "clone");
122
+ const clone = new MLACache({
123
+ kvLoraRank: c.kvLoraRank,
124
+ ropeHeadDim: c.ropeHeadDim,
125
+ ...(c.dsa ? { dsa: { headDim: c.dsa.headDim } } : {}),
126
+ maxTokens: c.maxTokens,
127
+ role: c.role,
128
+ });
129
+ clone.restoreCompressedState(
130
+ context.mlaView(c.latent!, c.offset),
131
+ context.mlaView(c.rope!, c.offset),
132
+ c.dsa ? context.mlaView(c.dsa.data!, c.offset) : null,
133
+ c.offset,
134
+ );
135
+ return clone;
136
+ };
137
+
138
+ const loadMla = (entry: CacheHeaderEntry, context: LoadContext): Cache => {
139
+ const hasDsa = entry.kind === "mla-dsa";
140
+ if (entry.kind === "mtp-mla" && entry.dsaHeadDim !== undefined)
141
+ throw new Error(`${context.path}: native MTP cache cannot contain DSA state`);
142
+ const cache = new MLACache({
143
+ kvLoraRank: entry.kvLoraRank!,
144
+ ropeHeadDim: entry.ropeHeadDim!,
145
+ ...(hasDsa ? { dsa: { headDim: entry.dsaHeadDim! } } : {}),
146
+ maxTokens: entry.maxTokens,
147
+ role: entry.kind === "mtp-mla" ? "mtp" : "target",
148
+ });
149
+ cache.restoreCompressedState(
150
+ context.arr(entry.tensors[0]!),
151
+ context.arr(entry.tensors[1]!),
152
+ hasDsa ? context.arr(entry.tensors[2]!) : null,
153
+ entry.offset,
154
+ );
155
+ return cache;
156
+ };
157
+
158
+ /** The affine kernel set of the cache the restoring graph composed at an
159
+ * affine entry's layer (its `makeCache` prototype): a lego's, or affine
160
+ * storage's own kernels where the graph composes storage quantized from token
161
+ * zero. A prototype with neither is a file written under another KV scheme. */
162
+ const composedKernelSet = (context: LoadContext, entry: CacheHeaderEntry): AffineKernelSet => {
163
+ const kernelSet = (context.prototype as { readonly kernelSet?: AffineKernelSet }).kernelSet;
164
+ if (!kernelSet) throw new Error(`${context.path}: affine cache kind ${entry.kind} does not match the model's composed cache`);
165
+ return kernelSet;
166
+ };
167
+
168
+ /** One authoritative persistence algebra. Adding a cache storage kind must
169
+ * define all four operations here; save/clone/load and SSD eligibility
170
+ * therefore cannot grow independent instanceof/switch chains. */
171
+ const CACHE_CODECS = {
172
+ kv: {
173
+ matches: (cache) => cacheSignature(cache) === "kv:plain",
174
+ snapshot: (cache, context) => {
175
+ const c = cache as KVCache;
176
+ if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
177
+ context.push(context.liveSlice(c.keys, c.offset), true);
178
+ context.push(context.liveSlice(c.values, c.offset), true);
179
+ return { kind: "kv", offset: c.offset, tensors: context.slots };
180
+ },
181
+ clone: (cache, context) => {
182
+ const c = cache as KVCache;
183
+ const clone = new KVCache(undefined, c.turboQuantKernels, c.windowRead);
184
+ if (!c.keys || !c.values) return clone;
185
+ clone.restoreState(
186
+ context.liveView(c.keys, c.offset),
187
+ context.liveView(c.values, c.offset),
188
+ c.offset,
189
+ );
190
+ return clone;
191
+ },
192
+ load: (entry, context) => {
193
+ // A plain entry saved from the graph's KVCache at this layer: the restored
194
+ // cache carries the TurboQuant and window reads the graph composed into it.
195
+ const prototype = context.prototype as KVCache;
196
+ const cache = new KVCache(undefined, prototype.turboQuantKernels, prototype.windowRead);
197
+ cache.restoreState(
198
+ context.grownArr(entry.tensors[0]!, entry.offset),
199
+ context.grownArr(entry.tensors[1]!, entry.offset),
200
+ entry.offset,
201
+ );
202
+ return cache;
203
+ },
204
+ headerTrimmable: () => true,
205
+ },
206
+ rotating: {
207
+ matches: (cache) => cacheSignature(cache) === "kv:rotating-plain",
208
+ snapshot: (cache, context) => {
209
+ const c = cache as RotatingKVCache;
210
+ if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
211
+ context.push(c.keys, false);
212
+ context.push(c.values, false);
213
+ return { kind: "rotating", offset: c.offset, idx: c.ringIdx,
214
+ maxSize: c.maxSize, tensors: context.slots };
215
+ },
216
+ clone: (cache, context) => {
217
+ const c = cache as RotatingKVCache;
218
+ const clone = new RotatingKVCache(c.maxSize);
219
+ if (!c.keys || !c.values) return clone;
220
+ clone.restoreState(context.view(c.keys), context.view(c.values), c.offset, c.ringIdx);
221
+ return clone;
222
+ },
223
+ load: (entry, context) => {
224
+ const cache = new RotatingKVCache(entry.maxSize!);
225
+ cache.restoreState(
226
+ context.arr(entry.tensors[0]!), context.arr(entry.tensors[1]!),
227
+ entry.offset, entry.idx!,
228
+ );
229
+ return cache;
230
+ },
231
+ headerTrimmable: (entry) => entry.offset < (entry.maxSize ?? 0),
232
+ },
233
+ qkv: {
234
+ matches: (cache) => cacheSignature(cache).startsWith("kv:quant:"),
235
+ snapshot: (cache, context) => {
236
+ const c = cache as QuantizedKVCache;
237
+ if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
238
+ context.pushTriple(c.keys, c.offset);
239
+ context.pushTriple(c.values, c.offset);
240
+ return { kind: "qkv", offset: c.offset, groupSize: c.groupSize,
241
+ bits: c.bits, tensors: context.slots };
242
+ },
243
+ clone: (cache, context) => {
244
+ const c = cache as QuantizedKVCache;
245
+ const clone = new QuantizedKVCache(c.groupSize, c.bits, c.kernels);
246
+ if (!c.keys || !c.values) return clone;
247
+ clone.restoreState(
248
+ context.tripleView(c.keys, c.offset),
249
+ context.tripleView(c.values, c.offset),
250
+ c.offset,
251
+ );
252
+ return clone;
253
+ },
254
+ load: (entry, context) => {
255
+ const keys = context.triple(entry.tensors, 0), values = context.triple(entry.tensors, 3);
256
+ // Affine storage was saved from the lego the graph composed at this
257
+ // layer: the restored storage attends with that lego's kernel set.
258
+ const cache = new QuantizedKVCache(entry.groupSize!, entry.bits!,
259
+ composedKernelSet(context, entry)(entry.bits!, entry.groupSize!, keys.scales.dtype));
260
+ cache.restoreState(keys, values, entry.offset);
261
+ return cache;
262
+ },
263
+ headerTrimmable: () => true,
264
+ },
265
+ "rotating-qkv": {
266
+ matches: (cache) => cacheSignature(cache).startsWith("kv:rotating-quant:"),
267
+ snapshot: (cache, context) => {
268
+ const c = cache as RotatingQuantizedKVCache;
269
+ if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
270
+ context.pushTriple(c.keys, null);
271
+ context.pushTriple(c.values, null);
272
+ return { kind: "rotating-qkv", offset: c.offset, idx: c.ringIdx,
273
+ maxSize: c.maxSize, groupSize: c.groupSize, bits: c.bits,
274
+ tensors: context.slots };
275
+ },
276
+ clone: (cache, context) => {
277
+ const c = cache as RotatingQuantizedKVCache;
278
+ const clone = new RotatingQuantizedKVCache(c.maxSize, c.groupSize, c.bits, c.kernels);
279
+ if (!c.keys || !c.values) return clone;
280
+ clone.restoreState(
281
+ context.tripleView(c.keys, null), context.tripleView(c.values, null),
282
+ c.offset, c.ringIdx,
283
+ );
284
+ return clone;
285
+ },
286
+ load: (entry, context) => {
287
+ const keys = context.triple(entry.tensors, 0), values = context.triple(entry.tensors, 3);
288
+ // Affine storage was saved from the lego the graph composed at this
289
+ // layer: the restored storage attends with that lego's kernel set.
290
+ const cache = new RotatingQuantizedKVCache(
291
+ entry.maxSize!, entry.groupSize!, entry.bits!,
292
+ composedKernelSet(context, entry)(entry.bits!, entry.groupSize!, keys.scales.dtype),
293
+ );
294
+ cache.restoreState(keys, values, entry.offset, entry.idx!);
295
+ return cache;
296
+ },
297
+ headerTrimmable: (entry) => entry.offset < (entry.maxSize ?? 0),
298
+ },
299
+ ssm: {
300
+ matches: (cache) => cacheSignature(cache) === "ssm",
301
+ snapshot: (cache, context) => {
302
+ const c = cache as SSMCache;
303
+ if (!c.conv || !c.recurrent) throw new Error("cannot persist an empty cache");
304
+ context.push(c.conv, false);
305
+ context.push(c.recurrent, false);
306
+ return { kind: "ssm", offset: c.offset, tensors: context.slots };
307
+ },
308
+ clone: (cache, context) => {
309
+ const c = cache as SSMCache;
310
+ if (!c.conv || !c.recurrent) throw new Error("cannot clone an empty cache");
311
+ const clone = new SSMCache();
312
+ clone.conv = context.view(c.conv);
313
+ clone.recurrent = context.view(c.recurrent);
314
+ clone.offset = c.offset;
315
+ return clone;
316
+ },
317
+ load: (entry, context) => {
318
+ const cache = new SSMCache();
319
+ cache.conv = context.arr(entry.tensors[0]!);
320
+ cache.recurrent = context.arr(entry.tensors[1]!);
321
+ cache.offset = entry.offset;
322
+ return cache;
323
+ },
324
+ headerTrimmable: () => false,
325
+ },
326
+ turboquant: {
327
+ matches: (cache) => cacheSignature(cache).startsWith("kv:turboquant:"),
328
+ snapshot: (cache, context) => {
329
+ const c = cache as TurboQuantKVCache;
330
+ const state = c.state();
331
+ if (state.length === 0) throw new Error("cannot persist an empty cache");
332
+ for (const array of state) context.push(array, true);
333
+ return { kind: "turboquant", offset: c.offset, kBits: c.kBits,
334
+ vBits: c.vBits, headDim: c.headDim ?? 0, tensors: context.slots };
335
+ },
336
+ clone: (cache, context) => {
337
+ const c = cache as TurboQuantKVCache;
338
+ const state = c.state();
339
+ if (state.length === 0) throw new Error("cannot clone an empty cache");
340
+ try {
341
+ const [kIdx, kScales, kZeros, vPacked, vScales] = state;
342
+ const clone = new TurboQuantKVCache(c.kBits, c.vBits, c.turboQuantKernels, c.fusedDecode);
343
+ clone.restoreState({
344
+ kIdx: context.view(kIdx!),
345
+ kScales: context.view(kScales!),
346
+ kZeros: context.view(kZeros!),
347
+ vPacked: context.view(vPacked!),
348
+ vScales: context.view(vScales!),
349
+ }, c.offset, c.headDim!);
350
+ return clone;
351
+ } finally {
352
+ for (const array of state) array.dispose();
353
+ }
354
+ },
355
+ load: (entry, context) => {
356
+ // TurboQuant converts only a graph's full-attention KVCache layers, so the
357
+ // graph's cache at this layer is that KVCache: the restored cache attends
358
+ // with the TurboQuant read the graph composed into it (dense for Qwen
359
+ // 3.5, deferred for Gemma 4 and MiniCPM5).
360
+ const cache = new TurboQuantKVCache(entry.kBits!, entry.vBits!, (context.prototype as KVCache).turboQuantKernels);
361
+ const tensors: TurboQuantTensor = {
362
+ kIdx: context.arr(entry.tensors[0]!),
363
+ kScales: context.arr(entry.tensors[1]!),
364
+ kZeros: context.arr(entry.tensors[2]!),
365
+ vPacked: context.arr(entry.tensors[3]!),
366
+ vScales: context.arr(entry.tensors[4]!),
367
+ };
368
+ cache.restoreState(tensors, entry.offset, entry.headDim!);
369
+ return cache;
370
+ },
371
+ headerTrimmable: () => true,
372
+ },
373
+ paged: pagedCacheCodec,
374
+ mla: {
375
+ matches: (cache) => cacheSignature(cache) === "kv:mla:target",
376
+ snapshot: (cache, context) => snapshotMla("mla", cache, context),
377
+ clone: cloneMla,
378
+ load: loadMla,
379
+ headerTrimmable: () => true,
380
+ },
381
+ "mla-dsa": {
382
+ matches: (cache) => cacheSignature(cache) === "kv:mla:target:dsa",
383
+ snapshot: (cache, context) => snapshotMla("mla-dsa", cache, context),
384
+ clone: cloneMla,
385
+ load: loadMla,
386
+ headerTrimmable: () => true,
387
+ },
388
+ "mtp-mla": {
389
+ matches: (cache) => cacheSignature(cache) === "kv:mla:mtp",
390
+ snapshot: (cache, context) => snapshotMla("mtp-mla", cache, context),
391
+ clone: cloneMla,
392
+ load: loadMla,
393
+ headerTrimmable: () => true,
394
+ },
395
+ } satisfies Record<CacheKind, CacheCodec>;
396
+
397
+ export function createCacheCodecProvider(
398
+ id: string, entries: Readonly<Partial<Record<CacheKind, CacheCodec>>>,
399
+ ): CacheCodecProvider {
400
+ if (!id.trim()) throw new Error("cache codec provider needs an identity");
401
+ const table = new Map(Object.entries(entries).map(([kind, codec]) => [kind, Object.freeze({
402
+ matches: codec.matches.bind(codec), snapshot: codec.snapshot.bind(codec), clone: codec.clone.bind(codec),
403
+ load: codec.load.bind(codec), headerTrimmable: codec.headerTrimmable.bind(codec),
404
+ })]));
405
+ const candidates = [...table.values()];
406
+ const selected = new WeakMap<Cache, CacheCodec>();
407
+ return Object.freeze({
408
+ id,
409
+ forCache(cache: Cache) {
410
+ const bound = selected.get(cache);
411
+ if (bound) return bound;
412
+ const matches = candidates.filter((codec) => codec.matches(cache));
413
+ if (matches.length !== 1)
414
+ throw new Error(`cache codec provider ${id}: ${matches.length} matches for ${cacheSignature(cache)}`);
415
+ selected.set(cache, matches[0]!);
416
+ return matches[0]!;
417
+ },
418
+ forHeader(entry: CacheHeaderEntry) {
419
+ const codec = table.get(entry.kind);
420
+ if (!codec) throw new Error(`cache codec provider ${id}: unsupported cache kind ${entry.kind}`);
421
+ return codec;
422
+ },
423
+ });
424
+ }
425
+
426
+ export const legacyCacheCodecs = createCacheCodecProvider("mlx-cache-v3", CACHE_CODECS);
427
+
428
+ /** Old TQ files lack the conversion boundary. Keep them reusable at their
429
+ * stored offset, but never infer that an earlier precision state is valid. */
430
+ export function cacheHeaderMinimumReusableOffset(entry: CacheHeaderEntry): number {
431
+ const offset = entry.minimumReusableOffset ?? (entry.kind === "turboquant" ? entry.offset : 0);
432
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > entry.offset)
433
+ throw new Error("invalid cache minimum reusable offset");
434
+ return offset;
435
+ }
436
+ export const cacheHeadersMinimumReusableOffset = (caches: readonly CacheHeaderEntry[]): number =>
437
+ Math.max(0, ...caches.map(cacheHeaderMinimumReusableOffset));
438
+
439
+ export const cacheHeadersTrimmable = (
440
+ caches: readonly CacheHeaderEntry[], codecs: CacheCodecProvider = legacyCacheCodecs,
441
+ ): boolean => caches.every((entry) => codecs.forHeader(entry).headerTrimmable(entry));
442
+
443
+ function snapshotCache(c: Cache, dataOffset: number, codecs: CacheCodecProvider): { entry: CacheHeaderEntry; sources: TensorSource[]; next: number } {
444
+ const slots: TensorSlot[] = [];
445
+ const sources: TensorSource[] = [];
446
+ let cursor = dataOffset;
447
+ const HASH_PLACEHOLDER = "0".repeat(16); // fixed-width; patched at write
448
+ const push = (a: MlxArray, disposeAfter: boolean): void => {
449
+ const off = alignUp(cursor);
450
+ slots.push({ off, bytes: a.nbytes, shape: a.shape, dtype: a.dtype, hash: HASH_PLACEHOLDER });
451
+ sources.push({ arr: a, disposeAfter });
452
+ cursor = off + a.nbytes;
453
+ };
454
+ const liveSlice = (a: MlxArray, upTo: number): MlxArray => {
455
+ const [B, H, , D] = a.shape as [number, number, number, number];
456
+ return a.slice([0, 0, 0, 0], [B, H, upTo, D]);
457
+ };
458
+ const liveMlaSlice = (a: MlxArray, upTo: number): MlxArray => {
459
+ const [B, , D] = a.shape as [number, number, number];
460
+ return a.slice([0, 0, 0], [B, upTo, D]);
461
+ };
462
+ const pushTriple = (t: ops.QuantizedTensor, upTo: number | null): void => {
463
+ for (const a of [t.packed, t.scales, t.biases]) {
464
+ if (upTo === null) push(a, false);
465
+ else push(liveSlice(a, upTo), true);
466
+ }
467
+ };
468
+
469
+ try {
470
+ const entry = codecs.forCache(c).snapshot(c, { slots, push, liveSlice, liveMlaSlice, pushTriple });
471
+ if (c.minimumReusableOffset !== undefined) entry.minimumReusableOffset = c.minimumReusableOffset;
472
+ return { entry, sources, next: cursor };
473
+ } catch (error) {
474
+ for (const source of sources) if (source.disposeAfter) source.arr.dispose();
475
+ throw error;
476
+ }
477
+ }
478
+
479
+ /** Persist `caches` (+ the exact token prefix they encode) to `path`.
480
+ * ATOMIC: written to `<path>.tmp`, fsync'd, renamed — a crash mid-write
481
+ * leaves only a .tmp orphan (ignored + reaped by SsdCacheStore.scan). */
482
+ /** Zero-copy in-memory clone of a cache list at its CURRENT state — new
483
+ * cache instances holding slice VIEWS of the live tensors (mlx arrays are
484
+ * immutable: continued generation replaces the source caches' internal
485
+ * arrays, it never mutates the shared buffers, so views are stable).
486
+ * Built for the prompt-boundary prompt-cache snapshot (multi-turn agent
487
+ * traffic: the reply's decode→encode roundtrip drift makes prompt+gen
488
+ * entries untrimmable at context > sliding window — a prompt-only entry
489
+ * is ALWAYS an exact prefix of the next turn, immune to drift). Must be
490
+ * called while the caches hold EXACTLY the state to snapshot (post-wrap
491
+ * rings cannot be rewound later). Same per-kind dispatch as
492
+ * snapshotCache/loadKvCache. */
493
+ export function cloneKvCaches(caches: Cache[], codecs: CacheCodecProvider = legacyCacheCodecs): Cache[] {
494
+ const pending: MlxArray[] = [];
495
+ const hold = (array: MlxArray): MlxArray => { pending.push(array); return array; };
496
+ const view = (a: MlxArray): MlxArray => hold(a.slice(a.shape.map(() => 0), [...a.shape]));
497
+ const liveView = (a: MlxArray, upTo: number): MlxArray => {
498
+ const [B, H, , D] = a.shape as [number, number, number, number];
499
+ return hold(a.slice([0, 0, 0, 0], [B, H, upTo, D]));
500
+ };
501
+ const mlaView = (a: MlxArray, upTo: number): MlxArray => {
502
+ const [B, , D] = a.shape as [number, number, number];
503
+ return hold(a.slice([0, 0, 0], [B, upTo, D]));
504
+ };
505
+ const tripleView = (t: ops.QuantizedTensor, upTo: number | null): ops.QuantizedTensor => ({
506
+ packed: upTo === null ? view(t.packed) : liveView(t.packed, upTo),
507
+ scales: upTo === null ? view(t.scales) : liveView(t.scales, upTo),
508
+ biases: upTo === null ? view(t.biases) : liveView(t.biases, upTo),
509
+ });
510
+ const out: Cache[] = [];
511
+ try {
512
+ for (const cache of caches) {
513
+ // A composed cache clones as its storage, composed again by the same lego.
514
+ const c = cache.composed ? cache.composed.storage() : cache;
515
+ const stored = codecs.forCache(c).clone(c, { view, liveView, mlaView, tripleView });
516
+ if (c.minimumReusableOffset !== undefined) stored.minimumReusableOffset = c.minimumReusableOffset;
517
+ pending.length = 0; // transferred to the stored clone
518
+ out.push(cache.composed ? cache.composed.over(stored) : stored);
519
+ }
520
+ } catch (err) {
521
+ cleanupFailure(err, () => disposeResources([...pending, ...out]));
522
+ }
523
+ return out;
524
+ }
525
+
526
+ /** Shared file layout and codec planning for synchronous and worker writes. */
527
+ function planKvCache(tokens: number[], caches: Cache[], meta: KvSaveMeta, codecs: CacheCodecProvider): { header: KvFileHeader; sources: TensorSource[] } {
528
+ // Plan pass: header entries + lazy tensor sources, NO bytes materialized.
529
+ const entries: CacheHeaderEntry[] = [];
530
+ const sources: TensorSource[] = [];
531
+ let dataOffset = 0; // relative; rebased after header is sized
532
+ try {
533
+ for (const cache of caches) {
534
+ // A composed cache saves its storage: what the convert-after path saved.
535
+ const s = snapshotCache(cache.composed ? cache.composed.storage() : cache, dataOffset, codecs);
536
+ entries.push(s.entry);
537
+ sources.push(...s.sources);
538
+ dataOffset = s.next;
539
+ }
540
+
541
+ const { attachments: liveAttachments, ...identity } = meta;
542
+ const attachments = liveAttachments?.map((attachment) => ({
543
+ schema: attachment.schema,
544
+ metadata: { ...attachment.metadata },
545
+ tensors: attachment.tensors.map((arr) => {
546
+ const slot: TensorSlot = { off: dataOffset, bytes: arr.nbytes,
547
+ shape: [...arr.shape], dtype: arr.dtype, hash: "0000000000000000" };
548
+ dataOffset = alignUp(dataOffset + slot.bytes);
549
+ sources.push({ arr, disposeAfter: false });
550
+ return slot;
551
+ }),
552
+ }));
553
+
554
+ // Header size is final NOW: hashes are fixed-width placeholders that get
555
+ // patched in place (same byte length) after the data pass computes them.
556
+ const header: KvFileHeader = {
557
+ formatVersion: attachments?.length ? 4 : 3, createdAt: Date.now(), ...identity,
558
+ ...(attachments?.length ? { attachments } : {}), codecProvider: codecs.id, tokens, caches: entries,
559
+ };
560
+ return { header, sources };
561
+ } catch (error) {
562
+ for (const source of sources) if (source.disposeAfter) source.arr.dispose();
563
+ throw error;
564
+ }
565
+ }
566
+
567
+ function* saveKvCacheSteps(path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta, codecs: CacheCodecProvider): Generator<void, void, void> {
568
+ const { header, sources } = planKvCache(tokens, caches, meta, codecs);
569
+ const entries = header.caches, attachments = header.attachments;
570
+ try {
571
+ const headerLen = new TextEncoder().encode(JSON.stringify(header)).length;
572
+ const dataStart = alignUp(PREFIX_LEN + headerLen);
573
+
574
+ const tmp = `${path}.tmp`;
575
+ const fd = openSync(tmp, "w");
576
+ try {
577
+ // Data pass: materialize → hash → write → drop, ONE tensor at a time.
578
+ // The hash+write read a ZERO-COPY view of the contiguous mlx buffer
579
+ // (rawBytesView) — never a JS-heap copy. rawBytes() here looked
580
+ // "streamed" but each call allocated a dead per-tensor JS-heap copy
581
+ // whose reclamation is GC-timing-dependent — up to a whole extra
582
+ // entry of heap on a busy server (the A7 residual). Now zero JS
583
+ // allocations; the only real transient is the one mlx-side
584
+ // contiguous copy (allocator-pooled; none at all for contiguous
585
+ // sources — mlx save-transient measured 0 bytes, 2026-07-07). NOTE:
586
+ // the hash+write still CPU-touch the live entry's unified-memory
587
+ // pages, which makes them VISIBLE to ps RSS — accounting, not an
588
+ // allocation (see `02d723a:scripts/bench-serve.ts`' per-leg RSS note).
589
+ let srcIdx = 0;
590
+ for (const e of [...entries, ...(attachments ?? [])]) {
591
+ for (const slot of e.tensors) {
592
+ const src = sources[srcIdx++]!;
593
+ const c = ops.contiguous(src.arr);
594
+ try {
595
+ const bytes = c.rawBytesView(); // evals; aliases the mlx buffer
596
+ if (bytes.length !== slot.bytes)
597
+ throw new Error(`tensor byte-length drift: planned ${slot.bytes}, got ${bytes.length}`);
598
+ slot.hash = hash64(bytes);
599
+ writeSync(fd, bytes, 0, bytes.length, dataStart + slot.off);
600
+ } finally {
601
+ c.dispose(); // the view dies with the buffer — nothing retains it
602
+ }
603
+ if (src.disposeAfter) { src.arr.dispose(); src.disposeAfter = false; }
604
+ yield;
605
+ }
606
+ }
607
+
608
+ // Header pass: real hashes in, byte length unchanged by construction.
609
+ const headerJson = new TextEncoder().encode(JSON.stringify(header));
610
+ if (headerJson.length !== headerLen)
611
+ throw new Error(`header length drift: planned ${headerLen}, got ${headerJson.length}`);
612
+ const pre = new Uint8Array(dataStart);
613
+ pre.set(new TextEncoder().encode(MAGIC), 0);
614
+ const dv = new DataView(pre.buffer);
615
+ dv.setUint32(MAGIC.length, headerJson.length, true);
616
+ dv.setUint32(MAGIC.length + 4, dataStart, true);
617
+ dv.setBigUint64(MAGIC.length + 8, BigInt(Bun.hash(headerJson)), true);
618
+ pre.set(headerJson, PREFIX_LEN);
619
+ writeSync(fd, pre, 0, pre.length, 0);
620
+ fsyncSync(fd);
621
+ } catch (err) {
622
+ closeSync(fd);
623
+ try { rmSync(tmp, { force: true }); } catch {}
624
+ throw err;
625
+ }
626
+ closeSync(fd);
627
+ renameSync(tmp, path);
628
+ } finally {
629
+ // Slices created in the plan pass are ours to free on EVERY path.
630
+ for (const s of sources) if (s.disposeAfter) s.arr.dispose();
631
+ }
632
+ }
633
+
634
+ export function saveKvCache(path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta = {}, codecs: CacheCodecProvider = legacyCacheCodecs): void {
635
+ for (const _ of saveKvCacheSteps(path, tokens, caches, meta, codecs)) { /* drain */ }
636
+ }
637
+
638
+ /** Prepare immutable native storage on the owner thread, then pack, hash and
639
+ * write it on the CPU worker. The optional runner covers preparation only.
640
+ * Callers retain cache and attachment ownership until this promise settles. */
641
+ export async function saveKvCacheAsync(
642
+ path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta = {},
643
+ runStep?: <T>(step: () => T) => Promise<T>, codecs: CacheCodecProvider = legacyCacheCodecs,
644
+ storage: Pick<KvWriteRequest, "layout" | "blockBytes" | "segmented"> = {},
645
+ ): Promise<void> {
646
+ const { header, sources } = planKvCache(tokens, caches, meta, codecs);
647
+ try {
648
+ // Snapshot preparation belongs to the MLX owner. After publication the
649
+ // worker sees only immutable bytes and never evaluates or calls MLX.
650
+ const prepare = () => sources.map(source => source.arr.storageView());
651
+ const tensors = runStep ? await runStep(prepare) : prepare();
652
+ await kvWriter.write({ path, header, tensors, ...storage });
653
+ } finally {
654
+ for (const source of sources) if (source.disposeAfter) source.arr.dispose();
655
+ }
656
+ }
657
+
658
+ /** Serial snapshot queue with optional capacity shedding. TieredPromptCache
659
+ * disables shedding and owns residency itself. Interrupted-generation
660
+ * persistence uses the cap for supersedable intervals. Queue ownership ends
661
+ * after each write settles; the cache can retain independent shared views. */
662
+ type SpillRec = SpillItem & {
663
+ bytes: number;
664
+ dropped: boolean;
665
+ settle: (stored: boolean) => void;
666
+ };
667
+
668
+ export class SpillQueue {
669
+ #queue: SpillRec[] = [];
670
+ #bytes = 0;
671
+ #dropped = 0;
672
+ #failed = 0;
673
+ #inFlight: object | null = null;
674
+ #chain: Promise<void> = Promise.resolve();
675
+
676
+ constructor(
677
+ readonly capBytes: number,
678
+ /** Byte size of a clone set (prompt-cache's cacheBytes). */
679
+ readonly bytesOf: (caches: Cache[]) => number,
680
+ /** The asynchronous storage operation. */
681
+ readonly store: (item: SpillItem) => Promise<unknown>,
682
+ /** Clone disposal — frees the pinned GPU memory. */
683
+ readonly disposeClones: (caches: Cache[]) => void,
684
+ /** Zero-byte control barriers may need to survive capacity shedding. */
685
+ readonly canDrop: (item: SpillItem) => boolean = () => true,
686
+ ) {}
687
+
688
+ /** Bytes pinned by queued (not yet flushed) clones. */
689
+ get pendingBytes(): number { return this.#bytes; }
690
+ get pendingCount(): number { return this.#queue.length; }
691
+ /** Spills dropped by the cap since start (each = one future cache miss). */
692
+ get droppedCount(): number { return this.#dropped; }
693
+ /** Store calls that threw or returned false. */
694
+ get failedCount(): number { return this.#failed; }
695
+
696
+ enqueue(item: SpillItem): Promise<boolean> {
697
+ let settle!: (stored: boolean) => void;
698
+ const result = new Promise<boolean>((resolve) => { settle = resolve; });
699
+ const rec: SpillRec = {
700
+ ...item,
701
+ bytes: this.bytesOf(item.caches) + attachmentBytes(item.attachments),
702
+ dropped: false,
703
+ settle,
704
+ };
705
+ this.#queue.push(rec);
706
+ this.#bytes += rec.bytes;
707
+ while (this.#bytes > this.capBytes) {
708
+ const victim = this.#queue.find(
709
+ (p) => !p.dropped && p !== this.#inFlight && p !== rec && this.canDrop(p),
710
+ );
711
+ if (!victim) break; // only the new item (or in-flight) left — soft cap
712
+ this.#drop(victim);
713
+ }
714
+ this.#chain = this.#chain.then(async () => {
715
+ if (rec.dropped) return; // clones already disposed at drop time
716
+ this.#inFlight = rec;
717
+ try {
718
+ const stored = await this.store(rec);
719
+ if (stored === false) this.#failed++;
720
+ rec.settle(stored !== false);
721
+ } catch {
722
+ // best-effort tier — the entry simply won't restore
723
+ this.#failed++;
724
+ rec.settle(false);
725
+ } finally {
726
+ this.#inFlight = null;
727
+ this.#remove(rec);
728
+ disposeResources([{ dispose: () => this.disposeClones(rec.caches) },
729
+ { dispose: () => disposeAttachments(rec.attachments) }]);
730
+ }
731
+ });
732
+ return result;
733
+ }
734
+
735
+ /** Cancel queued ownership, excluding the write already in flight. */
736
+ cancelWhere(matches: (item: SpillItem) => boolean): void {
737
+ for (const rec of [...this.#queue])
738
+ if (!rec.dropped && rec !== this.#inFlight && matches(rec)) this.#drop(rec);
739
+ }
740
+
741
+ /** Settles when everything enqueued so far has flushed or dropped
742
+ * (tests and graceful shutdown). */
743
+ drain(): Promise<void> { return this.#chain; }
744
+
745
+ #drop(rec: SpillRec): void {
746
+ rec.dropped = true;
747
+ this.#dropped++;
748
+ rec.settle(false);
749
+ this.#remove(rec);
750
+ disposeResources([{ dispose: () => this.disposeClones(rec.caches) },
751
+ { dispose: () => disposeAttachments(rec.attachments) }]);
752
+ }
753
+
754
+ #remove(rec: SpillRec): void {
755
+ const i = this.#queue.indexOf(rec);
756
+ if (i >= 0) {
757
+ this.#queue.splice(i, 1);
758
+ this.#bytes -= rec.bytes;
759
+ }
760
+ }
761
+ }
762
+
763
+ /** Read only the header (cheap — for prefix matching across many files).
764
+ * Always verifies the header hash; throws on any structural mismatch. */
765
+ export function readKvHeader(path: string): KvFileHeader & { dataStart: number } {
766
+ const fd = openSync(path, "r");
767
+ try {
768
+ const head = new Uint8Array(PREFIX_LEN);
769
+ readSync(fd, head, 0, head.length, 0);
770
+ if (new TextDecoder().decode(head.subarray(0, MAGIC.length)) !== MAGIC)
771
+ throw new Error(`${path}: not an mlx-bun KV v2 cache file`);
772
+ const dv = new DataView(head.buffer);
773
+ const len = dv.getUint32(MAGIC.length, true);
774
+ const dataStart = dv.getUint32(MAGIC.length + 4, true);
775
+ const expectHash = dv.getBigUint64(MAGIC.length + 8, true);
776
+ const body = new Uint8Array(len);
777
+ readSync(fd, body, 0, len, PREFIX_LEN);
778
+ if (BigInt(Bun.hash(body)) !== expectHash)
779
+ throw new Error(`${path}: header hash mismatch (truncated or corrupt)`);
780
+ const header = JSON.parse(new TextDecoder().decode(body)) as KvFileHeader;
781
+ if (header.formatVersion !== 3 && header.formatVersion !== 4 && header.formatVersion !== 5)
782
+ throw new Error(`${path}: unsupported formatVersion ${header.formatVersion}`);
783
+ return { ...header, dataStart };
784
+ } finally {
785
+ closeSync(fd);
786
+ }
787
+ }
788
+
789
+ /** Reject GLM cache/layout drift before opening the tensor mmap. Generic KV
790
+ * kinds may legitimately differ from makeCache() after runtime quantization;
791
+ * GLM's checkpoint-native cache geometry does not. */
792
+ function validateGlm52Prototype(
793
+ path: string,
794
+ entry: CacheHeaderEntry,
795
+ prototype: Cache,
796
+ ): void {
797
+ const glmKind = entry.kind === "mla" || entry.kind === "mla-dsa" ||
798
+ entry.kind === "mtp-mla";
799
+ const prototypeSignature = cacheSignature(prototype);
800
+ if (!prototypeSignature.startsWith("kv:mla:")) {
801
+ if (glmKind)
802
+ throw new Error(`${path}: GLM cache kind ${entry.kind} does not match model cache`);
803
+ return;
804
+ }
805
+ const glmPrototype = prototype as MLACache;
806
+ const expectedKind: CacheKind = glmPrototype.role === "mtp"
807
+ ? "mtp-mla"
808
+ : glmPrototype.dsa ? "mla-dsa" : "mla";
809
+ if (entry.kind !== expectedKind) {
810
+ throw new Error(
811
+ `${path}: cache kind ${entry.kind} != model cache kind ${expectedKind}`,
812
+ );
813
+ }
814
+ for (const [field, actual, expected] of [
815
+ ["kvLoraRank", entry.kvLoraRank, glmPrototype.kvLoraRank],
816
+ ["ropeHeadDim", entry.ropeHeadDim, glmPrototype.ropeHeadDim],
817
+ ["dsaHeadDim", entry.dsaHeadDim, glmPrototype.dsa?.headDim],
818
+ ["maxTokens", entry.maxTokens, glmPrototype.maxTokens],
819
+ ] as const) {
820
+ if (actual !== expected) {
821
+ throw new Error(
822
+ `${path}: ${field} ${String(actual)} != model ${String(expected)}`,
823
+ );
824
+ }
825
+ }
826
+ const expectedTensors = glmPrototype.dsa ? 3 : 2;
827
+ if (entry.tensors.length !== expectedTensors) {
828
+ throw new Error(
829
+ `${path}: ${entry.kind} has ${entry.tensors.length} tensors, ` +
830
+ `expected ${expectedTensors}`,
831
+ );
832
+ }
833
+ }
834
+
835
+ /** Copy-restore a full-attention KV tensor into STEP-rounded capacity with
836
+ * ≥1 token of slack (what a live mid-generation cache looks like) so the
837
+ * first post-restore updateAndFetch never takes the grow path — the old
838
+ * exactly-offset-sized restore made that first step concat-copy the ENTIRE
839
+ * entry into a fresh buffer. Zero padding is bit-safe: writes land at
840
+ * [offset..) before any read, and attention only reads [:offset+L).
841
+ * Evaluated EAGERLY so the host-copy leaf frees before the next tensor
842
+ * streams in (bounded transient). Plain KVCache only — rotating rings and
843
+ * quantized triples are position-exact layouts. */
844
+ function withStepCapacity(a: MlxArray, offset: number): MlxArray {
845
+ const [B, H, S, D] = a.shape as [number, number, number, number];
846
+ const cap = Math.ceil((offset + 1) / KVCache.STEP) * KVCache.STEP;
847
+ if (S >= cap) return a;
848
+ // Internals drained on throw (2026-07-07 review: a grown.eval() failure
849
+ // orphaned z/grown; `a` stays the CALLER's to free — loadKvCache's
850
+ // pending[] holds it).
851
+ let z: MlxArray | null = null;
852
+ let grown: MlxArray | null = null;
853
+ try {
854
+ z = ops.zeros([B, H, cap, D], a.dtype);
855
+ grown = ops.sliceUpdate(z, a, [0, 0, 0, 0], [B, H, S, D]);
856
+ z.dispose();
857
+ z = null;
858
+ grown.eval();
859
+ a.dispose();
860
+ const out = grown;
861
+ grown = null;
862
+ return out;
863
+ } finally {
864
+ z?.dispose();
865
+ grown?.dispose();
866
+ }
867
+ }
868
+
869
+ /** `stored` (restored or cloned storage) as the cache of a layer whose graph
870
+ * cache is `layer`: composed again by that layer's lego (`ComposedStorage.over`),
871
+ * so it converts as the lego that stored it would have. A cache that already is
872
+ * a lego stays, and so do paged pages, which the paged request state holds in
873
+ * the lego's place (`state/request-policy.ts`). */
874
+ export function composeAtLayer(layer: Cache, stored: Cache): Cache {
875
+ const composed = layer.composed;
876
+ return !composed || stored.composed || cacheSignature(stored) === "kv:paged" ? stored : composed.over(stored);
877
+ }
878
+
879
+ /** Checks the header against the expectations and the restoring graph's
880
+ * caches, and returns those caches (`model.makeCache()`, empty) as the
881
+ * prototypes the codecs load against. The caller disposes them. */
882
+ function validateKvHeader(path: string, header: KvFileHeader, model: { makeCache(): Cache[] },
883
+ expect: KvLoadExpect, codecs: CacheCodecProvider): Cache[] {
884
+ if ((header.codecProvider ?? legacyCacheCodecs.id) !== codecs.id)
885
+ throw new Error(`${path}: cache codec provider mismatch`);
886
+ for (const entry of header.caches) codecs.forHeader(entry);
887
+ for (const key of [
888
+ "modelId",
889
+ "configFingerprint",
890
+ "tokenizerHash",
891
+ "ns",
892
+ ] as const) {
893
+ if (expect[key] !== undefined && header[key] !== expect[key])
894
+ throw new Error(`${path}: ${key} mismatch (file ${header[key]}, expected ${expect[key]})`);
895
+ }
896
+ const proto = model.makeCache();
897
+ try {
898
+ if (header.caches.length !== proto.length) {
899
+ throw new Error(
900
+ `${path}: ${header.caches.length} cached layers but model has ${proto.length}`,
901
+ );
902
+ }
903
+ for (let index = 0; index < proto.length; index++) {
904
+ validateGlm52Prototype(path, header.caches[index]!, proto[index]!);
905
+ }
906
+ } catch (error) {
907
+ for (const c of proto) c.dispose();
908
+ throw error;
909
+ }
910
+ return proto;
911
+ }
912
+
913
+ /** Decode cache layouts independently of the transport supplying arrays.
914
+ * `prototypes` are the restoring graph's caches, one per header entry. */
915
+ function restoreKvArrays(path: string, header: KvFileHeader, prototypes: readonly Cache[], codecs: CacheCodecProvider,
916
+ readArray: (slot: TensorSlot) => MlxArray): LoadedKvCache {
917
+ // Every tensor materialized for the CURRENT entry, drained on any throw
918
+ // (2026-07-07 review: a hash mismatch under --ssd-cache-verify — or any
919
+ // per-tensor failure — orphaned the entry's already-built tensors: the
920
+ // catch below only saw completed caches). Cleared once the entry's cache
921
+ // takes ownership; disposing an array withStepCapacity already consumed
922
+ // is safe (dispose is idempotent).
923
+ const pending: MlxArray[] = [];
924
+ const arr = (slot: TensorSlot): MlxArray => {
925
+ const a = readArray(slot); pending.push(a); return a;
926
+ };
927
+ /** withStepCapacity whose (possibly fresh) result is pending-tracked. */
928
+ const grownArr = (slot: TensorSlot, offset: number): MlxArray => {
929
+ const g = withStepCapacity(arr(slot), offset);
930
+ pending.push(g);
931
+ return g;
932
+ };
933
+ const triple = (slots: TensorSlot[], at: number): ops.QuantizedTensor =>
934
+ ({ packed: arr(slots[at]!), scales: arr(slots[at + 1]!), biases: arr(slots[at + 2]!) });
935
+
936
+ const caches: Cache[] = [];
937
+ const attachments: CheckpointAttachment[] = [];
938
+ try {
939
+ for (const [index, e] of header.caches.entries()) {
940
+ const codec = codecs.forHeader(e);
941
+ const minimumOffset = cacheHeaderMinimumReusableOffset(e);
942
+ const prototype = prototypes[index]!;
943
+ const stored = codec.load(e, { path, prototype, arr, grownArr, triple });
944
+ stored.minimumReusableOffset = minimumOffset;
945
+ // This entry's tensors are now owned by its storage — stop tracking them
946
+ // (the catch must not double-free through both pending AND caches).
947
+ pending.length = 0;
948
+ caches.push(stored);
949
+ // The restored storage takes the cache the graph composed at this layer:
950
+ // a lego converts it as the saved lego would have.
951
+ caches[caches.length - 1] = composeAtLayer(prototype, stored);
952
+ }
953
+ for (const attachment of header.attachments ?? []) {
954
+ attachments.push({ schema: attachment.schema, metadata: { ...attachment.metadata },
955
+ tensors: attachment.tensors.map(arr) });
956
+ pending.length = 0;
957
+ }
958
+ } catch (err) {
959
+ disposeAttachments(attachments);
960
+ for (const a of pending) a.dispose(); // the mid-entry orphans
961
+ for (const c of caches) c.dispose();
962
+ throw err;
963
+ }
964
+ return { tokens: header.tokens, header, caches, ...(attachments.length ? { attachments } : {}) };
965
+ }
966
+
967
+ /** CPU-only asynchronous reads into unpublished allocations; no tensor copy
968
+ * crosses the event loop. Native array construction uses the shared codecs. */
969
+ export async function loadKvCacheAsync(path: string, model: { makeCache(): Cache[] },
970
+ expect: KvLoadExpect = {}, codecs: CacheCodecProvider = legacyCacheCodecs): Promise<LoadedKvCache> {
971
+ const header = readKvHeader(path);
972
+ const prototypes = validateKvHeader(path, header, model, expect, codecs);
973
+ const slots = [...header.caches, ...(header.attachments ?? [])].flatMap(e => e.tensors);
974
+ const buffers = new Map<TensorSlot, HostBuffer>();
975
+ try {
976
+ for (const slot of slots) buffers.set(slot, new HostBuffer(slot.bytes));
977
+ await kvReader.read({ path, header, pointers: slots.map(slot => buffers.get(slot)!.pointer), verify: expect.verify });
978
+ return restoreKvArrays(path, header, prototypes, codecs, slot =>
979
+ MlxArray.adoptHostBuffer(buffers.get(slot)!, slot.shape, slot.dtype as Dtype));
980
+ } finally {
981
+ for (const buffer of buffers.values()) buffer.dispose();
982
+ for (const c of prototypes) c.dispose();
983
+ }
984
+ }
985
+
986
+ /** Reload by STREAMED COPY (see the header note): every tensor is copied
987
+ * into an mlx-owned leaf; the mapping is read-only, its pages dropped
988
+ * per-tensor, and unmapped before returning — the caches own their bytes
989
+ * outright (no dtor contract, no pinned mapping, nothing to retain).
990
+ * `model` is anything with makeCache() — the entry count is validated
991
+ * against the DONOR cache list (model.layers.length was wrong for
992
+ * KV-shared models like e4b, whose makeCache() returns donors only), and
993
+ * each restored cache takes what the graph composed into its own cache for
994
+ * that layer. */
995
+ export function loadKvCache(
996
+ path: string,
997
+ model: { makeCache(): Cache[] },
998
+ expect: KvLoadExpect = {}, codecs: CacheCodecProvider = legacyCacheCodecs,
999
+ ): LoadedKvCache {
1000
+ const header = readKvHeader(path);
1001
+ const prototypes = validateKvHeader(path, header, model, expect, codecs);
1002
+ try {
1003
+ if (header.formatVersion === 5) {
1004
+ return restoreKvArrays(path, header, prototypes, codecs, slot => {
1005
+ const bytes = new Uint8Array(slot.bytes);
1006
+ let offset = 0;
1007
+ for (const block of slot.blocks ?? []) {
1008
+ if (!/^[a-f0-9]{64}$/.test(block.hash) || block.bytes <= 0 || offset + block.bytes > bytes.length)
1009
+ throw new Error(`${path}: invalid KV block reference`);
1010
+ const part = readFileSync(join(dirname(path), "blocks", block.hash));
1011
+ if (part.length !== block.bytes || (expect.verify && createHash("sha256").update(part).digest("hex") !== block.hash))
1012
+ throw new Error(`${path}: KV block mismatch`);
1013
+ bytes.set(part, offset); offset += part.length;
1014
+ }
1015
+ if (offset !== bytes.length)
1016
+ throw new Error(`${path}: KV tensor mismatch`);
1017
+ return MlxArray.fromBytesCopy(bytes, slot.shape, slot.dtype as Dtype);
1018
+ });
1019
+ }
1020
+ const mmap = MmapFile.open(path, "ro"), dataStart = header.dataStart;
1021
+ try {
1022
+ return restoreKvArrays(path, header, prototypes, codecs, slot => {
1023
+ const view = mmap.view(dataStart + slot.off, slot.bytes);
1024
+ if (expect.verify && hash64(view) !== slot.hash)
1025
+ throw new Error(`${path}: tensor hash mismatch at offset ${slot.off}`);
1026
+ const array = MlxArray.fromBytesCopy(view, slot.shape, slot.dtype as Dtype);
1027
+ const len = Math.min(alignUp(slot.bytes), mmap.size - (dataStart + slot.off));
1028
+ mmap.advise(dataStart + slot.off, len, MADV_DONTNEED);
1029
+ return array;
1030
+ });
1031
+ } finally { mmap.unmap(); }
1032
+ } finally { for (const c of prototypes) c.dispose(); }
1033
+ }
1034
+
1035
+ import { CacheCodec,CacheCodecProvider,CacheHeaderEntry,CacheKind,CloneContext,KvFileHeader,KvLoadExpect,KvSaveMeta,LoadContext,LoadedKvCache,SnapshotContext,SpillItem,TensorSlot } from "./persistence-types";
1036
+ export { type CacheCodec,type CacheCodecProvider,type CacheHeaderEntry,type CacheKind,type CloneContext,type KvFileHeader,type KvLoadExpect,type KvSaveMeta,type LoadContext,type LoadedKvCache,type SnapshotContext,type SpillItem,type TensorSlot } from "./persistence-types";