mlx-bun 0.2.0 → 0.3.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 (364) hide show
  1. package/README.md +71 -588
  2. package/docs/README.md +91 -0
  3. package/docs/{design → archive/investigations}/audio-input-plan.md +4 -4
  4. package/docs/{design → archive/investigations}/diffusion-gemma-port.md +3 -3
  5. package/docs/{design → archive/investigations}/faithful-l1-consolidation.md +4 -4
  6. package/docs/{design → archive/investigations}/minicpm5-decode-megakernel.md +11 -11
  7. package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
  8. package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
  9. package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
  10. package/docs/design/batching.md +569 -0
  11. package/docs/design/decode-speed-program.md +3178 -169
  12. package/docs/design/dreaming-nightly-pipeline.md +675 -282
  13. package/docs/design/generic-model-support.md +579 -334
  14. package/docs/design/kv-cache.md +680 -0
  15. package/docs/design/orpo-training.md +673 -1239
  16. package/docs/design/speculative-decoding.md +1627 -0
  17. package/docs/design/turboquant.md +2102 -0
  18. package/docs/design/unified-engine-frontier-plan.md +1831 -727
  19. package/docs/design/web-chat-redesign.md +666 -1083
  20. package/docs/planning/PRODUCT_ROADMAP.md +34 -1
  21. package/docs/planning/release-notes-v0.3.0.md +35 -0
  22. package/docs/planning/video-series.md +2 -2
  23. package/docs/reference/benchmarks.md +1162 -0
  24. package/docs/reference/cli.md +394 -159
  25. package/docs/reference/distribution.md +321 -182
  26. package/docs/reference/environment.md +348 -0
  27. package/docs/reference/glossary.md +39 -0
  28. package/docs/reference/library-api.md +566 -168
  29. package/docs/reference/memory.md +227 -64
  30. package/docs/reference/models.md +104 -6
  31. package/docs/reference/server-api.md +886 -425
  32. package/docs/reference/server-config.md +638 -330
  33. package/docs/reference/training.md +393 -254
  34. package/docs/reference/troubleshooting.md +166 -0
  35. package/package.json +9 -4
  36. package/src/audio/decode.ts +2 -2
  37. package/src/audio/features.ts +2 -2
  38. package/src/backends/mlx/autoregressive.ts +108 -0
  39. package/src/backends/mlx/batch-group.ts +1646 -0
  40. package/src/backends/mlx/diffusion.ts +39 -0
  41. package/src/backends/mlx/draft-target.ts +47 -0
  42. package/src/backends/mlx/evaluation.ts +105 -0
  43. package/src/backends/mlx/fill-append.ts +45 -0
  44. package/src/backends/mlx/gateway-binding.ts +71 -0
  45. package/src/backends/mlx/graph.ts +38 -0
  46. package/src/backends/mlx/memory-client.ts +189 -0
  47. package/src/backends/mlx/memory-guard.ts +40 -0
  48. package/src/backends/mlx/methods.ts +90 -0
  49. package/src/backends/mlx/model-prompt.ts +164 -0
  50. package/src/backends/mlx/model-serving.ts +79 -0
  51. package/src/backends/mlx/prefill.ts +33 -0
  52. package/src/backends/mlx/request-policy.ts +20 -0
  53. package/src/backends/mlx/rollback.ts +51 -0
  54. package/src/backends/mlx/serial-executor.ts +273 -0
  55. package/src/backends/mlx/speculative.ts +114 -0
  56. package/src/backends/mlx/state-views.ts +26 -0
  57. package/src/backends/mlx/text-engine.ts +73 -0
  58. package/src/cli.ts +58 -25
  59. package/src/client.ts +66 -0
  60. package/src/config.ts +21 -4
  61. package/src/contracts/completion.ts +16 -0
  62. package/src/contracts/execution.ts +49 -0
  63. package/src/contracts/generation.ts +103 -0
  64. package/src/contracts/host.ts +7 -0
  65. package/src/contracts/jobs.ts +66 -0
  66. package/src/contracts/pi-provider.ts +30 -0
  67. package/src/contracts/pi.ts +259 -0
  68. package/src/contracts/resources.ts +18 -0
  69. package/src/contracts/scheduling.ts +27 -0
  70. package/src/contracts/tsconfig.json +14 -0
  71. package/src/diffusion/diffusion-generate.ts +218 -153
  72. package/src/embed.ts +1 -1
  73. package/src/engine/admission.ts +59 -0
  74. package/src/engine/cancellation.ts +38 -0
  75. package/src/engine/engine.ts +56 -0
  76. package/src/engine/execution-coordinator.ts +70 -0
  77. package/src/engine/execution-plan.ts +51 -0
  78. package/src/engine/resources.ts +42 -0
  79. package/src/engine/scheduler.ts +67 -0
  80. package/src/engine/session.ts +247 -0
  81. package/src/eval/README.md +44 -1
  82. package/src/eval/kl.ts +236 -3
  83. package/src/eval/reference-logits.ts +207 -0
  84. package/src/eval/runner.ts +8 -96
  85. package/src/fill/echo-index.ts +188 -0
  86. package/src/fill/fill-session.ts +452 -0
  87. package/src/fill/proposal.ts +68 -0
  88. package/src/fill/schema-rows.ts +506 -0
  89. package/src/fill/tool-boundary.ts +103 -0
  90. package/src/fit.ts +1 -1
  91. package/src/generate.ts +656 -268
  92. package/src/grammar.ts +12 -7
  93. package/src/harness-pi.ts +14 -33
  94. package/src/hub-rest.ts +4 -4
  95. package/src/index.ts +7 -0
  96. package/src/inference/denoising.ts +17 -0
  97. package/src/inference/graph.ts +33 -0
  98. package/src/inference/prefill.ts +39 -0
  99. package/src/inference/rollback.ts +7 -0
  100. package/src/jobs/db.ts +4 -0
  101. package/src/jobs/index.ts +2 -0
  102. package/src/jobs/job-entry.ts +12 -12
  103. package/src/jobs/runner.ts +98 -13
  104. package/src/jobs/task-client.ts +19 -0
  105. package/src/jobs/types.ts +2 -58
  106. package/src/kv-store.ts +109 -49
  107. package/src/lab/README.md +18 -0
  108. package/src/{assets → lab/curve}/curve-designer.html +2 -2
  109. package/src/{curve-sampler.ts → lab/curve/curve-sampler.ts} +2 -2
  110. package/src/{expert-trace.ts → lab/expert-trace/expert-trace.ts} +5 -5
  111. package/src/{model → lab/paged-kv}/paged-kv.ts +9 -6
  112. package/src/library.ts +40 -0
  113. package/src/lora.ts +18 -0
  114. package/src/media-fetch.ts +1 -1
  115. package/src/memory/article.ts +1 -1
  116. package/src/memory/chunk.ts +1 -1
  117. package/src/memory/model.ts +30 -169
  118. package/src/memory/prompts.ts +1 -1
  119. package/src/memory/rest.ts +1 -1
  120. package/src/memory/schedule.ts +1 -1
  121. package/src/memory/stages.ts +4 -1
  122. package/src/memory/vault.ts +1 -3
  123. package/src/mlx/array.ts +9 -10
  124. package/src/mlx/compile.ts +12 -2
  125. package/src/mlx/ffi.ts +58 -11
  126. package/src/mlx/materialize.ts +23 -0
  127. package/src/mlx/metal-kernel.ts +10 -11
  128. package/src/mlx/ops.ts +33 -13
  129. package/src/mlx/quantized-rows.ts +36 -0
  130. package/src/mlx/token-bitmask.ts +52 -0
  131. package/src/mlx/turboquant-kv-decode.ts +82 -0
  132. package/src/mlx/turboquant-kv-inverse.ts +122 -0
  133. package/src/mlx/turboquant-ops.ts +33 -3
  134. package/src/mlx/turboquant-tables.ts +2 -2
  135. package/src/model/batched-mask.ts +8 -2
  136. package/src/model/batched-quant.ts +1 -1
  137. package/src/model/batched-rotating-quant.ts +2 -2
  138. package/src/model/batched-rotating.ts +1 -1
  139. package/src/model/compiled-decode.ts +1 -1
  140. package/src/model/diffusion-gemma.ts +8 -8
  141. package/src/model/factory.ts +76 -33
  142. package/src/model/fingerprint.ts +1 -1
  143. package/src/model/flash-attention.ts +3 -3
  144. package/src/model/gemma4-base.ts +26 -9
  145. package/src/model/gemma4.ts +6 -6
  146. package/src/model/generated/gemma4-12b.ts +2 -2
  147. package/src/model/generated/gemma4-26b.ts +2 -2
  148. package/src/model/generated/gemma4-e4b.ts +2 -2
  149. package/src/model/glm52-dsa.ts +13 -9
  150. package/src/model/glm52-memory.ts +1 -1
  151. package/src/model/implementation.ts +69 -0
  152. package/src/model/minicpm5.ts +6 -8
  153. package/src/model/profile.ts +12 -2
  154. package/src/model/qwen-append.ts +12 -0
  155. package/src/model/qwen-conv.ts +52 -0
  156. package/src/model/qwen3_5-checkpoint.ts +164 -0
  157. package/src/model/qwen3_5.ts +140 -56
  158. package/src/model/support.ts +1 -1
  159. package/src/model/trellis-balanced-scatter.ts +51 -0
  160. package/src/model/trellis-linear.ts +672 -0
  161. package/src/model/trellis-shared-m.ts +151 -0
  162. package/src/model/trellis-shared-scatter.ts +144 -0
  163. package/src/model/trellis-splitk-prefill.ts +158 -0
  164. package/src/model/trellis-tiled-prefill.ts +136 -0
  165. package/src/model/trellis-vector-expand.ts +62 -0
  166. package/src/model/trellis-wide-prefill.ts +94 -0
  167. package/src/model/universal/dense.ts +1 -1
  168. package/src/model/universal/modules.ts +8 -1
  169. package/src/native-pack.ts +6 -5
  170. package/src/pi-provider.ts +6 -64
  171. package/src/pi-web.ts +8 -315
  172. package/src/prompt-cache.ts +96 -53
  173. package/src/quantize/allocator.ts +1 -1
  174. package/src/quantize/config-writer.ts +10 -3
  175. package/src/quantize/rotate.ts +4 -4
  176. package/src/quantize/trellis.ts +586 -0
  177. package/src/responses.ts +28 -1
  178. package/src/runtime-config.ts +18 -4
  179. package/src/sampler.ts +4 -4
  180. package/src/serve/batch-scheduler.ts +8 -1671
  181. package/src/serve/chat-request.ts +391 -0
  182. package/src/serve/chat-stage.ts +195 -0
  183. package/src/serve/checkpoint-identity.ts +64 -0
  184. package/src/serve/completion-executor.ts +15 -7
  185. package/src/serve/discovery-routes.ts +12 -13
  186. package/src/serve/generation-gateway.ts +154 -193
  187. package/src/serve/http.ts +159 -0
  188. package/src/serve/inference-request.ts +79 -0
  189. package/src/serve/isolate.ts +297 -105
  190. package/src/serve/lab-routes.ts +3 -0
  191. package/src/serve/lane-registry.ts +2 -1
  192. package/src/serve/model-admin-routes.ts +7 -5
  193. package/src/serve/model-binding.ts +43 -0
  194. package/src/serve/model-host.ts +512 -0
  195. package/src/serve/openai-wire.ts +103 -0
  196. package/src/serve/parent-application.ts +56 -0
  197. package/src/serve/pipeline.ts +33 -0
  198. package/src/serve/preparation.ts +33 -0
  199. package/src/serve/request-plan.ts +19 -15
  200. package/src/serve/request-prep.ts +346 -0
  201. package/src/serve/response-lease.ts +24 -0
  202. package/src/serve/responses-client.ts +69 -0
  203. package/src/serve/session-completion-engine.ts +110 -0
  204. package/src/serve/session-search.ts +1 -1
  205. package/src/serve/static-routes.ts +5 -5
  206. package/src/serve/text-completion-stage.ts +104 -0
  207. package/src/serve/token-streams.ts +519 -0
  208. package/src/server.ts +317 -2649
  209. package/src/spec/assistant-source.ts +10 -44
  210. package/src/spec/deepspec-source.ts +6 -6
  211. package/src/spec/dflash-source.ts +8 -8
  212. package/src/spec/dspark/ab-stats.ts +3 -3
  213. package/src/spec/dspark/calibration.ts +2 -2
  214. package/src/spec/dspark/deepspec-module.ts +2 -2
  215. package/src/spec/dspark/generate-dflash.ts +2 -2
  216. package/src/spec/dspark/generate.ts +2 -2
  217. package/src/spec/dspark/loader.ts +1 -1
  218. package/src/spec/dspark/module-dflash.ts +4 -3
  219. package/src/spec/dspark/module.ts +3 -3
  220. package/src/spec/dspark/quantize-drafter.ts +1 -1
  221. package/src/spec/generate.ts +1 -1
  222. package/src/spec/glm52-mtp-source.ts +1 -1
  223. package/src/spec/ngram-source.ts +1 -1
  224. package/src/spec/prefix-state.ts +45 -0
  225. package/src/spec/qwen-mtp-source.ts +166 -69
  226. package/src/spec/serve-loop.ts +154 -156
  227. package/src/spec/source.ts +42 -10
  228. package/src/spec/two-model.ts +1 -1
  229. package/src/ssd-cache.ts +80 -12
  230. package/src/ssd-durability.ts +25 -13
  231. package/src/tool-call.ts +51 -14
  232. package/src/train/flash-cce.ts +2 -2
  233. package/src/train/forward.ts +6 -0
  234. package/src/train/loss.ts +1 -1
  235. package/src/train/prefix-shared.ts +5 -1
  236. package/src/train/segmented.ts +5 -3
  237. package/src/train/trainer.ts +6 -6
  238. package/src/vision/prompt.ts +1 -1
  239. package/src/vision/qwen3vl-tower.ts +52 -10
  240. package/src/web/app.html +1 -1
  241. package/src/web/app.js +5 -3
  242. package/src/web/src/adapters-panel.ts +3 -3
  243. package/src/web/src/assistant.ts +2 -36
  244. package/src/web/src/chat.ts +4 -4
  245. package/src/web/src/composer.ts +4 -4
  246. package/src/web/src/hub.ts +1 -1
  247. package/src/web/src/markdown.ts +0 -0
  248. package/src/web/src/memory-panel.ts +1 -1
  249. package/src/web/src/model-picker.ts +1 -1
  250. package/src/web/src/palette.ts +1 -1
  251. package/src/web/src/protocol.ts +3 -39
  252. package/src/web/src/sessions.ts +1 -1
  253. package/src/web/src/ui-catalog.ts +4 -18
  254. package/src/web-assets.ts +47 -0
  255. package/src/weights.ts +55 -9
  256. package/docs/archive/investigations/curve-runs/bisector-route.json +0 -98
  257. package/docs/archive/investigations/curve-runs/bisector-trace.json +0 -1393
  258. package/docs/archive/investigations/curve-runs/curve-terrain.json +0 -1179
  259. package/docs/archive/investigations/curve-runs/curve-terrain.svg +0 -42
  260. package/docs/archive/investigations/hlg-ab.html +0 -34
  261. package/docs/archive/investigations/hlg-explorer.html +0 -238
  262. package/docs/archive/investigations/hlg-figs/cliff.svg +0 -1
  263. package/docs/archive/investigations/hlg-figs/cross.svg +0 -1
  264. package/docs/archive/investigations/hlg-figs/edges.svg +0 -1
  265. package/docs/archive/investigations/hlg-figs/k20-grids.svg +0 -1
  266. package/docs/archive/investigations/hlg-figs/k20-peaks.svg +0 -1
  267. package/docs/archive/investigations/hlg-figs/lw-curve.svg +0 -1
  268. package/docs/archive/investigations/hlg-figs/terrain.svg +0 -1
  269. package/docs/archive/investigations/hlg-report.html +0 -262
  270. package/docs/archive/investigations/hlg-runs/hlg-ab.json +0 -149
  271. package/docs/archive/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
  272. package/docs/archive/investigations/hlg-runs/hlg-blind.json +0 -107
  273. package/docs/archive/investigations/hlg-runs/hlg-cliff.json +0 -197
  274. package/docs/archive/investigations/hlg-runs/hlg-cross.json +0 -505
  275. package/docs/archive/investigations/hlg-runs/hlg-edge.json +0 -388
  276. package/docs/archive/investigations/hlg-runs/hlg-eval1.json +0 -25
  277. package/docs/archive/investigations/hlg-runs/hlg-frontier.json +0 -86
  278. package/docs/archive/investigations/hlg-runs/hlg-grid.json +0 -242
  279. package/docs/archive/investigations/hlg-runs/hlg-grid2.json +0 -343
  280. package/docs/archive/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
  281. package/docs/archive/investigations/hlg-runs/hlg-map.json +0 -383
  282. package/docs/archive/investigations/hlg-runs/run0-original.log +0 -159
  283. package/docs/archive/investigations/hlg-runs/runA-recommended.log +0 -131
  284. package/docs/archive/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
  285. package/docs/archive/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
  286. package/docs/archive/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
  287. package/docs/archive/investigations/hlg-runs/runAD-ab-samples.log +0 -8
  288. package/docs/archive/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
  289. package/docs/archive/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
  290. package/docs/archive/investigations/hlg-runs/runAG-eval-config2.log +0 -16
  291. package/docs/archive/investigations/hlg-runs/runB-gain-variants.log +0 -192
  292. package/docs/archive/investigations/hlg-runs/runC-pivots.log +0 -162
  293. package/docs/archive/investigations/hlg-runs/runD-oetf.log +0 -132
  294. package/docs/archive/investigations/hlg-runs/runE-eotf.log +0 -162
  295. package/docs/archive/investigations/hlg-runs/runF-shaper-working.log +0 -86
  296. package/docs/archive/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
  297. package/docs/archive/investigations/hlg-runs/runH-wsweep.log +0 -176
  298. package/docs/archive/investigations/hlg-runs/runI-asweep.log +0 -176
  299. package/docs/archive/investigations/hlg-runs/runJ-knobmap.log +0 -49
  300. package/docs/archive/investigations/hlg-runs/runK-frontier.log +0 -19
  301. package/docs/archive/investigations/hlg-runs/runL-widemap.log +0 -65
  302. package/docs/archive/investigations/hlg-runs/runM-frontier-emb.log +0 -15
  303. package/docs/archive/investigations/hlg-runs/runN-verify.log +0 -116
  304. package/docs/archive/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
  305. package/docs/archive/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
  306. package/docs/archive/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
  307. package/docs/archive/investigations/hlg-runs/runR-blind-transcript.log +0 -79
  308. package/docs/archive/investigations/hlg-runs/runS-cross-coherence.log +0 -33
  309. package/docs/archive/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
  310. package/docs/archive/investigations/hlg-runs/runU-cross-12b.log +0 -33
  311. package/docs/archive/investigations/hlg-runs/runV-lw-e4b.log +0 -22
  312. package/docs/archive/investigations/hlg-runs/runW-lw-12b.log +0 -22
  313. package/docs/archive/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
  314. package/docs/archive/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
  315. package/docs/archive/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
  316. package/docs/archive/mlx-bun-lab-report.html +0 -275
  317. package/docs/archive/wwdc-mlx-bun.html +0 -848
  318. package/docs/design/adapters-end-to-end.md +0 -205
  319. package/docs/design/batching-perf-path.md +0 -255
  320. package/docs/design/batching-v2-plan.md +0 -529
  321. package/docs/design/bucketing-stage.md +0 -197
  322. package/docs/design/compat-cli-surface-design.md +0 -230
  323. package/docs/design/docs-reorg-plan.md +0 -165
  324. package/docs/design/dspark-serving-program.md +0 -371
  325. package/docs/design/dspark-speculative-decoding.md +0 -318
  326. package/docs/design/grammar-spec-batching-integration.md +0 -301
  327. package/docs/design/memory-inference-path.md +0 -422
  328. package/docs/design/memory-synthesis.md +0 -287
  329. package/docs/design/memory-system.md +0 -237
  330. package/docs/design/mlx-lm-serving-execution-seam.md +0 -81
  331. package/docs/design/mlx-lm-tool-parity-plan.md +0 -545
  332. package/docs/design/omlx-adoption-map.md +0 -98
  333. package/docs/design/orpo-dynamic-lambda.md +0 -220
  334. package/docs/design/orpo-future-enhancements.md +0 -340
  335. package/docs/design/paged-kv-cache.md +0 -214
  336. package/docs/design/parallel-slots.md +0 -567
  337. package/docs/design/parity-tier-dag.md +0 -161
  338. package/docs/design/repo-cleanup-plan.md +0 -231
  339. package/docs/design/runtime-isolation.md +0 -263
  340. package/docs/design/segmented-backward-training.md +0 -543
  341. package/docs/design/spec-decode-larger-targets.md +0 -229
  342. package/docs/design/ssd-kv-cold-tier.md +0 -415
  343. package/docs/design/structured-output.md +0 -484
  344. package/docs/design/tauri-desktop-app.md +0 -127
  345. package/docs/design/the-dreaming-master-plan.md +0 -445
  346. package/docs/design/turboquant-kv.md +0 -235
  347. package/docs/design/turboquant-weights.md +0 -467
  348. package/docs/design/w4a16-compute-precision-spike.md +0 -232
  349. package/docs/design/web-chat-beat-matrix.md +0 -315
  350. package/docs/design/write-pipeline-entity.md +0 -468
  351. package/docs/planning/IDEAS.md +0 -69
  352. package/docs/reference/embedding.md +0 -197
  353. package/docs/reference/features-matrix.md +0 -101
  354. package/docs/reference/orpo-quickstart.md +0 -135
  355. package/src/flags.ts +0 -11
  356. /package/docs/{design → archive/investigations}/colibri-glm52-port.md +0 -0
  357. /package/docs/{planning → archive/planning}/chunk-task-roadmap.md +0 -0
  358. /package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +0 -0
  359. /package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +0 -0
  360. /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
  361. /package/docs/{planning → archive/planning}/release-notes-v0.2.0.md +0 -0
  362. /package/src/{memory-plan.ts → admission-plan.ts} +0 -0
  363. /package/{docs/archive/investigations → src/serve/assets}/curve-terrain.html +0 -0
  364. /package/{docs/dag → src/serve/assets}/training-inference-map.html +0 -0
