@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,430 @@
1
+ // Batched sliding-window (rotating) KV cache for `--batch N` serving — the
2
+ // hard half of dynamic-B that BatchedDecodeMaskCache (full-attention) doesn't
3
+ // cover. Faithful port of mlx-lm's BatchRotatingKVCache (models/cache.py):
4
+ // a shared ring buffer whose rotation state (_idx / _offset / rotated) is
5
+ // SCALAR across the batch (all rows advance one token per step, so the write
6
+ // column is the same for every row), with PER-ROW `offset` (absolute position,
7
+ // drives RoPE) and `leftPad` (padding columns in the buffer, masked out).
8
+ //
9
+ // Includes merge, one-token writes, block prefill, padding finalization, masks
10
+ // and row extraction. Prefill preparation is a storage capability; the serving
11
+ // scheduler's use of whole prompt cohorts is a separate concern.
12
+ // Positions (offset/leftPad/_idx/_offset/
13
+ // rotated) are tracked on the HOST (small deterministic ints) so make_mask is
14
+ // built in JS like buildBatchedDecodeMask; only K/V live on device.
15
+ //
16
+ // The genuinely tricky bit is make_mask once the ring wraps: the buffer is no
17
+ // longer in temporal order, so the causal+window+padding mask is built in
18
+ // temporal coordinates then ROLLED to the ring's physical layout — see
19
+ // buildBatchedRotatingMask (a column-by-column port of mlx-lm make_mask,
20
+ // including the `roll(shift=idx+1)`). Gated model-free against mlx-lm
21
+ // (`02d723a:tests/unit/batched-rotating.test.ts`) + end-to-end vs a long-context Gemma oracle.
22
+
23
+ import { MlxArray } from "@mlx-bun/mlx/array";
24
+ import * as ops from "@mlx-bun/mlx/ops";
25
+ import { RotatingKVCache } from "./rotating-kv";
26
+ import { type AttentionCache, type AttentionRead, type BatchableCache, type Cache, type Mask, type PaddedPrefillCache, type PrefillPadding } from "../contracts/mlx/cache";
27
+ import { AttentionMasks, sdpaRead, withLease, type MaskLease } from "./attention-read";
28
+ import { isRotatingPlainCache } from "./capabilities";
29
+ import { BatchedRotatingState, type RotatingPositionSnapshot } from "./batched-rotating-state";
30
+ import {
31
+ appendRotatingStorage,
32
+ mergeStorageRows,
33
+ plainRowStorage,
34
+ temporalStorageView,
35
+ rowRollIndices,
36
+ } from "./batched-row-storage";
37
+
38
+ const STEP = 256;
39
+
40
+ /** Port of BatchRotatingKVCache.make_mask for the DECODE case. Returns a
41
+ * [B,1,N,S] bool mask (S == min(maxSize-1, offsetScalar) + N) for the step
42
+ * ABOUT to be written: causal AND sliding-window AND per-row left-padding,
43
+ * rolled to the ring's physical layout once wrapped.
44
+ *
45
+ * Inputs are the PRE-write scalar ring state (`idx`=_idx, `offsetScalar`=_offset,
46
+ * `rotated`) and per-row `leftPad`. The leftPad adjustments here are LOCAL to
47
+ * the mask (mlx-lm decrements left_padding both in make_mask AND in update; the
48
+ * update's decrement is the persistent one, applied separately by the cache). */
49
+ export function buildBatchedRotatingMask(
50
+ B: number, N: number, leftPad: number[],
51
+ maxSize: number, window: number, idx: number, offsetScalar: number, rotated: boolean, block = N > 1,
52
+ ): MlxArray {
53
+ const off = Math.min(maxSize - 1, offsetScalar);
54
+ const S = off + N;
55
+
56
+ // Local (non-persisted) leftPad for the mask: trim + rotation shrink it. A
57
+ // block write first puts the ring in temporal order, so its history is the
58
+ // ring's active length (the whole ring once rotated), not the write head.
59
+ const history = block && rotated ? maxSize : idx;
60
+ const trimSize = history - maxSize + (block ? 1 : 0);
61
+ const isRot = !block && (rotated || idx >= maxSize);
62
+ const lp = leftPad.map((x) => x - (trimSize > 0 ? trimSize : 0) - (isRot ? 1 : 0));
63
+
64
+ // roll(shift): physical column = (temporal column + shift) mod S.
65
+ const shift = isRot ? (idx >= maxSize ? 0 : idx) + 1 : 0;
66
+
67
+ const data = new Int32Array(B * N * S);
68
+ for (let b = 0; b < B; b++) {
69
+ const pad = lp[b]!;
70
+ for (let i = 0; i < N; i++) {
71
+ const lind = off + i; // query temporal position
72
+ for (let j = 0; j < S; j++) {
73
+ // temporal key position j: causal, within window, past this row's pad.
74
+ const allow = lind >= j && lind < j + window && j >= pad;
75
+ if (!allow) continue;
76
+ const col = ((j + shift) % S + S) % S;
77
+ data[(b * N + i) * S + col] = 1;
78
+ }
79
+ }
80
+ }
81
+ const intArr = MlxArray.fromInt32(data, [B, 1, N, S]);
82
+ const zero = MlxArray.fromInt32(new Int32Array([0]), []);
83
+ const mask = ops.less(zero, intArr); // 0 < x → bool
84
+ zero.dispose();
85
+ intArr.dispose();
86
+ return mask;
87
+ }
88
+
89
+ /** Port of mlx-lm BatchRotatingKVCache; padded singleton chunks use its
90
+ * concat operation with the matching block mask until finalization.
91
+ *
92
+ * Reads: the sliding window is `maxSize`. Decode, which runs with no prefill
93
+ * padding pending (finalize first), writes one position in place at the ring
94
+ * head; a window writes by concatenation, which also serves padded prefill
95
+ * chunks of any length. Each attends with the stock fused SDPA under mlx-lm's
96
+ * batched rotating mask (`buildBatchedRotatingMask`) for its write, built once
97
+ * per forward through `masks`. The deprecated `updateAndFetch` picks the write
98
+ * from the length instead, so a one-row window without pending padding wrote in
99
+ * place there and concatenates here: the same bits until the ring wraps, a
100
+ * different key order after. */
101
+ export class BatchedRotatingCache implements AttentionCache, BatchableCache, PaddedPrefillCache {
102
+ keys: MlxArray | null = null;
103
+ values: MlxArray | null = null;
104
+ #rows: BatchedRotatingState;
105
+ #ropeArr: MlxArray | null = null;
106
+ #ropeForOffset = -1;
107
+ readonly maxSize: number;
108
+
109
+ constructor(
110
+ maxSize: number,
111
+ leftPad: number[],
112
+ /** The per-forward masks of the model this layout serves. */
113
+ readonly masks = new AttentionMasks(),
114
+ ) {
115
+ this.maxSize = maxSize;
116
+ this.#rows = new BatchedRotatingState(maxSize, leftPad);
117
+ }
118
+
119
+ appendDecode(k: MlxArray, v: MlxArray): AttentionRead {
120
+ return withLease(this.#mask(1, false), mask => {
121
+ const [keys, values] = this.#updateInPlace(k, v);
122
+ return sdpaRead(keys, values, mask);
123
+ });
124
+ }
125
+
126
+ appendWindow(k: MlxArray, v: MlxArray): AttentionRead {
127
+ return withLease(this.#mask(k.shape[2]!, true), mask => {
128
+ const [keys, values] = this.#updateConcat(k, v);
129
+ return sdpaRead(keys, values, mask);
130
+ });
131
+ }
132
+
133
+ /** The mask of an `N`-position write before it: `block` for concatenation,
134
+ * not for an in-place write. */
135
+ #mask(N: number, block: boolean): MaskLease {
136
+ const rows = this.#rows;
137
+ const key = `${N}|${block}|${rows.leftPad.join(",")}|${rows.ringIndex}|${rows.totalOffset}|${rows.rotated}`;
138
+ return this.masks.lease(`batched-rotating:${this.maxSize}`, key, () => ({
139
+ mode: "array",
140
+ arr: buildBatchedRotatingMask(rows.batchSize, N, rows.leftPad, this.maxSize, this.maxSize,
141
+ rows.ringIndex, rows.totalOffset, rows.rotated, block),
142
+ }));
143
+ }
144
+
145
+ /** Same signature as the serial RotatingKVCache — the scheduler's merge
146
+ * guards recognize a batched ring as "rotating-plain + RowBatchCache"
147
+ * (packages/inference/src/execution/batch-group.ts #mergeJoiner prevRot branch); without this the next
148
+ * join sees "unknown", skips the prevRot rows, and replaces the batch's
149
+ * sliding-layer KV with a one-row cache (B≥3 collapse). Guard order
150
+ * everywhere is isRowBatchCache FIRST, so the shared string never
151
+ * misroutes a batched cache into a serial-only path. */
152
+ signature(): string { return "kv:rotating-plain"; }
153
+
154
+ get positionSnapshot(): RotatingPositionSnapshot { return this.#rows.snapshot(); }
155
+ validOffset(row: number): number { return this.#rows.validOffset(row); }
156
+ preparePrefill(padding: PrefillPadding, rightPaddedBatch?: boolean): void { this.#rows.preparePrefill(padding, rightPaddedBatch); this.releaseRopeArr(); }
157
+ finalizePrefill(): void {
158
+ const shifts = this.#rows.prefillRoll();
159
+ if (shifts?.some(shift => shift !== 0) && this.keys && this.values) {
160
+ using indices = rowRollIndices(this.keys.shape[2]!, shifts);
161
+ const keys = plainRowStorage.rollRows(this.keys, indices);
162
+ let values: MlxArray;
163
+ try { values = plainRowStorage.rollRows(this.values, indices); }
164
+ catch (error) { keys.dispose(); throw error; }
165
+ this.keys.dispose(); this.values.dispose(); this.keys = keys; this.values = values;
166
+ }
167
+ this.#rows.finalizePrefill(); this.releaseRopeArr();
168
+ }
169
+ /** Adopt owned tensors without changing their physical ring columns. */
170
+ static adoptPhysical(keys: MlxArray | null, values: MlxArray | null, position: RotatingPositionSnapshot,
171
+ masks = new AttentionMasks()): BatchedRotatingCache {
172
+ const cache = new BatchedRotatingCache(position.maxSize, [...position.leftPad], masks);
173
+ cache.#rows.restore(position); cache.keys = keys; cache.values = values;
174
+
175
+ return cache;
176
+ }
177
+
178
+ get offsetArr(): number[] { return this.#rows.offsets; }
179
+ get rowOffsets(): readonly number[] { return this.#rows.offsets; }
180
+ get leftPad(): number[] { return this.#rows.leftPad; }
181
+ get batchSize(): number { return this.#rows.batchSize; }
182
+ makeEmptyBatch(): BatchedRotatingCache { return new BatchedRotatingCache(this.maxSize, [], this.masks); }
183
+ /** Bytes one row holds for `tokens` more positions, capped at the window. */
184
+ projectedBytes(tokens: number): number {
185
+ if (!this.keys || !this.values || !this.batchSize) return 0;
186
+ return (this.keys.nbytes + this.values.nbytes) / (this.batchSize * this.keys.shape[2]!) * Math.min(tokens, this.maxSize);
187
+ }
188
+
189
+ /** Current batch size — tracks filter() (which shrinks the per-row arrays). */
190
+ get #B(): number {
191
+ return this.#rows.batchSize;
192
+ }
193
+
194
+ /** mlx-lm uses `cache.offset` (the per-row array) for the scalar interface
195
+ * too; we expose the scalar total as `offset` and the per-row positions via
196
+ * `ropeOffsetArr` (the model's per-row RoPE path). */
197
+ get offset(): number {
198
+ return this.#rows.totalOffset;
199
+ }
200
+
201
+ get ropeOffsetArr(): MlxArray {
202
+ if (this.#ropeArr && this.#ropeForOffset === this.offset) return this.#ropeArr;
203
+ this.#ropeArr?.dispose();
204
+ this.#ropeArr = MlxArray.fromInt32(Int32Array.from(this.offsetArr), [this.#B]);
205
+ this.#ropeForOffset = this.offset;
206
+ return this.#ropeArr;
207
+ }
208
+
209
+ makeMask(N: number, windowSize: number | null): Mask {
210
+ const window = windowSize ?? this.maxSize;
211
+ return {
212
+ mode: "array",
213
+ arr: buildBatchedRotatingMask(
214
+ this.#B, N, this.leftPad, this.maxSize, window,
215
+ this.#rows.ringIndex, this.offset, this.#rows.rotated, N > 1 || this.#rows.hasPendingPadding,
216
+ ),
217
+ };
218
+ }
219
+
220
+ updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
221
+ if (k.shape[2] !== 1 || this.#rows.hasPendingPadding) return this.#updateConcat(k, v);
222
+ return this.#updateInPlace(k, v);
223
+ }
224
+
225
+ /** N=1 decode update — port of _update_in_place. */
226
+ #updateInPlace(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
227
+ const [B, H, S, D] = k.shape as [number, number, number, number];
228
+ const vD = v.shape[3]!;
229
+ const prev = this.offset;
230
+
231
+ // Grow the buffer (in STEP chunks) until it reaches maxSize.
232
+ if (!this.keys || (prev >= this.keys.shape[2]! && this.keys.shape[2]! < this.maxSize)) {
233
+ const newSize = Math.min(STEP, this.maxSize - prev);
234
+ const newK = ops.zeros([B, H, newSize, D], k.dtype);
235
+ const newV = ops.zeros([B, H, newSize, vD], v.dtype);
236
+ if (this.keys && this.values) {
237
+ const ck = ops.concatAxis([this.keys, newK], 2);
238
+ const cv = ops.concatAxis([this.values, newV], 2);
239
+ for (const a of [this.keys, this.values, newK, newV]) a.dispose();
240
+ this.keys = ck;
241
+ this.values = cv;
242
+ } else {
243
+ this.keys = newK;
244
+ this.values = newV;
245
+ }
246
+ this.#rows.markGrown(prev);
247
+ }
248
+
249
+ // Trim any overshoot past maxSize (decrements left padding persistently).
250
+ const trimSize = this.keys!.shape[2]! - this.maxSize;
251
+ if (trimSize > 0) {
252
+ const tk = this.keys!.slice([0, 0, trimSize, 0], this.keys!.shape as number[]);
253
+ const tv = this.values!.slice([0, 0, trimSize, 0], this.values!.shape as number[]);
254
+ this.keys!.dispose();
255
+ this.values!.dispose();
256
+ this.keys = tk;
257
+ this.values = tv;
258
+ this.#rows.trimOvershoot(trimSize);
259
+ }
260
+
261
+ // Rotate when the write head reaches the end of the ring.
262
+ const writeIndex = this.#rows.beginWrite(S);
263
+
264
+ // Write the new K/V at the ring head.
265
+ const [, , SK, DK] = this.keys!.shape as [number, number, number, number];
266
+ const k2 = ops.sliceUpdate(this.keys!, k, [0, 0, writeIndex, 0], [B, H, writeIndex + S, DK]);
267
+ const v2 = ops.sliceUpdate(this.values!, v, [0, 0, writeIndex, 0], [B, H, writeIndex + S, vD]);
268
+ this.keys!.dispose();
269
+ this.values!.dispose();
270
+ this.keys = k2;
271
+ this.values = v2;
272
+ this.#rows.commitWrite(S);
273
+
274
+ // Return the populated prefix (ring not yet full) or the whole buffer.
275
+ if (this.offset < this.maxSize) {
276
+ return [
277
+ this.keys.slice([0, 0, 0, 0], [B, H, this.offset, DK]),
278
+ this.values.slice([0, 0, 0, 0], [B, H, this.offset, vD]),
279
+ ];
280
+ }
281
+ return [
282
+ this.keys.slice([0, 0, 0, 0], [B, H, SK, DK]),
283
+ this.values.slice([0, 0, 0, 0], [B, H, SK, vD]),
284
+ ];
285
+ }
286
+
287
+ #updateConcat(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
288
+ const keys = appendRotatingStorage(plainRowStorage, this.keys, k, this.#rows);
289
+ let values: MlxArray;
290
+ try { values = appendRotatingStorage(plainRowStorage, this.values, v, this.#rows); }
291
+ catch (error) { keys.dispose(); throw error; }
292
+ const history = this.keys ? this.#rows.activeLength : 0;
293
+ this.keys?.dispose(); this.values?.dispose();
294
+ this.keys = keys; this.values = values;
295
+ this.#rows.commitConcat(k.shape[2]!, history);
296
+ return [
297
+ plainRowStorage.slice(keys, 0, this.#B, 0, this.#rows.ringIndex),
298
+ plainRowStorage.slice(values, 0, this.#B, 0, this.#rows.ringIndex),
299
+ ];
300
+ }
301
+
302
+ /** Free the per-step RoPE array without disposing KV (wrapper-rebuild path). */
303
+ releaseRopeArr(): void {
304
+ this.#ropeArr?.dispose();
305
+ this.#ropeArr = null;
306
+ this.#ropeForOffset = -1;
307
+ }
308
+
309
+ /** Ring contents in temporal order, cut to the valid length (extract). */
310
+ temporalView(): [MlxArray, MlxArray] {
311
+ if (!this.keys || !this.values) throw new Error("cache is empty");
312
+ return [
313
+ temporalStorageView(plainRowStorage, this.keys, this.#rows, {
314
+ from: Math.max(0, this.#rows.activeLength - this.maxSize), to: this.#rows.activeLength,
315
+ }),
316
+ temporalStorageView(plainRowStorage, this.values, this.#rows, {
317
+ from: Math.max(0, this.#rows.activeLength - this.maxSize), to: this.#rows.activeLength,
318
+ }),
319
+ ];
320
+ }
321
+
322
+ /** mlx-lm `BatchRotatingKVCache.extract` (models/cache.py:1417): row `i`
323
+ * as a fresh SERIAL RotatingKVCache — de-rolled to temporal order (the
324
+ * oracle's roll(-_idx) when rotated), left padding stripped
325
+ * (`max(0, left_padding[idx])` — post-wrap pads go negative), OWNED
326
+ * contiguous copies. `offset` = the row's absolute position (may exceed
327
+ * the buffer: wrapped rings have evicted tokens); ring idx = the new
328
+ * buffer length — temporal order ⇔ write head at the end, exactly the
329
+ * oracle's `cache._idx = cache.keys.shape[2]`. Bit-exact vs a solo run:
330
+ * merge/decode/filter keep each row's ring bytes identical to the serial
331
+ * cache's (`02d723a:tests/unit/batched-rotating.test.ts`) and this is a pure
332
+ * slice+copy. */
333
+ extractRow(i: number, limit?: number): RotatingKVCache {
334
+ if (!this.keys || !this.values) return new RotatingKVCache(this.maxSize);
335
+ const pad = Math.max(0, this.leftPad[i]!, limit === undefined ? 0 : this.#rows.activeLength - limit);
336
+ const c = new RotatingKVCache(this.maxSize);
337
+ const k = temporalStorageView(plainRowStorage, this.keys, this.#rows, {
338
+ row: i, from: pad, to: this.#rows.activeLength, copy: true,
339
+ });
340
+ const v = temporalStorageView(plainRowStorage, this.values, this.#rows, {
341
+ row: i, from: pad, to: this.#rows.activeLength, copy: true,
342
+ });
343
+ c.restoreState(k, v, this.offsetArr[i]!, k.shape[2]!);
344
+ return c;
345
+ }
346
+
347
+ /** Keep only `keep` rows along the batch axis (eviction). */
348
+ filter(keep: number[]): void {
349
+ if (this.keys && this.values) {
350
+ const k = plainRowStorage.takeRows(this.keys, keep);
351
+ const v = plainRowStorage.takeRows(this.values, keep);
352
+ this.keys.dispose();
353
+ this.values.dispose();
354
+ this.keys = k;
355
+ this.values = v;
356
+ }
357
+ this.#rows.filter(keep);
358
+ this.releaseRopeArr();
359
+ }
360
+
361
+ filterRows(keep: readonly number[]): void { this.filter([...keep]); }
362
+
363
+ state(): MlxArray[] {
364
+ return this.keys && this.values ? [this.keys, this.values] : [];
365
+ }
366
+ isTrimmable(): boolean {
367
+ return this.#rows.trimmable;
368
+ }
369
+ trim(n: number): void {
370
+ this.#rows.trim(n);
371
+ }
372
+ dispose(): void {
373
+ this.keys?.dispose();
374
+ this.values?.dispose();
375
+ this.keys = this.values = null;
376
+ this.#ropeArr?.dispose();
377
+ this.#ropeArr = null;
378
+ this.masks.clear();
379
+ }
380
+
381
+ /** Join rows into this empty ring: sources are running rings, whose rows are
382
+ * cut out in temporal order, or serial RotatingKVCaches (an adopted lone row
383
+ * or a joiner), in row order. Sources are borrowed. */
384
+ mergeRows(rows: readonly Cache[]): void {
385
+ if (this.#rows.batchSize) throw new Error("BatchedRotatingCache.mergeRows needs an empty batch");
386
+ const parts: { keys: MlxArray; values: MlxArray }[] = [], offsets: number[] = [], held: MlxArray[] = [];
387
+ const own = (array: MlxArray) => { held.push(array); return array; };
388
+ try {
389
+ for (const source of rows) {
390
+ if (source instanceof BatchedRotatingCache) {
391
+ const [k0, v0] = source.temporalView(); own(k0); own(v0);
392
+ const [, H, valid, D] = k0.shape as [number, number, number, number];
393
+ const vD = v0.shape[3]!;
394
+ for (let b = 0; b < source.batchSize; b++) {
395
+ const pad = Math.max(0, source.leftPad[b]!);
396
+ parts.push({ keys: own(k0.slice([b, 0, pad, 0], [b + 1, H, valid, D])),
397
+ values: own(v0.slice([b, 0, pad, 0], [b + 1, H, valid, vD])) });
398
+ offsets.push(source.offsetArr[b]!);
399
+ }
400
+ } else if (isRotatingPlainCache(source)) {
401
+ const [keys, values] = (source as RotatingKVCache).temporalView();
402
+ parts.push({ keys: own(keys), values: own(values) }); offsets.push(source.offset);
403
+ } else throw new Error(`sliding-window batch cannot merge ${source.signature()}`);
404
+ }
405
+ const merged = BatchedRotatingCache.merge(parts, offsets, this.maxSize);
406
+ this.#rows = merged.#rows; this.keys = merged.keys; this.values = merged.values;
407
+ merged.keys = merged.values = null;
408
+ } finally { for (const array of held) array.dispose(); }
409
+ }
410
+
411
+ /** Assemble a batch from per-row temporal KV slices (port of merge). Each row
412
+ * is its solo cache's temporalView ([1,H,Li,D], Li ≤ maxSize, temporal
413
+ * order) and `offsets[i]` its absolute position. Left-pads to the longest
414
+ * row; the result is in temporal order (rotated=false, idx=offset=width). */
415
+ static merge(
416
+ rows: { keys: MlxArray; values: MlxArray }[], offsets: number[], maxSize: number,
417
+ masks = new AttentionMasks(),
418
+ ): BatchedRotatingCache {
419
+ const lens = rows.map((r) => r.keys.shape[2]!);
420
+ const width = Math.max(...lens, 0);
421
+ const leftPad = lens.map((l) => width - l);
422
+ const cache = new BatchedRotatingCache(maxSize, leftPad, masks);
423
+ cache.#rows.restoreMerged(width, offsets);
424
+ if (width === 0) return cache;
425
+
426
+ cache.keys = mergeStorageRows(plainRowStorage, rows.map((row) => row.keys), leftPad);
427
+ cache.values = mergeStorageRows(plainRowStorage, rows.map((row) => row.values), leftPad);
428
+ return cache;
429
+ }
430
+ }
@@ -0,0 +1,179 @@
1
+ import { MlxArray } from "@mlx-bun/mlx/array";
2
+ import * as ops from "@mlx-bun/mlx/ops";
3
+ import { BatchedRotatingState } from "./batched-rotating-state";
4
+ import { Dtype } from "@mlx-bun/mlx/ffi";
5
+
6
+ export interface RowStorage<T> {
7
+ shape(value: T): readonly [number, number, number, number];
8
+ slice(value: T, batchFrom: number, batchTo: number, tokenFrom: number, tokenTo: number): T;
9
+ concatTokens(values: readonly T[]): T;
10
+ concatRows(values: readonly T[]): T;
11
+ padLeft(value: T, tokens: number): T;
12
+ takeRows(value: T, keep: readonly number[]): T;
13
+ copy(value: T): T;
14
+ rollRows(value: T, indices: MlxArray): T;
15
+ dispose(value: T): void;
16
+ }
17
+
18
+ export const plainRowStorage: RowStorage<MlxArray> = {
19
+ shape: (value) => value.shape as [number, number, number, number],
20
+ slice(value, batchFrom, batchTo, tokenFrom, tokenTo) {
21
+ const [, H, , D] = value.shape as [number, number, number, number];
22
+ return value.slice([batchFrom, 0, tokenFrom, 0], [batchTo, H, tokenTo, D]);
23
+ },
24
+ concatTokens: (values) => ops.concatAxis([...values], 2),
25
+ concatRows: (values) => ops.concatAxis([...values], 0),
26
+ padLeft(value, tokens) {
27
+ if (tokens === 0) return value.slice([0, 0, 0, 0], value.shape as number[]);
28
+ const [B, H, , D] = value.shape as [number, number, number, number];
29
+ const zeros = ops.zeros([B, H, tokens, D], value.dtype);
30
+ const out = ops.concatAxis([zeros, value], 2);
31
+ zeros.dispose();
32
+ return out;
33
+ },
34
+ takeRows(value, keep) {
35
+ const indices = MlxArray.fromInt32(Int32Array.from(keep), [keep.length]);
36
+ const out = ops.takeAxis(value, indices, 0);
37
+ indices.dispose();
38
+ return out;
39
+ },
40
+ copy: (value) => ops.copyOf(value),
41
+ rollRows: (value, indices) => ops.takeAlongAxis(value, indices, 2),
42
+ dispose: (value) => value.dispose(),
43
+ };
44
+
45
+ export const quantizedRowStorage: RowStorage<ops.QuantizedTensor> = {
46
+ shape: (value) => value.packed.shape as [number, number, number, number],
47
+ slice(value, batchFrom, batchTo, tokenFrom, tokenTo) {
48
+ const cut = (array: MlxArray): MlxArray => {
49
+ const [, H, , D] = array.shape as [number, number, number, number];
50
+ return array.slice([batchFrom, 0, tokenFrom, 0], [batchTo, H, tokenTo, D]);
51
+ };
52
+ return { packed: cut(value.packed), scales: cut(value.scales), biases: cut(value.biases) };
53
+ },
54
+ concatTokens(values) {
55
+ return {
56
+ packed: ops.concatAxis(values.map((value) => value.packed), 2),
57
+ scales: ops.concatAxis(values.map((value) => value.scales), 2),
58
+ biases: ops.concatAxis(values.map((value) => value.biases), 2),
59
+ };
60
+ },
61
+ concatRows(values) {
62
+ return {
63
+ packed: ops.concatAxis(values.map((value) => value.packed), 0),
64
+ scales: ops.concatAxis(values.map((value) => value.scales), 0),
65
+ biases: ops.concatAxis(values.map((value) => value.biases), 0),
66
+ };
67
+ },
68
+ padLeft(value, tokens) {
69
+ const pad = (array: MlxArray): MlxArray => plainRowStorage.padLeft(array, tokens);
70
+ return { packed: pad(value.packed), scales: pad(value.scales), biases: pad(value.biases) };
71
+ },
72
+ takeRows(value, keep) {
73
+ const take = (array: MlxArray): MlxArray => plainRowStorage.takeRows(array, keep);
74
+ return { packed: take(value.packed), scales: take(value.scales), biases: take(value.biases) };
75
+ },
76
+ copy(value) {
77
+ return {
78
+ packed: ops.copyOf(value.packed),
79
+ scales: ops.copyOf(value.scales),
80
+ biases: ops.copyOf(value.biases),
81
+ };
82
+ },
83
+ rollRows(value, indices) {
84
+ const packed = ops.takeAlongAxis(value.packed, indices, 2);
85
+ let scales: MlxArray | undefined;
86
+ try {
87
+ scales = ops.takeAlongAxis(value.scales, indices, 2);
88
+ return { packed, scales, biases: ops.takeAlongAxis(value.biases, indices, 2) };
89
+ } catch (error) { packed.dispose(); scales?.dispose(); throw error; }
90
+ },
91
+ dispose(value) {
92
+ value.packed.dispose();
93
+ value.scales.dispose();
94
+ value.biases.dispose();
95
+ },
96
+ };
97
+
98
+ /** Per-row cyclic shift, shared by plain and encoded storage planes.
99
+ * This is mlx-lm's dynamic_roll indexing; only positions cross the FFI. */
100
+ export function rowRollIndices(length: number, shifts: readonly number[]): MlxArray {
101
+ using columns = ops.arange(0, length, 1, Dtype.int32);
102
+ using positions = ops.reshape(columns, [1, 1, length, 1]);
103
+ using starts = ops.fromInt32(shifts.map(shift => ((-shift % length) + length) % length), [shifts.length, 1, 1, 1]);
104
+ using raw = ops.add(positions, starts);
105
+ using width = ops.fromInt32([length], []);
106
+ using wrapped = ops.sub(raw, width);
107
+ using over = ops.greaterEqual(raw, width);
108
+ return ops.where(over, wrapped, raw);
109
+ }
110
+
111
+ export function mergeStorageRows<T>(
112
+ storage: RowStorage<T>,
113
+ rows: readonly T[],
114
+ leftPad: readonly number[],
115
+ ): T {
116
+ const padded = rows.map((row, index) => storage.padLeft(row, leftPad[index]!));
117
+ try {
118
+ return storage.concatRows(padded);
119
+ } finally {
120
+ for (const row of padded) storage.dispose(row);
121
+ }
122
+ }
123
+
124
+ /** De-roll storage into temporal order, then cut one row or the whole batch. */
125
+ export function temporalStorageView<T>(
126
+ storage: RowStorage<T>,
127
+ value: T,
128
+ state: BatchedRotatingState,
129
+ options: { row?: number; from?: number; to?: number; copy?: boolean } = {},
130
+ ): T {
131
+ const [batch, , tokens] = storage.shape(value);
132
+ const batchFrom = options.row ?? 0;
133
+ const batchTo = options.row === undefined ? batch : options.row + 1;
134
+ const parts = state.temporalRanges(tokens).map(([from, to]) =>
135
+ storage.slice(value, batchFrom, batchTo, from, to)
136
+ );
137
+ const ordered = parts.length === 1 ? parts[0]! : storage.concatTokens(parts);
138
+ if (parts.length > 1) for (const part of parts) storage.dispose(part);
139
+ const cut = storage.slice(
140
+ ordered,
141
+ 0,
142
+ batchTo - batchFrom,
143
+ options.from ?? 0,
144
+ options.to ?? state.validLength,
145
+ );
146
+ storage.dispose(ordered);
147
+ if (!options.copy) return cut;
148
+ const owned = storage.copy(cut);
149
+ storage.dispose(cut);
150
+ return owned;
151
+ }
152
+
153
+ /** Borrow storage, return an owned chronological block without decoding the
154
+ * representation. Port of mlx-lm's BatchRotatingKVCache._update_concat
155
+ * (MIT): discard spare capacity, retain the full window for the first query. */
156
+ export function appendRotatingStorage<T>(
157
+ storage: RowStorage<T>, previous: T | null, incoming: T, state: BatchedRotatingState,
158
+ ): T {
159
+ if (previous === null) return storage.slice(incoming, 0, storage.shape(incoming)[0], 0, storage.shape(incoming)[2]);
160
+ const active = state.activeLength;
161
+ const shifts = state.prefillRoll();
162
+ if (shifts?.some(shift => shift !== 0)) {
163
+ const chronological = temporalStorageView(storage, previous, state, { to: active });
164
+ let rolled: T | undefined, history: T | undefined;
165
+ try {
166
+ using indices = rowRollIndices(active, shifts);
167
+ rolled = storage.rollRows(chronological, indices);
168
+ history = storage.slice(rolled, 0, state.batchSize, Math.max(0, active - state.maxSize + 1), active);
169
+ return storage.concatTokens([history, incoming]);
170
+ } finally {
171
+ storage.dispose(chronological); if (rolled) storage.dispose(rolled); if (history) storage.dispose(history);
172
+ }
173
+ }
174
+ const history = temporalStorageView(storage, previous, state, {
175
+ from: Math.max(0, active - state.maxSize + 1), to: active,
176
+ });
177
+ try { return storage.concatTokens([history, incoming]); }
178
+ finally { storage.dispose(history); }
179
+ }
@@ -0,0 +1,54 @@
1
+ import { SsmPrefillPadding } from "./ssm-prefill-padding";
2
+ import type { PaddedPrefillCache, PrefillPadding } from "../contracts/mlx/cache";
3
+ import * as ops from "@mlx-bun/mlx/ops";
4
+ import type { MlxArray } from "@mlx-bun/mlx/array";
5
+ import type { BatchableCache, Cache } from "../contracts/mlx/cache";
6
+ import { SSMCache } from "./ssm";
7
+
8
+ /** Recurrent storage behind the same row-layout port as attention KV.
9
+ * Inputs to merge are borrowed; request membership is owned by the caller. */
10
+ export class BatchedSSMCache extends SSMCache implements BatchableCache, PaddedPrefillCache {
11
+ constructor() { super(); this.offsets = []; }
12
+ preparePrefill(padding: PrefillPadding): void {
13
+ if (!this.offsets!.length) this.offsets = padding.lengths.map(() => 0);
14
+ const padded = padding.leftPadding?.some(n => n > 0) || padding.rightPadding?.some(n => n > 0);
15
+ this.prefillPadding = padded ? new SsmPrefillPadding(padding) : null;
16
+ }
17
+ finalizePrefill(): void { this.prefillPadding = null; }
18
+ get rowOffsets(): readonly number[] { return this.offsets!; }
19
+ get leftPad(): readonly number[] { return this.rowOffsets.map(() => 0); }
20
+ makeEmptyBatch(): BatchedSSMCache { return new BatchedSSMCache(); }
21
+ /** Recurrent state cannot be trimmed: only a row whose own advance count is the key. */
22
+ canPublishRow(row: number, tokens: number): boolean { return !!this.conv && this.rowOffset(row) === tokens; }
23
+ projectedBytes(_tokens: number): number {
24
+ return this.batchSize ? this.state().reduce((bytes, array) => bytes + array.nbytes, 0) / this.batchSize : 0;
25
+ }
26
+
27
+ mergeRows(rows: readonly Cache[]): void {
28
+ if (!rows.length) { this.dispose(); return; }
29
+ const sources = rows as readonly SSMCache[];
30
+ const offsets = sources.flatMap(source => source.offsets ?? [source.offset]);
31
+ const merge = (field: "conv" | "recurrent"): MlxArray | null => {
32
+ const prototype = sources.find(source => source[field])?.[field];
33
+ if (!prototype) return null;
34
+ if (sources.length === 1) return ops.copyOf(prototype);
35
+ const empty: MlxArray[] = [];
36
+ try {
37
+ return ops.concatAxis(sources.map(source => {
38
+ if (source[field]) return source[field]!;
39
+ const zeros = ops.zeros([source.offsets?.length ?? 1, ...prototype.shape.slice(1)], prototype.dtype);
40
+ empty.push(zeros); return zeros;
41
+ }), 0);
42
+ } finally { for (const array of empty) array.dispose(); }
43
+ };
44
+ const conv = merge("conv");
45
+ let recurrent;
46
+ try { recurrent = merge("recurrent"); }
47
+ catch (error) { conv?.dispose(); throw error; }
48
+ this.dispose();
49
+ this.conv = conv; this.recurrent = recurrent;
50
+ this.offsets = offsets; this.offset = Math.max(0, ...offsets);
51
+ }
52
+
53
+ override dispose(): void { super.dispose(); this.offset = 0; this.offsets = []; }
54
+ }