@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,773 @@
1
+ // Whisper 30 s-window decoding — a port of mlx-whisper's decoding.py
2
+ // (DecodingTask: greedy/sampled decoding, logit filters, language
3
+ // detection, ranking) plus the beam search the oracle leaves unimplemented
4
+ // (ported from openai-whisper's BeamSearchDecoder, which mlx-whisper
5
+ // mirrors everywhere else). Per-step pre-filter logits are gated
6
+ // bit-exact vs the oracle for the greedy path (`02d723a:tests/parity/whisper.test.ts`).
7
+
8
+ import { deflateSync } from "node:zlib";
9
+ import { MlxArray } from "@mlx-bun/mlx/array";
10
+ import { Dtype } from "@mlx-bun/mlx/ffi";
11
+ import * as ops from "@mlx-bun/mlx/ops";
12
+ import type { FastFilterConfig, WhisperDecoderCache, WhisperFastCache, WhisperGraph } from "../../contracts/mlx/whisper";
13
+ import { WHISPER_CHUNK_SECONDS } from "../../input/audio/whisper-mel";
14
+ import { normalizeWhisperLanguage, type WhisperTask, type WhisperTokenizer } from "../../input/audio/whisper-tokenizer";
15
+
16
+ export interface WhisperDecodingOptions {
17
+ task?: WhisperTask;
18
+ /** null → detect. */
19
+ language?: string | null;
20
+ temperature?: number;
21
+ sampleLen?: number | null;
22
+ bestOf?: number | null;
23
+ beamSize?: number | null;
24
+ patience?: number | null;
25
+ lengthPenalty?: number | null;
26
+ prompt?: string | number[] | null;
27
+ prefix?: string | number[] | null;
28
+ /** Token ids to suppress; "-1" = the tokenizer's non-speech set (default). */
29
+ suppressTokens?: number[] | "-1" | null;
30
+ /** Execution path: `true` (default) runs the optimized graphs
31
+ * (the graph's fast path) for temperature-0 decoding; `false` forces the
32
+ * faithful oracle graph. Sampling (temperature > 0) always uses the
33
+ * faithful path. */
34
+ fast?: boolean;
35
+ /** Lab (whisper.cpp `-ac`): encode only the first `audioCtx` of the 1500
36
+ * encoder positions (2·audioCtx mel frames). Cheaper for short clips
37
+ * but NOT the trained context — measured hallucinated repeats below
38
+ * ~1024 on the fixture clips. null = full context. Fast path only. */
39
+ audioCtx?: number | null;
40
+ /** Parity instrumentation: the decode window's encoder output and each
41
+ * step's PRE-filter last-position logits [n_group, V] f32 (borrowed —
42
+ * valid only during the callback). */
43
+ observer?: WhisperDecodeObserver;
44
+ suppressBlank?: boolean;
45
+ withoutTimestamps?: boolean;
46
+ maxInitialTimestamp?: number | null;
47
+ }
48
+
49
+ export interface WhisperDecodeObserver {
50
+ onAudioFeatures?(features: MlxArray): void;
51
+ onStepLogits?(step: number, logits: MlxArray): void;
52
+ /** After each step: the live rows (token histories) and their cumulative log-probs. */
53
+ onStepRows?(step: number, rows: number[][], sumLogprobs: number[]): void;
54
+ }
55
+
56
+ export interface WhisperDecodingResult {
57
+ language: string;
58
+ languageProbs?: Record<string, number>;
59
+ tokens: number[];
60
+ text: string;
61
+ avgLogprob: number;
62
+ noSpeechProb: number;
63
+ temperature: number;
64
+ compressionRatio: number;
65
+ }
66
+
67
+ export function compressionRatio(text: string): number {
68
+ const bytes = Buffer.from(text, "utf8");
69
+ return bytes.length / deflateSync(bytes).length;
70
+ }
71
+
72
+ const dispose = (old: MlxArray, next: MlxArray): MlxArray => {
73
+ old.dispose();
74
+ return next;
75
+ };
76
+
77
+ /** DecodingTask over one audio window (n_audio = 1). */
78
+ export class WhisperDecodingTask {
79
+ readonly model: WhisperGraph;
80
+ readonly tokenizer: WhisperTokenizer;
81
+ readonly options: Required<Omit<WhisperDecodingOptions, "language">> & { language: string | null };
82
+ readonly nGroup: number;
83
+ readonly nCtx: number;
84
+ readonly sampleLen: number;
85
+ readonly sotSequence: number[];
86
+ readonly initialTokens: number[];
87
+ readonly sampleBegin: number;
88
+ readonly sotIndex: number;
89
+ readonly #suppressMask: MlxArray | null;
90
+ readonly #blankMask: MlxArray | null;
91
+ readonly #suppressIds: number[];
92
+ readonly #blankIds: number[] | null;
93
+ readonly #maxInitialTimestampIndex: number | null;
94
+ readonly #useTimestampRules: boolean;
95
+ /** Language for the tokenizer sequence (oracle: options.language or "en"). */
96
+ readonly tokenizerLanguage: string;
97
+
98
+ constructor(model: WhisperGraph, tokenizer: WhisperTokenizer, options: WhisperDecodingOptions) {
99
+ this.model = model;
100
+ this.tokenizer = tokenizer;
101
+ const o = {
102
+ task: options.task ?? "transcribe",
103
+ language: options.language == null ? null : normalizeWhisperLanguage(options.language),
104
+ temperature: options.temperature ?? 0,
105
+ sampleLen: options.sampleLen ?? null,
106
+ bestOf: options.bestOf ?? null,
107
+ beamSize: options.beamSize ?? null,
108
+ patience: options.patience ?? null,
109
+ lengthPenalty: options.lengthPenalty ?? null,
110
+ prompt: options.prompt ?? null,
111
+ prefix: options.prefix ?? null,
112
+ suppressTokens: options.suppressTokens === undefined ? "-1" as const : options.suppressTokens,
113
+ suppressBlank: options.suppressBlank ?? true,
114
+ withoutTimestamps: options.withoutTimestamps ?? false,
115
+ maxInitialTimestamp: options.maxInitialTimestamp === undefined ? 1.0 : options.maxInitialTimestamp,
116
+ observer: options.observer ?? {},
117
+ fast: options.fast ?? true,
118
+ audioCtx: options.audioCtx ?? null,
119
+ };
120
+ if (o.audioCtx !== null && (!Number.isInteger(o.audioCtx) || o.audioCtx < 64 || o.audioCtx > model.dims.nAudioCtx))
121
+ throw new Error(`audio_ctx must be an integer in [64, ${model.dims.nAudioCtx}]`);
122
+ if (o.beamSize !== null && o.bestOf !== null) throw new Error("beam_size and best_of can't be given together");
123
+ if (o.temperature === 0 && o.bestOf !== null) throw new Error("best_of with greedy sampling (T=0) is not compatible");
124
+ if (o.patience !== null && o.beamSize === null) throw new Error("patience requires beam_size to be given");
125
+ if (o.lengthPenalty !== null && !(o.lengthPenalty >= 0 && o.lengthPenalty <= 1))
126
+ throw new Error("length_penalty (alpha) should be a value between 0 and 1");
127
+ this.options = o;
128
+ this.tokenizerLanguage = o.language ?? "en";
129
+ this.nGroup = o.beamSize ?? o.bestOf ?? 1;
130
+ this.nCtx = model.dims.nTextCtx;
131
+ this.sampleLen = o.sampleLen ?? Math.floor(model.dims.nTextCtx / 2);
132
+ this.sotSequence = tokenizer.sotSequence(this.tokenizerLanguage, o.task);
133
+ if (o.withoutTimestamps) this.sotSequence = [...this.sotSequence, tokenizer.noTimestamps];
134
+ this.initialTokens = this.#initialTokens();
135
+ this.sampleBegin = this.initialTokens.length;
136
+ this.sotIndex = this.initialTokens.indexOf(tokenizer.sot);
137
+
138
+ const V = model.dims.nVocab;
139
+ this.#blankIds = o.suppressBlank ? [...tokenizer.encode(" "), tokenizer.eot] : null;
140
+ this.#blankMask = this.#blankIds ? maskOf(V, this.#blankIds) : null;
141
+ const suppress = this.#suppressTokens();
142
+ this.#suppressIds = suppress;
143
+ this.#suppressMask = suppress.length ? maskOf(V, suppress) : null;
144
+ this.#useTimestampRules = !o.withoutTimestamps;
145
+ const precision = WHISPER_CHUNK_SECONDS / model.dims.nAudioCtx; // 0.02
146
+ this.#maxInitialTimestampIndex = o.maxInitialTimestamp ? Math.round(o.maxInitialTimestamp / precision) : null;
147
+ }
148
+
149
+ #initialTokens(): number[] {
150
+ const t = this.tokenizer;
151
+ let tokens = [...this.sotSequence];
152
+ const { prefix, prompt } = this.options;
153
+ if (prefix) {
154
+ let prefixTokens = typeof prefix === "string" ? t.encode(" " + prefix.trim()) : prefix;
155
+ const maxPrefixLen = Math.floor(this.nCtx / 2) - this.sampleLen;
156
+ prefixTokens = prefixTokens.slice(Math.max(0, prefixTokens.length - maxPrefixLen));
157
+ if (maxPrefixLen <= 0) prefixTokens = []; // python: list[-0:] → whole list; guard matches sample_len default
158
+ tokens = [...tokens, ...prefixTokens];
159
+ }
160
+ if (prompt && prompt.length) {
161
+ const promptTokens = typeof prompt === "string" ? t.encode(" " + prompt.trim()) : prompt;
162
+ const keep = Math.floor(this.nCtx / 2) - 1;
163
+ tokens = [t.sotPrev, ...promptTokens.slice(Math.max(0, promptTokens.length - keep)), ...tokens];
164
+ }
165
+ return tokens;
166
+ }
167
+
168
+ #suppressTokens(): number[] {
169
+ const t = this.tokenizer;
170
+ let suppress: number[];
171
+ const opt = this.options.suppressTokens;
172
+ if (opt === "-1") suppress = [...t.nonSpeechTokens];
173
+ else if (opt === null || opt.length === 0) suppress = [];
174
+ else if (opt.includes(-1)) suppress = [...opt.filter((x) => x >= 0), ...t.nonSpeechTokens];
175
+ else suppress = [...opt];
176
+ suppress.push(t.transcribe, t.translate, t.sot, t.sotPrev, t.sotLm, t.noSpeech);
177
+ return [...new Set(suppress)].sort((a, b) => a - b);
178
+ }
179
+
180
+ /** Prompt-token budget the transcribe loop may carry (n_ctx/2 − 1). */
181
+ get promptBudget(): number {
182
+ return Math.floor(this.nCtx / 2) - 1;
183
+ }
184
+
185
+ // --- logit filters ----------------------------------------------------------
186
+
187
+ /** Host-side timestamp-rule mask (ApplyTimestampRules, numpy part). */
188
+ #timestampMask(tokens: number[][]): Float32Array {
189
+ const t = this.tokenizer;
190
+ const V = this.model.dims.nVocab;
191
+ const B = tokens.length;
192
+ const mask = new Float32Array(B * V);
193
+ const fill = (row: number, from: number, to: number) => {
194
+ for (let i = from; i < to; i++) mask[row * V + i] = -Infinity;
195
+ };
196
+ for (let k = 0; k < B; k++) mask[k * V + t.noTimestamps] = -Infinity;
197
+ for (let k = 0; k < B; k++) {
198
+ const seq = tokens[k]!.slice(this.sampleBegin);
199
+ const lastWasTimestamp = seq.length >= 1 && seq[seq.length - 1]! >= t.timestampBegin;
200
+ const penultimateWasTimestamp = seq.length < 2 || seq[seq.length - 2]! >= t.timestampBegin;
201
+ if (lastWasTimestamp) {
202
+ if (penultimateWasTimestamp) fill(k, t.timestampBegin, V);
203
+ else fill(k, 0, t.eot);
204
+ }
205
+ // Monotonic timestamps (openai-whisper decoding.py): forbid timestamp
206
+ // tokens below the last one emitted, and force a nonzero segment
207
+ // length. mlx-whisper's port collects sequence INDICES here and slices
208
+ // `mask[k, timestamp_begin:index]` — an empty slice, so its rule never
209
+ // fires; we keep the reference semantics (pre-filter logits stay
210
+ // bit-exact; the only difference is which timestamp a beam may pick).
211
+ let lastTs = -1;
212
+ for (const v of seq) if (v >= t.timestampBegin) lastTs = v;
213
+ if (lastTs >= 0) {
214
+ const timestampLast = lastWasTimestamp && !penultimateWasTimestamp ? lastTs : lastTs + 1;
215
+ fill(k, t.timestampBegin, Math.min(V, timestampLast));
216
+ }
217
+ }
218
+ if (tokens[0]!.length === this.sampleBegin) {
219
+ for (let k = 0; k < B; k++) fill(k, 0, t.timestampBegin);
220
+ if (this.#maxInitialTimestampIndex !== null) {
221
+ const lastAllowed = t.timestampBegin + this.#maxInitialTimestampIndex;
222
+ for (let k = 0; k < B; k++) fill(k, lastAllowed + 1, V);
223
+ }
224
+ }
225
+ return mask;
226
+ }
227
+
228
+ /** Apply the configured filters to `logits` [B, V] f32 → new array. */
229
+ applyFilters(logits: MlxArray, tokens: number[][]): MlxArray {
230
+ let out = logits;
231
+ let owned = false;
232
+ if (this.#blankMask && tokens[0]!.length === this.sampleBegin) {
233
+ out = ops.add(out, this.#blankMask);
234
+ owned = true;
235
+ }
236
+ if (this.#suppressMask) {
237
+ const next = ops.add(out, this.#suppressMask);
238
+ if (owned) out.dispose();
239
+ out = next;
240
+ owned = true;
241
+ }
242
+ if (this.#useTimestampRules) {
243
+ const t = this.tokenizer;
244
+ const B = tokens.length;
245
+ const V = this.model.dims.nVocab;
246
+ const mask = MlxArray.fromFloat32(this.#timestampMask(tokens), [B, V]);
247
+ const lse = ops.logsumexpAxis(out, -1, true);
248
+ const logprobs = ops.sub(out, lse);
249
+ lse.dispose();
250
+ const tsPart = logprobs.slice([0, t.timestampBegin], [B, V]);
251
+ const tsLogprob = ops.logsumexpAxis(tsPart, -1, true);
252
+ tsPart.dispose();
253
+ const textPart = logprobs.slice([0, 0], [B, t.timestampBegin]);
254
+ const maxText = ops.maxAxis(textPart, -1, true);
255
+ textPart.dispose();
256
+ logprobs.dispose();
257
+ const cond = ops.greater(tsLogprob, maxText);
258
+ tsLogprob.dispose();
259
+ maxText.dispose();
260
+ const maskText = mask.slice([0, 0], [B, t.timestampBegin]);
261
+ const maskTs = mask.slice([0, t.timestampBegin], [B, V]);
262
+ const negInf = MlxArray.fromFloat32(new Float32Array([-Infinity]), []);
263
+ const masked = ops.where(cond, negInf, maskText);
264
+ const full = ops.concatAxis([masked, maskTs], 1);
265
+ for (const a of [cond, maskText, maskTs, negInf, masked, mask]) a.dispose();
266
+ const next = ops.add(out, full);
267
+ full.dispose();
268
+ if (owned) out.dispose();
269
+ out = next;
270
+ owned = true;
271
+ }
272
+ return owned ? out : ops.copyOf(out);
273
+ }
274
+
275
+ // --- language detection -------------------------------------------------------
276
+
277
+ /** detect_language on encoded audio features [1, 1500, D]. */
278
+ detectLanguage(audioFeatures: MlxArray): { token: number; code: string; probs: Record<string, number> } {
279
+ const t = this.tokenizer;
280
+ const cache = this.model.makeCache();
281
+ const x = ops.fromInt32([t.sot], [1, 1]);
282
+ const { logits: raw } = this.model.decode(x, audioFeatures, cache);
283
+ x.dispose();
284
+ cache.dispose();
285
+ const V = this.model.dims.nVocab;
286
+ let logits = raw.slice([0, 0, 0], [1, 1, V]);
287
+ raw.dispose();
288
+ logits = dispose(logits, ops.reshape(logits, [1, V]));
289
+ // mask: -inf everywhere except language tokens (float32 → promotes f16 logits)
290
+ const m = new Float32Array(V).fill(-Infinity);
291
+ for (const id of t.allLanguageTokens) m[id] = 0;
292
+ const mask = MlxArray.fromFloat32(m, [V]);
293
+ logits = dispose(logits, ops.add(logits, mask));
294
+ mask.dispose();
295
+ const arg = ops.argmaxAxis(logits, -1);
296
+ const token = arg.toIntTokens()[0]!;
297
+ arg.dispose();
298
+ const probsArr = ops.softmaxAxis(logits, -1, false);
299
+ const probs = probsArr.toFloat32();
300
+ probsArr.dispose();
301
+ logits.dispose();
302
+ const out: Record<string, number> = {};
303
+ t.allLanguageTokens.forEach((id, i) => { out[t.allLanguageCodes[i]!] = probs[id]!; });
304
+ return { token, code: t.allLanguageCodes[t.allLanguageTokens.indexOf(token)]!, probs: out };
305
+ }
306
+
307
+ // --- main loop ----------------------------------------------------------------
308
+
309
+ /** Whether this task decodes on the optimized path. */
310
+ get useFast(): boolean {
311
+ return this.options.fast && this.options.temperature === 0;
312
+ }
313
+
314
+ /** Encoder for this task's path. */
315
+ encode(mel: MlxArray): MlxArray {
316
+ if (this.options.audioCtx !== null) return this.model.fast.encode(mel, this.options.audioCtx);
317
+ return this.options.fast ? this.model.fast.encode(mel) : this.model.encode(mel);
318
+ }
319
+
320
+ #fastFilterConfig(): FastFilterConfig {
321
+ return {
322
+ nVocab: this.model.dims.nVocab,
323
+ eot: this.tokenizer.eot,
324
+ timestampBegin: this.tokenizer.timestampBegin,
325
+ noTimestamps: this.tokenizer.noTimestamps,
326
+ suppressIds: this.#suppressIds,
327
+ blankIds: this.#blankIds,
328
+ useTimestampRules: this.#useTimestampRules,
329
+ maxInitialTimestampIndex: this.#maxInitialTimestampIndex,
330
+ };
331
+ }
332
+
333
+ /** Run on a mel window [1, 3000, n_mels] (already the model dtype) or
334
+ * pre-encoded audio features [1, 1500, D]. */
335
+ run(melOrFeatures: MlxArray): WhisperDecodingResult {
336
+ const { model, tokenizer: t } = this;
337
+ const d = model.dims;
338
+ // encoder output has the text width (1280); a mel window has n_mels (128)
339
+ const isFeatures = melOrFeatures.shape[2] === d.nAudioState;
340
+ const audioFeatures = isFeatures ? melOrFeatures : this.encode(melOrFeatures);
341
+ try {
342
+ return this.#runOnFeatures(audioFeatures);
343
+ } finally {
344
+ if (!isFeatures) audioFeatures.dispose();
345
+ }
346
+ }
347
+
348
+ #runGreedyFast(audioFeatures: MlxArray, rows: number[][], language: string, languageProbs: Record<string, number> | undefined): WhisperDecodingResult {
349
+ const { model, tokenizer: t } = this;
350
+ const B = rows.length;
351
+ const cache = model.fast.makeCache();
352
+ const cfg = this.#fastFilterConfig();
353
+ model.fast.crossKv(audioFeatures, cache);
354
+ const sumLp: number[] = new Array(B).fill(0);
355
+ let noSpeechProb = Number.NaN;
356
+ const initial = rows[0]!;
357
+ const owned: MlxArray[] = [];
358
+ try {
359
+ // step 0: eager prefill + host filters (sample_begin rules)
360
+ const inputTokens = ops.fromInt32(rows.flat(), [B, initial.length]);
361
+ const raw = model.fast.prefill(inputTokens, cache);
362
+ inputTokens.dispose();
363
+ const preLogits = raw.astype(Dtype.float32);
364
+ raw.dispose();
365
+ const L = preLogits.shape[1]!;
366
+ const V = preLogits.shape[2]!;
367
+ let last = preLogits.slice([0, L - 1, 0], [B, L, V]);
368
+ last = dispose(last, ops.reshape(last, [B, V]));
369
+ let atSot = preLogits.slice([0, this.sotIndex, 0], [B, this.sotIndex + 1, V]);
370
+ atSot = dispose(atSot, ops.reshape(atSot, [B, V]));
371
+ const probs = ops.softmaxAxis(atSot, -1, false);
372
+ atSot.dispose();
373
+ const colView = probs.slice([0, t.noSpeech], [B, t.noSpeech + 1]);
374
+ probs.dispose();
375
+ const col = ops.contiguous(colView);
376
+ colView.dispose();
377
+ noSpeechProb = col.toFloat32()[0]!;
378
+ col.dispose();
379
+ preLogits.dispose();
380
+ const filtered = this.applyFilters(last, rows);
381
+ last.dispose();
382
+ let next = ops.argmaxAxis(filtered, -1);
383
+ next = dispose(next, next.astype(Dtype.int32));
384
+ const lse = ops.logsumexpAxis(filtered, -1, true);
385
+ const logprobs = ops.sub(filtered, lse);
386
+ lse.dispose();
387
+ filtered.dispose();
388
+ const idx = ops.reshape(next, [B, 1]);
389
+ let lp = ops.takeAlongAxis(logprobs, idx, 1);
390
+ idx.dispose();
391
+ logprobs.dispose();
392
+ lp = dispose(lp, ops.reshape(lp, [B]));
393
+ const tb = t.timestampBegin;
394
+ const tsBeginArr = ops.fromInt32([tb], [1]);
395
+ const minusOne = ops.fromInt32(new Array(B).fill(-1), [B]);
396
+ const isTs0 = ops.greaterEqual(next, tsBeginArr);
397
+ let lastTs = ops.where(isTs0, next, minusOne);
398
+ isTs0.dispose();
399
+ let lastTok = next; // token appended at step 0 becomes seq[-1]
400
+ let penultTok: MlxArray = minusOne; // seq has one element after step 0
401
+ owned.push(tsBeginArr, minusOne);
402
+ ops.asyncEvalAll([next, lp, lastTs, ...cache.k, ...cache.v]);
403
+ // pending = step whose token the host has not consumed yet
404
+ let pending: { next: MlxArray; lp: MlxArray } = { next, lp };
405
+ let done = false;
406
+ for (let i = 1; i < this.sampleLen && !done; i++) {
407
+ if (rows[0]!.length + 1 > this.nCtx) break;
408
+ // issue step i BEFORE reading step i-1's token (pipelining)
409
+ const out = model.fast.greedyStep({ tok: lastTok, lastTok, penultTok, lastTs }, cache, cfg);
410
+ ops.asyncEvalAll([out.next, out.lp, out.lastTs, ...cache.k, ...cache.v]);
411
+ // consume step i-1
412
+ const toks = pending.next.toIntTokens();
413
+ const lps = pending.lp.toFloat32();
414
+ done = true;
415
+ for (let b = 0; b < B; b++) {
416
+ const r = rows[b]!;
417
+ const wasEot = r[r.length - 1] === t.eot;
418
+ const tok = wasEot ? t.eot : toks[b]!;
419
+ if (!wasEot) sumLp[b] = sumLp[b]! + lps[b]!;
420
+ r.push(tok);
421
+ if (tok !== t.eot) done = false;
422
+ }
423
+ pending.lp.dispose();
424
+ out.pre.dispose();
425
+ // rotate state: step i's inputs were (lastTok=tok_{i-1}); its outputs feed step i+1
426
+ if (penultTok !== minusOne) penultTok.dispose();
427
+ penultTok = lastTok;
428
+ lastTok = out.next;
429
+ lastTs.dispose();
430
+ lastTs = out.lastTs;
431
+ pending = { next: out.next, lp: out.lp };
432
+ }
433
+ if (!done) {
434
+ // consume the last issued step (loop bound hit)
435
+ const toks = pending.next.toIntTokens();
436
+ const lps = pending.lp.toFloat32();
437
+ for (let b = 0; b < B; b++) {
438
+ const r = rows[b]!;
439
+ const wasEot = r[r.length - 1] === t.eot;
440
+ if (!wasEot) { sumLp[b] = sumLp[b]! + lps[b]!; r.push(toks[b]!); } else r.push(t.eot);
441
+ }
442
+ }
443
+ pending.lp.dispose();
444
+ if (lastTok !== pending.next) lastTok.dispose();
445
+ pending.next.dispose();
446
+ if (penultTok !== minusOne) penultTok.dispose();
447
+ lastTs.dispose();
448
+ } finally {
449
+ for (const a of owned) a.dispose();
450
+ cache.dispose();
451
+ }
452
+ // finalize like GreedyDecoder: ensure EOT, cut at first EOT
453
+ const seqs = rows.map((r) => {
454
+ const s = [...r.slice(this.sampleBegin), t.eot];
455
+ return s.slice(0, s.indexOf(t.eot));
456
+ });
457
+ const tokens = seqs[0]!;
458
+ const text = t.decode(tokens).trim();
459
+ return {
460
+ language, languageProbs, tokens, text,
461
+ avgLogprob: sumLp[0]! / (tokens.length + 1),
462
+ noSpeechProb, temperature: 0, compressionRatio: compressionRatio(text),
463
+ };
464
+ }
465
+
466
+ #runOnFeatures(audioFeatures: MlxArray): WhisperDecodingResult {
467
+ const { model, tokenizer: t } = this;
468
+ this.options.observer.onAudioFeatures?.(audioFeatures);
469
+ const initial = [...this.initialTokens];
470
+ let language = this.options.language;
471
+ let languageProbs: Record<string, number> | undefined;
472
+ if (language === null) {
473
+ const det = this.detectLanguage(audioFeatures);
474
+ language = det.code;
475
+ languageProbs = det.probs;
476
+ initial[this.sotIndex + 1] = det.token;
477
+ }
478
+ const nGroup = this.nGroup;
479
+ const rows: number[][] = Array.from({ length: nGroup }, () => [...initial]);
480
+ const fast = this.useFast;
481
+ if (fast && this.options.beamSize === null && !this.options.observer.onStepLogits)
482
+ return this.#runGreedyFast(audioFeatures, rows, language!, languageProbs);
483
+ const cache: WhisperDecoderCache = fast ? model.fast.makeCache() : model.makeCache();
484
+ const fastCfg = fast ? this.#fastFilterConfig() : null;
485
+ if (fast) model.fast.crossKv(audioFeatures, cache as WhisperFastCache);
486
+ const decoder = this.options.beamSize !== null
487
+ ? new BeamSearchDecoder(this.options.beamSize, t.eot, cache, this.options.patience)
488
+ : new GreedyDecoder(this.options.temperature, t.eot);
489
+ let sumLogprobs = ops.zeros([nGroup], Dtype.float32);
490
+ let noSpeechProb = Number.NaN;
491
+ try {
492
+ let completed = false;
493
+ for (let i = 0; i < this.sampleLen; i++) {
494
+ if (rows[0]!.length > this.nCtx) break;
495
+ let last: MlxArray;
496
+ let filtered: MlxArray;
497
+ if (i === 0 || !fast) {
498
+ const inputTokens = i === 0
499
+ ? ops.fromInt32(rows.flat(), [nGroup, initial.length])
500
+ : ops.fromInt32(rows.map((r) => r[r.length - 1]!), [nGroup, 1]);
501
+ const raw = fast
502
+ ? model.fast.prefill(inputTokens, cache as WhisperFastCache)
503
+ : model.decode(inputTokens, audioFeatures, cache).logits;
504
+ inputTokens.dispose();
505
+ const preLogits = raw.astype(Dtype.float32);
506
+ raw.dispose();
507
+ const L = preLogits.shape[1]!;
508
+ const V = preLogits.shape[2]!;
509
+ last = preLogits.slice([0, L - 1, 0], [nGroup, L, V]);
510
+ last = dispose(last, ops.reshape(last, [nGroup, V]));
511
+ if (i === 0) {
512
+ // no_speech_probs = softmax(pre_logits[:, sot_index])[:, no_speech]
513
+ let atSot = preLogits.slice([0, this.sotIndex, 0], [nGroup, this.sotIndex + 1, V]);
514
+ atSot = dispose(atSot, ops.reshape(atSot, [nGroup, V]));
515
+ const probs = ops.softmaxAxis(atSot, -1, false);
516
+ atSot.dispose();
517
+ const colView = probs.slice([0, t.noSpeech], [nGroup, t.noSpeech + 1]);
518
+ probs.dispose();
519
+ const col = ops.contiguous(colView); // strided view → materialize before readback
520
+ colView.dispose();
521
+ noSpeechProb = col.toFloat32()[0]!;
522
+ col.dispose();
523
+ }
524
+ preLogits.dispose();
525
+ this.options.observer.onStepLogits?.(i, last);
526
+ filtered = this.applyFilters(last, rows);
527
+ } else {
528
+ const tb = t.timestampBegin;
529
+ const seqs = rows.map((r) => r.slice(this.sampleBegin));
530
+ const state = {
531
+ lastTok: seqs.map((q) => q[q.length - 1] ?? -1),
532
+ penultTok: seqs.map((q) => q.length >= 2 ? q[q.length - 2]! : -1),
533
+ lastTs: seqs.map((q) => { let v = -1; for (const x of q) if (x >= tb) v = x; return v; }),
534
+ atBegin: rows[0]!.length === this.sampleBegin,
535
+ };
536
+ if (decoder instanceof BeamSearchDecoder) {
537
+ const out = model.fast.beamStep(rows.map((r) => r[r.length - 1]!), cache as WhisperFastCache, fastCfg!, decoder.beamSize + 1, state);
538
+ this.options.observer.onStepLogits?.(i, out.pre);
539
+ out.pre.dispose();
540
+ const step = decoder.updateFromTopK(rows, out.idx, out.vals, sumLogprobs);
541
+ sumLogprobs = step.sumLogprobs;
542
+ completed = step.completed;
543
+ if (this.options.observer.onStepRows)
544
+ this.options.observer.onStepRows(i, rows.map((r) => [...r]), [...sumLogprobs.toFloat32()]);
545
+ if (completed) break;
546
+ continue;
547
+ }
548
+ const out = model.fast.step(rows.map((r) => r[r.length - 1]!), cache as WhisperFastCache, fastCfg!, state);
549
+ last = out.pre;
550
+ filtered = out.filtered;
551
+ this.options.observer.onStepLogits?.(i, last);
552
+ }
553
+ last.dispose();
554
+ const step = decoder.update(rows, filtered, sumLogprobs);
555
+ filtered.dispose();
556
+ sumLogprobs = step.sumLogprobs;
557
+ completed = step.completed;
558
+ if (this.options.observer.onStepRows)
559
+ this.options.observer.onStepRows(i, rows.map((r) => [...r]), [...sumLogprobs.toFloat32()]);
560
+ if (completed) break;
561
+ }
562
+ const fin = decoder.finalize(rows, sumLogprobs);
563
+ const seqs = fin.tokens.map((r) => {
564
+ const s = r.slice(this.sampleBegin);
565
+ const e = s.indexOf(t.eot);
566
+ return e >= 0 ? s.slice(0, e) : s;
567
+ });
568
+ const lps = fin.sumLogprobs;
569
+ // MaximumLikelihoodRanker
570
+ const scores = seqs.map((s, k) => {
571
+ const length = s.length;
572
+ const penalty = this.options.lengthPenalty === null ? length : ((5 + length) / 6) ** this.options.lengthPenalty;
573
+ return lps[k]! / penalty;
574
+ });
575
+ let best = 0;
576
+ for (let k = 1; k < scores.length; k++) if (scores[k]! > scores[best]!) best = k;
577
+ const tokens = seqs[best]!;
578
+ const text = t.decode(tokens).trim();
579
+ return {
580
+ language: language!,
581
+ languageProbs,
582
+ tokens,
583
+ text,
584
+ avgLogprob: lps[best]! / (tokens.length + 1),
585
+ noSpeechProb,
586
+ temperature: this.options.temperature,
587
+ compressionRatio: compressionRatio(text),
588
+ };
589
+ } finally {
590
+ sumLogprobs.dispose();
591
+ cache.dispose();
592
+ }
593
+ }
594
+ }
595
+
596
+ function maskOf(V: number, ids: number[]): MlxArray {
597
+ const m = new Float32Array(V);
598
+ for (const id of ids) m[id] = -Infinity;
599
+ return MlxArray.fromFloat32(m, [V]);
600
+ }
601
+
602
+ interface TokenDecoder {
603
+ /** Mutates `rows` (appends the chosen token per row). Takes ownership of
604
+ * `sumLogprobs` and returns the new one. */
605
+ update(rows: number[][], logits: MlxArray, sumLogprobs: MlxArray): { completed: boolean; sumLogprobs: MlxArray };
606
+ finalize(rows: number[][], sumLogprobs: MlxArray): { tokens: number[][]; sumLogprobs: number[] };
607
+ }
608
+
609
+ /** GreedyDecoder (temperature 0 → argmax, else categorical sampling). */
610
+ class GreedyDecoder implements TokenDecoder {
611
+ constructor(readonly temperature: number, readonly eot: number) {}
612
+
613
+ update(rows: number[][], logits: MlxArray, sumLogprobs: MlxArray) {
614
+ const B = rows.length;
615
+ let nextArr: MlxArray;
616
+ if (this.temperature === 0) nextArr = ops.argmaxAxis(logits, -1);
617
+ else {
618
+ const temp = ops.scalarLike(this.temperature, logits);
619
+ const scaled = ops.div(logits, temp);
620
+ temp.dispose();
621
+ // mx.random.categorical(logits / temp) on the GLOBAL key, like the oracle
622
+ nextArr = ops.randomCategorical(scaled, null);
623
+ scaled.dispose();
624
+ }
625
+ nextArr = dispose(nextArr, nextArr.astype(Dtype.int32));
626
+ const lse = ops.logsumexpAxis(logits, -1, true);
627
+ const logprobs = ops.sub(logits, lse);
628
+ lse.dispose();
629
+ const idx = ops.reshape(nextArr, [B, 1]);
630
+ let current = ops.takeAlongAxis(logprobs, idx, 1);
631
+ idx.dispose();
632
+ logprobs.dispose();
633
+ current = dispose(current, ops.reshape(current, [B]));
634
+ const lastWasEot = rows.map((r) => (r[r.length - 1] === this.eot ? 1 : 0));
635
+ const notEot = MlxArray.fromFloat32(new Float32Array(lastWasEot.map((e) => 1 - e)), [B]);
636
+ const contrib = ops.mul(current, notEot);
637
+ current.dispose();
638
+ notEot.dispose();
639
+ const newSum = ops.add(sumLogprobs, contrib);
640
+ contrib.dispose();
641
+ sumLogprobs.dispose();
642
+ const next = nextArr.toIntTokens();
643
+ nextArr.dispose();
644
+ let completed = true;
645
+ for (let k = 0; k < B; k++) {
646
+ const tok = lastWasEot[k] ? this.eot : next[k]!;
647
+ rows[k]!.push(tok);
648
+ if (tok !== this.eot) completed = false;
649
+ }
650
+ return { completed, sumLogprobs: newSum };
651
+ }
652
+
653
+ finalize(rows: number[][], sumLogprobs: MlxArray) {
654
+ const lp = [...sumLogprobs.toFloat32()];
655
+ return { tokens: rows.map((r) => [...r, this.eot]), sumLogprobs: lp };
656
+ }
657
+
658
+ }
659
+
660
+ /** BeamSearchDecoder (openai-whisper decoding.py) for n_audio = 1: keeps
661
+ * `beamSize` live hypotheses as cache rows, collects finished sequences
662
+ * per audio, stops after `beamSize × patience` finished sequences. Cumulative
663
+ * log-probs are tracked on the host (beam scores are sums of read-back
664
+ * candidate log-probs, same arithmetic as the reference's python floats). */
665
+ class BeamSearchDecoder implements TokenDecoder {
666
+ readonly maxCandidates: number;
667
+ #finished = new Map<string, { tokens: number[]; logprob: number }>();
668
+ constructor(
669
+ readonly beamSize: number, readonly eot: number, readonly cache: { rearrange(indices: number[]): void },
670
+ patience: number | null,
671
+ ) {
672
+ this.maxCandidates = Math.round(beamSize * (patience ?? 1.0));
673
+ if (this.maxCandidates <= 0) throw new Error(`Invalid beam size (${beamSize}) or patience (${patience})`);
674
+ }
675
+
676
+ update(rows: number[][], logits: MlxArray, sumLogprobs: MlxArray) {
677
+ const B = rows.length;
678
+ if (B !== this.beamSize) throw new Error(`beam rows ${B} != beam size ${this.beamSize}`);
679
+ const k = this.beamSize + 1;
680
+ const lse = ops.logsumexpAxis(logits, -1, true);
681
+ const logprobs = ops.sub(logits, lse);
682
+ lse.dispose();
683
+ // top-k per row on device: argpartition puts the k SMALLEST first, so
684
+ // partition the negated log-probs; values gathered, sorted on host.
685
+ const neg = ops.neg(logprobs);
686
+ const part = ops.argpartitionAxis(neg, k - 1, -1);
687
+ neg.dispose();
688
+ // The [B, k] slice is a strided VIEW of the [B, V] partition; host
689
+ // readback walks memory linearly, so materialize it first.
690
+ const idxView = part.slice([0, 0], [B, k]);
691
+ part.dispose();
692
+ const idx = ops.contiguous(idxView);
693
+ idxView.dispose();
694
+ const vals = ops.takeAlongAxis(logprobs, idx, 1);
695
+ logprobs.dispose();
696
+ return this.updateFromTopK(rows, idx, vals, sumLogprobs);
697
+ }
698
+
699
+ /** Beam update from device top-k `idx`/`vals` [B, k] (owned, disposed here). */
700
+ updateFromTopK(rows: number[][], idx: MlxArray, vals: MlxArray, sumLogprobs: MlxArray) {
701
+ const B = rows.length;
702
+ const k = this.beamSize + 1;
703
+ const topIdx = idx.toIntTokens();
704
+ idx.dispose();
705
+ const topVals = vals.toFloat32();
706
+ vals.dispose();
707
+ const prevSum = [...sumLogprobs.toFloat32()];
708
+ sumLogprobs.dispose();
709
+ // reference: for each beam, its top-(beam+1) candidates by logprob;
710
+ // scores collected in a dict keyed by the sequence (dedup), then the
711
+ // best `beamSize` unfinished are kept.
712
+ const scores = new Map<string, number>();
713
+ const sources = new Map<string, number>();
714
+ const seqOf = new Map<string, number[]>();
715
+ for (let j = 0; j < B; j++) {
716
+ const cands: { lp: number; tok: number }[] = [];
717
+ for (let c = 0; c < k; c++) cands.push({ lp: topVals[j * k + c]!, tok: topIdx[j * k + c]! });
718
+ cands.sort((a, b) => b.lp - a.lp);
719
+ for (const { lp, tok } of cands) {
720
+ const seq = [...rows[j]!, tok];
721
+ const key = seq.join(",");
722
+ scores.set(key, prevSum[j]! + lp);
723
+ sources.set(key, j);
724
+ seqOf.set(key, seq);
725
+ }
726
+ }
727
+ // python dict insertion order + sorted(scores, key=scores.get, reverse=True)
728
+ // — a stable sort, so ties keep insertion order.
729
+ const ordered = [...scores.keys()].sort((a, b) => scores.get(b)! - scores.get(a)!);
730
+ const nextRows: number[][] = [];
731
+ const nextSum: number[] = [];
732
+ const sourceIdx: number[] = [];
733
+ let saved = 0;
734
+ for (const key of ordered) {
735
+ const seq = seqOf.get(key)!;
736
+ if (seq[seq.length - 1] === this.eot) {
737
+ if (!this.#finished.has(key)) this.#finished.set(key, { tokens: seq, logprob: scores.get(key)! });
738
+ } else {
739
+ nextRows.push(seq);
740
+ nextSum.push(scores.get(key)!);
741
+ sourceIdx.push(sources.get(key)!);
742
+ saved++;
743
+ if (saved === this.beamSize) break;
744
+ }
745
+ }
746
+ // pad (only when fewer than beamSize unfinished exist — all EOT)
747
+ while (nextRows.length < this.beamSize) {
748
+ const j = nextRows.length;
749
+ nextRows.push([...rows[j]!, this.eot]);
750
+ nextSum.push(prevSum[j]!);
751
+ sourceIdx.push(j);
752
+ }
753
+ for (let j = 0; j < B; j++) rows[j] = nextRows[j]!;
754
+ this.cache.rearrange(sourceIdx);
755
+ const completed = this.#finished.size >= this.maxCandidates;
756
+ return { completed, sumLogprobs: MlxArray.fromFloat32(new Float32Array(nextSum), [B]) };
757
+ }
758
+
759
+ finalize(rows: number[][], sumLogprobs: MlxArray) {
760
+ const prev = [...sumLogprobs.toFloat32()];
761
+ // collect all finished sequences, including patience; add unfinished
762
+ // ones if not enough
763
+ const finished = [...this.#finished.values()];
764
+ if (finished.length < this.beamSize) {
765
+ const order = [...prev.keys()].sort((a, b) => prev[b]! - prev[a]!);
766
+ for (const j of order) {
767
+ finished.push({ tokens: [...rows[j]!, this.eot], logprob: prev[j]! });
768
+ if (finished.length >= this.beamSize) break;
769
+ }
770
+ }
771
+ return { tokens: finished.map((f) => f.tokens), sumLogprobs: finished.map((f) => f.logprob) };
772
+ }
773
+ }