mlx-bun 0.0.13 → 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 (427) hide show
  1. package/README.md +71 -578
  2. package/bin/mlx-bun.mjs +1 -1
  3. package/docs/README.md +91 -0
  4. package/docs/{design → archive}/hlg-sampling.md +3 -3
  5. package/docs/{design → archive/investigations}/audio-input-plan.md +4 -4
  6. package/docs/{design → archive/investigations}/colibri-glm52-port.md +1 -1
  7. package/docs/{design → archive/investigations}/diffusion-gemma-port.md +3 -3
  8. package/docs/{design → archive/investigations}/faithful-l1-consolidation.md +4 -4
  9. package/docs/{investigations → archive/investigations}/hlg-sampling-investigation.md +1 -1
  10. package/docs/{design → archive/investigations}/minicpm5-decode-megakernel.md +11 -11
  11. package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
  12. package/docs/archive/investigations/pareto-specialized-runtime-findings.md +92 -0
  13. package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
  14. package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
  15. package/docs/{planning → archive/planning}/chat-ui-vision.md +1 -1
  16. package/docs/{planning → archive/planning}/chunk-task-roadmap.md +1 -1
  17. package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +4 -4
  18. package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +1 -1
  19. package/docs/{planning → archive/planning}/release-notes-v0.0.9.md +1 -1
  20. package/docs/archive/planning/release-notes-v0.2.0.md +49 -0
  21. package/docs/{planning → archive/planning}/website-readme-pass-plan.md +1 -1
  22. package/docs/design/batching.md +569 -0
  23. package/docs/design/decode-speed-program.md +3178 -169
  24. package/docs/design/dreaming-nightly-pipeline.md +675 -282
  25. package/docs/design/generic-model-support.md +579 -334
  26. package/docs/design/kv-cache.md +680 -0
  27. package/docs/design/orpo-training.md +673 -1239
  28. package/docs/design/speculative-decoding.md +1627 -0
  29. package/docs/design/turboquant.md +2102 -0
  30. package/docs/design/unified-engine-frontier-plan.md +1831 -727
  31. package/docs/design/web-chat-redesign.md +666 -1083
  32. package/docs/planning/PRODUCT_ROADMAP.md +34 -1
  33. package/docs/planning/release-notes-v0.3.0.md +35 -0
  34. package/docs/planning/video-series.md +421 -0
  35. package/docs/reference/benchmarks.md +1162 -0
  36. package/docs/reference/cli.md +395 -149
  37. package/docs/reference/distribution.md +321 -181
  38. package/docs/reference/environment.md +348 -0
  39. package/docs/reference/glossary.md +39 -0
  40. package/docs/reference/library-api.md +580 -142
  41. package/docs/reference/memory.md +227 -64
  42. package/docs/reference/models.md +137 -6
  43. package/docs/reference/server-api.md +938 -422
  44. package/docs/reference/server-config.md +652 -320
  45. package/docs/reference/training.md +393 -254
  46. package/docs/reference/troubleshooting.md +166 -0
  47. package/package.json +10 -5
  48. package/src/admission-plan.ts +18 -0
  49. package/src/anthropic.ts +92 -3
  50. package/src/audio/decode.ts +2 -2
  51. package/src/audio/features.ts +2 -2
  52. package/src/backends/mlx/autoregressive.ts +108 -0
  53. package/src/backends/mlx/batch-group.ts +1646 -0
  54. package/src/backends/mlx/diffusion.ts +39 -0
  55. package/src/backends/mlx/draft-target.ts +47 -0
  56. package/src/backends/mlx/evaluation.ts +105 -0
  57. package/src/backends/mlx/fill-append.ts +45 -0
  58. package/src/backends/mlx/gateway-binding.ts +71 -0
  59. package/src/backends/mlx/graph.ts +38 -0
  60. package/src/backends/mlx/memory-client.ts +189 -0
  61. package/src/backends/mlx/memory-guard.ts +40 -0
  62. package/src/backends/mlx/methods.ts +90 -0
  63. package/src/backends/mlx/model-prompt.ts +164 -0
  64. package/src/backends/mlx/model-serving.ts +79 -0
  65. package/src/backends/mlx/prefill.ts +33 -0
  66. package/src/backends/mlx/request-policy.ts +20 -0
  67. package/src/backends/mlx/rollback.ts +51 -0
  68. package/src/backends/mlx/serial-executor.ts +273 -0
  69. package/src/backends/mlx/speculative.ts +114 -0
  70. package/src/backends/mlx/state-views.ts +26 -0
  71. package/src/backends/mlx/text-engine.ts +73 -0
  72. package/src/cli.ts +189 -61
  73. package/src/client.ts +66 -0
  74. package/src/config.ts +21 -4
  75. package/src/contracts/completion.ts +16 -0
  76. package/src/contracts/execution.ts +49 -0
  77. package/src/contracts/generation.ts +103 -0
  78. package/src/contracts/host.ts +7 -0
  79. package/src/contracts/jobs.ts +66 -0
  80. package/src/contracts/pi-provider.ts +30 -0
  81. package/src/contracts/pi.ts +259 -0
  82. package/src/contracts/resources.ts +18 -0
  83. package/src/contracts/scheduling.ts +27 -0
  84. package/src/contracts/tsconfig.json +14 -0
  85. package/src/diffusion/diffusion-generate.ts +218 -153
  86. package/src/embed.ts +1 -1
  87. package/src/engine/admission.ts +59 -0
  88. package/src/engine/cancellation.ts +38 -0
  89. package/src/engine/engine.ts +56 -0
  90. package/src/engine/execution-coordinator.ts +70 -0
  91. package/src/engine/execution-plan.ts +51 -0
  92. package/src/engine/resources.ts +42 -0
  93. package/src/engine/scheduler.ts +67 -0
  94. package/src/engine/session.ts +247 -0
  95. package/src/eval/README.md +44 -1
  96. package/src/eval/kl.ts +263 -23
  97. package/src/eval/reference-logits.ts +207 -0
  98. package/src/eval/runner.ts +10 -79
  99. package/src/eval/tasks/bfcl.ts +2 -1
  100. package/src/eval/tasks/gsm8k.ts +2 -1
  101. package/src/eval/tasks/hashhop.ts +2 -1
  102. package/src/eval/tasks/humaneval.ts +5 -4
  103. package/src/eval/tasks/ifeval.ts +2 -1
  104. package/src/eval/tasks/mmlu.ts +2 -1
  105. package/src/expert-io.ts +3 -1
  106. package/src/expert-offload.ts +3 -1
  107. package/src/fill/echo-index.ts +188 -0
  108. package/src/fill/fill-session.ts +452 -0
  109. package/src/fill/proposal.ts +68 -0
  110. package/src/fill/schema-rows.ts +506 -0
  111. package/src/fill/tool-boundary.ts +103 -0
  112. package/src/fit.ts +24 -43
  113. package/src/generate.ts +740 -337
  114. package/src/grammar.ts +14 -8
  115. package/src/harness-pi.ts +15 -34
  116. package/src/hub-rest.ts +4 -4
  117. package/src/index.ts +27 -0
  118. package/src/inference/denoising.ts +17 -0
  119. package/src/inference/graph.ts +33 -0
  120. package/src/inference/prefill.ts +39 -0
  121. package/src/inference/rollback.ts +7 -0
  122. package/src/jobs/db.ts +4 -0
  123. package/src/jobs/index.ts +2 -0
  124. package/src/jobs/job-entry.ts +15 -14
  125. package/src/jobs/runner.ts +98 -13
  126. package/src/jobs/task-client.ts +19 -0
  127. package/src/jobs/types.ts +2 -58
  128. package/src/kv-scheme.ts +199 -0
  129. package/src/kv-store.ts +459 -234
  130. package/src/lab/README.md +18 -0
  131. package/src/{assets → lab/curve}/curve-designer.html +2 -2
  132. package/src/{curve-sampler.ts → lab/curve/curve-sampler.ts} +2 -2
  133. package/src/{expert-trace.ts → lab/expert-trace/expert-trace.ts} +7 -5
  134. package/src/{model → lab/paged-kv}/paged-kv.ts +9 -6
  135. package/src/library.ts +40 -0
  136. package/src/lora.ts +18 -0
  137. package/src/media-fetch.ts +20 -4
  138. package/src/memory/article.ts +1 -1
  139. package/src/memory/chunk.ts +1 -1
  140. package/src/memory/model.ts +32 -170
  141. package/src/memory/prompts.ts +1 -1
  142. package/src/memory/rest.ts +1 -1
  143. package/src/memory/schedule.ts +1 -1
  144. package/src/memory/stages.ts +4 -1
  145. package/src/memory/vault.ts +3 -4
  146. package/src/mlx/array.ts +9 -10
  147. package/src/mlx/compile.ts +12 -2
  148. package/src/mlx/ffi.ts +66 -22
  149. package/src/mlx/materialize.ts +23 -0
  150. package/src/mlx/metal-kernel.ts +10 -11
  151. package/src/mlx/ops.ts +64 -19
  152. package/src/mlx/quantized-rows.ts +36 -0
  153. package/src/mlx/token-bitmask.ts +52 -0
  154. package/src/mlx/turboquant-kv-decode.ts +82 -0
  155. package/src/mlx/turboquant-kv-inverse.ts +122 -0
  156. package/src/mlx/turboquant-ops.ts +33 -3
  157. package/src/mlx/turboquant-tables.ts +2 -2
  158. package/src/model/batched-mask.ts +9 -3
  159. package/src/model/batched-quant.ts +2 -2
  160. package/src/model/batched-rotating-quant.ts +45 -99
  161. package/src/model/batched-rotating-state.ts +73 -0
  162. package/src/model/batched-rotating.ts +57 -106
  163. package/src/model/batched-row-storage.ts +127 -0
  164. package/src/model/compiled-decode.ts +4 -3
  165. package/src/model/diffusion-gemma.ts +8 -8
  166. package/src/model/factory.ts +87 -49
  167. package/src/model/fingerprint.ts +1 -1
  168. package/src/model/flash-attention.ts +3 -3
  169. package/src/model/gemma4-base.ts +187 -17
  170. package/src/model/gemma4.ts +6 -6
  171. package/src/model/generated/gemma4-12b.ts +52 -148
  172. package/src/model/generated/gemma4-26b.ts +34 -94
  173. package/src/model/generated/gemma4-e4b.ts +28 -76
  174. package/src/model/glm52-cache.ts +19 -9
  175. package/src/model/glm52-dsa.ts +13 -9
  176. package/src/model/glm52-memory.ts +28 -1
  177. package/src/model/implementation.ts +69 -0
  178. package/src/model/minicpm5.ts +7 -8
  179. package/src/model/profile.ts +443 -0
  180. package/src/model/qwen-append.ts +12 -0
  181. package/src/model/qwen-conv.ts +52 -0
  182. package/src/model/qwen3-delta.ts +98 -2
  183. package/src/model/qwen3-mrope.ts +252 -0
  184. package/src/model/qwen3_5-checkpoint.ts +164 -0
  185. package/src/model/qwen3_5.ts +336 -60
  186. package/src/model/support.ts +1 -1
  187. package/src/model/trellis-balanced-scatter.ts +51 -0
  188. package/src/model/trellis-linear.ts +672 -0
  189. package/src/model/trellis-shared-m.ts +151 -0
  190. package/src/model/trellis-shared-scatter.ts +144 -0
  191. package/src/model/trellis-splitk-prefill.ts +158 -0
  192. package/src/model/trellis-tiled-prefill.ts +136 -0
  193. package/src/model/trellis-vector-expand.ts +62 -0
  194. package/src/model/trellis-wide-prefill.ts +94 -0
  195. package/src/model/universal/dense.ts +1 -1
  196. package/src/model/universal/modules.ts +8 -1
  197. package/src/native/frame_extract.swift +140 -0
  198. package/src/native-pack.ts +15 -7
  199. package/src/pi-provider.ts +6 -64
  200. package/src/pi-web.ts +14 -320
  201. package/src/prompt-cache.ts +97 -54
  202. package/src/quantize/allocator.ts +1 -1
  203. package/src/quantize/atomic-output.ts +38 -0
  204. package/src/quantize/calibration.ts +2 -1
  205. package/src/quantize/config-writer.ts +16 -3
  206. package/src/quantize/index.ts +28 -1
  207. package/src/quantize/job.ts +11 -0
  208. package/src/quantize/quantizer.ts +265 -100
  209. package/src/quantize/rotate.ts +603 -0
  210. package/src/quantize/safetensors-writer.ts +79 -1
  211. package/src/quantize/sensitivity.ts +41 -59
  212. package/src/quantize/trellis.ts +586 -0
  213. package/src/quantize/weight-transform.ts +292 -0
  214. package/src/responses.ts +130 -3
  215. package/src/runtime-config.ts +81 -0
  216. package/src/sampler.ts +276 -4
  217. package/src/serve/admin-routes.ts +184 -0
  218. package/src/serve/aux-routes.ts +132 -0
  219. package/src/serve/batch-scheduler.ts +8 -1558
  220. package/src/serve/chat-request.ts +391 -0
  221. package/src/serve/chat-stage.ts +195 -0
  222. package/src/serve/checkpoint-identity.ts +64 -0
  223. package/src/serve/completion-executor.ts +354 -0
  224. package/src/serve/completion-sink.ts +157 -0
  225. package/src/serve/discovery-routes.ts +204 -0
  226. package/src/serve/generation-gateway.ts +211 -243
  227. package/src/serve/http.ts +159 -0
  228. package/src/serve/inference-request.ts +79 -0
  229. package/src/serve/isolate.ts +297 -105
  230. package/src/serve/kv-budget.ts +14 -0
  231. package/src/serve/lab-routes.ts +341 -0
  232. package/src/serve/lane-registry.ts +2 -1
  233. package/src/serve/model-admin-routes.ts +155 -0
  234. package/src/serve/model-binding.ts +43 -0
  235. package/src/serve/model-host.ts +512 -0
  236. package/src/serve/openai-wire.ts +103 -0
  237. package/src/serve/parent-application.ts +56 -0
  238. package/src/serve/pipeline.ts +33 -0
  239. package/src/serve/preparation.ts +33 -0
  240. package/src/serve/prompt-response-trace.ts +147 -0
  241. package/src/serve/request-plan.ts +162 -0
  242. package/src/serve/request-prep.ts +346 -0
  243. package/src/serve/response-lease.ts +24 -0
  244. package/src/serve/responses-client.ts +69 -0
  245. package/src/serve/session-completion-engine.ts +110 -0
  246. package/src/serve/session-search.ts +1 -1
  247. package/src/serve/static-routes.ts +101 -0
  248. package/src/serve/text-completion-stage.ts +104 -0
  249. package/src/serve/token-streams.ts +519 -0
  250. package/src/server.ts +523 -3551
  251. package/src/spec/assistant-source.ts +10 -44
  252. package/src/spec/deepspec-source.ts +6 -6
  253. package/src/spec/dflash-source.ts +9 -8
  254. package/src/spec/dspark/ab-stats.ts +3 -3
  255. package/src/spec/dspark/calibration.ts +2 -2
  256. package/src/spec/dspark/deepspec-module.ts +2 -2
  257. package/src/spec/dspark/generate-dflash.ts +2 -2
  258. package/src/spec/dspark/generate.ts +2 -2
  259. package/src/spec/dspark/loader.ts +1 -1
  260. package/src/spec/dspark/module-dflash.ts +5 -4
  261. package/src/spec/dspark/module.ts +3 -3
  262. package/src/spec/dspark/quantize-drafter.ts +1 -1
  263. package/src/spec/generate.ts +1 -1
  264. package/src/spec/glm52-mtp-source.ts +9 -1
  265. package/src/spec/ngram-source.ts +1 -1
  266. package/src/spec/prefix-state.ts +45 -0
  267. package/src/spec/qwen-mtp-source.ts +528 -0
  268. package/src/spec/serve-loop.ts +179 -213
  269. package/src/spec/source.ts +42 -10
  270. package/src/spec/two-model.ts +1 -1
  271. package/src/ssd-cache.ts +95 -24
  272. package/src/ssd-durability.ts +207 -0
  273. package/src/tool-call.ts +51 -14
  274. package/src/train/flash-cce.ts +14 -13
  275. package/src/train/forward.ts +38 -1
  276. package/src/train/loss.ts +3 -2
  277. package/src/train/prefix-shared.ts +5 -1
  278. package/src/train/segmented.ts +9 -6
  279. package/src/train/trainer.ts +9 -8
  280. package/src/vision/preprocess.ts +1 -1
  281. package/src/vision/prompt.ts +48 -2
  282. package/src/vision/qwen3vl-preprocess.ts +409 -0
  283. package/src/vision/qwen3vl-prompt.ts +196 -0
  284. package/src/vision/qwen3vl-tower.ts +555 -0
  285. package/src/vision/video-frames.ts +148 -0
  286. package/src/web/app.html +1 -1
  287. package/src/web/app.js +15 -6
  288. package/src/web/src/adapters-panel.ts +3 -3
  289. package/src/web/src/assistant.ts +2 -36
  290. package/src/web/src/chat.ts +16 -9
  291. package/src/web/src/composer.ts +4 -4
  292. package/src/web/src/hub.ts +1 -1
  293. package/src/web/src/markdown.ts +0 -0
  294. package/src/web/src/memory-panel.ts +1 -1
  295. package/src/web/src/model-picker.ts +1 -1
  296. package/src/web/src/palette.ts +1 -1
  297. package/src/web/src/protocol.ts +3 -39
  298. package/src/web/src/sessions.ts +1 -1
  299. package/src/web/src/ui-catalog.ts +4 -18
  300. package/src/web-assets.ts +47 -0
  301. package/src/weights.ts +134 -33
  302. package/docs/archive/mlx-bun-lab-report.html +0 -275
  303. package/docs/archive/wwdc-mlx-bun.html +0 -848
  304. package/docs/design/adapters-end-to-end.md +0 -205
  305. package/docs/design/batching-perf-path.md +0 -255
  306. package/docs/design/batching-v2-plan.md +0 -529
  307. package/docs/design/bucketing-stage.md +0 -197
  308. package/docs/design/compat-cli-surface-design.md +0 -230
  309. package/docs/design/docs-reorg-plan.md +0 -165
  310. package/docs/design/dspark-serving-program.md +0 -371
  311. package/docs/design/dspark-speculative-decoding.md +0 -268
  312. package/docs/design/grammar-spec-batching-integration.md +0 -301
  313. package/docs/design/memory-inference-path.md +0 -422
  314. package/docs/design/memory-synthesis.md +0 -287
  315. package/docs/design/memory-system.md +0 -237
  316. package/docs/design/mlx-lm-tool-parity-plan.md +0 -545
  317. package/docs/design/omlx-adoption-map.md +0 -98
  318. package/docs/design/orpo-dynamic-lambda.md +0 -220
  319. package/docs/design/orpo-future-enhancements.md +0 -340
  320. package/docs/design/paged-kv-cache.md +0 -214
  321. package/docs/design/parallel-slots.md +0 -567
  322. package/docs/design/parity-tier-dag.md +0 -161
  323. package/docs/design/repo-cleanup-plan.md +0 -189
  324. package/docs/design/runtime-isolation.md +0 -263
  325. package/docs/design/segmented-backward-training.md +0 -543
  326. package/docs/design/spec-decode-larger-targets.md +0 -229
  327. package/docs/design/ssd-kv-cold-tier.md +0 -288
  328. package/docs/design/structured-output.md +0 -484
  329. package/docs/design/tauri-desktop-app.md +0 -127
  330. package/docs/design/the-dreaming-master-plan.md +0 -445
  331. package/docs/design/turboquant-kv.md +0 -235
  332. package/docs/design/w4a16-compute-precision-spike.md +0 -232
  333. package/docs/design/web-chat-beat-matrix.md +0 -315
  334. package/docs/design/write-pipeline-entity.md +0 -468
  335. package/docs/investigations/curve-runs/bisector-route.json +0 -98
  336. package/docs/investigations/curve-runs/bisector-trace.json +0 -1393
  337. package/docs/investigations/curve-runs/curve-terrain.json +0 -1179
  338. package/docs/investigations/curve-runs/curve-terrain.svg +0 -42
  339. package/docs/investigations/hlg-ab.html +0 -34
  340. package/docs/investigations/hlg-explorer.html +0 -238
  341. package/docs/investigations/hlg-figs/cliff.svg +0 -1
  342. package/docs/investigations/hlg-figs/cross.svg +0 -1
  343. package/docs/investigations/hlg-figs/edges.svg +0 -1
  344. package/docs/investigations/hlg-figs/k20-grids.svg +0 -1
  345. package/docs/investigations/hlg-figs/k20-peaks.svg +0 -1
  346. package/docs/investigations/hlg-figs/lw-curve.svg +0 -1
  347. package/docs/investigations/hlg-figs/terrain.svg +0 -1
  348. package/docs/investigations/hlg-report.html +0 -262
  349. package/docs/investigations/hlg-runs/hlg-ab.json +0 -149
  350. package/docs/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
  351. package/docs/investigations/hlg-runs/hlg-blind.json +0 -107
  352. package/docs/investigations/hlg-runs/hlg-cliff.json +0 -197
  353. package/docs/investigations/hlg-runs/hlg-cross.json +0 -505
  354. package/docs/investigations/hlg-runs/hlg-edge.json +0 -388
  355. package/docs/investigations/hlg-runs/hlg-eval1.json +0 -25
  356. package/docs/investigations/hlg-runs/hlg-frontier.json +0 -86
  357. package/docs/investigations/hlg-runs/hlg-grid.json +0 -242
  358. package/docs/investigations/hlg-runs/hlg-grid2.json +0 -343
  359. package/docs/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
  360. package/docs/investigations/hlg-runs/hlg-map.json +0 -383
  361. package/docs/investigations/hlg-runs/run0-original.log +0 -159
  362. package/docs/investigations/hlg-runs/runA-recommended.log +0 -131
  363. package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
  364. package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
  365. package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
  366. package/docs/investigations/hlg-runs/runAD-ab-samples.log +0 -8
  367. package/docs/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
  368. package/docs/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
  369. package/docs/investigations/hlg-runs/runAG-eval-config2.log +0 -16
  370. package/docs/investigations/hlg-runs/runB-gain-variants.log +0 -192
  371. package/docs/investigations/hlg-runs/runC-pivots.log +0 -162
  372. package/docs/investigations/hlg-runs/runD-oetf.log +0 -132
  373. package/docs/investigations/hlg-runs/runE-eotf.log +0 -162
  374. package/docs/investigations/hlg-runs/runF-shaper-working.log +0 -86
  375. package/docs/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
  376. package/docs/investigations/hlg-runs/runH-wsweep.log +0 -176
  377. package/docs/investigations/hlg-runs/runI-asweep.log +0 -176
  378. package/docs/investigations/hlg-runs/runJ-knobmap.log +0 -49
  379. package/docs/investigations/hlg-runs/runK-frontier.log +0 -19
  380. package/docs/investigations/hlg-runs/runL-widemap.log +0 -65
  381. package/docs/investigations/hlg-runs/runM-frontier-emb.log +0 -15
  382. package/docs/investigations/hlg-runs/runN-verify.log +0 -116
  383. package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
  384. package/docs/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
  385. package/docs/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
  386. package/docs/investigations/hlg-runs/runR-blind-transcript.log +0 -79
  387. package/docs/investigations/hlg-runs/runS-cross-coherence.log +0 -33
  388. package/docs/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
  389. package/docs/investigations/hlg-runs/runU-cross-12b.log +0 -33
  390. package/docs/investigations/hlg-runs/runV-lw-e4b.log +0 -22
  391. package/docs/investigations/hlg-runs/runW-lw-12b.log +0 -22
  392. package/docs/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
  393. package/docs/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
  394. package/docs/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
  395. package/docs/planning/IDEAS.md +0 -69
  396. package/docs/reference/embedding.md +0 -197
  397. package/docs/reference/features-matrix.md +0 -100
  398. package/docs/reference/orpo-quickstart.md +0 -135
  399. package/src/flags.ts +0 -13
  400. /package/docs/{investigations → archive/investigations}/chunk-finetune-experiment.md +0 -0
  401. /package/docs/{investigations → archive/investigations}/colibri-oracle-pin.md +0 -0
  402. /package/docs/{investigations → archive/investigations}/curve-bisector-routing.md +0 -0
  403. /package/docs/{investigations → archive/investigations}/decode-roofline-lookagain.md +0 -0
  404. /package/docs/{investigations → archive/investigations}/dspark-handoff.md +0 -0
  405. /package/docs/{investigations → archive/investigations}/expert-offload-single-user-moe.md +0 -0
  406. /package/docs/{investigations → archive/investigations}/indexshare-performance-spike.md +0 -0
  407. /package/docs/{investigations → archive/investigations}/kernel-perf-review-2026-07.md +0 -0
  408. /package/docs/{investigations → archive/investigations}/lab-build-journal.md +0 -0
  409. /package/docs/{investigations → archive/investigations}/openwiki-evaluation.md +0 -0
  410. /package/docs/{investigations → archive/investigations}/orpo-base-uf-experiment-and-directions.md +0 -0
  411. /package/docs/{investigations → archive/investigations}/orpo-flash-cce-pin-leak.md +0 -0
  412. /package/docs/{investigations → archive/investigations}/orpo-uf-testing-handoff.md +0 -0
  413. /package/docs/{investigations → archive/investigations}/pi-builtin-investigation.md +0 -0
  414. /package/docs/{investigations → archive/investigations}/segmented-backward-handoff.md +0 -0
  415. /package/docs/{investigations → archive/investigations}/starter-model-port-handoff.md +0 -0
  416. /package/docs/{investigations → archive/investigations}/steel-flash-cce-handoff.md +0 -0
  417. /package/docs/{investigations → archive/investigations}/trainer-validation-experiment.md +0 -0
  418. /package/docs/{planning → archive/planning}/chat-ui-vision-memory-first.md +0 -0
  419. /package/docs/{planning → archive/planning}/journal.md +0 -0
  420. /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
  421. /package/docs/{planning → archive/planning}/release-notes-v0.0.10.md +0 -0
  422. /package/docs/{planning → archive/planning}/release-notes-v0.0.11.md +0 -0
  423. /package/docs/{planning → archive/planning}/release-notes-v0.0.12.md +0 -0
  424. /package/docs/{planning → archive/planning}/release-notes-v0.0.13.md +0 -0
  425. /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
  426. /package/{docs/investigations → src/serve/assets}/curve-terrain.html +0 -0
  427. /package/{docs/dag → src/serve/assets}/training-inference-map.html +0 -0