@@ -27,9 +27,10 @@ import {
27
27
  type RequestRejection,
28
28
  } from "./request-plan";
29
29
  import type { PromptResponseTrace } from "./prompt-response-trace";
30
+ import { cleanupFailure } from "../engine/resources";
30
31
 
31
32
  export interface CompletionEngine {
32
- place(shape: RequestShape): GenerationPlacement;
33
+ place(shape: RequestShape, options?: GenerateOptions): GenerationPlacement;
33
34
  run(
34
35
  promptIds: number[],
35
36
  options: GenerateOptions & { stopSequences?: string[] },
@@ -118,6 +119,8 @@ export interface CompletionUsage {
118
119
  completionTokens: number;
119
120
  totalTokens: number;
120
121
  speculation?: GenerateStats["spec"];
122
+ /** Token fast-forwarding telemetry (serial lane, MLX_BUN_FILL=strict). */
123
+ fill?: GenerateStats["fill"];
121
124
  }
122
125
 
123
126
  export interface CompletionSummary {
@@ -199,7 +202,7 @@ function finalLane(
199
202
  stats: GenerateStats,
200
203
  ): Lane {
201
204
  if (mechanism === "continuous") return "batched";
202
- return stats.spec ? "serial+spec" : lane;
205
+ return stats.spec ? "serial+spec" : "serial";
203
206
  }
204
207
 
205
208
  function finishReason(
@@ -209,7 +212,7 @@ function finishReason(
209
212
  ): CompletionFinishReason {
210
213
  if (result.toolCalls.length) return "tool_calls";
211
214
  if (result.stopped) return "stop";
212
- return stats.generatedTokens >= maxTokens ? "length" : "stop";
215
+ return stats.finishReason ?? (stats.generatedTokens >= maxTokens ? "length" : "stop");
213
216
  }
214
217
 
215
218
  function combineControl(
@@ -251,12 +254,14 @@ export class CompletionExecutor {
251
254
  try {
252
255
  if (usageProgress) control.onUsageProgress!(usageProgress);
253
256
  const closePlacement = control.trace?.begin("completion.placement");
254
- const enginePlacement = this.engine.place(planned.shape);
257
+ const enginePlacement = this.engine.place(planned.shape, planned.options);
255
258
  closePlacement?.();
256
259
  if (enginePlacement.shape !== planned.shape)
257
260
  throw new Error("generation placement does not belong to this request shape");
258
261
  const mechanism = enginePlacement.mechanism;
259
- const lane = initialLane(mechanism, planned.shape);
262
+ const lane = enginePlacement.execution
263
+ ? (mechanism === "continuous" ? "batched" : enginePlacement.execution.method === "speculative" ? "serial+spec" : "serial")
264
+ : initialLane(mechanism, planned.shape);
260
265
  recordLane(input.requestId, lane);
261
266
  const placement = { mechanism, lane, shape: planned.shape };
262
267
  input.onPlacement?.(placement);
@@ -314,6 +319,8 @@ export class CompletionExecutor {
314
319
  usageProgress.totalTokens = stats.promptTokens + stats.generatedTokens;
315
320
  if (stats.spec) usageProgress.speculation = stats.spec;
316
321
  else delete usageProgress.speculation;
322
+ if (stats.fill) usageProgress.fill = stats.fill;
323
+ else delete usageProgress.fill;
317
324
  }
318
325
  const summary = {
319
326
  content: result.content,
@@ -332,15 +339,16 @@ export class CompletionExecutor {
332
339
  completionTokens: stats.generatedTokens,
333
340
  totalTokens: stats.promptTokens + stats.generatedTokens,
334
341
  ...(stats.spec ? { speculation: stats.spec } : {}),
342
+ ...(stats.fill ? { fill: stats.fill } : {}),
335
343
  },
336
344
  logprobs: logprobs?.payload() ?? null,
337
345
  };
346
+ planned.dispose(); // retained preparation reservations outlive the native run
338
347
  closeCompletion?.();
339
348
  return summary;
340
349
  } catch (error) {
341
350
  closeCompletion?.();
342
- planned.dispose();
343
- throw error;
351
+ cleanupFailure(error, () => planned.dispose());
344
352
  }
345
353
  }
346
354
  }
@@ -1,9 +1,8 @@
1
+ import { modelServingBinding } from "../backends/mlx/model-serving";
1
2
  import pkgJson from "../../package.json" with { type: "json" };
2
- import type { ServerContext } from "../server";
3
+ import type { ServingContext } from "./model-host";
3
4
  import type { GenerationGateway } from "./generation-gateway";
4
5
  import { fit } from "../fit";
5
- import { isEmbeddingModel } from "../embed";
6
- import { Glm52Model } from "../model/glm52";
7
6
 
8
7
  const pkgVersion = (pkgJson as { version: string }).version;
9
8
 
@@ -32,7 +31,7 @@ export interface DiscoveryRoutes {
32
31
  }
33
32
 
34
33
  export function createDiscoveryRoutes(
35
- ctx: ServerContext,
34
+ ctx: ServingContext,
36
35
  gateway: Pick<GenerationGateway, "batchMode">,
37
36
  startedAt: number,
38
37
  ): DiscoveryRoutes {
@@ -46,7 +45,7 @@ export function createDiscoveryRoutes(
46
45
  async handle(url, request) {
47
46
  switch (matchDiscoveryRoute(request.method, url.pathname)) {
48
47
  case "library": {
49
- if (!libraryCache || Date.now() - libraryCache.at > 30_000) {
48
+ if (url.searchParams.get("refresh") === "1" || !libraryCache || Date.now() - libraryCache.at > 30_000) {
50
49
  const { Registry, visionCapable, audioCapable } = await import("../registry");
51
50
  const { loadModelConfig } = await import("../config");
52
51
  const { supportTier } = await import("../model/support");
@@ -132,7 +131,7 @@ export function createDiscoveryRoutes(
132
131
  top_p: ctx.genDefaults.topP ?? null,
133
132
  top_k: ctx.genDefaults.topK ?? null,
134
133
  };
135
- const isGlm52 = ctx.model instanceof Glm52Model;
134
+ const capabilities = modelServingBinding(ctx).discovery;
136
135
  const data: Array<Record<string, unknown>> = [{
137
136
  id: ctx.modelId,
138
137
  object: "model",
@@ -147,10 +146,10 @@ export function createDiscoveryRoutes(
147
146
  batch_mode: gateway.batchMode,
148
147
  tools: true,
149
148
  structured_output: true,
150
- embeddings: isEmbeddingModel(ctx.model),
151
- adapters: !isGlm52,
152
- training: !isGlm52,
153
- dsa: ctx.model instanceof Glm52Model && ctx.model.capabilities.dsa,
149
+ embeddings: capabilities.embeddings,
150
+ adapters: capabilities.adapters,
151
+ training: capabilities.training,
152
+ dsa: capabilities.dsa,
154
153
  mtp: ctx.draft?.provider.id === "glm52-native-mtp",
155
154
  capabilities: {
156
155
  chat_completions: true,
@@ -161,11 +160,11 @@ export function createDiscoveryRoutes(
161
160
  tools: true,
162
161
  structured_output: true,
163
162
  logprobs: true,
164
- embeddings: isEmbeddingModel(ctx.model),
163
+ embeddings: capabilities.embeddings,
165
164
  vision: !!(ctx.vision || ctx.loadVision),
166
165
  audio: !!(ctx.audio || ctx.loadAudio),
167
- adapters: !isGlm52,
168
- training: !isGlm52,
166
+ adapters: capabilities.adapters,
167
+ training: capabilities.training,
169
168
  },
170
169
  gen_defaults: genDefaults,
171
170
  }];
@@ -1,3 +1,6 @@
1
+ import { AdmissionPool } from "../engine/admission";
2
+ import { acquireReservation } from "./preparation";
3
+ import { cleanupFailure, disposeResources } from "../engine/resources";
1
4
  // GenerationGateway is the scheduling seam between CompletionExecutor and
2
5
  // the inference runtime. It declares one mechanism per request: the preserved
3
6
  // strict/dedicated serial executor, or the continuous scheduler (`--batch N`).
@@ -32,25 +35,19 @@
32
35
  // AND sliding-window (Gemma) models both batch — the scheduler assembles each
33
36
  // layer's cache by type. Prompt-cache prefix reuse works on the batch lane
34
37
  // too (Phase 3.2): joiners take() at admission; never-merged lone rows put()
35
- // back on finish (merged rows' KV is not extracted — their entries age out).
38
+ // back zero-copy on finish; merged rows with ≥256 prompt tokens are extracted
39
+ // and put back by BatchScheduler.#extractAndPut (see docs/design/batching.md).
36
40
 
37
41
  import { MlxArray } from "../mlx/array";
38
42
  import type { RuntimeModel } from "../model/factory";
39
- import { DiffusionGemmaModel } from "../model/diffusion-gemma";
40
- import {
41
- KVCache,
42
- RotatingKVCache,
43
- isBatchableCache,
44
- isPlainKvCache,
45
- isRotatingPlainCache,
46
- } from "../model/gemma4-base";
47
- import { SSMCache } from "../model/qwen3-delta";
48
- import { UniversalDenseModel } from "../model/universal/dense";
49
43
  import type { GenerateOptions, GenerateStats, TokenLogprobs } from "../generate";
50
44
  import type { KvScheme } from "../kv-scheme";
51
45
  import { makeStepSampler } from "../sampler";
52
- import { BatchScheduler, type RowPromptCache } from "./batch-scheduler";
53
- import { runtimeValue } from "../runtime-config";
46
+ import type { RowPromptCache } from "../backends/mlx/batch-group";
47
+ import type { MlxMemoryBudget } from "../backends/mlx/memory-guard";
48
+ import { bindMlxGateway, type MlxGatewayBinding, type MlxBatchGroup } from "../backends/mlx/gateway-binding";
49
+ import type { RuntimeConfig } from "../runtime-config";
50
+ import type { ExecutionRequirements, ResolvedExecution } from "../contracts/execution";
54
51
  import type { PromptResponseTrace } from "./prompt-response-trace";
55
52
 
56
53
  /** Async mutex: acquire() resolves to a release fn; releases run FIFO. */
@@ -61,18 +58,38 @@ class AsyncMutex {
61
58
  get locked(): boolean {
62
59
  return this.#pending > 0;
63
60
  }
64
- acquire(): Promise<() => void> {
61
+ acquire(signal?: AbortSignal): Promise<() => void> {
62
+ if (signal?.aborted) return Promise.reject(signal.reason);
65
63
  this.#pending++;
66
64
  let release!: () => void;
67
65
  const gate = new Promise<void>((r) => { release = r; });
68
66
  const wait = this.#tail;
69
67
  this.#tail = this.#tail.then(() => gate);
70
68
  let released = false;
71
- return wait.then(() => () => {
69
+ const unlock = () => {
72
70
  if (released) return; // idempotent: a double release must not skew #pending
73
71
  released = true;
74
72
  this.#pending--;
75
73
  release();
74
+ };
75
+ return new Promise((resolve, reject) => {
76
+ let settled = false;
77
+ const abort = () => {
78
+ if (settled) return;
79
+ settled = true;
80
+ signal?.removeEventListener("abort", abort);
81
+ // Release this waiter's gate, not its predecessor's. Later acquirers
82
+ // still wait for the active holder, preserving mutual exclusion.
83
+ unlock();
84
+ reject(signal!.reason);
85
+ };
86
+ signal?.addEventListener("abort", abort, { once: true });
87
+ wait.then(() => {
88
+ if (settled) return;
89
+ settled = true;
90
+ signal?.removeEventListener("abort", abort);
91
+ resolve(unlock);
92
+ });
76
93
  });
77
94
  }
78
95
  }
@@ -85,7 +102,7 @@ export type Vision = {
85
102
  embeddings: MlxArray;
86
103
  /** bool [L] image-token mask for the bidirectional attention overlay.
87
104
  * Absent when the prompt carries ANY audio — audio(-containing) prompts
88
- * run fully causal (audio-input-plan.md §3.3 Q1). */
105
+ * run fully causal (docs/design/generic-model-support.md §3.3 Q1). */
89
106
  imageMask?: MlxArray;
90
107
  /** bool [L] union multimodal soft-token mask (image | audio) for
91
108
  * per-layer-input id zeroing. Absent on the legacy vision-only shape,
@@ -112,53 +129,11 @@ export type SerialRun = (
112
129
  onToken: OnToken,
113
130
  vision?: Vision,
114
131
  trace?: PromptResponseTrace,
132
+ execution?: ResolvedExecution,
115
133
  ) => Promise<GenerateStats>;
116
134
 
117
135
  /** What the batchable decision needs from a request (cheap to compute). */
118
- export interface RequestShape {
119
- readonly hasVision: boolean;
120
- readonly hasAdapters: boolean;
121
- readonly hasRepetitionPenalty: boolean;
122
- /** The user explicitly set `seed` (reproducibility) — not the random default. */
123
- readonly userSeed: boolean;
124
- /** KV quantization is active. Per-layer configs may use the continuous
125
- * scheduler; uniform schemes use the serial mechanism. */
126
- readonly kvQuant: boolean;
127
- /** TurboQuant is active (docs/design/turboquant-kv.md). Solo-only in v1:
128
- * TurboQuantKVCache is a novel Cache implementation (not a KVCache/
129
- * RotatingKVCache subclass), so #modelCachesBatchable() already excludes
130
- * it automatically once a request's cache is converted — this flag is the
131
- * BELT on top of that automatic BRACES, an explicit refusal at the
132
- * request-shape level so a turbo request never reaches the scheduler even
133
- * before any cache conversion has happened. Both layers exist on purpose. */
134
- readonly turboQuant: boolean;
135
- /** Any of the mlx-lm sampler/processor extensions is active: min_p, XTC,
136
- * logit_bias, presence/frequency penalty. INFORMATIONAL ONLY: since the
137
- * continuous scheduler grew per-row samplers/logits processors, these all
138
- * compose. Placement deliberately does not gate on this field (see the
139
- * repetition-penalty note in #supportsContinuous). Kept so /stats and
140
- * scheduling traces can show what
141
- * a request carries. */
142
- readonly hasLogitsExtras: boolean;
143
- /** A grammar controller compiled for this request (response_format /
144
- * guided_*). Degrade-path requests (compile failed → prompt injection)
145
- * have NO controller and stay batchable — the injection already happened
146
- * at the prompt level. B0 routes grammar to serial; B1 makes it batchable
147
- * (per-row matchers), with MLX_BUN_GRAMMAR_BATCH=0 forcing the B0 serial
148
- * fallback as the A/B + kill switch. */
149
- readonly hasGrammar: boolean;
150
- /** The request asked for logprobs/top_logprobs capture. Batch-lane logprobs
151
- * are deferred (the scheduler's per-row sampler doesn't capture or read
152
- * back logprob arrays yet), so these route to the serial lane like the
153
- * other mlx-lm request extensions above. */
154
- readonly wantsLogprobs: boolean;
155
- /** A draft model is configured (serve --draft-model). Server-level and
156
- * upstream-parity: mlx_lm.server sets is_batchable = (draft is None), so
157
- * every request routes serial while a draft is mounted — speculation is a
158
- * B=1 latency optimization, batching a throughput one; they are different
159
- * mechanisms by design (grammar-spec-batching-integration.md). */
160
- readonly hasDraft: boolean;
161
- }
136
+ export interface RequestShape extends ExecutionRequirements {}
162
137
 
163
138
  export type GenerationMechanism = "serial" | "continuous";
164
139
 
@@ -167,12 +142,22 @@ export type GenerationMechanism = "serial" | "continuous";
167
142
  export interface GenerationPlacement {
168
143
  readonly shape: RequestShape;
169
144
  readonly mechanism: GenerationMechanism;
145
+ readonly execution?: ResolvedExecution;
146
+ }
147
+
148
+ export function disposeUnstartedRequest(options: GenerateOptions, vision?: Vision): void {
149
+ disposeResources([options.grammar, vision?.embeddings, vision?.imageMask,
150
+ vision?.multimodalMask, options.visionPixels].filter((resource) => resource != null));
170
151
  }
171
152
 
172
153
  export class GenerationGateway {
154
+ #closed = false;
155
+ readonly #runtime: RuntimeConfig;
156
+ readonly #binding: MlxGatewayBinding;
157
+ readonly #requests: AdmissionPool;
173
158
  readonly #mutex = new AsyncMutex();
174
159
  readonly #batch: number;
175
- #scheduler: BatchScheduler | null = null;
160
+ #scheduler: MlxBatchGroup | null = null;
176
161
  #rowsSubmitted = 0;
177
162
  /** Serial-lane requests waiting for / holding the mutex. While > 0 the
178
163
  * scheduler pauses admission (drain) so they can't be starved. */
@@ -182,11 +167,14 @@ export class GenerationGateway {
182
167
  /** Lazy, memoized: can the configured KV scheme convert every named cache? */
183
168
  #kvSchemeBatchable: boolean | null = null;
184
169
  constructor(
185
- private readonly model: RuntimeModel,
170
+ model: RuntimeModel | MlxGatewayBinding,
186
171
  batch: number,
187
172
  private readonly serialRun: SerialRun,
188
173
  private readonly opts: {
189
174
  kvBudgetBytes?: number;
175
+ memoryBudget?: MlxMemoryBudget;
176
+ checkpoints?: boolean;
177
+ stateCodecs?: import("../kv-store").CacheCodecProvider;
190
178
  /** The server-wide KV scheme (server.ts kvScheme) — threaded to the
191
179
  * scheduler at construction when batchable (Phase 3.1). turboQuant is
192
180
  * never threaded to the scheduler (always solo-only — see placement);
@@ -197,9 +185,16 @@ export class GenerationGateway {
197
185
  * back on finish. Safe to share with the serial lane — both use it
198
186
  * only inside this gateway's mutual-exclusion domain. */
199
187
  promptCache?: RowPromptCache;
188
+ /** Finish cache writes before taking the generation lock. Writers
189
+ * need that same lock, so this hook must never run inside it. */
190
+ beforeSerial?: () => Promise<void>;
200
191
  } = {},
201
192
  ) {
193
+ this.#binding = "createBatchGroup" in model ? model : bindMlxGateway(model);
194
+ this.#runtime = this.#binding.runtime;
195
+ this.opts = Object.freeze({ ...opts });
202
196
  this.#batch = Math.max(1, Math.floor(batch));
197
+ this.#requests = new AdmissionPool(this.#batch);
203
198
  }
204
199
 
205
200
  /** True if `--batch N` (N>1) is on (batchability is then per-request). */
@@ -229,7 +224,15 @@ export class GenerationGateway {
229
224
  * running/pending. activeRows/pendingRows alone MISS the serial lane —
230
225
  * a serial generation holds the mutex but shows zero rows. */
231
226
  get busy(): boolean {
232
- return this.#mutex.locked || this.activeRows > 0 || this.pendingRows > 0;
227
+ return this.#requests.active > 0 || this.#requests.queued > 0 ||
228
+ this.#mutex.locked || this.activeRows > 0 || this.pendingRows > 0;
229
+ }
230
+
231
+ async close(): Promise<void> {
232
+ this.#closed = true;
233
+ this.#requests.close();
234
+ await this.#scheduler?.close();
235
+ await this.onIdle();
233
236
  }
234
237
 
235
238
  /** Resolves once the engine is idle (poll-based, ~`pollMs` granularity —
@@ -257,113 +260,22 @@ export class GenerationGateway {
257
260
  };
258
261
  }
259
262
 
260
- /** Cache-capability gate (mirrors mlx-lm server.py's all-caches-have-merge
261
- * check): the scheduler's dynamic-B ops (temporalView/merge/filter) exist
262
- * on KVCache, RotatingKVCache, and SSMCache (hybrid gated-DeltaNet models
263
- * — Qwen3.5 — batch via SSMCache.mergeRows/filter; MLX_BUN_BATCH_SSM=0 is
264
- * the kill switch back to serial routing). */
265
263
  #modelCachesBatchable(): boolean {
266
- if (this.#cacheBatchable === null) {
267
- // Tier-0 universal models: batchable for PLAIN full-attention archs
268
- // only. The 2026-07-03 uneven-row bug (scalar-offset RoPE decoded
269
- // padded rows at wrong positions) is fixed via
270
- // UniversalRope.applyDynamic + ropeOffsetArr and GATED token-exact vs
271
- // mlx-lm B=2 on Llama-3.2-3B (static + dynamic join/leave,
272
- // tests/batched-decode-parity.test.ts "Llama 3B Tier-0"). The
273
- // maskArray archs (gemma2-family: forwardLayers builds a pad-blind
274
- // causal mask) and sliding-window universal archs remain UNVALIDATED
275
- // cells → serial, per the per-model-cell discipline.
276
- if (this.model instanceof UniversalDenseModel) {
277
- const a = this.model.args;
278
- this.#cacheBatchable =
279
- !a.maskArray && !a.layerTypes?.includes("sliding_attention");
280
- return this.#cacheBatchable;
281
- }
282
- const ssmOk = runtimeValue("MLX_BUN_BATCH_SSM") !== "0";
283
- const proto = this.model.makeCache(); // fresh caches hold no buffers
284
- this.#cacheBatchable = proto.every(
285
- (c) =>
286
- c instanceof KVCache ||
287
- c instanceof RotatingKVCache ||
288
- isBatchableCache(c) ||
289
- (ssmOk && c instanceof SSMCache),
290
- );
291
- for (const c of proto) c.dispose();
292
- }
293
- return this.#cacheBatchable;
264
+ return this.#cacheBatchable ??= this.#binding.cachesBatchable();
294
265
  }
295
266
 
296
- /** Phase 3.1 + milestone 2: the server's kv scheme batches iff it is a
297
- * per-layer kvConfig (the L2 mixed scheme) whose every configured layer
298
- * is a full-attention KVCache (BatchedQuantDecodeMaskCache + quantized
299
- * merge/extend/filter) or a rotating RotatingKVCache
300
- * (BatchedRotatingQuantCache — milestone 2, gemma's kv_config). Uniform
301
- * kvBits (quantizedKvStart threshold semantics via the serial no-byLayer
302
- * path) and configs naming SSM layers stay serial. */
303
267
  #kvBatchable(): boolean {
304
- if (this.#kvSchemeBatchable === null) {
305
- const scheme = this.opts.kvScheme;
306
- if (scheme?.kind !== "affine-config") {
307
- this.#kvSchemeBatchable = false;
308
- } else {
309
- const proto = this.model.makeCache();
310
- try {
311
- this.#kvSchemeBatchable = scheme.batchable(
312
- this.model.config,
313
- (layerIdx) =>
314
- isPlainKvCache(proto[layerIdx]) || isRotatingPlainCache(proto[layerIdx]),
315
- );
316
- } finally {
317
- for (const cache of proto) cache.dispose();
318
- }
319
- }
320
- }
321
- return this.#kvSchemeBatchable;
322
- }
323
-
324
- /** Whether the declared execution composition is implemented by the
325
- * continuous scheduler. This is a support check only: it never rewrites
326
- * MTP/drafting, KV, TurboQuant, grammar, adapters, or sampling. */
327
- #supportsContinuous(shape: RequestShape): boolean {
328
- // DiffusionGemma is non-autoregressive — the batch scheduler assumes the AR
329
- // KV-cache decode path, so it always runs serially through generate().
330
- if (this.model instanceof DiffusionGemmaModel) return false;
331
- if (!this.batchingEnabled) return false;
332
- if (!this.#modelCachesBatchable()) return false;
333
- // repetitionPenalty / logits extras (min_p, XTC, logit_bias,
334
- // presence/frequency penalties) batch through the same StepSampler and
335
- // per-row device history as the serial lane. (Load-bearing beyond opt-in
336
- // knobs: some models — Qwen3.5 — ship a default repetition penalty in
337
- // generation_config.json, which used to route EVERY request serial.)
338
- return (
339
- !shape.hasVision &&
340
- !shape.hasAdapters &&
341
- !shape.wantsLogprobs &&
342
- !shape.userSeed &&
343
- // Phase 3.1: a kv-quant request batches when the server's scheme is
344
- // the batchable kvConfig composition (the scheduler applies it);
345
- // otherwise it routes serial exactly as before.
346
- !(shape.kvQuant && !this.#kvBatchable()) &&
347
- // TurboQuant: solo-only in v1, unconditionally — never partially
348
- // batchable like kvConfig. The automatic instanceof exclusion in
349
- // #modelCachesBatchable() already covers this once a cache converts;
350
- // this is the explicit belt-and-braces refusal at request-shape time
351
- // (mirrors the kvQuant pattern above — both layers exist on purpose).
352
- !shape.turboQuant &&
353
- !shape.hasDraft &&
354
- // Grammar: B1 makes it batchable (per-row matchers) unless the kill
355
- // switch forces serial. MLX_BUN_GRAMMAR_BATCH=0 = B0 behavior (serial),
356
- // the A/B + kill lever for the new code, house style.
357
- !(shape.hasGrammar && runtimeValue("MLX_BUN_GRAMMAR_BATCH") === "0")
358
- );
268
+ return this.#kvSchemeBatchable ??= !!this.opts.kvScheme && this.#binding.kvBatchable(this.opts.kvScheme);
359
269
  }
360
270
 
361
- place(shape: RequestShape): GenerationPlacement {
271
+ place(shape: RequestShape, options: GenerateOptions = {}): GenerationPlacement {
362
272
  const frozenShape = Object.freeze(shape);
363
- return Object.freeze({
364
- shape: frozenShape,
365
- mechanism: this.#supportsContinuous(frozenShape) ? "continuous" : "serial",
273
+ const execution = this.#binding.plan(frozenShape, options, {
274
+ continuous: this.batchingEnabled,
275
+ quantizedBatch: shape.kvQuant && this.#kvBatchable(),
276
+ checkpoints: this.opts.checkpoints === true,
366
277
  });
278
+ return Object.freeze({ shape: frozenShape, mechanism: execution.mechanism, execution });
367
279
  }
368
280
 
369
281
  /** Run `fn` with exclusive ownership of the GPU + shared model state (the
@@ -372,26 +284,37 @@ export class GenerationGateway {
372
284
  * mount/unmount all come through here, so nothing runs concurrently with
373
285
  * batched decode steps (or with each other). Registers as a serial waiter
374
286
  * so the scheduler drains (stops admitting) until `fn` has run. */
287
+ /** Shared by inference, preparation and managed GPU subprocesses. */
288
+ async acquireExecutionLease(
289
+ signal?: AbortSignal,
290
+ trace?: PromptResponseTrace,
291
+ ): Promise<import("../contracts/resources").DisposableResource> {
292
+ if (this.#closed) throw new Error("generation gateway is closed");
293
+ this.#serialWaiters++;
294
+ const closeAdmission = trace?.begin("engine.admission_wait", { mechanism: "serial" });
295
+ const resume = () => {
296
+ this.#serialWaiters--;
297
+ if (this.#serialWaiters === 0) this.#scheduler?.kick();
298
+ };
299
+ try {
300
+ const release = await this.#mutex.acquire(signal);
301
+ let released = false;
302
+ return { dispose: () => {
303
+ if (released) return;
304
+ released = true;
305
+ release(); resume();
306
+ } };
307
+ } catch (error) { resume(); throw error; }
308
+ finally { closeAdmission?.(); }
309
+ }
310
+
375
311
  async runExclusive<T>(
376
312
  fn: () => Promise<T>,
377
313
  trace?: PromptResponseTrace,
314
+ signal?: AbortSignal,
378
315
  ): Promise<T> {
379
- this.#serialWaiters++;
380
- const closeAdmission = trace?.begin("engine.admission_wait", {
381
- mechanism: "serial",
382
- });
383
- try {
384
- const release = await this.#mutex.acquire();
385
- closeAdmission?.();
386
- try {
387
- return await fn();
388
- } finally {
389
- release();
390
- }
391
- } finally {
392
- this.#serialWaiters--;
393
- if (this.#serialWaiters === 0) this.#scheduler?.kick(); // resume admission
394
- }
316
+ const lease = await this.acquireExecutionLease(signal, trace);
317
+ try { return await fn(); } finally { lease.dispose(); }
395
318
  }
396
319
 
397
320
  /** Run one generation on the appropriate lane. onToken is invoked per emitted
@@ -406,8 +329,26 @@ export class GenerationGateway {
406
329
  signal?: AbortSignal,
407
330
  trace?: PromptResponseTrace,
408
331
  ): Promise<GenerateStats> {
332
+ let reservation;
333
+ try { reservation = await acquireReservation(this.#requests, signal); }
334
+ catch (error) { cleanupFailure(error, () => disposeUnstartedRequest(options, vision)); }
335
+ try { return await this.#run(promptIds, options, onToken, vision, shape, placement, signal, trace); }
336
+ finally { reservation.dispose(); }
337
+ }
338
+
339
+ async #run(
340
+ promptIds: number[],
341
+ options: GenerateOptions & { stopSequences?: string[] },
342
+ onToken: OnToken,
343
+ vision: Vision | undefined,
344
+ shape: RequestShape,
345
+ placement: GenerationPlacement,
346
+ signal?: AbortSignal,
347
+ trace?: PromptResponseTrace,
348
+ ): Promise<GenerateStats> {
349
+ const disposeUnstarted = () => disposeUnstartedRequest(options, vision);
409
350
  if (placement.shape !== shape) {
410
- options.grammar?.dispose();
351
+ disposeUnstarted();
411
352
  throw new Error("generation placement does not belong to this request shape");
412
353
  }
413
354
  try {
@@ -415,7 +356,7 @@ export class GenerationGateway {
415
356
  } catch (e) {
416
357
  // The gateway is the first component that accepts ownership of a
417
358
  // compiled controller. An already-aborted request reaches neither lane.
418
- options.grammar?.dispose();
359
+ disposeUnstarted();
419
360
  throw e;
420
361
  }
421
362
  if (placement.mechanism === "serial") {
@@ -430,28 +371,40 @@ export class GenerationGateway {
430
371
  let lastHop = performance.now();
431
372
  const hop = (): Promise<void> => new Promise<void>((r) => setImmediate(r));
432
373
  const hoppingOnToken: OnToken = (token, lp) => {
433
- if (signal?.aborted) return false;
374
+ signal?.throwIfAborted();
434
375
  const r = onToken(token, lp);
435
376
  if (r === false) return false;
436
377
  if (performance.now() - lastHop < 25) return r;
437
378
  lastHop = performance.now();
438
379
  if (r instanceof Promise)
439
- return r.then((v) =>
440
- v === false || signal?.aborted ? false : hop().then(() => signal?.aborted ? false : v),
441
- );
442
- return hop().then(() => signal?.aborted ? false : r);
380
+ return r.then(async (v) => {
381
+ if (v === false) return false;
382
+ signal?.throwIfAborted();
383
+ await hop();
384
+ signal?.throwIfAborted();
385
+ return v;
386
+ });
387
+ return hop().then(() => {
388
+ signal?.throwIfAborted();
389
+ return r;
390
+ });
443
391
  };
444
- return this.runExclusive(async () => {
392
+ let started = false;
393
+ return Promise.resolve().then(() => this.opts.beforeSerial?.()).then(() => this.runExclusive(async () => {
445
394
  signal?.throwIfAborted();
395
+ started = true;
446
396
  const stats = await this.serialRun(
447
- promptIds, options, hoppingOnToken, vision, trace,
397
+ promptIds, signal ? { ...options, signal } : options, hoppingOnToken, vision, trace, placement.execution,
448
398
  );
449
399
  signal?.throwIfAborted();
450
400
  return stats;
451
- }, trace)
401
+ }, trace, signal))
452
402
  // Covers a serial waiter aborted before serialRun takes ownership.
453
403
  // generate() also disposes defensively; the operation is idempotent.
454
- .finally(() => options.grammar?.dispose());
404
+ .finally(() => {
405
+ if (started) options.grammar?.dispose();
406
+ else disposeUnstarted();
407
+ });
455
408
  }
456
409
 
457
410
  // The scheduler owns grammar ready/accept sequencing. StepSampler owns the
@@ -473,8 +426,9 @@ export class GenerationGateway {
473
426
  try {
474
427
  st = await this.#ensureScheduler().submit({
475
428
  promptIds,
429
+ compiledDecode: placement.execution?.compiledDecode,
476
430
  maxTokens: options.maxTokens ?? 512,
477
- eosTokenIds: options.eosTokenIds ?? this.model.config.eosTokenIds,
431
+ eosTokenIds: options.eosTokenIds ?? this.#binding.config.eosTokenIds,
478
432
  sample,
479
433
  plainGreedy: stepSampler.isPlainGreedy,
480
434
  onToken,
@@ -500,16 +454,23 @@ export class GenerationGateway {
500
454
  promptTokens: st.promptTokens,
501
455
  cachedTokens: st.cachedTokens,
502
456
  generatedTokens: st.generatedTokens,
503
- prefillMs: 0, decodeMs: 0, prefillTps: 0, decodeTps: 0,
457
+ finishReason: st.finishReason,
458
+ prefillMs: st.prefillMs,
459
+ decodeMs: st.decodeMs,
460
+ prefillTps: st.prefillMs > 0 ? ((st.promptTokens - st.cachedTokens) / st.prefillMs) * 1000 : 0,
461
+ decodeTps: st.decodeMs > 0 && st.generatedTokens > 1 ? ((st.generatedTokens - 1) / st.decodeMs) * 1000 : 0,
504
462
  cacheTokens: [],
505
463
  };
506
464
  }
507
465
 
508
- #ensureScheduler(): BatchScheduler {
466
+ #ensureScheduler(): MlxBatchGroup {
509
467
  if (!this.#scheduler)
510
- this.#scheduler = new BatchScheduler(this.model, {
468
+ this.#scheduler = this.#binding.createBatchGroup({
469
+ runtime: this.#runtime,
511
470
  maxBatch: this.#batch,
471
+ stateCodecs: this.opts.stateCodecs,
512
472
  kvBudgetBytes: this.opts.kvBudgetBytes,
473
+ memoryBudget: this.opts.memoryBudget,
513
474
  // Phase 3.1: the batchable kvConfig composition is applied by the
514
475
  // scheduler (solo rows convert at serial chunk boundaries, then
515
476
  // merge as quantized triples). Only threaded when the scheme