mlx-bun 0.5.0 → 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 (640) hide show
  1. package/README.md +1449 -109
  2. package/THIRD_PARTY_NOTICES.md +882 -0
  3. package/bin/mlx-bun.convert.mjs +4 -0
  4. package/bin/mlx-bun.fuse.mjs +4 -0
  5. package/bin/mlx-bun.generate.mjs +4 -0
  6. package/bin/mlx-bun.lora.mjs +4 -0
  7. package/bin/mlx-bun.mjs +13 -21
  8. package/bin/mlx-bun.server.mjs +4 -0
  9. package/bin/mlx-bun.upload.mjs +4 -0
  10. package/dist/web/app.js +9584 -0
  11. package/package.json +50 -53
  12. package/src/cli/ane-install.ts +76 -0
  13. package/src/cli/args.ts +179 -0
  14. package/src/cli/engine-entry.ts +68 -0
  15. package/src/cli/inference.ts +214 -0
  16. package/src/cli/job-entry.ts +97 -0
  17. package/src/cli/library-host.ts +117 -0
  18. package/src/cli/main.ts +57 -0
  19. package/src/cli/memory-engine.ts +163 -0
  20. package/src/cli/memory.ts +36 -0
  21. package/src/cli/mlx-lm-aliases.ts +309 -0
  22. package/src/cli/model-choice.ts +33 -0
  23. package/src/cli/model-selection.ts +84 -0
  24. package/src/cli/module-host.ts +11 -0
  25. package/src/cli/module-verbs.ts +50 -0
  26. package/src/cli/numerical-policy.ts +12 -0
  27. package/src/cli/perplexity.ts +89 -0
  28. package/src/cli/serve-host.ts +423 -0
  29. package/src/cli/serve-isolated.ts +217 -0
  30. package/src/cli/serve-options.ts +106 -0
  31. package/src/cli/serve-state.ts +205 -0
  32. package/src/cli/serve.ts +500 -0
  33. package/src/cli/served-model-host.ts +102 -0
  34. package/src/cli/server-entry.ts +148 -0
  35. package/src/cli/serving-unit.ts +177 -0
  36. package/src/{tui.ts → cli/terminal.ts} +13 -8
  37. package/src/cli/worker-adapters.ts +34 -0
  38. package/src/cli/worker-entry.ts +272 -0
  39. package/src/cli/worker-unit.ts +179 -0
  40. package/src/engine/adapter-operation.ts +26 -0
  41. package/src/engine/cache-services.ts +114 -0
  42. package/src/engine/completion.ts +39 -0
  43. package/src/engine/generation-gateway.ts +358 -0
  44. package/src/engine/index.ts +59 -0
  45. package/src/engine/kv-budget.ts +120 -0
  46. package/src/engine/media-preparation.ts +232 -0
  47. package/src/engine/model-binding.ts +24 -0
  48. package/src/engine/model-host.ts +513 -0
  49. package/src/engine/model-serving.ts +89 -0
  50. package/src/{serve → engine}/preparation.ts +7 -5
  51. package/src/{serve → engine}/session-completion-engine.ts +6 -6
  52. package/src/engine/telemetry.ts +109 -0
  53. package/src/hub/downloads.ts +116 -0
  54. package/src/jobs/db.ts +16 -7
  55. package/src/jobs/events.ts +29 -0
  56. package/src/jobs/executable.ts +5 -0
  57. package/src/jobs/host.ts +51 -0
  58. package/src/jobs/in-process.ts +55 -0
  59. package/src/{contracts/jobs.ts → jobs/protocol.ts} +7 -9
  60. package/src/jobs/runner.ts +150 -231
  61. package/src/jobs/service.ts +75 -0
  62. package/src/jobs/sse.ts +59 -23
  63. package/src/jobs/worker-memory.ts +46 -0
  64. package/src/jobs/worker-process.ts +163 -0
  65. package/src/jobs/worker-supervisor.ts +258 -0
  66. package/src/modules.ts +58 -0
  67. package/src/publishing/catalog-hub.ts +18 -0
  68. package/src/publishing/credentials.ts +31 -0
  69. package/src/publishing/upload.ts +37 -0
  70. package/src/residency/leased-adapters.ts +21 -0
  71. package/src/residency/local-records.ts +16 -0
  72. package/src/residency/model-residency.ts +405 -0
  73. package/src/residency/record-index.ts +28 -0
  74. package/src/residency/resident-estimate.ts +50 -0
  75. package/src/{anthropic.ts → server/anthropic.ts} +3 -81
  76. package/src/server/cache-routes.ts +36 -0
  77. package/src/{serve → server}/chat-request.ts +10 -10
  78. package/src/{serve → server}/chat-stage.ts +14 -14
  79. package/src/{client.ts → server/client.ts} +28 -2
  80. package/src/{serve → server}/completion-executor.ts +33 -41
  81. package/src/server/decision-body.ts +55 -0
  82. package/src/server/decision-routes.ts +134 -0
  83. package/src/server/decision-wire.ts +228 -0
  84. package/src/{serve → server}/discovery-routes.ts +25 -82
  85. package/src/{serve → server}/http.ts +25 -13
  86. package/src/{serve → server}/inference-request.ts +5 -5
  87. package/src/server/job-routes.ts +20 -0
  88. package/src/server/media-prompt.ts +63 -0
  89. package/src/server/memory-completion-client.ts +46 -0
  90. package/src/server/model-routes.ts +192 -0
  91. package/src/{serve → server}/openai-wire.ts +3 -2
  92. package/src/{serve → server}/pipeline.ts +1 -1
  93. package/src/server/prompt-builder.ts +13 -0
  94. package/src/server/prompt-contracts.ts +17 -0
  95. package/src/server/proxy-routes.ts +187 -0
  96. package/src/server/publishing-routes.ts +41 -0
  97. package/src/{serve → server}/request-plan.ts +6 -8
  98. package/src/{serve → server}/request-prep.ts +47 -50
  99. package/src/{serve → server}/responses-client.ts +30 -13
  100. package/src/{responses.ts → server/responses.ts} +12 -93
  101. package/src/server/routes.ts +205 -0
  102. package/src/server/start.ts +72 -0
  103. package/src/server/status-routes.ts +108 -0
  104. package/src/{serve → server}/text-completion-stage.ts +25 -4
  105. package/src/server/text-prompt.ts +12 -0
  106. package/src/{serve → server}/token-streams.ts +36 -38
  107. package/src/server/worker-routes.ts +311 -0
  108. package/src/storage/paths.ts +61 -0
  109. package/src/web/assets.ts +37 -0
  110. package/src/web/{src → browser}/api.ts +5 -23
  111. package/src/web/browser/chat-host.ts +17 -0
  112. package/src/web/browser/installed-panels.ts +6 -0
  113. package/src/web/browser/main.ts +55 -0
  114. package/src/web/browser/markdown.ts +279 -0
  115. package/src/web/browser/memory-panel.ts +9 -0
  116. package/src/web/browser/palette.ts +64 -0
  117. package/src/web/browser/protocol.ts +13 -0
  118. package/src/web/browser/shell.ts +467 -0
  119. package/src/web/{src → browser}/status.ts +32 -36
  120. package/src/web/build.ts +51 -0
  121. package/src/web/public/app.html +1054 -0
  122. package/src/web/{sw.js → public/sw.js} +4 -3
  123. package/src/web/{vendor → public/vendor}/hljs-theme.css +2 -2
  124. package/src/web/{vendor → public/vendor}/hljs.js +2 -2
  125. package/THIRD_PARTY_LICENSES.md +0 -51
  126. package/docs/README.md +0 -107
  127. package/docs/archive/hlg-sampling.md +0 -350
  128. package/docs/archive/investigations/audio-input-plan.md +0 -356
  129. package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +0 -3617
  130. package/docs/archive/investigations/chunk-finetune-experiment.md +0 -84
  131. package/docs/archive/investigations/colibri-glm52-port.md +0 -1486
  132. package/docs/archive/investigations/colibri-oracle-pin.md +0 -1284
  133. package/docs/archive/investigations/curve-bisector-routing.md +0 -123
  134. package/docs/archive/investigations/decode-roofline-lookagain.md +0 -348
  135. package/docs/archive/investigations/diffusion-gemma-port.md +0 -602
  136. package/docs/archive/investigations/dspark-handoff.md +0 -224
  137. package/docs/archive/investigations/expert-offload-single-user-moe.md +0 -292
  138. package/docs/archive/investigations/faithful-l1-consolidation.md +0 -310
  139. package/docs/archive/investigations/hlg-sampling-investigation.md +0 -970
  140. package/docs/archive/investigations/indexshare-performance-spike.md +0 -357
  141. package/docs/archive/investigations/kernel-perf-review-2026-07.md +0 -171
  142. package/docs/archive/investigations/lab-build-journal.md +0 -157
  143. package/docs/archive/investigations/minicpm5-decode-megakernel.md +0 -489
  144. package/docs/archive/investigations/openjarvis-fit.md +0 -198
  145. package/docs/archive/investigations/openwiki-evaluation.md +0 -249
  146. package/docs/archive/investigations/optimization_plan.md +0 -346
  147. package/docs/archive/investigations/orpo-base-uf-experiment-and-directions.md +0 -120
  148. package/docs/archive/investigations/orpo-flash-cce-pin-leak.md +0 -167
  149. package/docs/archive/investigations/orpo-uf-testing-handoff.md +0 -126
  150. package/docs/archive/investigations/pareto-specialized-runtime-findings.md +0 -92
  151. package/docs/archive/investigations/pi-builtin-investigation.md +0 -308
  152. package/docs/archive/investigations/pre-colibri-stabilization.md +0 -329
  153. package/docs/archive/investigations/qwen38-27b-campaign-log.md +0 -1092
  154. package/docs/archive/investigations/segmented-backward-handoff.md +0 -148
  155. package/docs/archive/investigations/starter-model-port-handoff.md +0 -342
  156. package/docs/archive/investigations/steel-flash-cce-handoff.md +0 -489
  157. package/docs/archive/investigations/the-dreaming-handoff.md +0 -141
  158. package/docs/archive/investigations/trainer-validation-experiment.md +0 -173
  159. package/docs/archive/planning/chat-ui-vision-memory-first.md +0 -199
  160. package/docs/archive/planning/chat-ui-vision.md +0 -50
  161. package/docs/archive/planning/chunk-task-roadmap.md +0 -77
  162. package/docs/archive/planning/curve-sampler-research-plan.md +0 -331
  163. package/docs/archive/planning/journal.md +0 -316
  164. package/docs/archive/planning/memory-docs-and-dag-plan.md +0 -194
  165. package/docs/archive/planning/pi-integration-review.md +0 -178
  166. package/docs/archive/planning/release-notes-v0.0.10.md +0 -57
  167. package/docs/archive/planning/release-notes-v0.0.11.md +0 -239
  168. package/docs/archive/planning/release-notes-v0.0.12.md +0 -80
  169. package/docs/archive/planning/release-notes-v0.0.13.md +0 -77
  170. package/docs/archive/planning/release-notes-v0.0.9.md +0 -122
  171. package/docs/archive/planning/release-notes-v0.2.0.md +0 -49
  172. package/docs/archive/planning/release-notes-v0.3.0.md +0 -35
  173. package/docs/archive/planning/release-notes-v0.4.0.md +0 -45
  174. package/docs/archive/planning/release-notes-v0.5.0.md +0 -37
  175. package/docs/archive/planning/web-ui-pass-plan.md +0 -213
  176. package/docs/archive/planning/website-readme-pass-plan.md +0 -224
  177. package/docs/design/batching.md +0 -2054
  178. package/docs/design/decode-speed-program.md +0 -2404
  179. package/docs/design/dreaming-nightly-pipeline.md +0 -684
  180. package/docs/design/generic-model-support.md +0 -821
  181. package/docs/design/kv-cache.md +0 -993
  182. package/docs/design/orpo-training.md +0 -680
  183. package/docs/design/speculative-decoding.md +0 -2129
  184. package/docs/design/turboquant.md +0 -2225
  185. package/docs/design/unified-engine-frontier-plan.md +0 -1987
  186. package/docs/design/web-chat-redesign.md +0 -695
  187. package/docs/planning/PRODUCT_ROADMAP.md +0 -593
  188. package/docs/planning/ResearchTopics.md +0 -119
  189. package/docs/planning/public-readiness-handoff.md +0 -244
  190. package/docs/planning/public-readiness.md +0 -149
  191. package/docs/planning/video-series.md +0 -421
  192. package/docs/reference/benchmarks.md +0 -3587
  193. package/docs/reference/cli.md +0 -646
  194. package/docs/reference/distribution.md +0 -380
  195. package/docs/reference/environment.md +0 -494
  196. package/docs/reference/glossary.md +0 -39
  197. package/docs/reference/library-api.md +0 -688
  198. package/docs/reference/memory.md +0 -260
  199. package/docs/reference/models.md +0 -346
  200. package/docs/reference/server-api.md +0 -1945
  201. package/docs/reference/server-config.md +0 -973
  202. package/docs/reference/training.md +0 -462
  203. package/docs/reference/troubleshooting.md +0 -158
  204. package/src/admission-plan.ts +0 -18
  205. package/src/audio/audiotoolbox.ts +0 -112
  206. package/src/audio/conformer.ts +0 -655
  207. package/src/audio/decode.ts +0 -133
  208. package/src/audio/features.ts +0 -202
  209. package/src/audio/mic-capture.ts +0 -98
  210. package/src/audio/silero-vad.ts +0 -379
  211. package/src/audio/transcode.ts +0 -67
  212. package/src/audio/whisper-decode.ts +0 -774
  213. package/src/audio/whisper-fast.ts +0 -732
  214. package/src/audio/whisper-format.ts +0 -73
  215. package/src/audio/whisper-mel.ts +0 -185
  216. package/src/audio/whisper-timing.ts +0 -274
  217. package/src/audio/whisper-tokenizer.ts +0 -239
  218. package/src/audio/whisper-transcribe.ts +0 -410
  219. package/src/backends/mlx/assistant-target.ts +0 -46
  220. package/src/backends/mlx/autoregressive-draft-rows.ts +0 -120
  221. package/src/backends/mlx/autoregressive.ts +0 -129
  222. package/src/backends/mlx/batch-group.ts +0 -1874
  223. package/src/backends/mlx/cache-attachment.ts +0 -67
  224. package/src/backends/mlx/cache-layout.ts +0 -31
  225. package/src/backends/mlx/checkpoint-state.ts +0 -53
  226. package/src/backends/mlx/continuation-persistence.ts +0 -107
  227. package/src/backends/mlx/continuation-request.ts +0 -43
  228. package/src/backends/mlx/continuation.ts +0 -77
  229. package/src/backends/mlx/diffusion.ts +0 -39
  230. package/src/backends/mlx/draft-target.ts +0 -32
  231. package/src/backends/mlx/encoder-cache.ts +0 -25
  232. package/src/backends/mlx/evaluation.ts +0 -105
  233. package/src/backends/mlx/fill-append.ts +0 -58
  234. package/src/backends/mlx/fill-group.ts +0 -497
  235. package/src/backends/mlx/full-kv-row-append.ts +0 -32
  236. package/src/backends/mlx/full-kv-row-donor.ts +0 -54
  237. package/src/backends/mlx/full-transitioning-kv-rows.ts +0 -65
  238. package/src/backends/mlx/gateway-binding.ts +0 -174
  239. package/src/backends/mlx/graph.ts +0 -38
  240. package/src/backends/mlx/kv-codec.ts +0 -9
  241. package/src/backends/mlx/kv-maintenance.ts +0 -161
  242. package/src/backends/mlx/kv-tensor-rows.ts +0 -212
  243. package/src/backends/mlx/memory-client.ts +0 -189
  244. package/src/backends/mlx/methods.ts +0 -90
  245. package/src/backends/mlx/mixed-iteration.ts +0 -62
  246. package/src/backends/mlx/model-prompt.ts +0 -178
  247. package/src/backends/mlx/model-serving.ts +0 -80
  248. package/src/backends/mlx/paged-cache-codec.ts +0 -27
  249. package/src/backends/mlx/prefill-cohort.ts +0 -100
  250. package/src/backends/mlx/prefill-policy.ts +0 -26
  251. package/src/backends/mlx/prefill-rows.ts +0 -244
  252. package/src/backends/mlx/prefill.ts +0 -33
  253. package/src/backends/mlx/projected-context-rows.ts +0 -72
  254. package/src/backends/mlx/prompt-input.ts +0 -29
  255. package/src/backends/mlx/qwen-prompt-input.ts +0 -40
  256. package/src/backends/mlx/request-policy.ts +0 -20
  257. package/src/backends/mlx/request-state-policy.ts +0 -54
  258. package/src/backends/mlx/rollback.ts +0 -92
  259. package/src/backends/mlx/row-sampling.ts +0 -49
  260. package/src/backends/mlx/serial-executor.ts +0 -251
  261. package/src/backends/mlx/speculative-group.ts +0 -438
  262. package/src/backends/mlx/speculative-round.ts +0 -161
  263. package/src/backends/mlx/speculative.ts +0 -131
  264. package/src/backends/mlx/state-rows.ts +0 -104
  265. package/src/backends/mlx/state-views.ts +0 -46
  266. package/src/backends/mlx/target-layout-capability.ts +0 -35
  267. package/src/backends/mlx/text-engine.ts +0 -73
  268. package/src/backends/mlx/transitioning-kv-rows.ts +0 -173
  269. package/src/chat-template.ts +0 -382
  270. package/src/cli.ts +0 -3490
  271. package/src/config.ts +0 -399
  272. package/src/contracts/completion.ts +0 -16
  273. package/src/contracts/execution.ts +0 -70
  274. package/src/contracts/generation.ts +0 -103
  275. package/src/contracts/host.ts +0 -7
  276. package/src/contracts/object-cache.ts +0 -8
  277. package/src/contracts/pi-provider.ts +0 -30
  278. package/src/contracts/pi.ts +0 -261
  279. package/src/contracts/prefix-cache.ts +0 -33
  280. package/src/contracts/resources.ts +0 -25
  281. package/src/contracts/sampling.ts +0 -23
  282. package/src/contracts/scheduling.ts +0 -54
  283. package/src/contracts/tsconfig.json +0 -14
  284. package/src/dataset/generators.ts +0 -1087
  285. package/src/dataset/index.ts +0 -12
  286. package/src/dataset/job.ts +0 -81
  287. package/src/dataset/llm.ts +0 -71
  288. package/src/dataset/registry.ts +0 -528
  289. package/src/diffusion/diffusion-generate.ts +0 -507
  290. package/src/download.ts +0 -505
  291. package/src/embed.ts +0 -77
  292. package/src/engine/admission.ts +0 -59
  293. package/src/engine/cancellation.ts +0 -38
  294. package/src/engine/engine.ts +0 -56
  295. package/src/engine/execution-coordinator.ts +0 -70
  296. package/src/engine/execution-plan.ts +0 -64
  297. package/src/engine/execution-tasks.ts +0 -44
  298. package/src/engine/namespaced-cache.ts +0 -13
  299. package/src/engine/resources.ts +0 -53
  300. package/src/engine/scheduler.ts +0 -92
  301. package/src/engine/session.ts +0 -247
  302. package/src/eval/README.md +0 -130
  303. package/src/eval/ifeval.ts +0 -81
  304. package/src/eval/kl-prompts.ts +0 -29
  305. package/src/eval/kl.ts +0 -590
  306. package/src/eval/perplexity.ts +0 -164
  307. package/src/eval/quality-db.ts +0 -92
  308. package/src/eval/reference-logits.ts +0 -207
  309. package/src/eval/runner.ts +0 -124
  310. package/src/eval/score.ts +0 -49
  311. package/src/eval/tasks/bfcl.ts +0 -535
  312. package/src/eval/tasks/gsm8k.ts +0 -98
  313. package/src/eval/tasks/hashhop.ts +0 -139
  314. package/src/eval/tasks/humaneval.ts +0 -275
  315. package/src/eval/tasks/ifeval.ts +0 -525
  316. package/src/eval/tasks/mmlu.ts +0 -157
  317. package/src/eval/ultrafeedback.ts +0 -60
  318. package/src/evaldb.ts +0 -199
  319. package/src/expert-io.ts +0 -393
  320. package/src/expert-offload-build.ts +0 -77
  321. package/src/expert-offload.ts +0 -76
  322. package/src/expert-residency.ts +0 -1134
  323. package/src/expert-usage.ts +0 -415
  324. package/src/fill/echo-index.ts +0 -188
  325. package/src/fill/fill-session.ts +0 -463
  326. package/src/fill/proposal.ts +0 -68
  327. package/src/fill/schema-rows.ts +0 -506
  328. package/src/fill/tool-boundary.ts +0 -103
  329. package/src/fit.ts +0 -249
  330. package/src/generate.ts +0 -1352
  331. package/src/grammar.ts +0 -604
  332. package/src/harness-pi.ts +0 -184
  333. package/src/hf-push.ts +0 -628
  334. package/src/hub-rest.ts +0 -242
  335. package/src/index.ts +0 -63
  336. package/src/inference/denoising.ts +0 -17
  337. package/src/inference/draft-acceptance.ts +0 -41
  338. package/src/inference/draft-output.ts +0 -49
  339. package/src/inference/graph.ts +0 -33
  340. package/src/inference/prefill.ts +0 -45
  341. package/src/inference/rollback.ts +0 -7
  342. package/src/jobs/index.ts +0 -31
  343. package/src/jobs/job-entry.ts +0 -107
  344. package/src/jobs/task-client.ts +0 -19
  345. package/src/jobs/types.ts +0 -2
  346. package/src/kv-scheme.ts +0 -279
  347. package/src/kv-store.ts +0 -1135
  348. package/src/lab/README.md +0 -22
  349. package/src/lab/curve/curve-designer.html +0 -303
  350. package/src/lab/curve/curve-sampler.ts +0 -122
  351. package/src/lab/expert-trace/expert-trace.ts +0 -69
  352. package/src/lab/paged-kv/paged-attention.ts +0 -117
  353. package/src/lab/paged-kv/paged-kv-rows.ts +0 -111
  354. package/src/lab/paged-kv/paged-kv.ts +0 -336
  355. package/src/library.ts +0 -40
  356. package/src/lora.ts +0 -408
  357. package/src/md-modules.d.ts +0 -54
  358. package/src/media-fetch.ts +0 -442
  359. package/src/memory/article.ts +0 -558
  360. package/src/memory/chunk-validation.ts +0 -150
  361. package/src/memory/chunk.ts +0 -318
  362. package/src/memory/cluster.ts +0 -351
  363. package/src/memory/coverage.ts +0 -134
  364. package/src/memory/crosslink.ts +0 -0
  365. package/src/memory/db.ts +0 -253
  366. package/src/memory/entity.ts +0 -203
  367. package/src/memory/gate.ts +0 -187
  368. package/src/memory/history.ts +0 -152
  369. package/src/memory/ingest.ts +0 -335
  370. package/src/memory/model.ts +0 -183
  371. package/src/memory/normalize.ts +0 -671
  372. package/src/memory/parse.ts +0 -22
  373. package/src/memory/pipeline.ts +0 -275
  374. package/src/memory/prompts.ts +0 -65
  375. package/src/memory/query.ts +0 -334
  376. package/src/memory/reconcile.ts +0 -734
  377. package/src/memory/redirect.ts +0 -108
  378. package/src/memory/reindex.ts +0 -325
  379. package/src/memory/resolve.ts +0 -307
  380. package/src/memory/rest.ts +0 -245
  381. package/src/memory/route.ts +0 -272
  382. package/src/memory/schedule.ts +0 -165
  383. package/src/memory/stages.ts +0 -777
  384. package/src/memory/synthesize.ts +0 -1638
  385. package/src/memory/tools.ts +0 -749
  386. package/src/memory/vault.ts +0 -1194
  387. package/src/memory/wikify.ts +0 -1064
  388. package/src/mlx/array.ts +0 -425
  389. package/src/mlx/autograd.ts +0 -304
  390. package/src/mlx/checkpoint.ts +0 -149
  391. package/src/mlx/compile.ts +0 -168
  392. package/src/mlx/custom-vjp.ts +0 -188
  393. package/src/mlx/ffi.ts +0 -531
  394. package/src/mlx/materialize.ts +0 -23
  395. package/src/mlx/metal-kernel.ts +0 -159
  396. package/src/mlx/normalized-argmax.ts +0 -116
  397. package/src/mlx/ops.ts +0 -1108
  398. package/src/mlx/quantized-rows.ts +0 -36
  399. package/src/mlx/token-bitmask.ts +0 -52
  400. package/src/mlx/turboquant-kv-decode.ts +0 -82
  401. package/src/mlx/turboquant-kv-inverse.ts +0 -122
  402. package/src/mlx/turboquant-ops.ts +0 -861
  403. package/src/mlx/turboquant-tables.ts +0 -237
  404. package/src/mmap.ts +0 -90
  405. package/src/model/artifact-identity.ts +0 -73
  406. package/src/model/batched-kv.ts +0 -235
  407. package/src/model/batched-mask.ts +0 -279
  408. package/src/model/batched-quant.ts +0 -212
  409. package/src/model/batched-quantized-kv.ts +0 -144
  410. package/src/model/batched-rotating-quant.ts +0 -353
  411. package/src/model/batched-rotating-state.ts +0 -149
  412. package/src/model/batched-rotating.ts +0 -338
  413. package/src/model/batched-row-storage.ts +0 -179
  414. package/src/model/batched-ssm.ts +0 -52
  415. package/src/model/batched-turboquant-kv.ts +0 -94
  416. package/src/model/compiled-decode.ts +0 -712
  417. package/src/model/decoded-kv-donor.ts +0 -12
  418. package/src/model/delayed-quantized-kv.ts +0 -72
  419. package/src/model/delayed-rotating-quantized-kv.ts +0 -93
  420. package/src/model/delayed-turboquant-kv.ts +0 -57
  421. package/src/model/diffusion-gemma.ts +0 -1026
  422. package/src/model/factory.ts +0 -215
  423. package/src/model/fingerprint.ts +0 -45
  424. package/src/model/flash-attention.ts +0 -694
  425. package/src/model/full-prefill-padding.ts +0 -56
  426. package/src/model/full-prefill-row.ts +0 -105
  427. package/src/model/gemma4-base.ts +0 -2370
  428. package/src/model/gemma4.ts +0 -1395
  429. package/src/model/generated/gemma4-12b.ts +0 -429
  430. package/src/model/generated/gemma4-26b.ts +0 -448
  431. package/src/model/generated/gemma4-e4b.ts +0 -717
  432. package/src/model/generated/index.ts +0 -18
  433. package/src/model/glm52-atlas.ts +0 -511
  434. package/src/model/glm52-cache.ts +0 -792
  435. package/src/model/glm52-config.ts +0 -262
  436. package/src/model/glm52-container.ts +0 -305
  437. package/src/model/glm52-coupling.ts +0 -388
  438. package/src/model/glm52-dsa.ts +0 -606
  439. package/src/model/glm52-expert-layout.ts +0 -253
  440. package/src/model/glm52-layout.ts +0 -152
  441. package/src/model/glm52-memory.ts +0 -500
  442. package/src/model/glm52-mla.ts +0 -792
  443. package/src/model/glm52-moe.ts +0 -223
  444. package/src/model/glm52-pilot.ts +0 -537
  445. package/src/model/glm52-reference.ts +0 -445
  446. package/src/model/glm52-residency.ts +0 -557
  447. package/src/model/glm52-resident-weights.ts +0 -135
  448. package/src/model/glm52-streamed-experts.ts +0 -453
  449. package/src/model/glm52-streamed-metal.ts +0 -610
  450. package/src/model/glm52-weights.ts +0 -503
  451. package/src/model/glm52.ts +0 -1132
  452. package/src/model/implementation.ts +0 -69
  453. package/src/model/kv-attention-view.ts +0 -70
  454. package/src/model/minicpm5.ts +0 -376
  455. package/src/model/profile.ts +0 -468
  456. package/src/model/quantized-append-attention.ts +0 -31
  457. package/src/model/qwen-append.ts +0 -12
  458. package/src/model/qwen-conv.ts +0 -52
  459. package/src/model/qwen3-delta-state.ts +0 -74
  460. package/src/model/qwen3-delta.ts +0 -451
  461. package/src/model/qwen3-moe.ts +0 -404
  462. package/src/model/qwen3-mrope.ts +0 -250
  463. package/src/model/qwen3.ts +0 -277
  464. package/src/model/qwen3_5-checkpoint.ts +0 -164
  465. package/src/model/qwen3_5.ts +0 -881
  466. package/src/model/rotating-kv-donor.ts +0 -35
  467. package/src/model/rotating-kv-layout.ts +0 -262
  468. package/src/model/rotating-row-transaction.ts +0 -35
  469. package/src/model/speculative-rotating-kv.ts +0 -101
  470. package/src/model/ssm-prefill-padding.ts +0 -39
  471. package/src/model/support.ts +0 -110
  472. package/src/model/token-groups.ts +0 -66
  473. package/src/model/trellis-balanced-scatter.ts +0 -51
  474. package/src/model/trellis-linear.ts +0 -672
  475. package/src/model/trellis-shared-m.ts +0 -151
  476. package/src/model/trellis-shared-scatter.ts +0 -144
  477. package/src/model/trellis-splitk-prefill.ts +0 -158
  478. package/src/model/trellis-tiled-prefill.ts +0 -136
  479. package/src/model/trellis-vector-expand.ts +0 -62
  480. package/src/model/trellis-wide-prefill.ts +0 -94
  481. package/src/model/turboquant-codec.ts +0 -89
  482. package/src/model/universal/archs.ts +0 -332
  483. package/src/model/universal/dense.ts +0 -612
  484. package/src/model/universal/modules.ts +0 -170
  485. package/src/model/universal/rope.ts +0 -381
  486. package/src/model/whisper.ts +0 -435
  487. package/src/native/expert_io.c +0 -523
  488. package/src/native/frame_extract.swift +0 -140
  489. package/src/native/mic_capture.swift +0 -92
  490. package/src/native-pack.ts +0 -163
  491. package/src/pi-provider.ts +0 -53
  492. package/src/pi-session.ts +0 -81
  493. package/src/pi-terminal.ts +0 -274
  494. package/src/pi-web.ts +0 -1940
  495. package/src/preflight.ts +0 -158
  496. package/src/prompt-cache.ts +0 -682
  497. package/src/quantize/allocator.ts +0 -627
  498. package/src/quantize/atomic-output.ts +0 -38
  499. package/src/quantize/calibration.ts +0 -181
  500. package/src/quantize/config-writer.ts +0 -145
  501. package/src/quantize/index.ts +0 -84
  502. package/src/quantize/job.ts +0 -162
  503. package/src/quantize/quantizer.ts +0 -666
  504. package/src/quantize/rotate.ts +0 -603
  505. package/src/quantize/safetensors-writer.ts +0 -249
  506. package/src/quantize/sensitivity.ts +0 -216
  507. package/src/quantize/trellis.ts +0 -586
  508. package/src/quantize/weight-transform.ts +0 -292
  509. package/src/registry.ts +0 -602
  510. package/src/runtime-config.ts +0 -77
  511. package/src/safetensors.ts +0 -165
  512. package/src/sampler.ts +0 -1012
  513. package/src/serve/admin-routes.ts +0 -184
  514. package/src/serve/assets/curve-terrain.html +0 -105
  515. package/src/serve/assets/training-inference-map.html +0 -575
  516. package/src/serve/audio-routes.ts +0 -323
  517. package/src/serve/aux-routes.ts +0 -132
  518. package/src/serve/batch-scheduler.ts +0 -8
  519. package/src/serve/checkpoint-identity.ts +0 -64
  520. package/src/serve/generation-gateway.ts +0 -537
  521. package/src/serve/isolate.ts +0 -609
  522. package/src/serve/kv-budget.ts +0 -14
  523. package/src/serve/lab-routes.ts +0 -341
  524. package/src/serve/lane-registry.ts +0 -45
  525. package/src/serve/model-admin-routes.ts +0 -155
  526. package/src/serve/model-binding.ts +0 -49
  527. package/src/serve/model-host.ts +0 -511
  528. package/src/serve/parent-application.ts +0 -56
  529. package/src/serve/prompt-response-trace.ts +0 -161
  530. package/src/serve/response-lease.ts +0 -24
  531. package/src/serve/session-search.ts +0 -326
  532. package/src/serve/static-routes.ts +0 -101
  533. package/src/serve/transcription-server.ts +0 -64
  534. package/src/serve/transcription-service.ts +0 -502
  535. package/src/server.ts +0 -1282
  536. package/src/spec/assistant-rows.ts +0 -93
  537. package/src/spec/assistant-source.ts +0 -126
  538. package/src/spec/cache-identity.ts +0 -22
  539. package/src/spec/deepspec-rows.ts +0 -29
  540. package/src/spec/deepspec-source.ts +0 -145
  541. package/src/spec/dflash-source.ts +0 -149
  542. package/src/spec/drafter.ts +0 -304
  543. package/src/spec/dspark/ab-stats.ts +0 -136
  544. package/src/spec/dspark/calibration.ts +0 -112
  545. package/src/spec/dspark/data-dflash.ts +0 -156
  546. package/src/spec/dspark/data.ts +0 -201
  547. package/src/spec/dspark/deepspec-module.ts +0 -776
  548. package/src/spec/dspark/generate-dflash.ts +0 -245
  549. package/src/spec/dspark/generate.ts +0 -290
  550. package/src/spec/dspark/loader.ts +0 -23
  551. package/src/spec/dspark/loss.ts +0 -213
  552. package/src/spec/dspark/module-dflash.ts +0 -560
  553. package/src/spec/dspark/module.ts +0 -540
  554. package/src/spec/dspark/quantize-drafter.ts +0 -91
  555. package/src/spec/dspark/sample.ts +0 -112
  556. package/src/spec/generate.ts +0 -276
  557. package/src/spec/glm52-mtp-graph.ts +0 -29
  558. package/src/spec/glm52-mtp-rows.ts +0 -132
  559. package/src/spec/glm52-mtp-source.ts +0 -318
  560. package/src/spec/glm52-mtp-state.ts +0 -33
  561. package/src/spec/ngram-source.ts +0 -226
  562. package/src/spec/projected-draft-rows.ts +0 -73
  563. package/src/spec/qwen-mtp-module.ts +0 -171
  564. package/src/spec/qwen-mtp-rows.ts +0 -230
  565. package/src/spec/qwen-mtp-source.ts +0 -386
  566. package/src/spec/qwen-mtp-state.ts +0 -30
  567. package/src/spec/serve-loop.ts +0 -587
  568. package/src/spec/source.ts +0 -228
  569. package/src/spec/two-model-rows.ts +0 -62
  570. package/src/spec/two-model.ts +0 -113
  571. package/src/ssd-cache.ts +0 -431
  572. package/src/ssd-durability.ts +0 -209
  573. package/src/storage/host-buffer.ts +0 -24
  574. package/src/storage/kv-writer.ts +0 -83
  575. package/src/storage/kv-writer.worker.js +0 -264
  576. package/src/storage/retention-policy.ts +0 -37
  577. package/src/tiered-prompt-cache.ts +0 -45
  578. package/src/tokenizer.ts +0 -335
  579. package/src/tool-approvals.ts +0 -131
  580. package/src/tool-call.ts +0 -695
  581. package/src/train/dataset.ts +0 -484
  582. package/src/train/diffusion-lora.ts +0 -216
  583. package/src/train/export.ts +0 -32
  584. package/src/train/flash-cce.ts +0 -1234
  585. package/src/train/forward.ts +0 -266
  586. package/src/train/fuse.ts +0 -243
  587. package/src/train/index.ts +0 -46
  588. package/src/train/job.ts +0 -205
  589. package/src/train/lora-params.ts +0 -254
  590. package/src/train/loss.ts +0 -1591
  591. package/src/train/merge.ts +0 -195
  592. package/src/train/optimizer.ts +0 -183
  593. package/src/train/prefix-shared.ts +0 -490
  594. package/src/train/rank.ts +0 -137
  595. package/src/train/segmented.ts +0 -1828
  596. package/src/train/steel-qmm-header.ts +0 -1461
  597. package/src/train/trainer.ts +0 -1221
  598. package/src/train/watch.ts +0 -542
  599. package/src/vision/diffusion-vision.ts +0 -620
  600. package/src/vision/embedder.ts +0 -176
  601. package/src/vision/preprocess.ts +0 -247
  602. package/src/vision/prompt.ts +0 -409
  603. package/src/vision/qwen3vl-preprocess.ts +0 -409
  604. package/src/vision/qwen3vl-prompt.ts +0 -233
  605. package/src/vision/qwen3vl-tower.ts +0 -566
  606. package/src/vision/siglip.ts +0 -607
  607. package/src/vision/video-frames.ts +0 -148
  608. package/src/web/app.html +0 -2155
  609. package/src/web/app.js +0 -5863
  610. package/src/web/skills/memory/SKILL.md +0 -120
  611. package/src/web/skills/web-research/SKILL.md +0 -46
  612. package/src/web/skills.ts +0 -97
  613. package/src/web/src/adapters-panel.ts +0 -288
  614. package/src/web/src/assistant.ts +0 -451
  615. package/src/web/src/chat.ts +0 -1060
  616. package/src/web/src/composer.ts +0 -962
  617. package/src/web/src/dataset.ts +0 -132
  618. package/src/web/src/finetune.ts +0 -267
  619. package/src/web/src/hub.ts +0 -428
  620. package/src/web/src/main.ts +0 -98
  621. package/src/web/src/markdown.ts +0 -0
  622. package/src/web/src/memory-panel.ts +0 -572
  623. package/src/web/src/model-picker.ts +0 -159
  624. package/src/web/src/palette.ts +0 -346
  625. package/src/web/src/protocol.ts +0 -26
  626. package/src/web/src/quantize.ts +0 -167
  627. package/src/web/src/rag.ts +0 -274
  628. package/src/web/src/sessions.ts +0 -469
  629. package/src/web/src/shell.ts +0 -804
  630. package/src/web/src/ui-catalog.ts +0 -78
  631. package/src/web/src/voice.ts +0 -141
  632. package/src/web/vendor/README.md +0 -61
  633. package/src/web-assets.ts +0 -47
  634. package/src/web-tools.ts +0 -478
  635. package/src/weights.ts +0 -255
  636. /package/src/{serve → server}/completion-sink.ts +0 -0
  637. /package/src/{serve → server}/generated-token-history.ts +0 -0
  638. /package/src/web/{icon.svg → public/icon.svg} +0 -0
  639. /package/src/web/{manifest.webmanifest → public/manifest.webmanifest} +0 -0
  640. /package/src/web/{vendor → public/vendor}/hljs-LICENSE +0 -0