@@ -1,10 +1,14 @@
1
- // Generation gateway — the seam between the server's request handlers and the
2
- // two execution lanes: the serial single-queue path (today's default) and the
3
- // continuous-batching scheduler (`--batch N`). The handler builds an onToken
4
- // closure (its own StopMatcher + tool router + SSE stream) and calls run();
5
- // the gateway routes it to the right lane. Per-row SSE fan-out falls out for
6
- // free — each request keeps its own onToken/stream and the scheduler just
7
- // invokes the right row's onToken.
1
+ import { AdmissionPool } from "../engine/admission";
2
+ import { acquireReservation } from "./preparation";
3
+ import { cleanupFailure, disposeResources } from "../engine/resources";
4
+ // GenerationGateway is the scheduling seam between CompletionExecutor and
5
+ // the inference runtime. It declares one mechanism per request: the preserved
6
+ // strict/dedicated serial executor, or the continuous scheduler (`--batch N`).
7
+ // The scheduler itself chooses its B=1 fast path or B=N step from its active
8
+ // row count; placement does not predict a batch size. The executor owns each
9
+ // request's semantic token sink and passes its callback plus one immutable
10
+ // placement into run(). Each request keeps its own callback, so the scheduler
11
+ // can fan tokens out to the matching response stream.
8
12
  //
