@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,685 @@
1
+ import { type CheckpointAttachment } from "../contracts/mlx/checkpoint";
2
+ import { materializeCheckpoint, attachmentBytes, cloneAttachments, disposeAttachments } from "./checkpoint";
3
+ import { leaseCacheState } from "./leases";
4
+ import { minimumReusableOffset } from "./views";
5
+ import { withResource, cleanupFailure, disposeResources, ownResource } from "../runtime/resources";
6
+ // Byte-capped LRU prompt cache — the RAM tier of the Layer-0 KV store.
7
+ //
8
+ // The mlx-lm lesson: a count-capped cache of multi-GB KV
9
+ // entries is an OOM footgun. Ours accounts bytes (sum of KV array bytes
10
+ // per entry) and evicts least-recently-used until under the cap.
11
+ //
12
+ // Usage pattern (single generation queue, so take/put is race-free):
13
+ // const hit = cache.take(promptIds); // longest-prefix match
14
+ // generate(model, promptIds, { cache: hit?.caches ?? fresh })
15
+ // cache.put([...promptIds, ...generated], caches, ns, hit?.retain);
16
+ // take() lends zero-copy views and leaves every RAM donor in place, including
17
+ // recurrent and method state. SSD persistence runs independently of RAM reuse.
18
+ // put() supersedes same-ns prefix-ancestors when the new entry is trimmable.
19
+ //
20
+ // TIERING (Layer 0, unified-engine plan): when a ColdTier is attached,
21
+ // take() itself runs the two-tier dance — RAM peek vs cold find, restore
22
+ // (streamed copy) + trim when the cold tier holds a strictly longer
23
+ // prefix — so every consumer (the cache service the scheduler acquires
24
+ // through, direct generation) gets SSD restores through the same take()/put()
25
+ // it already calls. Eviction spills to the tier (the #spill hook); idle
26
+ // entries demote to it when policy permits. TieredPromptCache owns queued
27
+ // persistence and promotes restored prefixes into RAM.
28
+
29
+ import type { Cache } from "../contracts/mlx/cache";
30
+ import type { PrefixCache, PrefixCacheHit } from "../contracts/portable/prefix-cache";
31
+ import { LruRetention, type RetentionPolicy, type RetentionCandidate } from "./retention-policy";
32
+ import { cloneKvCaches } from "./persistence";
33
+
34
+ /** Reference-counted release: wraps an entry's `retain` (e.g. an mmap
35
+ * unmap) so DONOR + CLONES can each hold a share — the underlying hook
36
+ * runs once, after the LAST holder releases. Prefix sharing hands out
37
+ * zero-copy VIEWS of a donor's buffers; if the donor's unmap ran while a
38
+ * clone still referenced the mapped pages, the clone would read freed
39
+ * memory. */
40
+ function makeSharedRetain(retain: (() => void) | undefined): { acquire(): () => void } {
41
+ let count = 0;
42
+ let released = false;
43
+ return {
44
+ acquire() {
45
+ count++;
46
+ let mine = false;
47
+ return () => {
48
+ if (mine) return; // idempotent per share
49
+ mine = true;
50
+ count--;
51
+ if (count === 0 && !released) {
52
+ released = true;
53
+ retain?.();
54
+ }
55
+ };
56
+ },
57
+ };
58
+ }
59
+
60
+ /** The cold (SSD) tier the RAM cache tiers over. Structural — this module
61
+ * never imports ssd-cache (which imports us); the server binds
62
+ * SsdCacheStore + the model into this shape
63
+ * (apps/mlx-bun/src/engine/cache-services.ts). Lookup and
64
+ * restore failures return null; store returns false on failure. Operations
65
+ * that require persistence retain their donor if storing fails. */
66
+ export interface ColdTier {
67
+ /** Longest stored usable prefix for prompt/ns — index-only, no I/O.
68
+ * `handle` is the tier's opaque entry token, passed back to restore. */
69
+ find(prompt: number[], ns: string): { prefixLen: number; handle: unknown } | null;
70
+ /** Restore GPU-visible state. The default SSD backend copies into owned
71
+ * arrays. An optional backing release runs after consumers dispose their
72
+ * arrays. Null on any failure. */
73
+ restore(handle: unknown): { tokens: number[]; caches: Cache[]; attachments?: CheckpointAttachment[]; retain: () => void } | null;
74
+ findExact?(tokens: number[], ns: string): { prefixLen: number; handle: unknown } | null;
75
+ restoreAsync?(handle: unknown): Promise<ReturnType<ColdTier["restore"]>>;
76
+ /** Tensor objects have no target layers; restore with their own schema. */
77
+ restoreObjectAsync?(handle: unknown): Promise<ReturnType<ColdTier["restore"]>>;
78
+ /** Borrow live caches and finish persistence before returning. A false
79
+ * result prevents a required demotion. */
80
+ store(tokens: number[], caches: Cache[], ns: string, attachments?: CheckpointAttachment[]): boolean | void;
81
+ }
82
+
83
+ /** Spill sink for eviction/demotion (write-behind, 2026-07-06).
84
+ *
85
+ * `spillOwned` (preferred, NON-BLOCKING): the cache hands the sink an
86
+ * entry it OWNS — zero-copy CLONES of the caches (made via the same
87
+ * injectable cloner take() uses, BEFORE the donor is disposed; entries
88
+ * are immutable so the clones stay consistent forever) plus a copy of
89
+ * the tokens. The sink must dispose entry.caches when done, on BOTH
90
+ * fulfill and reject paths (the server chains storeAsync -> dispose on
91
+ * ssdWriteChain). A sink that throws SYNCHRONOUSLY has not taken
92
+ * ownership — the cache disposes the clones and the spill degrades to
93
+ * a no-op; eviction/demotion never unwinds.
94
+ *
95
+ * `spillSync` (fallback): called with the LIVE entry about to be
96
+ * disposed; must finish reading before returning. This is the old
97
+ * contract — a full synchronous serialize+write under the generation
98
+ * lock — kept for tests and simple embedders. */
99
+ export interface SpillSink {
100
+ spillOwned?: (entry: PromptCacheEntry) => void;
101
+ spillSync?: (entry: PromptCacheEntry) => void;
102
+ }
103
+
104
+ export interface PromptCacheEntry extends PrefixCacheHit<Cache[], CheckpointAttachment[]> {
105
+ /** Namespace key — adapter spec for LoRA requests ("" = base model).
106
+ * KV computed under one adapter must never seed another's prefill. */
107
+ ns: string;
108
+ /** Release hook for entries restored from the SSD cold tier: unmaps the
109
+ * backing COW file mapping. MUST run only after `caches` are disposed
110
+ * (they may alias file pages); every dispose path here honors that, and
111
+ * callers who take() an entry carry the thunk with it. */
112
+ retain?: () => void;
113
+ }
114
+
115
+ export function cacheBytes(caches: Cache[]): number {
116
+ let total = 0;
117
+ for (const c of caches) {
118
+ total += withResource(leaseCacheState(c), (state) => state.reduce((bytes, a) => bytes + a.nbytes, 0));
119
+ }
120
+ return total;
121
+ }
122
+
123
+ /** Length of the longest common prefix of two token sequences. */
124
+ export function commonPrefixLength(a: number[], b: number[]): number {
125
+ const n = Math.min(a.length, b.length);
126
+ let i = 0;
127
+ while (i < n && a[i] === b[i]) i++;
128
+ return i;
129
+ }
130
+
131
+ interface EntryRecord extends RetentionCandidate {
132
+ entry: PromptCacheEntry;
133
+ bytes: number;
134
+ lastUsed: number;
135
+ lastUsedMs: number;
136
+ /** Ref-counted retain: the donor holds one share (entry.retain); every
137
+ * clone handed out by take() holds another. */
138
+ share: { acquire(): () => void };
139
+ resident: boolean;
140
+ sessions: number;
141
+ }
142
+
143
+ export class PromptCache implements PrefixCache<Cache[], CheckpointAttachment[]> {
144
+ /** Exact tensor objects share this store's byte budget, eviction and writer.
145
+ * Empty token history denotes no conversation; the private namespace is the
146
+ * complete object identity. No fabricated language tokens enter the index. */
147
+ readonly #objects: import("../contracts/portable/object-cache").ObjectCache<CheckpointAttachment[]> = {
148
+ take: key => this.#takeObject(`object:${key}`),
149
+ put: (key, value) => this.put([], [], `object:${key}`, undefined, value),
150
+ };
151
+
152
+ get objects(): import("../contracts/portable/object-cache").ObjectCache<CheckpointAttachment[]> | undefined {
153
+ return this.maxBytes > 0 || this.#cold ? this.#objects : undefined;
154
+ }
155
+
156
+ async #takeObject(ns: string): Promise<{ value: CheckpointAttachment[]; dispose(): void } | null> {
157
+ let entry = this.findExact([], ns);
158
+ if (entry) this.objectHits++;
159
+ else this.objectMisses++;
160
+ if (!entry) {
161
+ const cold = this.#cold;
162
+ const hit = cold?.findExact?.([], ns);
163
+ if (!hit || !cold?.restoreObjectAsync) return null;
164
+ const epoch = this.#epoch;
165
+ const loaded = await cold.restoreObjectAsync(hit.handle);
166
+ if (!loaded) return null;
167
+ this.objectRestores++;
168
+ if (epoch !== this.#epoch) { this.#disposeEntry({ ...loaded, ns }, false); return null; }
169
+ // Another request may have published this immutable object during IO.
170
+ entry = this.findExact([], ns);
171
+ if (entry) this.#disposeEntry({ ...loaded, ns }, false);
172
+ else {
173
+ this.#publishingPrefetch = true;
174
+ try { this.put([], loaded.caches, ns, loaded.retain, loaded.attachments); }
175
+ catch (error) { this.#disposeEntry({ ...loaded, ns }, false); throw error; }
176
+ finally { this.#publishingPrefetch = false; }
177
+ entry = this.findExact([], ns);
178
+ }
179
+ }
180
+ if (!entry) return null;
181
+ const borrowed = this.#borrow(this.#records.get(entry)!, 0, ns);
182
+ try { this.evictToBudget(); this.reclaim(); }
183
+ catch (error) { this.#disposeEntry(borrowed, false); throw error; }
184
+ let released = false;
185
+ return { value: borrowed.attachments ?? [], dispose: () => {
186
+ if (released) return; released = true;
187
+ this.#disposeEntry(borrowed, false);
188
+ } };
189
+ }
190
+
191
+ readonly maxBytes: number;
192
+ #entries: EntryRecord[] = [];
193
+ #clock = 0;
194
+ #sessions = new Map<string, Map<string, EntryRecord>>();
195
+ #records = new WeakMap<PromptCacheEntry, EntryRecord>();
196
+ sessionHits = 0;
197
+ sessionMisses = 0;
198
+ prefixScans = 0;
199
+ objectHits = 0;
200
+ objectMisses = 0;
201
+ objectRestores = 0;
202
+
203
+ closeSession(sessionId: string): void {
204
+ for (const record of this.#sessions.get(sessionId)?.values() ?? []) record.sessions--;
205
+ this.#sessions.delete(sessionId);
206
+ }
207
+
208
+ #associate(sessionId: string | undefined, ns: string, record: EntryRecord): void {
209
+ if (!sessionId) return;
210
+ let entries = this.#sessions.get(sessionId);
211
+ if (!entries) this.#sessions.set(sessionId, entries = new Map());
212
+ const old = entries.get(ns);
213
+ if (old === record) return;
214
+ if (old) old.sessions--;
215
+ entries.set(ns, record); record.sessions++;
216
+ }
217
+
218
+ #sessionRecord(prompt: number[], ns: string, sessionId?: string): EntryRecord | undefined {
219
+ const record = sessionId ? this.#sessions.get(sessionId)?.get(ns) : undefined;
220
+ // A session labels affinity. Full-history callers may edit or branch;
221
+ // incompatible history uses ordinary lookup, never a request refusal.
222
+ return record && record.entry.tokens.length < prompt.length &&
223
+ commonPrefixLength(record.entry.tokens, prompt) === record.entry.tokens.length ? record : undefined;
224
+ }
225
+
226
+ #coldHit(prompt: number[], ns: string, sessionId?: string) {
227
+ const known = this.#sessionRecord(prompt, ns, sessionId);
228
+ return (known && this.#cold?.findExact?.(known.entry.tokens, ns)) || this.#cold?.find(prompt, ns) || null;
229
+ }
230
+
231
+ hits = 0;
232
+ misses = 0;
233
+ demotions = 0;
234
+ /** Cold-tier spill sinks (see SpillSink): spillOwned gets clones and
235
+ * runs the write off-lock; spillSync gets the live entry inline.
236
+ * Failures are the sink's problem; eviction proceeds regardless. */
237
+ readonly #spillOwned: ((entry: PromptCacheEntry) => void) | null;
238
+ readonly #spillSync: ((entry: PromptCacheEntry) => void) | null;
239
+ /** Optional cold tier: take() tiers over it, demoteIdle() spills into it. */
240
+ readonly #cold: ColdTier | null;
241
+ /** Observer after publication, independent of persistence policy. */
242
+ onPut: ((tokens: number[], ns: string) => void) | null = null;
243
+ /** The backend measures pressure; this store chooses which optional
244
+ * snapshots to release. No storage I/O runs at a reclaim boundary. */
245
+ pressure: { overBudget(): boolean; releasePending?(): void } | null = null;
246
+ /** Tier policy: a pending write is not a lower-tier copy. This controls
247
+ * eviction only; persistence completion does not itself remove an entry. */
248
+ canEvict: ((entry: PromptCacheEntry) => boolean) | null = null;
249
+ protected promoteRestores = false;
250
+ retention: RetentionPolicy = new LruRetention();
251
+ #prefetches = new Map<unknown, Promise<PromptCacheEntry | null>>();
252
+ #pins = new Map<PromptCacheEntry, number>();
253
+ #publishingPrefetch = false;
254
+ #epoch = 0;
255
+
256
+ /** Start cold reads during request preparation, coalescing identical IO.
257
+ * Active request interest keeps a just-restored donor resident until the
258
+ * caller has consumed it. Cancellation releases interest through the lease. */
259
+ async prefetch(prompt: number[], ns = "", sessionId?: string): Promise<() => void> {
260
+ const known = this.#sessionRecord(prompt, ns, sessionId);
261
+ if (known?.resident) return this.#pin(known.entry);
262
+ const cold = this.#cold;
263
+ const hit = cold?.restoreAsync ? this.#coldHit(prompt, ns, sessionId) : null;
264
+ if (!cold?.restoreAsync || !hit || (!known && hit.prefixLen <= this.peekPrefixLen(prompt, ns))) return () => {};
265
+ let pending = this.#prefetches.get(hit.handle);
266
+ if (!pending) {
267
+ const epoch = this.#epoch;
268
+ pending = (async () => {
269
+ const loaded = await cold.restoreAsync!(hit.handle);
270
+ if (!loaded) return null;
271
+ if (epoch !== this.#epoch) { this.#disposeEntry({ ...loaded, ns }, false); return null; }
272
+ this.#publishingPrefetch = true;
273
+ try { this.put(loaded.tokens, loaded.caches, ns, loaded.retain, loaded.attachments); }
274
+ catch (error) { this.#disposeEntry({ ...loaded, ns }, false); throw error; }
275
+ finally { this.#publishingPrefetch = false; }
276
+ return this.findExact(loaded.tokens, ns);
277
+ })();
278
+ this.#prefetches.set(hit.handle, pending);
279
+ }
280
+ let entry: PromptCacheEntry | null;
281
+ try { entry = await pending; }
282
+ finally { if (this.#prefetches.get(hit.handle) === pending) this.#prefetches.delete(hit.handle); }
283
+ if (!entry) return () => {};
284
+ this.#associate(sessionId, ns, this.#records.get(entry)!);
285
+ return this.#pin(entry);
286
+ }
287
+
288
+ #pin(entry: PromptCacheEntry): () => void {
289
+ this.#pins.set(entry, (this.#pins.get(entry) ?? 0) + 1);
290
+ let released = false;
291
+ return () => {
292
+ if (released) return; released = true;
293
+ const count = this.#pins.get(entry!) ?? 1;
294
+ if (count === 1) this.#pins.delete(entry!); else this.#pins.set(entry!, count - 1);
295
+ this.evictToBudget(); this.reclaim();
296
+ };
297
+ }
298
+
299
+ #victim(): EntryRecord | undefined {
300
+ const candidates = this.#entries.filter(record => !this.#pins.has(record.entry));
301
+ const unrelated = candidates.filter(record => record.sessions === 0);
302
+ return candidates.length ? this.retention.victim(unrelated.length ? unrelated : candidates) : undefined;
303
+ }
304
+
305
+ evictToBudget(): void {
306
+ if (this.#publishingPrefetch) return;
307
+ while (this.totalBytes > this.maxBytes) {
308
+ if (!this.#entries.length) break;
309
+ const oldest = this.#victim();
310
+ if (!oldest) break;
311
+ if (this.canEvict && !this.canEvict(oldest.entry)) break;
312
+ this.retention.evicted(oldest);
313
+ this.#entries = this.#entries.filter(r => r !== oldest);
314
+ try { this.#disposeEntry(oldest.entry, !this.canEvict); }
315
+ catch (error) { console.warn(`[prompt-cache] entry cleanup failed: ${error}`); }
316
+ }
317
+ }
318
+
319
+ reclaim(): void {
320
+ const pressure = this.pressure;
321
+ if (!pressure?.overBudget()) return;
322
+ pressure.releasePending?.();
323
+ while (this.#entries.length && pressure.overBudget()) {
324
+ const oldest = this.#victim();
325
+ if (!oldest) break;
326
+ if (this.canEvict && !this.canEvict(oldest.entry)) break;
327
+ this.retention.evicted(oldest);
328
+ this.#entries = this.#entries.filter(r => r !== oldest);
329
+ this.#disposeEntry(oldest.entry, false);
330
+ this.demotions++;
331
+ }
332
+ }
333
+
334
+ /** Zero-copy view cloner — injectable so model-free tests can stub it. */
335
+ readonly #clone: (caches: Cache[]) => Cache[];
336
+
337
+ constructor(
338
+ maxBytes: number,
339
+ // Bare function = legacy sync spill (tests, simple embedders).
340
+ spill: SpillSink | ((entry: PromptCacheEntry) => void) | null = null,
341
+ cold: ColdTier | null = null,
342
+ clone: (caches: Cache[]) => Cache[] = cloneKvCaches,
343
+ private readonly materialize: (caches: Cache[], attachments?: CheckpointAttachment[]) => void = materializeCheckpoint,
344
+ ) {
345
+ this.maxBytes = maxBytes;
346
+ this.#spillOwned = typeof spill === "function" ? null : spill?.spillOwned ?? null;
347
+ this.#spillSync = typeof spill === "function" ? spill : spill?.spillSync ?? null;
348
+ this.#cold = cold;
349
+ this.#clone = clone;
350
+ }
351
+
352
+ #disposeEntry(entry: PromptCacheEntry, spillFirst: boolean): void {
353
+ const record = this.#records.get(entry);
354
+ if (record) record.resident = false;
355
+ if (spillFirst) {
356
+ if (this.#spillOwned) {
357
+ // NON-BLOCKING spill: clone BEFORE disposing (clones are zero-copy
358
+ // views of the live arrays) and hand ownership to the sink, which
359
+ // flushes off the generation lock. The underlying GPU buffers stay
360
+ // alive until the sink disposes the clones — for demoteIdle that
361
+ // means the memory is freed one bounded write-chain hop later, not
362
+ // instantly; acceptable because the chain is serial and each write
363
+ // disposes its clones on settle (fulfill AND reject).
364
+ // Retain contract: entry.retain still runs below, possibly while
365
+ // the clones are in flight — safe because cold-restored entries
366
+ // OWN their bytes (streamed copy-restore, 2026-07-07): there is
367
+ // no backing mmap for retain to unmap, so nothing the clones
368
+ // could alias goes away.
369
+ let clones: Cache[] | null = null;
370
+ let attachments: CheckpointAttachment[] | undefined;
371
+ try {
372
+ clones = this.#clone(entry.caches);
373
+ attachments = cloneAttachments(entry.attachments);
374
+ this.#spillOwned({ tokens: [...entry.tokens], caches: clones, ns: entry.ns, attachments });
375
+ } catch {
376
+ // Failure degrades to no-spill; eviction/demotion never unwinds.
377
+ if (clones) for (const c of clones) try { c.dispose(); } catch {}
378
+ try { disposeAttachments(attachments); } catch {}
379
+ }
380
+ } else {
381
+ const spill =
382
+ this.#spillSync ??
383
+ (this.#cold
384
+ ? (e: PromptCacheEntry) => this.#cold!.store(e.tokens, e.caches, e.ns, e.attachments)
385
+ : null);
386
+ if (spill) try { spill(entry); } catch {}
387
+ }
388
+ }
389
+ disposeResources([...entry.caches, { dispose: () => disposeAttachments(entry.attachments) }]);
390
+ entry.retain?.(); // backing release requires successful cache disposal
391
+ }
392
+
393
+ get totalBytes(): number {
394
+ return this.#entries.reduce((a, e) => a + e.bytes, 0);
395
+ }
396
+
397
+ get size(): number {
398
+ return this.#entries.length;
399
+ }
400
+
401
+ /** Release LRU entries until the caller's live-memory check is satisfied.
402
+ * Unlike background eviction, pressure relief must finish the SSD write
403
+ * and release the buffers now, not enqueue clones that keep them alive.
404
+ * Call under the engine lock, after taking the request's reusable prefix. */
405
+ relievePressure(overBudget: () => boolean): number {
406
+ let count = 0;
407
+ while (this.#entries.length && overBudget()) {
408
+ const victim = this.#victim();
409
+ if (!victim) break;
410
+ const oldest = this.#entries.indexOf(victim);
411
+ const { entry } = victim;
412
+ // Keep the entry resident if persistence throws. A failed SSD write
413
+ // must not be presented as a successful demotion.
414
+ if (this.#cold) {
415
+ if (this.#cold.store(entry.tokens, entry.caches, entry.ns, entry.attachments) === false) break;
416
+ }
417
+ else this.#spillSync?.(entry);
418
+ this.retention.evicted(victim);
419
+ this.#entries.splice(oldest, 1);
420
+ this.#disposeEntry(entry, false);
421
+ this.demotions++;
422
+ count++;
423
+ }
424
+ return count;
425
+ }
426
+
427
+ /** Find the entry with the longest usable common prefix of `prompt`
428
+ * across BOTH tiers. Every RAM entry lends zero-copy views and retains its
429
+ * donor, including exact-boundary recurrent state and method attachments.
430
+ * The caller owns the returned views —
431
+ * dispose them or put() an extended entry, honoring `retain` either way.
432
+ *
433
+ * Usable prefix = common prefix capped at prompt.length - 1 (at least
434
+ * one token must be forwarded to produce logits). Entries longer than
435
+ * the prefix need cache.trim(); ring caches lose trimability once
436
+ * wrapped — those entries only match in full.
437
+ *
438
+ * Tier order: the cold tier wins only with a STRICTLY longer usable
439
+ * prefix (restore reads storage and materializes native arrays). A
440
+ * cold entry with an untrimmable divergent tail (ring post-wrap, SSM)
441
+ * is dropped and the RAM candidate (or a fresh prefill) serves.
442
+ * hits/misses count RAM candidacy only (cold restores are counted by
443
+ * the tier itself), preserving the /stats meaning. */
444
+ take(prompt: number[], ns = "", sessionId?: string): PromptCacheEntry | null {
445
+ const hit = this.#take(prompt, ns, sessionId);
446
+ // Keep the chosen state's owned views before releasing optional donors.
447
+ // This also covers a cold restore materializing its bytes at admission.
448
+ try { this.reclaim(); return hit; }
449
+ catch (error) {
450
+ if (hit) this.#disposeEntry(hit, false);
451
+ throw error;
452
+ }
453
+ }
454
+
455
+ #take(prompt: number[], ns: string, sessionId?: string): PromptCacheEntry | null {
456
+ const known = this.#sessionRecord(prompt, ns, sessionId);
457
+ if (known?.resident) {
458
+ this.sessionHits++; this.hits++;
459
+ return this.#borrow(known, known.entry.tokens.length, ns);
460
+ }
461
+ if (sessionId) this.sessionMisses++;
462
+ this.prefixScans++;
463
+ let bestIdx = -1;
464
+ let bestLen = 0;
465
+ for (let i = 0; i < this.#entries.length; i++) {
466
+ const e = this.#entries[i]!.entry;
467
+ if (e.ns !== ns) continue;
468
+ const p = Math.min(commonPrefixLength(e.tokens, prompt), prompt.length - 1);
469
+ if (p <= bestLen || p < minimumReusableOffset(e.caches)) continue;
470
+ const trimNeeded = e.tokens.length - p;
471
+ if (trimNeeded > 0 && (!!e.attachments?.length || !e.caches.every((c) => c.isTrimmable()))) continue;
472
+ bestLen = p;
473
+ bestIdx = i;
474
+ }
475
+ if (bestIdx === -1) this.misses++;
476
+ else this.hits++;
477
+
478
+ // Cold tier: strictly longer usable prefix ⇒ restore + trim; the RAM
479
+ // candidate stays put UNTOUCHED (the old serial-lane flow trimmed it
480
+ // before comparing — a needless entry degradation, gone now).
481
+ if (this.#cold) {
482
+ const hit = this.#coldHit(prompt, ns, sessionId);
483
+ if (hit && hit.prefixLen > bestLen) {
484
+ const loaded = this.#cold.restore(hit.handle);
485
+ if (loaded) {
486
+ const owner = ownResource(loaded, (entry) =>
487
+ this.#disposeEntry({ ...entry, ns }, false));
488
+ const result = withResource(owner, (entry) => {
489
+ const trimNeeded = entry.tokens.length - hit.prefixLen;
490
+ if (hit.prefixLen < minimumReusableOffset(entry.caches)) return null;
491
+ if (trimNeeded > 0 && (!!entry.attachments?.length || !entry.caches.every((cache) => cache.isTrimmable()))) {
492
+ console.warn(
493
+ `[prompt-cache] cold entry unusable: ${entry.tokens.length} tokens, ` +
494
+ `needs trim ${trimNeeded} but untrimmable (wrapped ring/SSM) — re-prefilling`,
495
+ );
496
+ return null;
497
+ }
498
+ if (trimNeeded > 0) for (const cache of entry.caches) cache.trim(trimNeeded);
499
+ const tokens = entry.tokens.slice(0, hit.prefixLen);
500
+ return { ...owner.transfer(), tokens, ns };
501
+ });
502
+ if (result) {
503
+ if (this.promoteRestores) {
504
+ // Cache the restored prefix immediately. A second request can
505
+ // reuse RAM even before this borrower finishes its generation.
506
+ const share = makeSharedRetain(result.retain);
507
+ result.retain = share.acquire();
508
+ const retained = share.acquire();
509
+ let clones: Cache[] = [];
510
+ let attachments: CheckpointAttachment[] | undefined;
511
+ try {
512
+ clones = this.#clone(result.caches);
513
+ attachments = cloneAttachments(result.attachments);
514
+ this.put([...result.tokens], clones, ns, retained, attachments, sessionId);
515
+ } catch (error) {
516
+ cleanupFailure(error, () => disposeResources([...clones,
517
+ { dispose: () => disposeAttachments(attachments) },
518
+ { dispose: retained }, { dispose: () => this.#disposeEntry(result, false) }]));
519
+ }
520
+ }
521
+ return result;
522
+ }
523
+ }
524
+ }
525
+ }
526
+
527
+ if (bestIdx === -1) return null;
528
+ // PREFIX SHARING (non-consuming serve): the caller gets zero-copy
529
+ // CLONES of the donor's caches, trimmed to the matched prefix; the
530
+ // donor stays in the cache untouched, ready to serve the next agent /
531
+ // session with the same prefix. Safe because mlx cache updates are
532
+ // FUNCTIONAL (updateAndFetch reassigns fresh arrays; buffer donation
533
+ // only fires at refcount 1, and the donor always holds a ref), so a
534
+ // clone extending itself can never mutate the donor's bytes. The old
535
+ // consume-and-trim semantics CANNIBALIZED donors: agent B borrowing
536
+ // agent A's 2k system prompt destroyed A's 10k entry to do it.
537
+ const rec = this.#entries[bestIdx]!;
538
+ this.#associate(sessionId, ns, rec);
539
+ return this.#borrow(rec, bestLen, ns);
540
+ }
541
+
542
+ #borrow(rec: EntryRecord, bestLen: number, ns: string): PromptCacheEntry {
543
+ rec.lastUsed = ++this.#clock;
544
+ rec.lastUsedMs = Date.now();
545
+ rec.uses++; rec.cost = bestLen; this.retention.accessed(rec);
546
+ // Lend immutable views for every backend state. RAM retention and SSD
547
+ // durability remain cache responsibilities while the caller extends its
548
+ // own views. This keeps synchronous persistence out of a RAM hit.
549
+ const clones = this.#clone(rec.entry.caches);
550
+ let attachments: CheckpointAttachment[] | undefined;
551
+ const trimNeeded = rec.entry.tokens.length - bestLen;
552
+ try {
553
+ attachments = cloneAttachments(rec.entry.attachments);
554
+ if (trimNeeded > 0) for (const c of clones) c.trim(trimNeeded);
555
+ } catch (error) {
556
+ cleanupFailure(error, () => disposeResources([...clones, { dispose: () => disposeAttachments(attachments) }]));
557
+ }
558
+ return {
559
+ tokens: rec.entry.tokens.slice(0, bestLen),
560
+ caches: clones,
561
+ attachments,
562
+ ns,
563
+ retain: rec.share.acquire(),
564
+ };
565
+ }
566
+
567
+ /** Longest usable prefix length available for `prompt` WITHOUT taking
568
+ * (the SSD tier compares this against its own best before restoring). */
569
+ peekPrefixLen(prompt: number[], ns = ""): number {
570
+ let best = 0;
571
+ for (const { entry: e } of this.#entries) {
572
+ if (e.ns !== ns) continue;
573
+ const p = Math.min(commonPrefixLength(e.tokens, prompt), prompt.length - 1);
574
+ if (p <= best || p < minimumReusableOffset(e.caches)) continue;
575
+ const trimNeeded = e.tokens.length - p; // same usability rule as take()
576
+ if (trimNeeded > 0 && (!!e.attachments?.length || !e.caches.every((c) => c.isTrimmable()))) continue;
577
+ best = p;
578
+ }
579
+ return best;
580
+ }
581
+
582
+ /** Read-only lookup of an immutable, cache-owned checkpoint. */
583
+ findExact(tokens: number[], ns = ""): PromptCacheEntry | null {
584
+ for (const { entry: e } of this.#entries) {
585
+ if (e.ns !== ns || e.tokens.length !== tokens.length) continue;
586
+ if (commonPrefixLength(e.tokens, tokens) === tokens.length) return e;
587
+ }
588
+ return null;
589
+ }
590
+
591
+ /** Insert (or reinsert) an entry; evicts LRU entries over the byte cap
592
+ * (spilling each to the cold tier first, when one is attached). If the
593
+ * entry itself exceeds the cap it is spilled + disposed, not stored. */
594
+ put(tokens: number[], caches: Cache[], ns = "", retain?: () => void, attachments?: CheckpointAttachment[], sessionId?: string): void {
595
+ // All cache-provided validation runs before ownership is adopted.
596
+ const bytes = cacheBytes(caches) + attachmentBytes(attachments);
597
+ const trimmable = !attachments?.length && caches.every((cache) => cache.isTrimmable());
598
+ // Storage takes resolved snapshots, independently of their producer.
599
+ this.materialize(caches, attachments);
600
+ const discard = (entry: PromptCacheEntry, spill: boolean) => {
601
+ try { this.#disposeEntry(entry, spill); }
602
+ catch (error) {
603
+ // The new entry is already owned here. Throwing back to its previous
604
+ // owner would invite disposal of state still held by this store.
605
+ console.warn(`[prompt-cache] entry cleanup failed: ${error}`);
606
+ }
607
+ };
608
+ const entry: PromptCacheEntry = { tokens, caches, ns, retain, attachments };
609
+ if (bytes > this.maxBytes && !this.canEvict) {
610
+ discard(entry, true);
611
+ return;
612
+ }
613
+ // The donor's retain becomes ref-counted so take()'s clones can hold
614
+ // shares; the entry's own retain is the donor's share.
615
+ const share = makeSharedRetain(retain);
616
+ entry.retain = share.acquire();
617
+ const rec: EntryRecord = { entry, bytes, cost: tokens.length, uses: 1, priority: 0, lastUsed: ++this.#clock, lastUsedMs: Date.now(), share, resident: true, sessions: 0 };
618
+ this.retention.accessed(rec);
619
+ this.#entries.push(rec);
620
+ this.#records.set(entry, rec);
621
+ this.#associate(sessionId, ns, rec);
622
+ // Exact duplicates (equal tokens) are redundant REGARDLESS of
623
+ // trimmability — the new entry serves exactly the matches the old one
624
+ // did (mlx-lm's trie replaces in place). Without this, every gemma ctx
625
+ // repeat left another full-size wrapped-ring entry behind (measured
626
+ // ~0.4 GB retained per repeat on the 12B bench).
627
+ for (const old of [...this.#entries]) {
628
+ if (old === rec || old.entry.ns !== ns) continue;
629
+ if (old.entry.tokens.length !== tokens.length) continue;
630
+ if (commonPrefixLength(old.entry.tokens, tokens) !== tokens.length) continue;
631
+ // Equivalent publication replaces the cache object, not the checkpoint
632
+ // identity. Every session sharing it follows the replacement.
633
+ if (old.sessions) for (const sessions of this.#sessions.values()) {
634
+ if (sessions.get(ns) !== old) continue;
635
+ sessions.set(ns, rec); old.sessions--; rec.sessions++;
636
+ }
637
+ this.#entries = this.#entries.filter((r) => r !== old);
638
+ discard(old.entry, false);
639
+ }
640
+ // SUPERSEDE strict prefix-ancestors (the SSD store's rule, brought to
641
+ // RAM — prefix sharing would otherwise grow an entry per turn): same-ns
642
+ // entries whose tokens are a prefix of the new entry are redundant
643
+ // and disposed WITHOUT spill (the new entry serves their prefixes and
644
+ // its own write-behind persists them) — but ONLY when the new entry's
645
+ // caches are all trimmable. An untrimmable new entry (wrapped ring)
646
+ // can only serve exact-length matches, so shorter ancestors — e.g.
647
+ // the prompt-boundary snapshot that makes drifted multi-turn agents
648
+ // hit — must survive.
649
+ if (trimmable) {
650
+ for (const old of [...this.#entries]) {
651
+ if (old === rec || old.entry.ns !== ns) continue;
652
+ if (old.entry.tokens.length >= tokens.length || old.entry.tokens.length < minimumReusableOffset(caches)) continue;
653
+ if (commonPrefixLength(old.entry.tokens, tokens) !== old.entry.tokens.length) continue;
654
+ this.#entries = this.#entries.filter((r) => r !== old);
655
+ discard(old.entry, false);
656
+ }
657
+ }
658
+ this.evictToBudget();
659
+ try { this.onPut?.(tokens, ns); } catch {}
660
+ }
661
+
662
+ /** Evict entries older than idleMs. Tiered residency policy first ensures
663
+ * SSD coverage. No-op without a cold tier. Returns entries removed from RAM. */
664
+ demoteIdle(idleMs: number, now = Date.now()): number {
665
+ if (!this.#cold) return 0;
666
+ let n = 0;
667
+ for (const rec of [...this.#entries]) {
668
+ if (now - rec.lastUsedMs < idleMs || this.#pins.has(rec.entry)) continue;
669
+ if (this.canEvict && !this.canEvict(rec.entry)) continue;
670
+ this.#entries = this.#entries.filter((r) => r !== rec);
671
+ this.#disposeEntry(rec.entry, !this.canEvict); // persisted entries need no duplicate write
672
+ this.demotions++;
673
+ n++;
674
+ }
675
+ return n;
676
+ }
677
+
678
+ clear(): void {
679
+ this.#sessions.clear();
680
+ this.#epoch++;
681
+ const entries = this.#entries;
682
+ this.#entries = [];
683
+ disposeResources(entries.map(({ entry }) => ({ dispose: () => this.#disposeEntry(entry, false) })));
684
+ }
685
+ }