@@ -1,1874 +0,0 @@
1
- import { runMixedTokenIteration, type MlxForwardWork, type MlxPreparationWork } from "./mixed-iteration";
2
- import type { MixedTokenModel } from "../../model/token-groups";
3
- import type { OrdinaryContinuation } from "./continuation";
4
- import type { MlxRequestStatePolicy } from "./request-state-policy";
5
- import { AdmissionRejected } from "../../engine/admission";
6
- import type { ExecutionContext, ExecutionGroup } from "../../contracts/scheduling";
7
- import { driveExecutionGroup } from "../../engine/scheduler";
8
- import { disposeResources, withResource } from "../../engine/resources";
9
- import type { DisposableResource } from "../../contracts/resources";
10
- import { MlxPrefillCohort, type PrefillState } from "./prefill-cohort";
11
- // Continuous-batching scheduler for `--batch N` serving (phase S2, the engine
12
- // loop). Owns ONE running batch and drives it forward one decode step at a
13
- // time, admitting waiting requests and evicting finished ones between steps —
14
- // iteration-level (continuous) scheduling, not static batching. See
15
- // docs/design/batching.md and docs/design/batching.md.
16
- //
17
- // The numerically-hard parts are verified primitives:
18
- // - the batched FORWARD (per-row RoPE/mask) is bit-parity with mlx-lm B=N
19
- // across all 4 models (tests/batched-decode-parity);
20
- // - the dynamic-B FULL-attention ops mergeKVRows / filterKVRows match mlx-lm
21
- // BatchKVCache (tests/batched-decode-parity), and the SLIDING-window
22
- // BatchedRotatingCache (merge/filter/decode/make_mask incl. ring-wrap)
23
- // matches mlx-lm BatchRotatingKVCache (tests/batched-rotating).
24
- // This module is the ORCHESTRATION on top: admission, the step loop, per-row
25
- // sampling + token accounting, eviction, and assembling each layer's batched
26
- // cache by type. Gate: tests/parity/batch-scheduler.test.ts (teacher-forced, KL).
27
- //
28
- // Per-layer cache types: a model interleaves full-attention layers (plain
29
- // KVCache, wrapped per step in a BatchedDecodeMaskCache) and sliding-window
30
- // layers (a persistent BatchedRotatingCache that is itself the batched cache +
31
- // mask). Full layers share one leftPad/offset (all rows advance together); the
32
- // rotating caches self-track per-row leftPad/offset as the ring wraps. The
33
- // per-row absolute position stays consistent across both (full: offset-leftPad;
34
- // rot: offsetArr) — see docs/design/batching.md. Hybrid gated-DeltaNet
35
- // models (Qwen3.5) add "ssm" layers: SSMCache state is plain [B,...] with no
36
- // temporal axis and no padding (rows solo-prefill unpadded, decode feeds one
37
- // real token per row), so merge/filter are B-axis concat/take and the cache
38
- // passes through the step unwrapped (no mask, no per-row RoPE — full layers
39
- // carry positions). Gate: GenerationGateway.place on cache capability.
40
- //
41
- // Engine mechanics (the serial decode loop's hygiene, transplanted —
42
- // batching-v2-plan step 3):
43
- // - PIPELINED decode: each step builds the NEXT step's graph from the
44
- // still-unread sampled-token array (asyncEval), then reads the PREVIOUS
45
- // step's tokens while the new step computes — mlx-lm GenerationBatch._step.
46
- // The pipeline is flushed (read + emit) before a join merges, so admission
47
- // never has to reconcile an in-flight token array with a new row.
48
- // - clearCache every 256 steps (serial's cadence; mlx-lm batched uses 512),
49
- // not every step — per-step clears trashed the buffer pool each token.
50
- // - CHUNKED, INTERLEAVED admission: a joiner prefills prefillChunkSize
51
- // tokens per loop iteration with one batch decode step run in between, so
52
- // running rows stall at most one chunk per joiner (mlx-lm interleaves the
53
- // same way), and the prefill transient stays bounded.
54
- // - Failure containment: one row's onToken throwing evicts THAT row (its
55
- // promise rejects); siblings keep decoding (mlx-lm `remove` semantics). A
56
- // forward/sampling error still drops the whole batch (can't be attributed
57
- // to a row).
58
- //
59
- // Bun-async, NO threads: a single detached driver loop owns the GPU for batched
60
- // mode (an ExclusiveLock keeps the serial fallback off the GPU concurrently).
61
- // When `admissionHeld` reports a waiting serial-lane request, the loop stops
62
- // admitting, finishes the running rows, and releases the lock so the serial
63
- // request runs (mlx-lm's drain_batch) — resumed via kick().
64
- // Joins re-merge the whole batch; the keep-the-running-batch `extend`
65
- // optimization is a later refinement (batching-v2-plan item a).
66
-
67
- import { MlxArray } from "../../mlx/array";
68
- import type { PrefillPolicy } from "../../inference/prefill";
69
- import { resolveMlxPrefillPolicy } from "./prefill-policy";
70
- import * as ops from "../../mlx/ops";
71
- import { activeMemory, cacheMemory, clearCache, Dtype, peakMemory } from "../../mlx/ffi";
72
- import { runtimeConfig, withRuntimeConfig, type RuntimeConfig } from "../../runtime-config";
73
- import { CompiledDecode } from "../../model/compiled-decode";
74
- import { legacyCompiledDecodeAvailable } from "./autoregressive";
75
- import type { Gemma4Model } from "../../model/gemma4";
76
- import {
77
- KVCache, QuantizedKVCache, RotatingKVCache, RotatingQuantizedKVCache,
78
- cacheSignature,
79
- isBatchableCache,
80
- isPlainKvCache,
81
- isQuantizedKvCache,
82
- isRotatingPlainCache,
83
- isRotatingQuantizedCache,
84
- isRowBatchCache,
85
- type BatchableCache,
86
- type Cache,
87
- } from "../../model/gemma4-base";
88
- import { BatchedDecodeMaskCache, mergeKVRows, extendKVRows, extractKVRow, filterKVRows } from "../../model/batched-mask";
89
- import {
90
- BatchedQuantDecodeMaskCache, extendQuantRows, extractQuantRow, filterQuantRows, mergeQuantRows,
91
- type QuantRow,
92
- } from "../../model/batched-quant";
93
- import { BatchedRotatingQuantCache } from "../../model/batched-rotating-quant";
94
- import { cloneSingleRowState, leaseCacheStates } from "./state-views";
95
- import { ownedCacheLayout } from "./cache-layout";
96
- import { createKvMaintenance, type KvMaintenance } from "./kv-maintenance";
97
- import type { KvScheme } from "../../kv-scheme";
98
- import { BatchedRotatingCache } from "../../model/batched-rotating";
99
- import { SSMCache } from "../../model/qwen3-delta";
100
- import type { RuntimeModel } from "../../model/factory";
101
- import type { GrammarController } from "../../grammar";
102
- import { independentGreedySampling } from "../../sampler";
103
- import { ExecutionTasks } from "../../engine/execution-tasks";
104
- import { CancellationSource, GenerationCancelled } from "../../engine/cancellation";
105
- import { acquireModelWiredLimit } from "../../generate";
106
- import { batchRowKvBytes } from "../../serve/kv-budget";
107
- import type { PromptResponseTrace } from "../../serve/prompt-response-trace";
108
-
109
- /** Decode-pipeline kill switch (read once at load, like the serial loop's
110
- * MLX_BUN_COMPILED_DECODE): 1 ⇒ read each step's tokens synchronously. */
111
-
112
- /** Per-step phase timing (MLX_BUN_BATCH_STEP_TRACE=1, debug-only): where a
113
- * decode step's wall time goes — graph BUILD+dispatch (host), the pipelined
114
- * READ of the previous step's tokens (GPU wait), row EMIT (onToken/SSE), and
115
- * the GAP between consecutive steps (drive-loop + everything else). Sums
116
- * print via `stepTraceReport()` (the b1 profile experiment calls it). */
117
- const STEP_T = { t0: 0, lastEnd: 0, build: 0, read: 0, emit: 0, gap: 0, n: 0 };
118
- export function stepTraceReport(): string {
119
- const per = (x: number) => (STEP_T.n ? (x / STEP_T.n).toFixed(3) : "0");
120
- const s = `steps=${STEP_T.n} build=${per(STEP_T.build)}ms read=${per(STEP_T.read)}ms emit=${per(STEP_T.emit)}ms gap=${per(STEP_T.gap)}ms (per step)`;
121
- STEP_T.t0 = STEP_T.lastEnd = STEP_T.build = STEP_T.read = STEP_T.emit = STEP_T.gap = STEP_T.n = 0;
122
- return s;
123
- }
124
-
125
- // NOTE — oMLX-style "adaptive burst decode" (several steps per event-loop
126
- // yield, omlx/engine_core.py _step_burst) was ported here 2026-07-02 and
127
- // REFUTED by measurement: on this runtime it REGRESSED cpm5 B=4 aggregate
128
- // 345→289 tok/s, batch-lane B=1 149→121, and TTFT ~+100 ms (first-token SSE
129
- // flush waits out the burst budget). Their win exists because each Python
130
- // step hand-off ping-pongs the GIL with asyncio/uvicorn (~1 ms/token); Bun's
131
- // setImmediate hop costs microseconds, so bursting here only delays socket
132
- // flushes. Don't re-add without new evidence — the per-yield step below is
133
- // the measured optimum (docs/design/batching.md P4 notes).
134
-
135
- /** A token sampler for one row: (logits [1,V], step) → token array [1] on
136
- * device. Greedy is `(l) => ops.argmaxAxis(l, -1)`; richer closures fold in
137
- * temperature / top-p / logits processors + history (built per request from
138
- * its sampler options, exactly like generate()'s sampleStep). Called once per
139
- * step 0..maxTokens-1 for this row (never beyond maxTokens). */
140
- export type RowSampler = (logits1V: MlxArray, step: number) => MlxArray;
141
-
142
- /** A method prepares and advances rows; the shared executor owns queueing. */
143
- export interface MlxGroupPreparation {
144
- readonly rows: readonly Row[];
145
- readonly tokenWeight?: number;
146
- readonly canAdmit?: boolean;
147
- readonly supportsMixedWork?: boolean;
148
- admit?(row: Row): void;
149
- advance(work?: MlxPreparationWork): Promise<boolean>;
150
- dispose(): void;
151
- }
152
- export interface MlxGroupMethodHost {
153
- readonly rows: readonly Row[];
154
- readonly runtime: RuntimeConfig;
155
- readonly prefillChunkSize: number;
156
- readonly promptCache: RowPromptCache | undefined;
157
- join(row: Row): void;
158
- filterRows(keep: number[]): void;
159
- publish(row: Row, token: number, logprobs?: import("../../generate").TokenLogprobs): Promise<void | boolean>;
160
- finish(row: Row, reason: "stop" | "length"): void;
161
- }
162
- export interface MlxGroupedMethod {
163
- /** Maximum target tokens in the next iteration, including candidates. */
164
- readonly runningTokens?: number;
165
- prepare(row: Row): MlxGroupPreparation;
166
- advance(work?: MlxForwardWork): Promise<void>;
167
- filterRows(keep: readonly number[], discard: boolean): void;
168
- dispose(): void;
169
- }
170
- export interface MlxGroupMethodRequest {
171
- /** Equal keys declare compatible method state, independent of queue size. */
172
- readonly key: string;
173
- readonly data: unknown;
174
- open(host: MlxGroupMethodHost): MlxGroupedMethod;
175
- }
176
-
177
- export type BatchRequest = BatchRequestFields & (
178
- | { method: MlxGroupMethodRequest; sample?: RowSampler }
179
- | { method?: undefined; sample: RowSampler }
180
- );
181
-
182
- interface BatchRequestFields {
183
- /** Borrowed prepared input; its owner retains it through preparation. */
184
- promptInput?: import("./prompt-input").MlxPromptInput;
185
- cacheSessionId?: string;
186
- /** Optional ordinary continuation policy; owns persistence and sampler recovery. */
187
- continuation?: OrdinaryContinuation;
188
- statePolicy?: MlxRequestStatePolicy;
189
- context?: ExecutionContext;
190
- /** Resolve loaded-state identity under the execution lease, after any
191
- * queued adapter replacement has finished. */
192
- cacheNamespace?: string | (() => string);
193
- /** Resolved replay permission; standalone group callers retain auto mode. */
194
- compiledDecode?: boolean;
195
- promptIds: number[];
196
- /** Request policy overrides the execution group's captured prefill default. */
197
- prefillChunkSize?: number;
198
- maxTokens: number;
199
- eosTokenIds: number[];
200
- /** Called per emitted (non-EOS) token, in order. Returning `false` halts this
201
- * row (a decoded-text stop sequence fired) — matches generate()'s onToken
202
- * contract. EOS terminates the row WITHOUT an onToken call. Throwing evicts
203
- * THIS row only (its submit promise rejects; siblings continue). May be
204
- * async; keep it cheap — it runs inline in the step loop. */
205
- onToken: (token: number, logprobs?: import("../../generate").TokenLogprobs) => void | boolean | Promise<void | boolean>;
206
- /** Diagnostic-only request-local trace. */
207
- trace?: PromptResponseTrace;
208
- /** Closes the gateway's queue/admission span when this row leaves pending. */
209
- onAdmitted?: () => void;
210
- /** Client/request lifetime. An aborted pending row is removed before
211
- * admission; a row in prefill or decode is evicted at the next safe
212
- * scheduler boundary. */
213
- signal?: AbortSignal;
214
- /** The row's sampler is PLAIN GREEDY (temperature 0, no curve) with no
215
- * logits processors and no grammar — set by the gateway when true. Lets
216
- * the scheduler take the vectorized sampling fast path (ONE
217
- * log-softmax+argmax over [B,V] instead of B slice/sample/concat graphs)
218
- * when EVERY live row qualifies. Numerically identical per row
219
- * (row-independent ops, same per-row shapes); the per-row closure path
220
- * is the fallback and the MLX_BUN_BATCH_VEC_SAMPLE=0 kill switch. */
221
- plainGreedy?: boolean;
222
- /** Grammar controller for this row (B1: per-row matchers). When set, the
223
- * scheduler drives it: `accept()` after each emitted token (fires the async
224
- * bitmask fill), `await ready()` before the row's next sample (the sample
225
- * closure applies `applyMask`). Termination (grammar satisfied) is an
226
- * additional per-row stop source. The gateway OWNS disposal (finally around
227
- * submit()); the scheduler uses but never owns. Null on the degrade path. */
228
- grammar?: GrammarController;
229
- /** Stable cache boundary from the server's template probe (the serial
230
- * lane's options.snapshotAt): the prompt prefix the NEXT turn's render
231
- * preserves. The scheduler snapshots a trim-free prompt-cache entry at
232
- * min(this, promptIds.length-1) during the solo prefill — the oracle
233
- * invariant (mlx-lm insert_segments) that gives wrapped-ring models a
234
- * reuse path. Absent = still capped at promptIds.length-1. */
235
- snapshotAt?: number;
236
- }
237
-
238
- export interface BatchStats {
239
- spec?: import("../../generate").GenerateStats["spec"];
240
- fill?: import("../../generate").GenerateStats["fill"];
241
- promptTokens: number;
242
- generatedTokens: number;
243
- /** Prompt tokens served from the prompt cache (Phase 3.2): a joiner's solo
244
- * prefill starts from the longest usable cached prefix, exactly like the
245
- * serial lane's runGeneration take(). 0 on a cold prefill. */
246
- cachedTokens: number;
247
- finishReason: "stop" | "length";
248
- /** Wall-clock from admission (leaving the pending queue) to the first
249
- * emitted token — the row's prefill span on the batch lane. */
250
- prefillMs: number;
251
- /** Wall-clock from the first emitted token to finish. */
252
- decodeMs: number;
253
- }
254
-
255
- let nextMixedWorkId = 0;
256
-
257
- /** The slice of PromptCache the scheduler drives (structural — the server's
258
- * PromptCache satisfies it). take() on admission (any joiner: a restored
259
- * prefix + suffix prefill is byte-safe whether the row later merges or
260
- * not). put() paths: rows that finish NEVER-MERGED (adopted lone rows,
261
- * Phase 3.2) hand their pristine serial caches over zero-copy; rows that
262
- * finish INSIDE a multi-row batch get their KV EXTRACTED per row into
263
- * fresh serial caches first (mlx-lm server.py:872 extract_cache — the
264
- * cross-request reuse concurrent agents live on). */
265
- export type RowPromptCache = import("./checkpoint-state").MlxPrefixCache;
266
-
267
- export interface Row {
268
- spec?: import("../../generate").GenerateStats["spec"];
269
- fill?: import("../../generate").GenerateStats["fill"];
270
- req: BatchRequest;
271
- resolve: (s: BatchStats) => void;
272
- reject: (e: unknown) => void;
273
- current: number; // last emitted token, fed at the next step (pipeline cold)
274
- generated: number; // tokens emitted so far (incl. a terminating EOS)
275
- sampled: number; // sample() calls so far (leads `generated` by 1 in-pipeline)
276
- promptTokens: number;
277
- /** Prompt tokens restored from the prompt cache at admission (stats). */
278
- cachedTokens: number;
279
- /** performance.now() marks for BatchStats timing (0 = not reached). */
280
- admittedAt: number;
281
- firstTokenAt: number;
282
- closeTokenZero?: () => void;
283
- cacheNamespace?: string;
284
- /** Generated tokens whose KV actually entered the cache (fed as a step
285
- * input) — serial generate()'s `forwarded` list. promptIds+fed is the
286
- * exact token coverage of the row's caches, the put() entry key.
287
- * PER-ROW EXACT for every row: pushes are gated on the pending
288
- * register's per-slot real/placeholder flags (#pendingReal), so a
289
- * doomed slot's placeholder is never recorded — and if one ever DID
290
- * feed a forward, `fedTainted` flips instead (see #step's read). */
291
- fed: number[];
292
- /** A placeholder slot value fed this row's KV (junk token in the cache):
293
- * the coverage key is no longer derivable — extraction refuses.
294
- * Structurally unreachable today (a doomed row always evicts at the
295
- * emit that follows its placeholder, filtering the slot before the
296
- * next forward; cold steps never contain doomed rows because a flush
297
- * emits every in-flight token) — this flag makes `fed` provably exact
298
- * instead of resting on that timing chain. */
299
- fedTainted: boolean;
300
- /** Ever merged into a multi-row batch (or poisoned by a reject): its KV
301
- * is interleaved in batched buffers — a finish put() must EXTRACT the
302
- * row (#extractRowCaches) rather than adopt the inners. */
303
- merged: boolean;
304
- }
305
-
306
- /** Held while the batch is active; the serial fallback acquires the same lock. */
307
- export interface ExclusiveLock {
308
- acquire(): Promise<() => void>;
309
- }
310
-
311
- export interface MlxBatchExecutionGroupOptions {
312
- /** Scheduling work budget for extending an existing preparation cohort. */
313
- prefillBatchTokenLimit?: number;
314
- runtime?: RuntimeConfig;
315
- maxQueued?: number;
316
- stateCodecs?: import("../../kv-store").CacheCodecProvider;
317
- /** Max rows in the running batch (mlx-lm `--decode-concurrency`). */
318
- maxBatch: number;
319
- lock?: ExclusiveLock;
320
- /** Drain signal: while true, no NEW rows are admitted (running rows finish,
321
- * the lock is released) so a waiting serial-lane request can run — mlx-lm's
322
- * drain_batch. Pair with kick() when it flips back to false. */
323
- admissionHeld?: () => boolean;
324
- /** Joiner prefill chunk length (default 2048, the serial loop's constant). */
325
- prefillChunkSize?: number;
326
- /** Aggregate KV-byte budget across all running rows (batching-perf-path
327
- * P3 admission, via `--kv-budget`). A joiner whose PROJECTED KV
328
- * (kvBytesAt(config, prompt + maxTokens), sliding window already capped)
329
- * would push the batch's projected total over this ceiling WAITS in the
330
- * queue (FIFO, head-of-line — no starvation, no reorder) until rows
331
- * evict. A request that can NEVER fit (over budget alone, empty batch)
332
- * is rejected instead of deadlocking. Unset = unlimited (v1 behavior). */
333
- kvBudgetBytes?: number;
334
- /** Authoritative server-wide scheme. Each joiner's solo prefill converts at
335
- * the same chunk boundaries as the serial path, so a row's quantized bytes
336
- * preserve the L2 composition. Unsupported schemes fail at construction. */
337
- kvScheme?: KvScheme;
338
- /** Model binding owns support for precision transitions. */
339
- kvBatchCapabilities?: { delayedAffine?: boolean };
340
- /** Prompt-cache hook (Phase 3.2): admission take()s the longest usable
341
- * prefix into the joiner's solo caches (suffix-only prefill — the
342
- * multi-turn chat TTFT path); rows that finish never-merged put() their
343
- * caches back. Adapter requests never reach the batch lane, so the
344
- * namespace is always "" here. Runs under the gateway mutex domain, so
345
- * take/put never race the serial lane's use of the same cache. */
346
- promptCache?: RowPromptCache;
347
- }
348
-
349
- type LayerInner =
350
- | KVCache
351
- | QuantizedKVCache
352
- | BatchedRotatingCache
353
- | BatchedRotatingQuantCache // rot layer under a kv_config scheme (milestone 2)
354
- | RotatingKVCache // adopted lone-row state only (see #mergeJoiner adopt)
355
- | RotatingQuantizedKVCache // adopted lone-row state, quantized rot layer
356
- | SSMCache
357
- | BatchableCache;
358
- type Row1 = { keys: MlxArray; values: MlxArray };
359
-
360
- export class MlxBatchExecutionGroup {
361
- readonly #tasks = new ExecutionTasks();
362
- readonly #runtime: RuntimeConfig;
363
- readonly #noPipeline: boolean;
364
- readonly #stepTrace: boolean;
365
- readonly #stateCodecs: import("../../kv-store").CacheCodecProvider | undefined;
366
- readonly #maxQueued: number;
367
- #running: Row[] = [];
368
- #inners: LayerInner[] | null = null; // per-layer batched KV; null when empty
369
- #fullLeftPad: number[] = []; // per-row padding for FULL layers (rot self-tracks)
370
- #pending: Row[] = [];
371
- #prefill: MlxGroupPreparation | null = null; // the (single) joiner mid-prefill
372
- #preparationPublishedOutput = false;
373
- /** Sampled-but-unread token array [B], aligned with #running — the decode
374
- * pipeline register. Filtered/disposed alongside the batched KV. */
375
- #pendingToks: MlxArray | null = null;
376
- /** Per-slot flags for #pendingToks: true = a REAL sampled token of that
377
- * row's stream; false = a doomed/terminated row's placeholder (fed-token
378
- * accounting must skip it — see Row.fed/fedTainted). Filtered/cleared in
379
- * lockstep with the register. */
380
- #pendingReal: boolean[] | null = null;
381
- #method: MlxGroupedMethod | undefined;
382
- #methodKey: string | undefined;
383
- #decodeStateKey: string | undefined;
384
- #steps = 0; // decode-step counter (clearCache cadence)
385
- #cacheMaintenanceSteps = 0;
386
- #looping = false;
387
- #closed = false;
388
- #driver: Promise<void> | null = null;
389
- #wake: (() => void) | null = null;
390
- readonly #maxBatch: number;
391
- readonly #lock: ExclusiveLock | undefined;
392
- readonly #admissionHeld: (() => boolean) | undefined;
393
- readonly #prefillChunkSize: number;
394
- readonly #prefillPolicy: PrefillPolicy;
395
- readonly #prefillBatchTokenLimit: number;
396
- readonly #prefillTailSplit: boolean;
397
- readonly #kvBudgetBytes: number | undefined;
398
- readonly #defaultPromptCache: RowPromptCache | undefined;
399
- #statePolicy?: MlxRequestStatePolicy;
400
- get #promptCache(): RowPromptCache | undefined {
401
- return this.#statePolicy ? this.#statePolicy.promptCache : this.#defaultPromptCache;
402
- }
403
- /** Retain hook of the currently-ADOPTED row's cache entry (at most one:
404
- * only a lone adopted row holds un-copied entry caches). Runs after the
405
- * adopted caches are disposed, or transfers back on put(). */
406
- #adoptedRetain: (() => void) | null = null;
407
- #contextKey: string | undefined;
408
- #releaseContext: (() => void) | undefined;
409
- readonly #kinds: ("full" | "rot" | "ssm" | "owned-batch")[];
410
- readonly #rotMaxSize: number[]; // per-layer sliding window (rot layers only)
411
- readonly #compressedProjectors: Array<(tokens: number) => number> | null;
412
- readonly #batchCacheMaxTokens: number | null;
413
- readonly #kvScheme: KvScheme | undefined;
414
- readonly #maintainKv: KvMaintenance | null;
415
- /** Compiled decode runner for the B=1 serial-class case (Phase 3.2) —
416
- * same eligibility gate as generate.ts (gemma dense, kill switch
417
- * MLX_BUN_COMPILED_DECODE); adapter requests disable replay in their plan. Set
418
- * to null permanently on a failed step (serial disables per
419
- * generation; the scheduler is one long-lived "generation"). */
420
- #compiled: CompiledDecode | null;
421
-
422
- constructor(private readonly model: RuntimeModel, opts: MlxBatchExecutionGroupOptions) {
423
- this.#runtime = opts.runtime ?? runtimeConfig();
424
- this.#noPipeline = this.#runtime.value("MLX_BUN_BATCH_NO_PIPELINE") === "1";
425
- this.#stepTrace = this.#runtime.value("MLX_BUN_BATCH_STEP_TRACE") === "1";
426
- this.#maxQueued = opts.maxQueued ?? 64;
427
- if (!Number.isSafeInteger(this.#maxQueued) || this.#maxQueued < 1) throw new Error("invalid batch queue limit");
428
- this.#stateCodecs = opts.stateCodecs;
429
- this.#maxBatch = Math.max(1, Math.floor(opts.maxBatch));
430
- this.#lock = opts.lock;
431
- this.#admissionHeld = opts.admissionHeld;
432
- this.#prefillPolicy = resolveMlxPrefillPolicy(model.config, this.#runtime, opts.prefillChunkSize);
433
- this.#prefillChunkSize = this.#prefillPolicy.chunkSize(0);
434
- this.#prefillBatchTokenLimit = opts.prefillBatchTokenLimit ?? 2048;
435
- this.#prefillTailSplit = this.#runtime.flag("MLX_BUN_PREFILL_TAIL_SPLIT", true);
436
- this.#kvBudgetBytes = opts.kvBudgetBytes;
437
- this.#defaultPromptCache = opts.promptCache;
438
- this.#kvScheme = opts.kvScheme;
439
- const proto = withRuntimeConfig(this.#runtime, () => model.makeCache()); // fresh caches hold no buffers
440
- if (this.#kvScheme && !this.#kvScheme.batchable(
441
- model.config,
442
- (layerIdx) =>
443
- isPlainKvCache(proto[layerIdx]) || isRotatingPlainCache(proto[layerIdx]),
444
- proto.length, opts.kvBatchCapabilities,
445
- )) {
446
- for (const cache of proto) cache.dispose();
447
- throw new Error(`unsupported KV scheme for batch scheduler: ${this.#kvScheme.kind}`);
448
- }
449
- this.#kinds = proto.map((c) =>
450
- isBatchableCache(c)
451
- ? "owned-batch"
452
- : isRotatingPlainCache(c)
453
- ? "rot"
454
- : cacheSignature(c) === "ssm"
455
- ? "ssm"
456
- : "full",
457
- );
458
- this.#compressedProjectors = proto.every(isBatchableCache)
459
- ? proto.map((cache) => (tokens: number) => cache.projectedBytes(tokens))
460
- : null;
461
- this.#batchCacheMaxTokens = proto.every(isBatchableCache)
462
- ? Math.min(...proto.map((cache) => cache.maxTokens ?? Infinity))
463
- : null;
464
- this.#maintainKv = this.#kvScheme?.quantized
465
- ? createKvMaintenance(this.#kvScheme.options)
466
- : null;
467
- this.#rotMaxSize = proto.map((c) => (isRotatingPlainCache(c) ? c.maxSize : 0));
468
- for (const c of proto) c.dispose();
469
- this.#compiled =
470
- this.#runtime.flag("MLX_BUN_COMPILED_DECODE", true) &&
471
- legacyCompiledDecodeAvailable(model)
472
- ? withRuntimeConfig(this.#runtime, () => CompiledDecode.for(model as Gemma4Model))
473
- : null;
474
- }
475
-
476
- get activeRows(): number {
477
- return this.#running.length;
478
- }
479
-
480
- get pendingRows(): number {
481
- return this.#pending.length + (this.#prefill?.rows.length ?? 0);
482
- }
483
-
484
- /** Projected KV bytes of one row at its worst case (full prompt + full
485
- * completion; the sliding-window term is window-capped by kvBytesAt). */
486
- #rowKvBytes(row: Row): number {
487
- return this.#compressedProjectors
488
- ? this.#compressedProjectors.reduce(
489
- (sum, project) => sum + project(row.promptTokens + row.req.maxTokens),
490
- 0,
491
- )
492
- : batchRowKvBytes(
493
- this.model.config,
494
- row.promptTokens,
495
- row.req.maxTokens,
496
- this.#kvScheme,
497
- );
498
- }
499
-
500
- async #forwardHidden(ids: MlxArray, cache: Cache[]): Promise<MlxArray> {
501
- const asyncModel = this.model as RuntimeModel & {
502
- forwardHiddenAsync?: (ids: MlxArray, cache: Cache[]) => Promise<MlxArray>;
503
- };
504
- return typeof asyncModel.forwardHiddenAsync === "function"
505
- ? await asyncModel.forwardHiddenAsync(ids, cache)
506
- : this.model.forwardHidden(ids, cache);
507
- }
508
-
509
- /** Projected aggregate KV of everything admitted (running + mid-prefill). */
510
- get projectedKvBytes(): number {
511
- let total = this.#running.reduce((a, r) => a + this.#rowKvBytes(r), 0);
512
- if (this.#prefill) for (const row of this.#prefill.rows) total += this.#rowKvBytes(row);
513
- return total;
514
- }
515
-
516
- get kvBudgetBytes(): number | undefined {
517
- return this.#kvBudgetBytes;
518
- }
519
-
520
- /** KV-budget admission for the queue head. True = admit now. A candidate
521
- * that cannot fit even alone is rejected here (never deadlocks the
522
- * queue); one that fits alone but not alongside the current batch waits. */
523
- #kvAdmits(candidate: Row): boolean {
524
- const requestedTokens = candidate.promptTokens + candidate.req.maxTokens;
525
- if (
526
- this.#batchCacheMaxTokens !== null &&
527
- requestedTokens > this.#batchCacheMaxTokens
528
- ) {
529
- this.#pending.shift();
530
- candidate.reject(new RangeError(
531
- `context limit: prompt ${candidate.promptTokens} + max_tokens ` +
532
- `${candidate.req.maxTokens} exceeds ${this.#batchCacheMaxTokens}`,
533
- ));
534
- return false;
535
- }
536
- if (this.#kvBudgetBytes === undefined) return true;
537
- const need = this.#rowKvBytes(candidate);
538
- if (need > this.#kvBudgetBytes && this.#running.length === 0 && !this.#prefill) {
539
- this.#pending.shift();
540
- candidate.reject(
541
- new Error(
542
- `kv budget: request needs ~${(need / 1e9).toFixed(2)} GB KV ` +
543
- `(prompt ${candidate.promptTokens} + max_tokens ${candidate.req.maxTokens}), ` +
544
- `over --kv-budget ${(this.#kvBudgetBytes / 1e9).toFixed(2)} GB — ` +
545
- `lower max_tokens or raise the budget`,
546
- ),
547
- );
548
- return false;
549
- }
550
- return this.projectedKvBytes + need <= this.#kvBudgetBytes;
551
- }
552
-
553
- /** Submit a request; resolves when its row finishes (EOS, stop, or length). */
554
- submit(req: BatchRequest): Promise<BatchStats> {
555
- if (this.#closed) return Promise.reject(new Error("scheduler closed"));
556
- if (req.signal?.aborted) return Promise.reject(req.signal.reason);
557
- if (this.#pending.length >= this.#maxQueued) return Promise.reject(new AdmissionRejected());
558
- req = { ...req, prefillChunkSize: req.prefillChunkSize ?? this.#prefillPolicy.chunkSize(req.promptIds.length) };
559
- return new Promise<BatchStats>((resolve, reject) => {
560
- let abortListener: (() => void) | null = null;
561
- const cleanup = () => {
562
- if (abortListener) req.signal?.removeEventListener("abort", abortListener);
563
- abortListener = null;
564
- };
565
- this.#pending.push({
566
- req,
567
- resolve: (stats) => { cleanup(); resolve(stats); },
568
- reject: (error) => { cleanup(); reject(error); },
569
- current: 0, generated: 0, sampled: 0, promptTokens: req.promptIds.length,
570
- admittedAt: 0, firstTokenAt: 0,
571
- cachedTokens: 0, fed: [], fedTainted: false, merged: false,
572
- });
573
- if (req.signal) {
574
- abortListener = () => this.kick();
575
- req.signal.addEventListener("abort", abortListener, { once: true });
576
- }
577
- this.#ensureLoop();
578
- });
579
- }
580
-
581
- /** Wake the driver loop (e.g. after admissionHeld flips back to false). */
582
- kick(): void {
583
- this.#ensureLoop();
584
- }
585
-
586
- /** Read-only native preparation borrows a boundary of the live execution
587
- * group. Model mutation still requires the gateway's exclusive drain. */
588
- runPreparation<T>(work: () => Promise<T>, signal?: AbortSignal): Promise<T> {
589
- if (this.#closed) return Promise.reject(new Error("scheduler closed"));
590
- if (signal?.aborted) return Promise.reject(signal.reason);
591
- const cancellation = signal ? new CancellationSource() : undefined;
592
- const abort = () => cancellation?.cancel("requested");
593
- signal?.addEventListener("abort", abort, { once: true });
594
- const result = this.#tasks.enqueue(work, cancellation).catch(error => {
595
- throw error instanceof GenerationCancelled && signal?.aborted ? signal.reason : error;
596
- }).finally(() => signal?.removeEventListener("abort", abort));
597
- this.#ensureLoop();
598
- return result;
599
- }
600
-
601
- #ensureLoop(): void {
602
- if (this.#wake) { this.#wake(); return; }
603
- if (this.#looping || this.#closed) return;
604
- this.#looping = true;
605
- this.#driver = withRuntimeConfig(this.#runtime, () => this.#drive());
606
- void this.#driver.catch((error) => console.error(`batch scheduler cleanup failed: ${error}`));
607
- }
608
-
609
- /** Stop at the next safe work boundary and release all queued/active runs. */
610
- async close(): Promise<void> {
611
- this.#closed = true;
612
- this.#wake?.();
613
- await this.#driver;
614
- }
615
-
616
- async #drive(): Promise<void> {
617
- const scheduler = this;
618
- const group: ExecutionGroup = {
619
- get pendingTasks() { return scheduler.#tasks.pending; },
620
- advanceTask: () => this.#tasks.advance(),
621
- get active() { return scheduler.#running.length; },
622
- get queued() { return scheduler.#pending.length; },
623
- get preparing() { return scheduler.#prefill !== null; },
624
- get preparingRows() { return scheduler.#prefill?.rows.length ?? 0; },
625
- get canPrepareMore() { return scheduler.#prefill?.canAdmit === true; },
626
- get preparingTokens() { return scheduler.#prefill?.tokenWeight ?? 0; },
627
- // Cache restoration belongs to preparation. A queued request's prompt
628
- // length is an upper bound until its cache owner resolves the prefix.
629
- get nextPreparationTokens() { return scheduler.#pending[0]?.promptTokens ?? 0; },
630
- get maxPreparationTokens() { return scheduler.#prefillBatchTokenLimit; },
631
- get preparationPublishedOutput() { return scheduler.#preparationPublishedOutput; },
632
- get maxActive() { return scheduler.#maxBatch; },
633
- get admissionHeld() { return scheduler.#admissionHeld?.() === true; },
634
- get closed() { return scheduler.#closed; },
635
- pruneCancelled() {
636
- for (let i = scheduler.#pending.length - 1; i >= 0; i--) {
637
- const row = scheduler.#pending[i]!;
638
- if (!row.req.signal?.aborted) continue;
639
- scheduler.#pending.splice(i, 1);
640
- row.reject(row.req.signal.reason);
641
- }
642
- },
643
- admitNext: () => this.#admitNext(),
644
- canBurst: () => this.#contextCompatible(this.#pending[0]!) &&
645
- (this.#kvBudgetBytes === undefined ||
646
- this.projectedKvBytes + this.#rowKvBytes(this.#pending[0]!) <= this.#kvBudgetBytes),
647
- get mixedPreparation() {
648
- return scheduler.#decodeStateKey === undefined && scheduler.#prefill?.supportsMixedWork !== false && scheduler.#runtime.flag("MLX_BUN_MIXED_PREFILL", false) &&
649
- (!scheduler.#method || scheduler.#method.runningTokens !== undefined) &&
650
- typeof (scheduler.model as RuntimeModel & Partial<MixedTokenModel>).forwardHiddenMixed === "function"
651
- ? { runningTokens: scheduler.#method?.runningTokens ?? scheduler.#running.length,
652
- minimumPreparationTokens: scheduler.#prefill?.rows.length ?? 0 }
653
- : undefined;
654
- },
655
- get maxIterationTokens() { return scheduler.#runtime.number("MLX_BUN_MIXED_TOKEN_BUDGET", 256); },
656
- advanceMixed: async tokenBudget => {
657
- this.#promptCache?.reclaim?.();
658
- const advanced = await runMixedTokenIteration({
659
- prepare: forward => this.#advancePreparation({ forward,
660
- maxTokens: tokenBudget - (this.#method?.runningTokens ?? this.#running.length) }),
661
- decode: forward => this.#method ? this.#method.advance(forward) : this.#step(forward),
662
- forward: async (ids, cache, options) => options?.captureLayer
663
- ? (this.model as RuntimeModel & MixedTokenModel).forwardHiddenMixed([{ ids, cache, ...options }])[0]!
664
- : this.#forwardHidden(ids, cache),
665
- mixed: groups => {
666
- const workId = `mixed:${++nextMixedWorkId}`;
667
- const tokens = groups.map(group => group.ids.shape[0]! * group.ids.shape[1]!);
668
- const closes = [...this.#running, ...(this.#prefill?.rows ?? [])].map(row =>
669
- row.req.trace?.begin("engine.mixed_forward", { workId,
670
- decodeTokens: tokens[0]!, prefillTokens: tokens[1]!, packedTokens: tokens[0]! + tokens[1]! }));
671
- try { return (this.model as RuntimeModel & MixedTokenModel).forwardHiddenMixed(groups); }
672
- finally { for (const close of closes) close?.(); }
673
- },
674
- });
675
- if (!advanced && this.#running.length) {
676
- if (this.#method) await this.#method.advance(); else await this.#step();
677
- }
678
- },
679
- advancePreparation: () => {
680
- this.#promptCache?.reclaim?.();
681
- return this.#advancePreparation();
682
- },
683
- advance: () => {
684
- if (this.#cacheMaintenanceSteps++ % 256 === 0) this.#promptCache?.reclaim?.();
685
- return this.#method ? this.#method.advance() : this.#step();
686
- },
687
- failActive: (error) => {
688
- for (const row of this.#running) row.reject(error);
689
- this.#applyFilter([], true); // failed state never enters the prefix store
690
- },
691
- failAll: (error) => this.#failAll(error),
692
- reserveResidency: () => {
693
- const release = acquireModelWiredLimit(this.model);
694
- return () => disposeResources([
695
- { dispose: () => this.#leaveContext() }, { dispose: release },
696
- ]);
697
- },
698
- ...(this.#lock ? { acquireExecution: () => this.#lock!.acquire() } : {}),
699
- waitForWork: async () => {
700
- await new Promise<void>((resolve) => { this.#wake = resolve; });
701
- this.#wake = null;
702
- },
703
- };
704
- try {
705
- await driveExecutionGroup(group, {
706
- now: () => performance.now(),
707
- yield: () => new Promise<void>((resolve) => setImmediate(resolve)),
708
- }, this.#runtime.flag("MLX_BUN_EARLY_FIRST_TOKEN", true));
709
- } finally { this.#looping = false; }
710
- }
711
-
712
- #contextCompatible(row: Row): boolean {
713
- return (!this.#running.length && !this.#prefill) || ((row.req.context?.key ?? "") === this.#contextKey && row.req.method?.key === this.#methodKey && row.req.statePolicy?.key === this.#statePolicy?.key && row.req.promptInput?.decodeState?.key === this.#decodeStateKey);
714
- }
715
-
716
- #leaveContext(): void {
717
- const release = this.#releaseContext;
718
- this.#releaseContext = undefined;
719
- this.#contextKey = undefined;
720
- release?.();
721
- }
722
-
723
- #admitNext(): boolean {
724
- if (!this.#contextCompatible(this.#pending[0]!) || !this.#kvAdmits(this.#pending[0]!)) return false;
725
- const row = this.#pending.shift()!;
726
- try {
727
- this.#statePolicy = row.req.statePolicy;
728
- this.#decodeStateKey = row.req.promptInput?.decodeState?.key;
729
- row.cacheNamespace = typeof row.req.cacheNamespace === "function"
730
- ? row.req.cacheNamespace() : row.req.cacheNamespace;
731
- const key = row.req.context?.key ?? "";
732
- if (key !== this.#contextKey) {
733
- this.#leaveContext();
734
- this.#releaseContext = row.req.context?.enter();
735
- this.#contextKey = key;
736
- }
737
- row.req.onAdmitted?.();
738
- row.admittedAt = performance.now();
739
- if (row.req.method?.key !== this.#methodKey) {
740
- this.#method?.dispose(); this.#method = undefined; this.#methodKey = undefined;
741
- if (row.req.method) {
742
- const executor = this;
743
- this.#method = row.req.method.open({
744
- get rows() { return executor.#running; }, runtime: this.#runtime,
745
- prefillChunkSize: this.#prefillChunkSize, promptCache: this.#promptCache,
746
- join: row => { this.#running.push(row); },
747
- filterRows: keep => this.#applyFilter(keep),
748
- publish: async (row, token, logprobs) => {
749
- row.req.signal?.throwIfAborted();
750
- this.#firstOutput(row);
751
- return row.req.onToken(token, logprobs);
752
- },
753
- finish: (row, reason) => this.#finish(row, reason),
754
- });
755
- this.#methodKey = row.req.method.key;
756
- }
757
- }
758
- if (this.#prefill) {
759
- this.#prefill.admit!(row);
760
- } else if (this.#method) {
761
- this.#prefill = this.#method.prepare(row);
762
- } else {
763
- const preparation = new MlxPrefillCohort({
764
- model: this.model, chunkSize: this.#prefillChunkSize, tailSplit: this.#prefillTailSplit,
765
- promptCache: this.#promptCache, stateCodecs: this.#stateCodecs, maintain: this.#maintainKv ?? undefined,
766
- forward: (ids, caches) => this.#forwardHidden(ids, caches),
767
- project: (hidden, caches, completed) => this.#projectPrefill(hidden, caches, completed),
768
- complete: (state, logits) => this.#completePrefill(state, logits),
769
- resume: state => this.#completePrefill(state, null),
770
- reject: (failed, error) => this.#rejectPreparingRows([failed], error),
771
- });
772
- preparation.admit(row); this.#prefill = preparation;
773
- }
774
- } catch (error) { row.reject(error); }
775
- return true;
776
- }
777
-
778
- #rejectPreparingRows(rows: readonly Row[], error: unknown): void {
779
- const retiring = new Set(rows);
780
- const keep = this.#running.flatMap((row, index) => retiring.has(row) ? [] : [index]);
781
- if (keep.length !== this.#running.length) this.#applyFilter(keep, true);
782
- for (const row of rows) row.reject(error);
783
- }
784
-
785
- async #advancePreparation(work?: MlxPreparationWork): Promise<void> {
786
- const p = this.#prefill!, rows = p.rows;
787
- this.#preparationPublishedOutput = false;
788
- try {
789
- if (await p.advance(work)) this.#prefill = null;
790
- } catch (error) {
791
- this.#prefill = null;
792
- let failure = error;
793
- try { p.dispose(); }
794
- catch (cleanupError) { failure = new AggregateError([error, cleanupError], "batch prefill and cleanup failed"); }
795
- try { this.#rejectPreparingRows(rows, failure); }
796
- catch (cleanupError) {
797
- failure = new AggregateError([failure, cleanupError], "batch admission retirement failed");
798
- for (const row of rows) row.reject(failure);
799
- this.#failAll(failure);
800
- }
801
- }
802
- }
803
-
804
- #failAll(error: unknown): void {
805
- this.#tasks.rejectAll(error);
806
- const p = this.#prefill;
807
- const rows = new Set([...this.#running, ...this.#pending, ...(p?.rows ?? [])]);
808
- const retain = this.#adoptedRetain;
809
- const resources = [p, this.#method, ...(this.#inners ?? []), this.#pendingToks,
810
- { dispose: () => retain?.() }]
811
- .filter((resource): resource is DisposableResource => resource != null);
812
- this.#pending = []; this.#running = []; this.#prefill = null;
813
- this.#method = undefined; this.#methodKey = undefined;
814
- this.#inners = null; this.#pendingToks = null; this.#pendingReal = null;
815
- this.#fullLeftPad = []; this.#adoptedRetain = null;
816
- for (const row of rows) row.reject(error);
817
- disposeResources(resources);
818
- }
819
-
820
- /** Per-layer mixed-precision conversion of a joiner's SOLO caches — the
821
- * scheduler-side mirror of the serial maybeQuantizeKv (generate.ts): same
822
- * per-layer map, same skip rules (empty cache, already quantized), same
823
- * streaming discipline (evalAll the converted layer's state so the bf16
824
- * source frees before the next layer converts). Called at every prefill
825
- * chunk boundary AND once before merge, exactly where the serial loop
826
- * calls maybeQuantizeKv — that placement is what makes a row's quantized
827
- * bytes bit-exact vs serial `--kv-quant config`. Gateway placement and the
828
- * constructor both guarantee every named cache can convert. */
829
- #quantizeSolo(solo: Cache[], trace?: PromptResponseTrace): void {
830
- if (!this.#maintainKv) return;
831
- const close = trace?.begin("prefill.kv_maintenance", {
832
- mechanism: "continuous",
833
- });
834
- try {
835
- this.#maintainKv(solo);
836
- } finally {
837
- close?.();
838
- }
839
- }
840
-
841
- /** The target owns projection geometry; samplers receive individual rows. */
842
- #projectPrefill(hidden: MlxArray, caches: Cache[], completed: readonly PrefillState[]): MlxArray {
843
- for (const p of completed) {
844
- p.closePrefill?.(); p.closePrefill = undefined;
845
- p.row.closeTokenZero = p.row.req.trace?.begin("token_zero.total", { mechanism: "continuous" });
846
- }
847
- const attributed = this.#runtime.value("MLX_BUN_P2R_SYNC") === "1"
848
- ? completed.flatMap(p => p.row.req.trace ? [p.row.req.trace] : []) : [];
849
- const begin = (phase: "token_zero.forward" | "token_zero.head") => attributed.map(trace => trace.begin(phase, {
850
- mechanism: "continuous", activeBytes: activeMemory(), cacheBytes: cacheMemory(), peakBytes: peakMemory(),
851
- }));
852
- if (attributed.length) {
853
- const closes = begin("token_zero.forward");
854
- try { withResource(leaseCacheStates(caches), state => ops.evalAll([hidden, ...state])); }
855
- finally { for (const close of closes) close(); }
856
- }
857
- // Project before selecting rows: M4 quantized projection can use different
858
- // arithmetic at M=1. Tail-split preparation reaches this with N=1.
859
- const logits = this.model.logitsFromHidden(hidden);
860
- try {
861
- if (attributed.length) {
862
- const closes = begin("token_zero.head");
863
- try { ops.evalAll([logits]); } finally { for (const close of closes) close(); }
864
- }
865
- return logits;
866
- } catch (error) { logits.dispose(); throw error; }
867
- }
868
-
869
- /** Sample borrowed logits from the shared target projection. */
870
- async #completePrefill(p: PrefillState, logits: MlxArray | null): Promise<void> {
871
- if (p.continuation) {
872
- const saved = p.continuation, row = p.row;
873
- row.req.continuation!.resumeSampling(saved);
874
- const interval = row.req.continuation!.interval;
875
- this.#checkpointAt.set(row, (Math.floor(saved.generatedTokens / interval) + 1) * interval);
876
- row.firstTokenAt = performance.now();
877
- row.closeTokenZero?.(); row.closeTokenZero = undefined;
878
- this.#preparationPublishedOutput = true;
879
- for (const token of saved.cacheTokens.slice(row.req.promptIds.length)) {
880
- row.req.signal?.throwIfAborted();
881
- if (await row.req.onToken(token) === false)
882
- throw new Error("saved generation prefix triggered a terminal stop while replaying");
883
- }
884
- row.fed = saved.cacheTokens.slice(row.req.promptIds.length);
885
- row.generated = saved.generatedTokens + 1;
886
- row.sampled = saved.generatedTokens + 1;
887
- row.cachedTokens = Math.min(saved.cacheTokens.length, row.promptTokens);
888
- const stop = await this.#emit(row, saved.pendingToken);
889
- if (stop !== "continue") {
890
- this.#putOrDispose(p.solo, saved.cacheTokens, p.retain, row.cacheNamespace, row.req.cacheSessionId);
891
- p.solo = [];
892
- this.#finish(row, stop);
893
- return;
894
- }
895
- await this.#mergeJoiner(p);
896
- return;
897
- }
898
- if (!logits) throw new Error("ordinary prefill completed without logits");
899
- const forceAttribution = !!p.row.req.trace && this.#runtime.value("MLX_BUN_P2R_SYNC") === "1";
900
- const V = logits.shape[2]!;
901
- const last2 = ops.reshape(logits, [1, V]);
902
- const closeSample = forceAttribution
903
- ? p.row.req.trace!.begin("token_zero.sample", { mechanism: "continuous" })
904
- : undefined;
905
- let sampled: MlxArray;
906
- try { sampled = p.row.req.sample!(last2, 0); } finally { last2.dispose(); }
907
- // The first admission can seed the existing device-token register. Its
908
- // first decode forward then overlaps token-zero readback, just like every
909
- // later pipeline step. Grammar and explicit eager-output modes retain
910
- // their read-before-advance order.
911
- if (!this.#inners && !p.row.req.grammar && !this.#noPipeline &&
912
- !this.#runtime.flag("MLX_BUN_EARLY_FIRST_TOKEN", true) && !forceAttribution) {
913
- let owned: MlxArray | null = sampled;
914
- try {
915
- ops.asyncEvalAll([sampled]);
916
- p.row.sampled = 1;
917
- this.#quantizeSolo(p.solo, p.row.req.trace);
918
- await this.#mergeJoiner(p);
919
- this.#pendingToks = sampled;
920
- this.#pendingReal = [true];
921
- owned = null;
922
- return;
923
- } finally { owned?.dispose(); }
924
- }
925
- const tok = this.#readToken(sampled);
926
- closeSample?.();
927
- p.row.sampled = 1;
928
- p.row.generated = 1;
929
-
930
- // Grammar (B1): the mask0 was applied inside the sample closure (the
931
- // controller is primed at compile). After reading token 0, advance the
932
- // matcher — fires the async fill for the NEXT step, overlapping the merge
933
- // below. If the grammar is already satisfied at token 0 (a 1-token grammar,
934
- // e.g. guided_choice landing on a single-token option), emit + finish("stop")
935
- // WITHOUT merging into the batch — the row never joins #running.
936
- if (p.row.req.grammar) {
937
- p.row.req.grammar.accept(tok);
938
- if (p.row.req.grammar.isTerminated) {
939
- const stop = await this.#emit(p.row, tok);
940
- // Token 0 was sampled but never fed — the caches cover exactly the
941
- // prompt, a clean prompt-only entry (put-or-dispose).
942
- this.#putOrDispose(p.solo, p.row.req.promptIds, p.retain, p.row.cacheNamespace, p.row.req.cacheSessionId);
943
- this.#finish(p.row, stop === "continue" ? "stop" : stop);
944
- return;
945
- }
946
- }
947
-
948
- const stop = await this.#emit(p.row, tok);
949
- if (stop !== "continue") {
950
- this.#putOrDispose(p.solo, p.row.req.promptIds, p.retain, p.row.cacheNamespace, p.row.req.cacheSessionId);
951
- this.#finish(p.row, stop);
952
- return;
953
- }
954
- // Default tail-split path: the caches were already converted at the head
955
- // boundary above (oracle composition: prefill ids[:-1] → convert → L=1
956
- // step-0), so this call is an idempotent no-op (converted caches match
957
- // neither serial class). It is LOAD-BEARING only under
958
- // MLX_BUN_PREFILL_TAIL_SPLIT=0 — the old serial order: token 0 sampled
959
- // from the unconverted final-chunk logits, THEN the caches convert
960
- // (before decode step 1 == before the merge).
961
- this.#quantizeSolo(p.solo, p.row.req.trace);
962
- await this.#mergeJoiner(p);
963
- return;
964
- }
965
-
966
- /** Merge a fully-prefilled joiner with the running batch, layer by layer
967
- * (re-merge; `extend` is the later refinement). Flushes the decode pipeline
968
- * first so the row set is settled and the next step starts cold. */
969
- async #mergeJoiner(p: PrefillState): Promise<void> {
970
- await this.#flushPipeline();
971
- this.#maintainKv?.prepareBatch?.(p.solo);
972
-
973
- // ADOPT, don't copy (unified-engine plan Phase 3.2): a row joining an
974
- // EMPTY batch keeps its solo caches as the batch inners — a pointer
975
- // handoff, zero bytes moved (the old path ran the full merge machinery
976
- // to produce a byte-identical [1,...] copy). The copy now happens only
977
- // when a SECOND row joins and a genuinely new layout must exist. The
978
- // prize beyond the saved copy: the lone row's caches stay SERIAL-CLASS
979
- // (KVCache / RotatingKVCache / QuantizedKVCache), so the B=1 step is
980
- // literally the serial graph, and compiled decode + prompt-cache
981
- // take/put become possible for it. The rot branch below knows how to
982
- // treat an adopted RotatingKVCache as the merge's first row.
983
- if (!this.#inners) {
984
- this.#inners = p.solo as LayerInner[]; p.solo = [];
985
- this.#fullLeftPad = [0];
986
- this.#adoptedRetain = p.retain ?? null; p.retain = undefined;
987
- this.#running.push(p.row);
988
- return;
989
- }
990
-
991
- const prev = this.#inners;
992
- const prevPad = this.#fullLeftPad;
993
- const B = this.#running.length;
994
- const newInners: LayerInner[] = [];
995
- let newFullPad = this.#fullLeftPad;
996
- for (let layer = 0; layer < this.#kinds.length; layer++) {
997
- const solo = p.solo[layer]!;
998
- const merged = ownedCacheLayout(solo);
999
- if (merged) {
1000
- const previous = prev?.[layer];
1001
- merged.mergeRows(previous ? [previous, solo] : [solo]);
1002
- newFullPad = [...merged.leftPad];
1003
- newInners.push(merged);
1004
- continue;
1005
- }
1006
- if (this.#kinds[layer] === "owned-batch")
1007
- throw new Error(`batch-capable layer ${layer} lost its cache capability`);
1008
- if (this.#kinds[layer] === "ssm") {
1009
- // No temporal axis, no left-pad: B-axis concat of the state slots.
1010
- // mergeRows steals the solo arrays when the batch starts cold, so the
1011
- // unconditional p.solo dispose below stays safe either way.
1012
- newInners.push(SSMCache.mergeRows(
1013
- (prev?.[layer] as SSMCache | undefined) ?? null,
1014
- p.solo[layer] as SSMCache,
1015
- ));
1016
- continue;
1017
- }
1018
- if (isQuantizedKvCache(p.solo[layer])) {
1019
- // Phase 3.1 — quantized full layer: same merge/extend shapes as the
1020
- // bf16 branch below, over (packed, scales, biases) triples. The solo
1021
- // row was converted by #quantizeSolo with the serial ops, so its
1022
- // bytes already bit-match serial `--kv-quant config`; this branch
1023
- // only re-arranges rows along the batch axis.
1024
- const qSolo = p.solo[layer] as QuantizedKVCache;
1025
- const [qk, qv] = qSolo.temporalView();
1026
- const qRow = { keys: qk, values: qv };
1027
- const dispose3 = (t: { packed: MlxArray; scales: MlxArray; biases: MlxArray }) => {
1028
- t.packed.dispose(); t.scales.dispose(); t.biases.dispose();
1029
- };
1030
- const prevQ = prev?.[layer] as QuantizedKVCache | undefined;
1031
- if (prevQ && this.#runtime.value("MLX_BUN_BATCH_EXTEND") !== "0") {
1032
- const [k0, v0] = prevQ.temporalView();
1033
- const ext = extendQuantRows(k0, v0, prevPad, qRow);
1034
- dispose3(k0); dispose3(v0);
1035
- newFullPad = ext.leftPad;
1036
- const c = new QuantizedKVCache(qSolo.groupSize, qSolo.bits);
1037
- c.restoreState(ext.keys, ext.values, ext.width);
1038
- newInners.push(c);
1039
- } else {
1040
- const rows: { keys: typeof qk; values: typeof qv }[] = [];
1041
- if (prevQ) {
1042
- const [k0, v0] = prevQ.temporalView(); // [B,H,off,*]
1043
- const S = k0.packed.shape[2]!;
1044
- for (let b = 0; b < B; b++) {
1045
- const pad = prevPad[b]!;
1046
- const cutRow = (t: typeof k0): typeof k0 => ({
1047
- packed: t.packed.slice([b, 0, pad, 0], [b + 1, t.packed.shape[1]!, S, t.packed.shape[3]!]),
1048
- scales: t.scales.slice([b, 0, pad, 0], [b + 1, t.scales.shape[1]!, S, t.scales.shape[3]!]),
1049
- biases: t.biases.slice([b, 0, pad, 0], [b + 1, t.biases.shape[1]!, S, t.biases.shape[3]!]),
1050
- });
1051
- rows.push({ keys: cutRow(k0), values: cutRow(v0) });
1052
- }
1053
- dispose3(k0); dispose3(v0);
1054
- }
1055
- rows.push(qRow);
1056
- const merged = mergeQuantRows(rows);
1057
- newFullPad = merged.leftPad;
1058
- const c = new QuantizedKVCache(qSolo.groupSize, qSolo.bits);
1059
- c.restoreState(merged.keys, merged.values, merged.width);
1060
- newInners.push(c);
1061
- for (const r of rows) { dispose3(r.keys); dispose3(r.values); }
1062
- }
1063
- // qRow views are disposed via the rows loop above or here for extend
1064
- if (prevQ && this.#runtime.value("MLX_BUN_BATCH_EXTEND") !== "0") { dispose3(qRow.keys); dispose3(qRow.values); }
1065
- else if (!prevQ) { /* disposed in the rows loop */ }
1066
- continue;
1067
- }
1068
- // NOTE: the quantized-rotating branch below never uses this layer's
1069
- // bf16 temporalView — calling it before the branch leaked the pair
1070
- // (six arrays per second-row join on quantized rotating layers,
1071
- // introduced in 859572d; 2026-07-07 review fix). The view is taken
1072
- // AFTER the branch, on the paths that actually consume it.
1073
- const soloC = p.solo[layer] as KVCache | RotatingKVCache;
1074
- if (this.#kinds[layer] === "rot" && isRotatingQuantizedCache(p.solo[layer])) {
1075
- // Milestone 2 — QUANTIZED rotating layer: the solo row converted at
1076
- // the serial boundaries (#quantizeSolo), so its ring bytes are the
1077
- // serial oracle's; this branch re-arranges temporal triples across
1078
- // the batch axis (the bf16 rot merge over triples).
1079
- const qSolo = p.solo[layer] as RotatingQuantizedKVCache;
1080
- const [qk, qv] = qSolo.temporalView();
1081
- const rows: QuantRow[] = [];
1082
- const offsets: number[] = [];
1083
- const dispose3 = (t: QuantRow) => {
1084
- t.keys.packed.dispose(); t.keys.scales.dispose(); t.keys.biases.dispose();
1085
- t.values.packed.dispose(); t.values.scales.dispose(); t.values.biases.dispose();
1086
- };
1087
- const prevC = prev?.[layer];
1088
- if (prevC && isRowBatchCache(prevC) && isRotatingQuantizedCache(prevC)) {
1089
- const batched = prevC as BatchedRotatingQuantCache;
1090
- const [k0, v0] = batched.temporalView(); // [B,H,valid,*] triples, temporal
1091
- const valid = k0.packed.shape[2]!;
1092
- for (let b = 0; b < B; b++) {
1093
- const pad = Math.max(0, batched.leftPad[b]!);
1094
- const cutRow = (t: typeof k0): typeof k0 => ({
1095
- packed: t.packed.slice([b, 0, pad, 0], [b + 1, t.packed.shape[1]!, valid, t.packed.shape[3]!]),
1096
- scales: t.scales.slice([b, 0, pad, 0], [b + 1, t.scales.shape[1]!, valid, t.scales.shape[3]!]),
1097
- biases: t.biases.slice([b, 0, pad, 0], [b + 1, t.biases.shape[1]!, valid, t.biases.shape[3]!]),
1098
- });
1099
- rows.push({ keys: cutRow(k0), values: cutRow(v0) });
1100
- offsets.push(batched.offsetArr[b]!);
1101
- }
1102
- for (const t of [k0, v0]) { t.packed.dispose(); t.scales.dispose(); t.biases.dispose(); }
1103
- } else if (isRotatingQuantizedCache(prevC)) {
1104
- // Adopted lone row (Phase 3.2 adopt): its chronological triples
1105
- // are the merge's first row, pad 0 by definition.
1106
- const [k0, v0] = prevC.temporalView();
1107
- rows.push({ keys: k0, values: v0 });
1108
- offsets.push(prevC.offset);
1109
- }
1110
- rows.push({ keys: qk, values: qv });
1111
- offsets.push(qSolo.offset);
1112
- newInners.push(BatchedRotatingQuantCache.merge(
1113
- rows, offsets, this.#rotMaxSize[layer]!, qSolo.groupSize, qSolo.bits,
1114
- ));
1115
- for (const r of rows) dispose3(r);
1116
- continue;
1117
- }
1118
- const [sk, sv] = soloC.temporalView();
1119
- const newRow: Row1 = { keys: sk, values: sv };
1120
- if (this.#kinds[layer] === "rot") {
1121
- const rows: Row1[] = [];
1122
- const offsets: number[] = [];
1123
- const prevC = prev?.[layer];
1124
- if (isRotatingPlainCache(prevC) && !isRowBatchCache(prevC)) {
1125
- // Adopted lone row (Phase 3.2): a plain serial rotating cache —
1126
- // its chronological view is the merge's first row, same as a
1127
- // fresh solo (pad 0 by definition).
1128
- const [k0, v0] = prevC.temporalView();
1129
- rows.push({ keys: k0, values: v0 });
1130
- offsets.push(prevC.offset);
1131
- }
1132
- // Route by CAPABILITY (isRowBatchCache), never by signature string,
1133
- // inside the rot branch: the running batch's ring is a row-batch
1134
- // cache whatever its storage kind. The 2026-08-22 agg×4 regression
1135
- // (443f333) came from a signature-based conjunct here silently
1136
- // dropping the running rows; since then Cache.signature() is
1137
- // REQUIRED (BatchedRotatingCache reports "kv:rotating-plain") and
1138
- // the quant family is dispatched by its own branch above.
1139
- const prevRot = prevC && isRowBatchCache(prevC)
1140
- ? prevC as unknown as BatchedRotatingCache
1141
- : undefined;
1142
- if (prevRot) {
1143
- const [k0, v0] = prevRot.temporalView(); // [B,H,valid,D]
1144
- const [, H, valid, D] = k0.shape as [number, number, number, number];
1145
- const vD = v0.shape[3]!;
1146
- for (let b = 0; b < B; b++) {
1147
- const pad = Math.max(0, prevRot.leftPad[b]!);
1148
- rows.push({
1149
- keys: k0.slice([b, 0, pad, 0], [b + 1, H, valid, D]),
1150
- values: v0.slice([b, 0, pad, 0], [b + 1, H, valid, vD]),
1151
- });
1152
- offsets.push(prevRot.offsetArr[b]!);
1153
- }
1154
- k0.dispose(); v0.dispose();
1155
- }
1156
- rows.push(newRow);
1157
- offsets.push(soloC.offset);
1158
- newInners.push(BatchedRotatingCache.merge(rows, offsets, this.#rotMaxSize[layer]!));
1159
- for (const r of rows) { r.keys.dispose(); r.values.dispose(); }
1160
- } else {
1161
- const prevFull = prev?.[layer] as KVCache | undefined;
1162
- if (prevFull && this.#runtime.value("MLX_BUN_BATCH_EXTEND") !== "0") {
1163
- // extend-join (mlx-lm BatchKVCache.extend semantics, P0): append
1164
- // the new right-justified row to the running buffer in ONE pad +
1165
- // ONE concat — no per-row extraction. Existing pads grow, never
1166
- // shrink (the re-merge below re-normalizes them instead; both are
1167
- // masked, both token-exact vs their mlx-lm protocol twin).
1168
- // MLX_BUN_BATCH_EXTEND=0 = the O(B·S) re-merge, kill switch/A-B.
1169
- const [k0, v0] = prevFull.temporalView(); // [B,H,off,D]
1170
- const ext = extendKVRows(k0, v0, prevPad, newRow);
1171
- k0.dispose(); v0.dispose();
1172
- newFullPad = ext.leftPad;
1173
- const c = new KVCache();
1174
- c.restoreState(ext.keys, ext.values, ext.width);
1175
- newInners.push(c);
1176
- newRow.keys.dispose(); newRow.values.dispose();
1177
- } else {
1178
- const rows: Row1[] = [];
1179
- if (prevFull) {
1180
- const [k0, v0] = prevFull.temporalView(); // [B,H,off,D]
1181
- const [, H, off, D] = k0.shape as [number, number, number, number];
1182
- const vD = v0.shape[3]!;
1183
- for (let b = 0; b < B; b++) {
1184
- const pad = prevPad[b]!;
1185
- rows.push({
1186
- keys: k0.slice([b, 0, pad, 0], [b + 1, H, off, D]),
1187
- values: v0.slice([b, 0, pad, 0], [b + 1, H, off, vD]),
1188
- });
1189
- }
1190
- k0.dispose(); v0.dispose();
1191
- }
1192
- rows.push(newRow);
1193
- const merged = mergeKVRows(rows);
1194
- newFullPad = merged.leftPad;
1195
- const c = new KVCache();
1196
- c.restoreState(merged.keys, merged.values, merged.width);
1197
- newInners.push(c);
1198
- for (const r of rows) { r.keys.dispose(); r.values.dispose(); }
1199
- }
1200
- }
1201
- }
1202
- if (prev) for (const c of prev) c.dispose();
1203
- // An adopted row's entry-backed arrays are gone after the prev dispose;
1204
- // run its retain now. The joiner's likewise after its solo dispose.
1205
- this.#adoptedRetain?.();
1206
- this.#adoptedRetain = null;
1207
- const completed = p.solo; p.solo = [];
1208
- const release = p.retain; p.retain = undefined;
1209
- disposeResources([...completed, { dispose: () => release?.() }]);
1210
- // Every row in a REAL merge has its KV interleaved in batched buffers —
1211
- // no longer prompt-cache put() candidates.
1212
- for (const r of this.#running) r.merged = true;
1213
- p.row.merged = true;
1214
- this.#inners = newInners;
1215
- this.#fullLeftPad = newFullPad;
1216
- this.#running.push(p.row);
1217
- }
1218
-
1219
- /** One PIPELINED batched decode step (mlx-lm GenerationBatch._step):
1220
- * 1. forward all rows from the UNREAD pending token array (or, pipeline
1221
- * cold, from the rows' last emitted tokens), sample each live row's next
1222
- * token on its [1,V] slice, asyncEval the new [B] token array;
1223
- * 2. THEN sync-read the previous step's tokens (overlapping the readback
1224
- * with this step's compute), emit them, and evict finished rows.
1225
- * Rows that finish get one extra harmless KV write from the already-built
1226
- * step; filter drops the row (mlx-lm behaves identically). Length-finished
1227
- * rows are known in advance and are NOT sampled (placeholder slot). */
1228
- async #step(forward?: MlxForwardWork): Promise<void> {
1229
- if (this.#stepTrace) {
1230
- const now = performance.now();
1231
- if (STEP_T.lastEnd) STEP_T.gap += now - STEP_T.lastEnd;
1232
- STEP_T.t0 = now;
1233
- }
1234
- const rows = this.#running;
1235
- const B = rows.length;
1236
- const inners = this.#inners!;
1237
- const decodeState = rows[0]?.req.promptInput?.decodeState;
1238
- if (decodeState) {
1239
- const states = rows.map(row => row.req.promptInput!.decodeState!);
1240
- forward = async (ids, caches) => decodeState.forward(ids, caches, states);
1241
- }
1242
-
1243
- // A row is live if it still needs tokens sampled; a row whose pending
1244
- // unread token is its last (sampled == maxTokens) only awaits emission.
1245
- const anyLive = rows.some((r) => r.sampled < r.req.maxTokens);
1246
- // Grammar (B1): if any row has a LIVE grammar controller (not terminated,
1247
- // still sampling), take the read-before-build shape — the matcher's
1248
- // acceptToken needs the token id as a JS number, so the previous step's
1249
- // [B] token array is read back NOW (before building the next graph),
1250
- // accept()ed per row (firing async fills that overlap the forward), then
1251
- // ready() is awaited before this step's sample. This is the serial loop's
1252
- // grammar resolution transplanted to the batch. Batches with NO live
1253
- // grammar row keep the pipelined path byte-identical (zero cost when
1254
- // unused). The trade: while a grammar row is live the batch runs
1255
- // effectively NO_PIPELINE (readback no longer overlaps GPU compute) —
1256
- // bounded by the readback (~0.1 ms) + fills (0.004–0.19 ms/row, overlapped
1257
- // with the graph build). Serial grammar pays the identical trade today.
1258
- const hasLiveGrammar = anyLive && rows.some(
1259
- (r) => r.req.grammar && !r.req.grammar.isTerminated,
1260
- );
1261
- if (hasLiveGrammar) return this.#stepGrammar(forward);
1262
-
1263
- let nextToks: MlxArray | null = null;
1264
- let nextReal: boolean[] | null = null;
1265
- if (anyLive) {
1266
- // Fed-token accounting (prompt-cache put): a COLD step feeds each
1267
- // row's `current` (values known now, always a real emitted token); a
1268
- // pipelined step feeds the pending array, whose values are pushed at
1269
- // the read below gated on the per-slot real flags.
1270
- if (!this.#pendingToks) for (const r of rows) r.fed.push(r.current);
1271
- // Per-layer forward cache: rot layers use the persistent
1272
- // BatchedRotatingCache directly; ssm layers are already [B,...] state
1273
- // with no padding (no mask, no per-row RoPE — pass through); full
1274
- // layers get a fresh BatchedDecodeMaskCache wrapper — UNLESS no row
1275
- // has left padding. UNPADDED FAST PATH (unified-engine plan Phase 2,
1276
- // the B=1 case above all): with every leftPad 0 the wrapper's two
1277
- // jobs vanish — the padding mask (KVCache.makeMask(1) is the empty
1278
- // mask, exactly the serial loop's) and the per-row rope positions
1279
- // (every row sits at the shared scalar offset). The bare cache then
1280
- // dispatches the SAME per-step graph serial builds; the wrapper
1281
- // otherwise costs a host mask build + ~8 device nodes PER FULL LAYER
1282
- // PER TOKEN (the Phase-0 constant ~4–6 ms/step host tax at B=1).
1283
- const unpadded = this.#fullLeftPad.every((p) => p === 0);
1284
- // Compiled decode at B=1 (Phase 3.2): after adopt-don't-copy, a lone
1285
- // row's caches are SERIAL-CLASS, so the serial engine's compiled step
1286
- // replays the same C++ graph here — closing the batch lane's last
1287
- // B=1 host-tax gap (e4b's ~7%). Guards: gemma dense (constructor),
1288
- // serial-class caches (supports — a merged batch's BatchedRotating
1289
- // layers fail it), unpadded, and a uint32 pipeline register (the
1290
- // trace signature; per-row int32 samplers take the graph path).
1291
- // Grammar batches use #stepGrammar and stay on the graph path.
1292
- let lg: MlxArray | null = null;
1293
- let evalWith: MlxArray[] = [];
1294
- if (
1295
- !forward && this.#compiled && B === 1 && unpadded && this.#running[0]!.req.compiledDecode !== false &&
1296
- (!this.#pendingToks || this.#pendingToks.dtype === Dtype.uint32) &&
1297
- // A filtered-to-one BATCHED rot-quant cache subclasses the serial
1298
- // class (so supports() passes) but carries batched ring state —
1299
- // exclude it; only truly-adopted serial caches replay compiled.
1300
- !inners.some((c) => isRowBatchCache(c) && isRotatingQuantizedCache(c)) &&
1301
- CompiledDecode.supports(inners as Cache[])
1302
- ) {
1303
- let cur = this.#pendingToks;
1304
- let owned = false;
1305
- if (!cur) {
1306
- const i = ops.fromInt32([rows[0]!.current], [1]);
1307
- cur = i.astype(Dtype.uint32);
1308
- i.dispose();
1309
- owned = true;
1310
- }
1311
- try {
1312
- const r = this.#compiled.step(cur, inners as Cache[]);
1313
- lg = r.logits; // [1,1,V]
1314
- evalWith = r.evalWith;
1315
- } catch (e) {
1316
- // A failed step is transactional (see generate.ts) — safe to
1317
- // re-forward the same token on the graph path below.
1318
- this.#compiled = null;
1319
- console.warn(`batch lane: compiled decode disabled: ${e}`);
1320
- } finally {
1321
- if (owned) cur.dispose();
1322
- }
1323
- }
1324
- let fwd: Cache[] | null = null;
1325
- try {
1326
- if (!lg) {
1327
- fwd = inners.map((c) =>
1328
- isRowBatchCache(c) || isBatchableCache(c) || unpadded
1329
- ? c
1330
- : isQuantizedKvCache(c)
1331
- ? new BatchedQuantDecodeMaskCache(c, B, this.#fullLeftPad)
1332
- : new BatchedDecodeMaskCache(c, B, this.#fullLeftPad, null),
1333
- );
1334
- const ids = this.#pendingToks
1335
- ? ops.reshape(this.#pendingToks, [B, 1]) // feed the unread tokens
1336
- : ops.fromInt32(rows.map((r) => r.current), [B, 1]); // pipeline cold
1337
- const h = await (forward ? forward(ids, fwd) : this.#forwardHidden(ids, fwd));
1338
- ids.dispose();
1339
- lg = this.model.logitsFromHidden(h); // [B,1,V]
1340
- h.dispose();
1341
- }
1342
- const V = lg.shape[2]!;
1343
- // Vectorized homogeneous sampling (batching-perf-path P0): when every
1344
- // LIVE row is plain greedy, one log-softmax+argmax over [B,V] replaces
1345
- // B slice/sample/concat graphs. Per-row identical math (row-independent
1346
- // ops, same per-row shapes — argmax over log-softmax mirrors the
1347
- // closure's toLogprobs→argmax exactly, tie behavior included). Doomed
1348
- // rows get a real argmax instead of the placeholder 0 — equally
1349
- // harmless (the slot is filtered before it is ever emitted; its only
1350
- // use is one KV write on the row's own, about-to-evict row).
1351
- const vecOk =
1352
- this.#runtime.value("MLX_BUN_BATCH_VEC_SAMPLE") !== "0" &&
1353
- rows.every((r) => r.sampled >= r.req.maxTokens || r.req.plainGreedy);
1354
- // Doomed slots hold placeholders (vec path: a real argmax value,
1355
- // equally not part of the row's stream) — flag them so the fed
1356
- // accounting at the read stays per-row exact.
1357
- nextReal = rows.map((r) => r.sampled < r.req.maxTokens);
1358
- if (vecOk) {
1359
- const flat = ops.reshape(lg, [B, V]);
1360
- lg.dispose();
1361
- try { nextToks = independentGreedySampling.sample(flat); }
1362
- finally { flat.dispose(); }
1363
- for (const row of rows) if (row.sampled < row.req.maxTokens) row.sampled++;
1364
- } else {
1365
- const sampled: MlxArray[] = [];
1366
- for (let b = 0; b < B; b++) {
1367
- const row = rows[b]!;
1368
- if (row.sampled >= row.req.maxTokens) {
1369
- // Length-doomed row: evicted right after the emission below ever
1370
- // uses this slot as input — placeholder keeps the [B] alignment.
1371
- sampled.push(ops.fromInt32([0], [1]));
1372
- continue;
1373
- }
1374
- const rl = lg.slice([b, 0, 0], [b + 1, 1, V]);
1375
- const rl2 = ops.reshape(rl, [1, V]);
1376
- rl.dispose();
1377
- sampled.push(row.req.sample!(rl2, row.sampled));
1378
- row.sampled++;
1379
- rl2.dispose();
1380
- }
1381
- lg.dispose();
1382
- nextToks = ops.concatAxis(sampled, 0); // [B]
1383
- for (const t of sampled) t.dispose();
1384
- }
1385
- // dispatch; read NEXT iteration. evalWith: the compiled step's
1386
- // cache-update nodes must ride the same async_eval (generate.ts).
1387
- ops.asyncEvalAll([nextToks, ...evalWith]);
1388
- } finally {
1389
- // Free the step's RoPE arrays; do NOT dispose (full wrappers would free
1390
- // their persistent inner; rot caches persist across steps).
1391
- if (fwd) for (const c of fwd) (c as { releaseRopeArr?: () => void }).releaseRopeArr?.();
1392
- }
1393
- }
1394
- this.#steps++;
1395
- if (this.#steps % 256 === 0) clearCache(); // serial's cadence, not per-step
1396
- if (this.#stepTrace) STEP_T.build += performance.now() - STEP_T.t0;
1397
-
1398
- // Read + emit the PREVIOUS step's tokens while the new step computes.
1399
- const prev = this.#pendingToks;
1400
- const prevReal = this.#pendingReal;
1401
- this.#pendingToks = nextToks;
1402
- this.#pendingReal = nextReal;
1403
- // Kill switch / A-B lever (house style, cf. MLX_BUN_COMPILED_DECODE=0):
1404
- // MLX_BUN_BATCH_NO_PIPELINE=1 reads THIS step's tokens synchronously —
1405
- // set from process start `prev` is always null, so the flush below IS the
1406
- // whole phase 2. Same math either way (pipelining is scheduling).
1407
- if (this.#noPipeline) {
1408
- if (this.#running.some(row => row.req.continuation)) this.#captureContinuations();
1409
- await this.#flushPipeline();
1410
- return;
1411
- }
1412
- if (prev) {
1413
- const tRead = this.#stepTrace ? performance.now() : 0;
1414
- const toks = prev.toIntTokens();
1415
- prev.dispose();
1416
- // These values were the step's forward input (fed) iff a forward ran.
1417
- // Placeholder slots are excluded from `fed` — and taint their row if
1418
- // they ever fed (see Row.fedTainted; structurally unreachable today).
1419
- if (anyLive)
1420
- for (let b = 0; b < B; b++) {
1421
- if (!prevReal || prevReal[b]) rows[b]!.fed.push(toks[b]!);
1422
- else rows[b]!.fedTainted = true;
1423
- }
1424
- if (this.#stepTrace) STEP_T.read += performance.now() - tRead;
1425
- const tEmit = this.#stepTrace ? performance.now() : 0;
1426
- await this.#emitRows(toks); // also filters #pendingToks on eviction
1427
- if (this.#stepTrace) { STEP_T.emit += performance.now() - tEmit; STEP_T.n++; }
1428
- }
1429
- if (this.#running.some(row => row.req.continuation)) this.#captureContinuations();
1430
- if (this.#stepTrace) STEP_T.lastEnd = performance.now();
1431
- }
1432
-
1433
- readonly #checkpointAt = new WeakMap<Row, number>();
1434
- /** Cache extraction belongs to this existing ordinary numerical driver;
1435
- * the optional request port owns checkpoint eligibility and persistence. */
1436
- #captureContinuations(): void {
1437
- if (!this.#pendingToks || !this.#running.some(row => row.req.continuation)) return;
1438
- let pending: number[] | undefined;
1439
- const failed = new Set<number>();
1440
- for (let index = 0; index < this.#running.length; index++) {
1441
- const row = this.#running[index]!, policy = row.req.continuation;
1442
- if (!policy || row.fedTainted || row.fed.length !== row.generated ||
1443
- row.generated < (this.#checkpointAt.get(row) ?? policy.interval) ||
1444
- this.#pendingReal?.[index] === false) continue;
1445
- pending ??= this.#pendingToks.toIntTokens();
1446
- const tokens = [...row.req.promptIds, ...row.fed];
1447
- const caches = this.#running.length === 1 && !row.merged
1448
- ? cloneSingleRowState(this.#inners! as Cache[], this.#stateCodecs)
1449
- : this.#extractRowCaches(index, tokens.length);
1450
- if (!caches) continue;
1451
- try {
1452
- policy.captureOwned({ caches, cacheTokens: tokens, generatedTokens: row.generated,
1453
- pendingToken: pending[index]! });
1454
- this.#checkpointAt.set(row, (Math.floor(row.generated / policy.interval) + 1) * policy.interval);
1455
- } catch (error) { row.reject(error); failed.add(index); }
1456
- }
1457
- if (failed.size) this.#applyFilter(this.#running.flatMap((_, index) => failed.has(index) ? [] : [index]), true);
1458
- }
1459
-
1460
- /** Read out the pipeline register (if any): emit its tokens and evict
1461
- * finished rows, leaving the pipeline cold. Called before a join merges. */
1462
- async #flushPipeline(): Promise<void> {
1463
- const prev = this.#pendingToks;
1464
- if (!prev) return;
1465
- this.#pendingToks = null;
1466
- this.#pendingReal = null; // flushed values never fed — nothing to account
1467
- const toks = prev.toIntTokens();
1468
- prev.dispose();
1469
- // Grammar rows: the flushed tokens are EMITTED below, so their matchers
1470
- // must advance here — #stepGrammar's accept only covers tokens it reads
1471
- // from a live pending array, and after this flush it cold-starts (no
1472
- // accept). Skipping this left the matcher one token behind its stream on
1473
- // every mid-decode join → one-step-stale masks → invalid output (found
1474
- // by the feature-matrix conformance gate, 2026-07-03; the fill fired
1475
- // here is awaited by the next #stepGrammar's ready()).
1476
- for (let b = 0; b < this.#running.length && b < toks.length; b++) {
1477
- const g = this.#running[b]!.req.grammar;
1478
- if (g && !g.isTerminated) g.accept(toks[b]!);
1479
- }
1480
- await this.#emitRows(toks);
1481
- }
1482
-
1483
- /** The read-before-build decode step for batches with ≥1 live grammar row
1484
- * (B1). Mirrors the serial loop's grammar resolution: read the previous
1485
- * step's [B] tokens NOW (acceptToken needs JS numbers), accept() per row
1486
- * (fires async fills), build the forward graph (fills overlap), await
1487
- * ready(), sample per live row (the closure applies applyMask), then emit
1488
- * the values read in step 1 — no second readback. Terminated grammar rows
1489
- * keep their [B] slot through the forward (placeholder, not sampled) and
1490
- * finish("stop") in #emitRows. On a cold start (first step after prefill)
1491
- * there is no pending array to read; the prefill already accepted token 0
1492
- * and fired the fill, so we just await ready() + sample. */
1493
- async #stepGrammar(forward?: MlxForwardWork): Promise<void> {
1494
- const rows = this.#running;
1495
- const B = rows.length;
1496
- const inners = this.#inners!;
1497
-
1498
- // (1) Read the pending [B] token array (host copy for accept). The device
1499
- // array is kept for the forward input below; disposed after sampling.
1500
- // Cold start: pendingToks null → prevVals empty (prefill handled tok0).
1501
- const prev = this.#pendingToks;
1502
- const prevVals: number[] =
1503
- prev ? prev.toIntTokens() : [];
1504
- if (this.#runtime.value("MLX_BUN_GRAMMAR_DEBUG") === "1")
1505
- console.log(`[sg] B=${B} prevVals=${JSON.stringify(prevVals)} current=${JSON.stringify(rows.map((r) => r.current))} sampled=${JSON.stringify(rows.map((r) => r.sampled))}`);
1506
-
1507
- // (2) accept() per live grammar row — fires that row's async bitmask fill.
1508
- for (let b = 0; b < B && prevVals.length; b++) {
1509
- const g = rows[b]!.req.grammar;
1510
- if (g && !g.isTerminated) g.accept(prevVals[b]!);
1511
- }
1512
-
1513
- let nextToks: MlxArray | null = null;
1514
- let nextReal: boolean[] | null = null;
1515
- const anyLive = rows.some((r) => r.sampled < r.req.maxTokens);
1516
- if (anyLive) {
1517
- // Fed-token accounting (prompt-cache put) — mirror of #step's read:
1518
- // real slots push, a placeholder that fed taints its row.
1519
- const prevReal = this.#pendingReal;
1520
- if (prev)
1521
- for (let b = 0; b < B; b++) {
1522
- if (!prevReal || prevReal[b]) rows[b]!.fed.push(prevVals[b]!);
1523
- else rows[b]!.fedTainted = true;
1524
- }
1525
- else for (const r of rows) r.fed.push(r.current);
1526
- // Unpadded fast path — same rule as #step (bare caches == serial graph).
1527
- const unpadded = this.#fullLeftPad.every((p) => p === 0);
1528
- const fwd: Cache[] = inners.map((c) =>
1529
- isRowBatchCache(c) || isBatchableCache(c) || unpadded
1530
- ? c
1531
- : isQuantizedKvCache(c)
1532
- ? new BatchedQuantDecodeMaskCache(c, B, this.#fullLeftPad)
1533
- : new BatchedDecodeMaskCache(c, B, this.#fullLeftPad, null),
1534
- );
1535
- try {
1536
- // (3) Build the forward graph (host-side; the fills overlap it).
1537
- const ids = prev
1538
- ? ops.reshape(prev, [B, 1]) // feed the unread tokens (device array)
1539
- : ops.fromInt32(rows.map((r) => r.current), [B, 1]); // pipeline cold
1540
- const h = await (forward ? forward(ids, fwd) : this.#forwardHidden(ids, fwd));
1541
- ids.dispose();
1542
- const lg = this.model.logitsFromHidden(h); // [B,1,V]
1543
- h.dispose();
1544
- const V = lg.shape[2]!;
1545
-
1546
- // (4) await ready() on every live grammar row — the fills fired in (2)
1547
- // overlapped the graph build above. Usually already resolved.
1548
- const liveGrammar = rows.filter(
1549
- (r) => r.req.grammar && !r.req.grammar.isTerminated,
1550
- );
1551
- if (liveGrammar.length)
1552
- await Promise.all(liveGrammar.map((r) => r.req.grammar!.ready()));
1553
-
1554
- // (5) Sample per live row (the closure applies applyMask after the
1555
- // logits processors). Terminated grammar rows + length-doomed rows
1556
- // take a placeholder (one harmless KV write, then evicted).
1557
- const sampled: MlxArray[] = [];
1558
- nextReal = rows.map(
1559
- (r) => r.sampled < r.req.maxTokens && !r.req.grammar?.isTerminated,
1560
- );
1561
- for (let b = 0; b < B; b++) {
1562
- const row = rows[b]!;
1563
- if (
1564
- row.sampled >= row.req.maxTokens ||
1565
- row.req.grammar?.isTerminated
1566
- ) {
1567
- sampled.push(ops.fromInt32([0], [1]));
1568
- continue;
1569
- }
1570
- const rl = lg.slice([b, 0, 0], [b + 1, 1, V]);
1571
- const rl2 = ops.reshape(rl, [1, V]);
1572
- rl.dispose();
1573
- sampled.push(row.req.sample!(rl2, row.sampled));
1574
- row.sampled++;
1575
- rl2.dispose();
1576
- }
1577
- lg.dispose();
1578
- nextToks = ops.concatAxis(sampled, 0); // [B]
1579
- for (const t of sampled) t.dispose();
1580
- ops.asyncEvalAll([nextToks]);
1581
- } finally {
1582
- for (const c of fwd)
1583
- (c as { releaseRopeArr?: () => void }).releaseRopeArr?.();
1584
- }
1585
- }
1586
- this.#steps++;
1587
- if (this.#steps % 256 === 0) clearCache();
1588
-
1589
- // The device array `prev` has now fed the forward + been read for accept;
1590
- // dispose it and install the new pending array. On cold start prev is null.
1591
- prev?.dispose();
1592
- this.#pendingToks = nextToks;
1593
- this.#pendingReal = nextReal;
1594
-
1595
- // (6) Emit the values read in (1) — no second readback. Terminated grammar
1596
- // rows finish("stop") via #emit's isTerminated check; the filter evicts.
1597
- // Cold start: prevVals empty (prefill emitted tok0) → nothing to emit.
1598
- if (prevVals.length) await this.#emitRows(prevVals);
1599
- if (this.#running.some(row => row.req.continuation)) this.#captureContinuations();
1600
- }
1601
-
1602
- /** Emit one read-back token per running row; evict finished rows. A row's
1603
- * onToken throwing rejects THAT row and evicts it — siblings continue. */
1604
- async #emitRows(toks: number[]): Promise<void> {
1605
- const rows = this.#running;
1606
- const B = rows.length;
1607
- const keep: number[] = [];
1608
- const done: { b: number; row: Row }[] = []; // clean finishes (stop/length)
1609
- for (let b = 0; b < B; b++) {
1610
- const row = rows[b]!;
1611
- row.generated++;
1612
- let disp: "continue" | "stop" | "length";
1613
- try {
1614
- disp = await this.#emit(row, toks[b]!);
1615
- } catch (e) {
1616
- row.reject(e); // containment: this row only (mlx-lm `remove`)
1617
- row.merged = true; // poison: a rejected row is never put() back
1618
- row.fedTainted = true;
1619
- continue; // not kept → evicted by the filter below
1620
- }
1621
- if (disp === "continue") keep.push(b);
1622
- else {
1623
- done.push({ b, row });
1624
- this.#finish(row, disp);
1625
- }
1626
- }
1627
- if (keep.length < B) {
1628
- // Per-row prompt-cache extraction (mlx-lm server.py:864-880 →
1629
- // BatchGenerator.extract_cache): a MERGED row's KV lives interleaved
1630
- // in the batched inners, so it is pulled out into fresh serial caches
1631
- // BEFORE filter() mutates them. Never-merged lone rows skip this —
1632
- // #applyFilter's keep=[] path put()s their adopted caches zero-copy.
1633
- // Rejected rows aren't in `done`; the whole-batch error path calls
1634
- // #applyFilter(dropOnly) directly and never reaches here.
1635
- for (const { b, row } of done) this.#extractAndPut(b, row);
1636
- this.#applyFilter(keep);
1637
- }
1638
- }
1639
-
1640
- /** Account one sampled token for a row. Mirrors generate(): EOS terminates
1641
- * WITHOUT an onToken call; otherwise onToken(token) runs and `false` halts;
1642
- * reaching maxTokens ends with "length". Advances row.current on continue. */
1643
- async #emit(row: Row, token: number, logprobs?: import("../../generate").TokenLogprobs): Promise<"continue" | "stop" | "length"> {
1644
- row.req.signal?.throwIfAborted();
1645
- this.#firstOutput(row);
1646
- if (row.generated === 1) clearCache();
1647
- if (row.req.eosTokenIds.includes(token)) return "stop";
1648
- const cont = await row.req.onToken(token, logprobs);
1649
- if (cont === false) return "stop";
1650
- // Grammar termination: the matcher is satisfied (e.g. closing `}` accepted).
1651
- // The final token has been delivered via onToken above; halt with "stop" so
1652
- // the row finishes + evicts rather than sampling into an all--inf mask.
1653
- if (row.req.grammar?.isTerminated) return "stop";
1654
- if (row.generated >= row.req.maxTokens) return "length";
1655
- row.current = token;
1656
- return "continue";
1657
- }
1658
-
1659
- #firstOutput(row: Row): void {
1660
- if (row.generated === 1) {
1661
- if (this.#prefill?.rows.includes(row)) this.#preparationPublishedOutput = true;
1662
- row.firstTokenAt = performance.now();
1663
- row.closeTokenZero?.();
1664
- row.closeTokenZero = undefined;
1665
- }
1666
- }
1667
-
1668
- #finish(row: Row, reason: "stop" | "length"): void {
1669
- row.req.continuation?.complete();
1670
- const now = performance.now();
1671
- const first = row.firstTokenAt || now;
1672
- row.resolve({
1673
- ...(row.spec ? { spec: row.spec } : {}),
1674
- ...(row.fill ? { fill: row.fill } : {}),
1675
- promptTokens: row.promptTokens,
1676
- generatedTokens: row.generated,
1677
- cachedTokens: row.cachedTokens,
1678
- finishReason: reason,
1679
- prefillMs: row.admittedAt ? first - row.admittedAt : 0,
1680
- decodeMs: row.firstTokenAt ? now - row.firstTokenAt : 0,
1681
- });
1682
- }
1683
-
1684
- /** Finish-time disposition of serial-class caches covering exactly
1685
- * `tokens`: put() into the prompt cache when the hook is present and the
1686
- * offset lines up (defensive — a mismatch means the accounting is wrong
1687
- * and the entry would corrupt future hits), else dispose. `retain` rides
1688
- * along per the PromptCacheEntry contract (runs after dispose). */
1689
- #putOrDispose(caches: Cache[], tokens: number[], retain?: () => void, namespace = "", sessionId?: string): void {
1690
- if (this.#promptCache) {
1691
- // A never-merged row can still own a row layout. Persistence receives
1692
- // its existing serial representation, just like merged-row retirement.
1693
- const extracted: Cache[] = [], replaced: Cache[] = [];
1694
- let persistent: Cache[];
1695
- try {
1696
- persistent = caches.map(cache => {
1697
- if (!isBatchableCache(cache) || cache.batchSize !== 1) return cache;
1698
- const row = cache.extractRow(0);
1699
- extracted.push(row); replaced.push(cache);
1700
- return row;
1701
- });
1702
- } catch (error) { disposeResources(extracted); throw error; }
1703
- try { disposeResources(replaced); }
1704
- catch (error) { disposeResources(extracted); throw error; }
1705
- caches = persistent;
1706
- const withOff = caches.find(
1707
- (c) => typeof (c as { offset?: unknown }).offset === "number",
1708
- ) as { offset: number } | undefined;
1709
- if (withOff && withOff.offset === tokens.length) {
1710
- this.#promptCache.put(tokens, caches, namespace, retain, undefined, sessionId);
1711
- return;
1712
- }
1713
- }
1714
- for (const c of caches) c.dispose();
1715
- retain?.();
1716
- }
1717
-
1718
- /** Extract a finishing MERGED row's KV into fresh serial caches and put()
1719
- * them keyed by [promptIds + fed] — the batch-lane mirror of mlx-lm
1720
- * server.py:872 (extract_cache → prompt_cache.insert_cache). Gates:
1721
- * promptTokens >= 256 or an explicit cache session, plus an exact
1722
- * coverage key (!fedTainted). Refusal
1723
- * (#extractRowCaches null) just disposes-by-omission — the row's KV dies
1724
- * with the filter, exactly the pre-extraction behavior. */
1725
- #extractAndPut(b: number, row: Row): void {
1726
- if (!this.#promptCache || !row.merged || row.fedTainted) return;
1727
- if (row.promptTokens < 256 && !row.req.cacheSessionId) return;
1728
- const tokens = [...row.req.promptIds, ...row.fed];
1729
- const caches = this.#extractRowCaches(b, tokens.length);
1730
- if (!caches) return;
1731
- // Materialize the owned copies without stalling the in-flight step (the
1732
- // slices depend on this step's KV writes): async — the batched source
1733
- // buffers free once the copies land, instead of being pinned by a lazy
1734
- // graph inside an idle cache entry.
1735
- withResource(leaseCacheStates(caches), state => ops.asyncEvalAll([...state]));
1736
- this.#putOrDispose(caches, tokens, undefined, row.cacheNamespace, row.req.cacheSessionId);
1737
- }
1738
-
1739
- /** Row `b` of every layer as OWNED serial-class caches, or null when a
1740
- * layer kind can't be extracted (then the caller drops the row's KV as
1741
- * before). Bit-exactness: merge/extend/filter/decode are byte-preserving
1742
- * per row (tests/batched-decode-parity, tests/batched-rotating,
1743
- * tests/batched-rotating-quant) and each extract is a pure slice+copy of
1744
- * those bytes (tests/batched-extract), so an extracted row's bytes ==
1745
- * the solo run's. */
1746
- #extractRowCaches(b: number, expectTokens: number): Cache[] | null {
1747
- const out: Cache[] = [];
1748
- for (const inner of this.#inners!) {
1749
- let c: Cache | null;
1750
- if (isBatchableCache(inner)) c = inner.extractRow(b);
1751
- else if (isRowBatchCache(inner) && cacheSignature(inner) === "ssm")
1752
- // Coverage gate: recurrent state is UNTRIMMABLE, so an entry is only
1753
- // valid when the row's own advance count equals the [promptIds+fed]
1754
- // key EXACTLY (a mismatched entry would silently corrupt every future
1755
- // exact-hit). Defensive — a miss here means the fed accounting broke.
1756
- c = (inner as SSMCache).conv && (inner as SSMCache).rowOffset(b) === expectTokens
1757
- ? inner.extractRow(b)
1758
- : null;
1759
- else if (isRowBatchCache(inner)) c = inner.extractRow(b);
1760
- else if (
1761
- isRotatingQuantizedCache(inner) || // adopted serial state never
1762
- isRotatingPlainCache(inner) // coexists with a merged row (defensive)
1763
- )
1764
- c = null;
1765
- else if (isQuantizedKvCache(inner))
1766
- c = inner.keys ? extractQuantRow(inner, this.#fullLeftPad[b]!, b) : null;
1767
- else if (isPlainKvCache(inner))
1768
- c = inner.keys ? extractKVRow(inner, this.#fullLeftPad[b]!, b) : null;
1769
- else c = null;
1770
- if (!c) {
1771
- for (const d of out) d.dispose();
1772
- return null;
1773
- }
1774
- out.push(c);
1775
- }
1776
- return out;
1777
- }
1778
-
1779
- /** Evict rows not in `keep` (sorted ascending) from the batched KV and the
1780
- * pipeline register. */
1781
- #applyFilter(keep: number[], dropOnly = false): void {
1782
- if (this.#method) {
1783
- this.#method.filterRows(keep, dropOnly);
1784
- this.#running = keep.map(row => this.#running[row]!);
1785
- return;
1786
- }
1787
- const inners = this.#inners!;
1788
- if (keep.length === 0) {
1789
- // Prompt-cache put (Phase 3.2): a lone NEVER-MERGED row's inners are
1790
- // its adopted serial-class caches, covering exactly prompt+fed — hand
1791
- // them back to the cache instead of disposing. dropOnly (the batch-
1792
- // drop error path) and poisoned rows dispose as before; MERGED rows'
1793
- // KV was already extracted per row in #emitRows (owned copies), so
1794
- // disposing the batched inners here is safe either way.
1795
- const solo =
1796
- !dropOnly && this.#running.length === 1 && !this.#running[0]!.merged
1797
- ? this.#running[0]!
1798
- : null;
1799
- if (solo) {
1800
- this.#putOrDispose(
1801
- inners as Cache[],
1802
- [...solo.req.promptIds, ...solo.fed],
1803
- this.#adoptedRetain ?? undefined,
1804
- solo.cacheNamespace, solo.req.cacheSessionId,
1805
- );
1806
- } else {
1807
- for (const c of inners) c.dispose();
1808
- this.#adoptedRetain?.();
1809
- }
1810
- this.#adoptedRetain = null;
1811
- this.#inners = null;
1812
- this.#fullLeftPad = [];
1813
- this.#running = [];
1814
- this.#pendingToks?.dispose();
1815
- this.#pendingToks = null;
1816
- this.#pendingReal = null;
1817
- return;
1818
- }
1819
- const keptFullPad = keep.map((i) => this.#fullLeftPad[i]!);
1820
- // mlx-lm BatchKVCache.filter removes padding shared by all survivors.
1821
- // Full-attention width and padding move together; each row's absolute
1822
- // RoPE position stays unchanged. Rotating caches keep their own layout.
1823
- const trimFull = this.#kinds.includes("full") ? Math.min(...keptFullPad) : 0;
1824
- const out: LayerInner[] = [];
1825
- for (const inner of inners) {
1826
- if (isBatchableCache(inner)) {
1827
- inner.filterRows(keep);
1828
- out.push(inner);
1829
- } else if (isRowBatchCache(inner)) {
1830
- inner.filterRows(keep);
1831
- out.push(inner);
1832
- } else if (isRotatingQuantizedCache(inner) || isRotatingPlainCache(inner)) {
1833
- // Adopted lone-row state exists only at B=1, where the only filter
1834
- // is the keep=[] dispose-all handled above — unreachable.
1835
- throw new Error("applyFilter: adopted serial rotating cache cannot be row-filtered");
1836
- } else if (isQuantizedKvCache(inner)) {
1837
- const [k0, v0] = inner.temporalView();
1838
- const f = filterQuantRows(k0, v0, keep, trimFull);
1839
- for (const t of [k0, v0]) { t.packed.dispose(); t.scales.dispose(); t.biases.dispose(); }
1840
- const c = new QuantizedKVCache(inner.groupSize, inner.bits);
1841
- c.restoreState(f.keys, f.values, inner.offset - trimFull);
1842
- out.push(c);
1843
- inner.dispose();
1844
- } else if (isPlainKvCache(inner)) {
1845
- const [k0, v0] = inner.temporalView();
1846
- const f = filterKVRows(k0, v0, keep, trimFull);
1847
- k0.dispose(); v0.dispose();
1848
- const c = new KVCache();
1849
- c.restoreState(f.keys, f.values, inner.offset - trimFull);
1850
- out.push(c);
1851
- inner.dispose();
1852
- } else {
1853
- throw new Error(`applyFilter: unsupported cache signature ${cacheSignature(inner)}`);
1854
- }
1855
- }
1856
- this.#inners = out;
1857
- this.#fullLeftPad = keptFullPad.map((padding) => padding - trimFull);
1858
- this.#running = keep.map((i) => this.#running[i]!);
1859
- if (this.#pendingToks) {
1860
- const idx = ops.fromInt32(keep, [keep.length]);
1861
- const next = ops.takeAxis(this.#pendingToks, idx, 0);
1862
- idx.dispose();
1863
- this.#pendingToks.dispose();
1864
- this.#pendingToks = next;
1865
- }
1866
- if (this.#pendingReal) this.#pendingReal = keep.map((i) => this.#pendingReal![i]!);
1867
- }
1868
-
1869
- #readToken(t: MlxArray): number {
1870
- const v = t.toIntTokens()[0]!;
1871
- t.dispose();
1872
- return v;
1873
- }
1874
- }