9
13
  // The two lanes are MUTUALLY EXCLUSIVE on the GPU (and on shared model state
10
14
  // like loraState, which generate() mutates per-generation assuming a serialized
@@ -31,19 +35,20 @@
31
35
  // AND sliding-window (Gemma) models both batch — the scheduler assembles each
32
36
  // layer's cache by type. Prompt-cache prefix reuse works on the batch lane
33
37
  // too (Phase 3.2): joiners take() at admission; never-merged lone rows put()
34
- // 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).
35
40
 
36
41
  import { MlxArray } from "../mlx/array";
37
- import * as ops from "../mlx/ops";
38
42
  import type { RuntimeModel } from "../model/factory";
39
- import { DiffusionGemmaModel } from "../model/diffusion-gemma";
40
- import { KVCache, RotatingKVCache, isBatchableCache } from "../model/gemma4-base";
41
- import { SSMCache } from "../model/qwen3-delta";
42
- import { UniversalDenseModel } from "../model/universal/dense";
43
43
  import type { GenerateOptions, GenerateStats, TokenLogprobs } from "../generate";
44
- import type { KvQuantSpec, TurboQuantScheme } from "../config";
45
- import { makeSampler, makeLogitsProcessors, toLogprobs } from "../sampler";
46
- import { BatchScheduler, type RowPromptCache } from "./batch-scheduler";
44
+ import type { KvScheme } from "../kv-scheme";
45
+ import { makeStepSampler } from "../sampler";
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";
51
+ import type { PromptResponseTrace } from "./prompt-response-trace";
47
52
 
48
53
  /** Async mutex: acquire() resolves to a release fn; releases run FIFO. */
49
54
  class AsyncMutex {
@@ -53,18 +58,38 @@ class AsyncMutex {
53
58
  get locked(): boolean {
54
59
  return this.#pending > 0;
55
60
  }
56
- acquire(): Promise<() => void> {
61
+ acquire(signal?: AbortSignal): Promise<() => void> {
62
+ if (signal?.aborted) return Promise.reject(signal.reason);
57
63
  this.#pending++;
58
64
  let release!: () => void;
59
65
  const gate = new Promise<void>((r) => { release = r; });
60
66
  const wait = this.#tail;
61
67
  this.#tail = this.#tail.then(() => gate);
62
68
  let released = false;
63
- return wait.then(() => () => {
69
+ const unlock = () => {
64
70
  if (released) return; // idempotent: a double release must not skew #pending
65
71
  released = true;
66
72
  this.#pending--;
67
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
+ });
68
93
  });
69
94
  }
70
95
  }
@@ -77,12 +102,15 @@ export type Vision = {
77
102
  embeddings: MlxArray;
78
103
  /** bool [L] image-token mask for the bidirectional attention overlay.
79
104
  * Absent when the prompt carries ANY audio — audio(-containing) prompts
80
- * run fully causal (audio-input-plan.md §3.3 Q1). */
105
+ * run fully causal (docs/design/generic-model-support.md §3.3 Q1). */
81
106
  imageMask?: MlxArray;
82
107
  /** bool [L] union multimodal soft-token mask (image | audio) for
83
108
  * per-layer-input id zeroing. Absent on the legacy vision-only shape,
84
109
  * where zeroing falls back to imageMask. */
85
110
  multimodalMask?: MlxArray;
111
+ /** Qwen3.5/3.8 vision: the request's mRoPE positions + decode delta,
112
+ * installed on the model for exactly this serial run (server-side). */
113
+ mrope?: import("../model/qwen3-mrope").MropeRequestState;
86
114
  };
87
115
 
88
116
  /** Per-token sink: returning `false` halts this generation (stop sequence).
@@ -100,87 +128,73 @@ export type SerialRun = (
100
128
  options: GenerateOptions & { stopSequences?: string[] },
101
129
  onToken: OnToken,
102
130
  vision?: Vision,
131
+ trace?: PromptResponseTrace,
132
+ execution?: ResolvedExecution,
103
133
  ) => Promise<GenerateStats>;
104
134
 
105
135
  /** What the batchable decision needs from a request (cheap to compute). */
106
- export interface RequestShape {
107
- hasVision: boolean;
108
- hasAdapters: boolean;
109
- hasRepetitionPenalty: boolean;
110
- /** The user explicitly set `seed` (reproducibility) — not the random default. */
111
- userSeed: boolean;
112
- /** KV quantization is active (kvConfig/kvBits) — batched is bf16-only in v1. */
113
- kvQuant: boolean;
114
- /** TurboQuant is active (docs/design/turboquant-kv.md). Solo-only in v1:
115
- * TurboQuantKVCache is a novel Cache implementation (not a KVCache/
116
- * RotatingKVCache subclass), so #modelCachesBatchable() already excludes
117
- * it automatically once a request's cache is converted — this flag is the
118
- * BELT on top of that automatic BRACES, an explicit refusal at the
119
- * request-shape level so a turbo request never reaches the scheduler even
120
- * before any cache conversion has happened. Both layers exist on purpose. */
121
- turboQuant: boolean;
122
- /** Any of the mlx-lm sampler/processor extensions is active: min_p, XTC,
123
- * logit_bias, presence/frequency penalty. INFORMATIONAL ONLY: since the
124
- * batched lane grew per-row samplers/logits processors these all batch —
125
- * willBatch() deliberately does not gate on this field (see its
126
- * repetitionPenalty note). Kept so /stats and lane tracing can show what
127
- * a request carries. */
128
- hasLogitsExtras: boolean;
129
- /** A grammar controller compiled for this request (response_format /
130
- * guided_*). Degrade-path requests (compile failed → prompt injection)
131
- * have NO controller and stay batchable — the injection already happened
132
- * at the prompt level. B0 routes grammar to serial; B1 makes it batchable
133
- * (per-row matchers), with MLX_BUN_GRAMMAR_BATCH=0 forcing the B0 serial
134
- * fallback as the A/B + kill switch. */
135
- hasGrammar: boolean;
136
- /** The request asked for logprobs/top_logprobs capture. Batch-lane logprobs
137
- * are deferred (the scheduler's per-row sampler doesn't capture or read
138
- * back logprob arrays yet), so these route to the serial lane like the
139
- * other mlx-lm request extensions above. */
140
- wantsLogprobs: boolean;
141
- /** A draft model is configured (serve --draft-model). Server-level and
142
- * upstream-parity: mlx_lm.server sets is_batchable = (draft is None), so
143
- * every request routes serial while a draft is mounted — speculation is a
144
- * B=1 latency optimization, batching a throughput one; they are different
145
- * modes by design (grammar-spec-batching-integration.md). */
146
- hasDraft: boolean;
136
+ export interface RequestShape extends ExecutionRequirements {}
137
+
138
+ export type GenerationMechanism = "serial" | "continuous";
139
+
140
+ /** One immutable scheduling declaration for one exact request shape.
141
+ * `continuous` means scheduler admission, not that another row is present. */
142
+ export interface GenerationPlacement {
143
+ readonly shape: RequestShape;
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));
147
151
  }
148
152
 
149
153
  export class GenerationGateway {
154
+ #closed = false;
155
+ readonly #runtime: RuntimeConfig;
156
+ readonly #binding: MlxGatewayBinding;
157
+ readonly #requests: AdmissionPool;
150
158
  readonly #mutex = new AsyncMutex();
151
159
  readonly #batch: number;
152
- #scheduler: BatchScheduler | null = null;
160
+ #scheduler: MlxBatchGroup | null = null;
153
161
  #rowsSubmitted = 0;
154
162
  /** Serial-lane requests waiting for / holding the mutex. While > 0 the
155
163
  * scheduler pauses admission (drain) so they can't be starved. */
156
164
  #serialWaiters = 0;
157
165
  /** Lazy, memoized: can this model's caches do the dynamic-B ops? */
158
166
  #cacheBatchable: boolean | null = null;
159
- /** Lazy, memoized: can the server's kv scheme run under batching?
160
- * (Phase 3.1: kvConfig whose every configured layerIdx is a plain
161
- * full-attention KVCache — uniform kvBits and rotating-layer configs
162
- * stay serial. The scheme is a server-lifetime constant, so one memo.) */
167
+ /** Lazy, memoized: can the configured KV scheme convert every named cache? */
163
168
  #kvSchemeBatchable: boolean | null = null;
164
-
165
169
  constructor(
166
- private readonly model: RuntimeModel,
170
+ model: RuntimeModel | MlxGatewayBinding,
167
171
  batch: number,
168
172
  private readonly serialRun: SerialRun,
169
173
  private readonly opts: {
170
174
  kvBudgetBytes?: number;
175
+ memoryBudget?: MlxMemoryBudget;
176
+ checkpoints?: boolean;
177
+ stateCodecs?: import("../kv-store").CacheCodecProvider;
171
178
  /** The server-wide KV scheme (server.ts kvScheme) — threaded to the
172
179
  * scheduler at construction when batchable (Phase 3.1). turboQuant is
173
- * never threaded to the scheduler (always solo-only — see willBatch);
180
+ * never threaded to the scheduler (always solo-only — see placement);
174
181
  * it's here only so the gateway can see it's active. */
175
- kvScheme?: { kvBits?: number; kvConfig?: KvQuantSpec[]; turboQuant?: TurboQuantScheme };
182
+ kvScheme?: KvScheme;
176
183
  /** The server's prompt cache (Phase 3.2): batch-lane joiners take()
177
184
  * the longest usable prefix at admission; never-merged rows put()
178
185
  * back on finish. Safe to share with the serial lane — both use it
179
186
  * only inside this gateway's mutual-exclusion domain. */
180
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>;
181
191
  } = {},
182
192
  ) {
193
+ this.#binding = "createBatchGroup" in model ? model : bindMlxGateway(model);
194
+ this.#runtime = this.#binding.runtime;
195
+ this.opts = Object.freeze({ ...opts });
183
196
  this.#batch = Math.max(1, Math.floor(batch));
197
+ this.#requests = new AdmissionPool(this.#batch);
184
198
  }
185
199
 
186
200
  /** True if `--batch N` (N>1) is on (batchability is then per-request). */
@@ -210,7 +224,15 @@ export class GenerationGateway {
210
224
  * running/pending. activeRows/pendingRows alone MISS the serial lane —
211
225
  * a serial generation holds the mutex but shows zero rows. */
212
226
  get busy(): boolean {
213
- 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();
214
236
  }
215
237
 
216
238
  /** Resolves once the engine is idle (poll-based, ~`pollMs` granularity —
@@ -238,106 +260,22 @@ export class GenerationGateway {
238
260
  };
239
261
  }
240
262
 
241
- /** Cache-capability gate (mirrors mlx-lm server.py's all-caches-have-merge
242
- * check): the scheduler's dynamic-B ops (temporalView/merge/filter) exist
243
- * on KVCache, RotatingKVCache, and SSMCache (hybrid gated-DeltaNet models
244
- * — Qwen3.5 — batch via SSMCache.mergeRows/filter; MLX_BUN_BATCH_SSM=0 is
245
- * the kill switch back to serial routing). */
246
263
  #modelCachesBatchable(): boolean {
247
- if (this.#cacheBatchable === null) {
248
- // Tier-0 universal models: batchable for PLAIN full-attention archs
249
- // only. The 2026-07-03 uneven-row bug (scalar-offset RoPE decoded
250
- // padded rows at wrong positions) is fixed via
251
- // UniversalRope.applyDynamic + ropeOffsetArr and GATED token-exact vs
252
- // mlx-lm B=2 on Llama-3.2-3B (static + dynamic join/leave,
253
- // tests/batched-decode-parity.test.ts "Llama 3B Tier-0"). The
254
- // maskArray archs (gemma2-family: forwardLayers builds a pad-blind
255
- // causal mask) and sliding-window universal archs remain UNVALIDATED
256
- // cells → serial, per the per-model-cell discipline.
257
- if (this.model instanceof UniversalDenseModel) {
258
- const a = this.model.args;
259
- this.#cacheBatchable =
260
- !a.maskArray && !a.layerTypes?.includes("sliding_attention");
261
- return this.#cacheBatchable;
262
- }
263
- const ssmOk = process.env.MLX_BUN_BATCH_SSM !== "0";
264
- const proto = this.model.makeCache(); // fresh caches hold no buffers
265
- this.#cacheBatchable = proto.every(
266
- (c) =>
267
- c instanceof KVCache ||
268
- c instanceof RotatingKVCache ||
269
- isBatchableCache(c) ||
270
- (ssmOk && c instanceof SSMCache),
271
- );
272
- for (const c of proto) c.dispose();
273
- }
274
- return this.#cacheBatchable;
264
+ return this.#cacheBatchable ??= this.#binding.cachesBatchable();
275
265
  }
276
266
 
277
- /** Phase 3.1 + milestone 2: the server's kv scheme batches iff it is a
278
- * per-layer kvConfig (the L2 mixed scheme) whose every configured layer
279
- * is a full-attention KVCache (BatchedQuantDecodeMaskCache + quantized
280
- * merge/extend/filter) or a rotating RotatingKVCache
281
- * (BatchedRotatingQuantCache — milestone 2, gemma's kv_config). Uniform
282
- * kvBits (quantizedKvStart threshold semantics via the serial no-byLayer
283
- * path) and configs naming SSM layers stay serial. */
284
267
  #kvBatchable(): boolean {
285
- if (this.#kvSchemeBatchable === null) {
286
- const scheme = this.opts.kvScheme;
287
- if (!scheme || (!scheme.kvBits && !scheme.kvConfig?.length)) {
288
- // Only consulted when shape.kvQuant is SET: a kv-quant request with
289
- // no scheme threaded to the gateway cannot be applied by the
290
- // scheduler — batching it would SILENTLY DROP the quantization (the
291
- // exact composition bug optiq serve ships). Route serial.
292
- this.#kvSchemeBatchable = false;
293
- } else if (scheme.kvBits || !scheme.kvConfig?.length) {
294
- this.#kvSchemeBatchable = false; // uniform bits: serial
295
- } else {
296
- const proto = this.model.makeCache();
297
- this.#kvSchemeBatchable = scheme.kvConfig.every(
298
- (e) => proto[e.layerIdx] instanceof KVCache ||
299
- proto[e.layerIdx] instanceof RotatingKVCache,
300
- );
301
- for (const c of proto) c.dispose();
302
- }
303
- }
304
- return this.#kvSchemeBatchable;
268
+ return this.#kvSchemeBatchable ??= !!this.opts.kvScheme && this.#binding.kvBatchable(this.opts.kvScheme);
305
269
  }
306
270
 
307
- /** Decide whether a request joins the batch or runs serially. */
308
- willBatch(shape: RequestShape): boolean {
309
- // DiffusionGemma is non-autoregressive — the batch scheduler assumes the AR
310
- // KV-cache decode path, so it always runs serially through generate().
311
- if (this.model instanceof DiffusionGemmaModel) return false;
312
- if (!this.batchingEnabled) return false;
313
- if (!this.#modelCachesBatchable()) return false;
314
- // repetitionPenalty / logits extras (min_p, XTC, logit_bias,
315
- // presence/frequency penalties) batch: the per-row sampler folds in the
316
- // same makeSampler/makeLogitsProcessors closures the serial lane runs,
317
- // over a per-row device-side token history. (Load-bearing beyond opt-in
318
- // knobs: some models — Qwen3.5 — ship a default repetition penalty in
319
- // generation_config.json, which used to route EVERY request serial.)
320
- return (
321
- !shape.hasVision &&
322
- !shape.hasAdapters &&
323
- !shape.wantsLogprobs &&
324
- !shape.userSeed &&
325
- // Phase 3.1: a kv-quant request batches when the server's scheme is
326
- // the batchable kvConfig composition (the scheduler applies it);
327
- // otherwise it routes serial exactly as before.
328
- !(shape.kvQuant && !this.#kvBatchable()) &&
329
- // TurboQuant: solo-only in v1, unconditionally — never partially
330
- // batchable like kvConfig. The automatic instanceof exclusion in
331
- // #modelCachesBatchable() already covers this once a cache converts;
332
- // this is the explicit belt-and-braces refusal at request-shape time
333
- // (mirrors the kvQuant pattern above — both layers exist on purpose).
334
- !shape.turboQuant &&
335
- !shape.hasDraft &&
336
- // Grammar: B1 makes it batchable (per-row matchers) unless the kill
337
- // switch forces serial. MLX_BUN_GRAMMAR_BATCH=0 = B0 behavior (serial),
338
- // the A/B + kill lever for the new code, house style.
339
- !(shape.hasGrammar && process.env.MLX_BUN_GRAMMAR_BATCH === "0")
340
- );
271
+ place(shape: RequestShape, options: GenerateOptions = {}): GenerationPlacement {
272
+ const frozenShape = Object.freeze(shape);
273
+ const execution = this.#binding.plan(frozenShape, options, {
274
+ continuous: this.batchingEnabled,
275
+ quantizedBatch: shape.kvQuant && this.#kvBatchable(),
276
+ checkpoints: this.opts.checkpoints === true,
277
+ });
278
+ return Object.freeze({ shape: frozenShape, mechanism: execution.mechanism, execution });
341
279
  }
342
280
 
343
281
  /** Run `fn` with exclusive ownership of the GPU + shared model state (the
@@ -346,19 +284,37 @@ export class GenerationGateway {
346
284
  * mount/unmount all come through here, so nothing runs concurrently with
347
285
  * batched decode steps (or with each other). Registers as a serial waiter
348
286
  * so the scheduler drains (stops admitting) until `fn` has run. */
349
- async runExclusive<T>(fn: () => Promise<T>): Promise<T> {
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");
350
293
  this.#serialWaiters++;
351
- try {
352
- const release = await this.#mutex.acquire();
353
- try {
354
- return await fn();
355
- } finally {
356
- release();
357
- }
358
- } finally {
294
+ const closeAdmission = trace?.begin("engine.admission_wait", { mechanism: "serial" });
295
+ const resume = () => {
359
296
  this.#serialWaiters--;
360
- if (this.#serialWaiters === 0) this.#scheduler?.kick(); // resume admission
361
- }
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
+
311
+ async runExclusive<T>(
312
+ fn: () => Promise<T>,
313
+ trace?: PromptResponseTrace,
314
+ signal?: AbortSignal,
315
+ ): Promise<T> {
316
+ const lease = await this.acquireExecutionLease(signal, trace);
317
+ try { return await fn(); } finally { lease.dispose(); }
362
318
  }
363
319
 
364
320
  /** Run one generation on the appropriate lane. onToken is invoked per emitted
@@ -369,17 +325,41 @@ export class GenerationGateway {
369
325
  onToken: OnToken,
370
326
  vision: Vision | undefined,
371
327
  shape: RequestShape,
328
+ placement: GenerationPlacement,
372
329
  signal?: AbortSignal,
330
+ trace?: PromptResponseTrace,
373
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);
350
+ if (placement.shape !== shape) {
351
+ disposeUnstarted();
352
+ throw new Error("generation placement does not belong to this request shape");
353
+ }
374
354
  try {
375
355
  signal?.throwIfAborted();
376
356
  } catch (e) {
377
357
  // The gateway is the first component that accepts ownership of a
378
358
  // compiled controller. An already-aborted request reaches neither lane.
379
- options.grammar?.dispose();
359
+ disposeUnstarted();
380
360
  throw e;
381
361
  }
382
- if (!this.willBatch(shape)) {
362
+ if (placement.mechanism === "serial") {
383
363
  // Serial decode is an unbroken microtask chain (FFI + generator
384
364
  // resumes): without a periodic macrotask hop the event loop starves for
385
365
  // the WHOLE generation — /stats, /health, and new-connection accepts
@@ -391,88 +371,69 @@ export class GenerationGateway {
391
371
  let lastHop = performance.now();
392
372
  const hop = (): Promise<void> => new Promise<void>((r) => setImmediate(r));
393
373
  const hoppingOnToken: OnToken = (token, lp) => {
394
- if (signal?.aborted) return false;
374
+ signal?.throwIfAborted();
395
375
  const r = onToken(token, lp);
396
376
  if (r === false) return false;
397
377
  if (performance.now() - lastHop < 25) return r;
398
378
  lastHop = performance.now();
399
379
  if (r instanceof Promise)
400
- return r.then((v) =>
401
- v === false || signal?.aborted ? false : hop().then(() => signal?.aborted ? false : v),
402
- );
403
- 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
+ });
404
391
  };
405
- return this.runExclusive(async () => {
392
+ let started = false;
393
+ return Promise.resolve().then(() => this.opts.beforeSerial?.()).then(() => this.runExclusive(async () => {
406
394
  signal?.throwIfAborted();
407
- const stats = await this.serialRun(promptIds, options, hoppingOnToken, vision);
395
+ started = true;
396
+ const stats = await this.serialRun(
397
+ promptIds, signal ? { ...options, signal } : options, hoppingOnToken, vision, trace, placement.execution,
398
+ );
408
399
  signal?.throwIfAborted();
409
400
  return stats;
410
- })
401
+ }, trace, signal))
411
402
  // Covers a serial waiter aborted before serialRun takes ownership.
412
403
  // generate() also disposes defensively; the operation is idempotent.
413
- .finally(() => options.grammar?.dispose());
404
+ .finally(() => {
405
+ if (started) options.grammar?.dispose();
406
+ else disposeUnstarted();
407
+ });
414
408
  }
415
409
 
416
- // Per-row sampler, mirroring generate()'s sampleStep: logits processors
417
- // (logit_bias / repetition / presence / frequency penalties — the same
418
- // device-side closures the serial lane runs) fold in over a per-row token
419
- // history seeded with the prompt, then the sampler. Greedy (temperature 0)
420
- // is argmax; temp>0 uses this request's own seed → independent per-row RNG.
421
- const sampler = makeSampler(options);
422
- const processors = makeLogitsProcessors(options);
423
- // Device-side history (prompt + generated), maintained only when a
424
- // processor needs it — generate()'s pushHistory, one per row. The sample
425
- // closure is called once per sampled token in order, so extending it
426
- // inside the closure keeps history exact.
427
- let history: MlxArray | null = null;
428
- if (processors.length > 0) history = ops.fromInt32(promptIds, [promptIds.length]);
429
- const sample = (logits1V: MlxArray, step: number): MlxArray => {
430
- let cur = logits1V; // caller-owned; only intermediates are disposed here
431
- for (const p of processors) {
432
- const next = p(history, cur);
433
- if (cur !== logits1V) cur.dispose();
434
- cur = next;
435
- }
436
- // Grammar mask (B1): after the standard processors, before the sampler —
437
- // same "grammar has the final say" ordering as serial sampleStep.
438
- // Precondition: the scheduler has awaited ready() before invoking this
439
- // closure (the mask for this step is materialized). applyMask is sync.
440
- if (options.grammar) {
441
- const masked = options.grammar.applyMask(cur);
442
- if (cur !== logits1V) cur.dispose();
443
- cur = masked;
444
- }
445
- const lp = toLogprobs(cur);
446
- if (cur !== logits1V) cur.dispose();
447
- const tok = sampler(lp, step);
448
- lp.dispose();
449
- if (history) {
450
- const t1 = ops.reshape(tok, [1]);
451
- const prev = history;
452
- history = ops.concatAxis([prev, t1], 0);
453
- prev.dispose();
454
- t1.dispose();
455
- }
456
- return tok;
457
- };
410
+ // The scheduler owns grammar ready/accept sequencing. StepSampler owns the
411
+ // shared processors -> mask -> logprobs -> sample -> history contract.
412
+ const stepSampler = makeStepSampler(options, {
413
+ tokenRepresentation: "device",
414
+ grammarWait: "external",
415
+ historyUpdate: "after-sample",
416
+ initialHistory: promptIds,
417
+ });
418
+ const sample = (logits1V: MlxArray, step: number): MlxArray =>
419
+ stepSampler.sample(logits1V, step).token;
458
420
 
459
421
  let st;
460
422
  this.#rowsSubmitted++;
423
+ const closeAdmission = trace?.begin("engine.admission_wait", {
424
+ mechanism: "continuous",
425
+ });
461
426
  try {
462
427
  st = await this.#ensureScheduler().submit({
463
428
  promptIds,
429
+ compiledDecode: placement.execution?.compiledDecode,
464
430
  maxTokens: options.maxTokens ?? 512,
465
- eosTokenIds: options.eosTokenIds ?? this.model.config.eosTokenIds,
431
+ eosTokenIds: options.eosTokenIds ?? this.#binding.config.eosTokenIds,
466
432
  sample,
467
- // Vectorized-sampling eligibility: mirrors makeSampler's greedy branch
468
- // (temperature 0, no curve override) with nothing per-row in the way
469
- // (no processors, no grammar mask).
470
- plainGreedy:
471
- (options.temperature ?? 0) === 0 &&
472
- !options.curve &&
473
- processors.length === 0 &&
474
- !options.grammar,
433
+ plainGreedy: stepSampler.isPlainGreedy,
475
434
  onToken,
435
+ onAdmitted: closeAdmission,
436
+ trace,
476
437
  ...(signal ? { signal } : {}),
477
438
  // B1: pass the per-row grammar controller through. The scheduler drives
478
439
  // accept/ready/terminate; this gateway OWNS disposal (finally below)
@@ -483,8 +444,8 @@ export class GenerationGateway {
483
444
  ...(options.snapshotAt !== undefined ? { snapshotAt: options.snapshotAt } : {}),
484
445
  });
485
446
  } finally {
486
- history?.dispose();
487
- history = null;
447
+ closeAdmission?.();
448
+ stepSampler.dispose();
488
449
  // The scheduler never owns per-row grammar state.
489
450
  options.grammar?.dispose();
490
451
  }
@@ -493,21 +454,28 @@ export class GenerationGateway {
493
454
  promptTokens: st.promptTokens,
494
455
  cachedTokens: st.cachedTokens,
495
456
  generatedTokens: st.generatedTokens,
496
- 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,
497
462
  cacheTokens: [],
498
463
  };
499
464
  }
500
465
 
501
- #ensureScheduler(): BatchScheduler {
466
+ #ensureScheduler(): MlxBatchGroup {
502
467
  if (!this.#scheduler)
503
- this.#scheduler = new BatchScheduler(this.model, {
468
+ this.#scheduler = this.#binding.createBatchGroup({
469
+ runtime: this.#runtime,
504
470
  maxBatch: this.#batch,
471
+ stateCodecs: this.opts.stateCodecs,
505
472
  kvBudgetBytes: this.opts.kvBudgetBytes,
473
+ memoryBudget: this.opts.memoryBudget,
506
474
  // Phase 3.1: the batchable kvConfig composition is applied by the
507
475
  // scheduler (solo rows convert at serial chunk boundaries, then
508
476
  // merge as quantized triples). Only threaded when the scheme
509
477
  // passed #kvBatchable — otherwise those requests never reach here.
510
- kvConfig: this.#kvBatchable() ? this.opts.kvScheme?.kvConfig : undefined,
478
+ kvScheme: this.#kvBatchable() ? this.opts.kvScheme : undefined,
511
479
  promptCache: this.opts.promptCache,
512
480
  lock: { acquire: () => this.#mutex.acquire() },
513
481
  // Drain: pause admission while any serial-lane request waits; the