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
@@ -1,529 +0,0 @@
1
- # Batching v2 — state map, debt, and the gap plan
2
-
3
- Status: **PLAN, largely EXECUTED** (written 2026-07-01 as a read-only
4
- audit; §1's state map is that day's snapshot). Landed since: steps 1–3
5
- (hotfix bundle, failure/fairness, engine hygiene — 2026-07-01); step 8
6
- SSM batched path (2026-07-02, superseded by batching-perf-path.md P5 —
7
- token-exact vs mlx-lm B=2); step 5 KV-budget admission (2026-07-03,
8
- executed as grammar-spec-batching-integration.md Phase D:
9
- `--kv-budget`, queue-don't-OOM, `/stats.batch` fields,
10
- tests/batch-kv-budget.test.ts — the `--prompt-concurrency` half still
11
- open); step 7's e1+e3 (min_p/XTC + penalties/logit_bias batch via
12
- per-row logits processors, 2026-07-02, batching-perf-path.md header
13
- item 2; e2 logprobs still routes serial). Still open: step 4 (extend
14
- join), step 6 (prompt-cache reuse), 7's e2, 9 (rolling), 10 (deferred).
15
- Scope: the `--batch N` continuous-batching engine (`src/serve/batch-scheduler.ts`,
16
- `src/serve/generation-gateway.ts`, `src/model/batched-mask.ts`,
17
- `src/model/batched-rotating.ts`, the `server.ts` wiring).
18
- Predecessor docs: [parallel-slots.md](parallel-slots.md) (the design, still
19
- accurate on the primitives), PLAN.md Phase 18 (history),
20
- [server-config.md](../reference/server-config.md) (user-facing modes doc).
21
-
22
- Mandate (Josh): batching is "kinda implemented, but is not great… make sure we
23
- have all the code really well done, and documented and exposed via the flags."
24
-
25
- **Standing decisions honored throughout (do not relitigate):**
26
- - `--batch N` is a **mode switch**, not a load-dependent fallback. Josh decided
27
- this explicitly (determinism: results must not depend on concurrency, and the
28
- drop-in-for-`mlx_lm.server` promise needs a predictable path). Auto-batching
29
- "when >1 request arrives" was considered and **rejected** — an idle-vs-loaded
30
- server would produce different numerics for the same request. This plan keeps
31
- `--batch` as the UX and documents the rejection.
32
- - The batched lane is **compat mode** (bit-parity with mlx-lm B=N, bf16, no
33
- perf flags) — see server-config.md "compat mode" section. All work below
34
- preserves that guarantee; anything that can't be oracle-gated is L3 and
35
- deferred (item f).
36
-
37
- **In-flight work this plan routes around (2026-07-01):**
38
- - **Same-ms seed collision fix** — `server.ts:1120` defaults
39
- `seed: req.seed ?? (Date.now() & 0xffffffff)`; two batched rows admitted in
40
- the same millisecond sample identical RNG streams. A fix is IN FLIGHT by
41
- another agent. Noted as context; **not planned here**.
42
- - **`src/model/gemma4-base.ts` is modified in another agent's working tree**
43
- (and `src/model/universal/` is a new in-flight subsystem). Items below that
44
- would naturally touch cache classes are designed to live in NEW files
45
- (`BatchedKVCache` beside `BatchedRotatingCache`) precisely so they don't
46
- collide; coordinate before touching gemma4-base.ts.
47
-
48
- ---
49
-
50
- ## 1. State map (verified against code, not docs)
51
-
52
- ### 1.1 Architecture as built
53
-
54
- ```
55
- handler (server.ts) GenerationGateway (generation-gateway.ts)
56
- promptIdsFor → options → RequestShape ──▶ willBatch(shape)?
57
- onToken closure (StopMatcher + ├─ no → AsyncMutex → serialRun
58
- tool router + thinking splitter + │ (= runGeneration: PromptCache
59
- SSE send / collector) │ take/put + generate())
60
- └─ yes → BatchScheduler.submit
61
- (batch-scheduler.ts)
62
- BatchScheduler #drive loop (detached, Bun-async, no threads):
63
- acquire mutex (held first-admit → batch-empty)
64
- ├─ #admit each pending row: SOLO prefill (one forward, whole prompt)
65
- │ → sample token 0 → re-MERGE whole batch per layer
66
- │ (full layers: temporalView + per-row slice + mergeKVRows → new KVCache;
67
- │ rot layers: BatchedRotatingCache.merge)
68
- ├─ #step: forward [B,1] (full layers wrapped per-step in
69
- │ BatchedDecodeMaskCache; rot caches persistent), per-row sample on
70
- │ [1,V] slices, SYNC readback [B], clearCache()
71
- ├─ #emit per row (EOS → "stop" w/o onToken; onToken false → "stop";
72
- │ generated ≥ maxTokens → "length")
73
- └─ #applyFilter(keep): full layers filterKVRows → new KVCache;
74
- rot layers filter() in place
75
- ```
76
-
77
- - **Scheduler loop shape**: iteration-level (continuous) scheduling — admit
78
- between steps, evict after each step. Correct design; the loop-body
79
- *mechanics* carry the debt (§2).
80
- - **Admission today**: count-only — `#running.length < #maxBatch`
81
- (batch-scheduler.ts:138). No byte projection, no KV budget. The server's
82
- per-request `context_over_budget` 400 (server.ts:~1780, from `fit()`) checks
83
- each request against **single-sequence** max-safe-context; N rows can jointly
84
- exceed the budget (documented as limitation #2 in server-config.md).
85
- - **Eviction**: per-step `#applyFilter` (batch-scheduler.ts:327-355) —
86
- full-attention layers rebuild a fresh `KVCache` from `filterKVRows`
87
- (takeAxis on B); rotating caches filter in place including padding
88
- reduction. Matches mlx-lm `filter` semantics (cache.py:1016-1033) except
89
- ours doesn't do the `min_left_pad` shift-left for full layers (mlx-lm
90
- reduces padding after filter; ours keeps stale left pad until the next
91
- merge — correct but wasteful width).
92
- - **Join**: `#admit` re-merges the ENTIRE batch (batch-scheduler.ts:193-254):
93
- per layer, temporalView of the running batch, B per-row slices, re-concat
94
- with the new row. O(B·S) traffic per join **plus** ~2B slice kernels per
95
- layer. mlx-lm instead `extend`s (cache.py:1035-1078): pad self+other once,
96
- one concat — and prefills joiners as a right-padded *batch* (see item a/d).
97
-
98
- ### 1.2 The `willBatch` gate — exact predicate today
99
-
100
- `generation-gateway.ts:113-127`: batches iff **all** of
101
-
102
- ```
103
- batch > 1
104
- AND model is not DiffusionGemmaModel (non-AR)
105
- AND !hasVision (offset-0 prefill + bidir mask)
106
- AND !hasAdapters (loraState is one global field)
107
- AND !hasRepetitionPenalty (no per-row logits processors)
108
- AND !hasLogitsExtras (min_p ∥ xtc_probability ∥ logit_bias
109
- ∥ presence ∥ frequency — one family gate)
110
- AND !wantsLogprobs (non-stream logprobs/top_logprobs capture)
111
- AND !userSeed (body.seed !== undefined)
112
- AND !kvQuant (request carries kvConfig/kvBits —
113
- i.e. any explicit --kv-quant)
114
- ```
115
-
116
- `RequestShape` is built **twice**, near-identically: chat (server.ts:1813-1826)
117
- and completions (server.ts:2085-2096, `hasVision: false`). mlx-lm's gate for
118
- comparison (server.py:371-374, 685-686): `draft_model is None AND all caches
119
- hasattr 'merge' AND args.seed is None`. **We have no cache-capability check**
120
- — see debt D1.
121
-
122
- ### 1.3 Which caches support which dynamic-B ops
123
-
124
- | Cache | merge | filter | extend | extract | batched mask | verified vs |
125
- | --- | --- | --- | --- | --- | --- | --- |
126
- | `KVCache` (full attn) | ✅ `mergeKVRows` (batched-mask.ts:171) | ✅ `filterKVRows` (:196) | ❌ (join = re-merge) | ✅ implicit (per-row slice in `#admit`) | ✅ `BatchedDecodeMaskCache` wrapper | mlx-lm `BatchKVCache` dynamic golden (gated) |
127
- | `RotatingKVCache` (sliding) | ✅ `BatchedRotatingCache.merge` (batched-rotating.ts:273) | ✅ `.filter` (:233) | ❌ | ✅ `.temporalView` (:207) | ✅ built-in `makeMask` incl. ring-roll | mlx-lm `BatchRotatingKVCache` model-free golden (FAST, incl. ring-wrap) |
128
- | `SSMCache` (Qwen3.5 gated-DeltaNet, qwen3-delta.ts:184) | ❌ | ❌ | ❌ | ❌ (no `temporalView`) | ❌ (`makeMask` returns empty; comment says "B=1 single-stream") | — |
129
- | `QuantizedKVCache` / rotating-quant | n/a — excluded by the `kvQuant` gate (bf16-only v1) | | | | | no oracle exists (L2/L3, item f) |
130
-
131
- **⇒ D1 (bug): a Qwen3.5 server with `--batch N` errors every batched
132
- request.** The scheduler's constructor classifies `SSMCache` as `"full"`
133
- (batch-scheduler.ts:105, `instanceof RotatingKVCache ? "rot" : "full"`), then
134
- `#admit` casts it to `KVCache` and calls `temporalView()`
135
- (batch-scheduler.ts:200-201) → TypeError; `#drive` catches and rejects the row,
136
- so **every** batched request 500s. `willBatch` has no family/capability gate
137
- (only DiffusionGemma is excluded). And **server-config.md:151-153 claims the
138
- opposite** ("full-attention (CPM), sliding-window (Gemma), and hybrid
139
- gated-DeltaNet (Qwen3.5) — all batch"). The doc claim is false today. Note an
140
- oracle for the real fix exists: mlx-lm's `ArraysCache` implements
141
- `filter/extend/extract/finalize/merge` (cache.py:632-702), which is how mlx-lm
142
- batches Mamba-family state.
143
-
144
- ### 1.4 Test coverage matrix
145
-
146
- | Behavior | Test | Tier | Gate quality |
147
- | --- | --- | --- | --- |
148
- | decode mask math (`buildBatchedDecodeMask`), `mergeKVRows`/`filterKVRows` pure | `tests/batched-decode-mask.test.ts` | FAST (no model) | unit, exact |
149
- | rotating merge/decode/mask/filter incl. **ring-wrap** | `tests/batched-rotating.test.ts` | FAST (fixture) | **oracle-gated** — replays `scripts/gen-rotating-golden.py` vs mlx-lm `BatchRotatingKVCache`, per-step mask + keys |
150
- | batched prefill+decode forward, 4 models | `tests/batched-decode-parity.test.ts` | GATED `MLX_BUN_TEST_BATCH_DECODE=1` + weights | **oracle-gated** — bit-exact vs mlx-lm B=2 goldens (`scripts/gen-batched-golden.py`) |
151
- | dynamic-B join/leave ({A,B}→+C→−A) | same file, CPM | GATED | **oracle-gated** — token-for-token vs `scripts/gen-batched-dynamic-golden.py` (mlx-lm merge/extract/filter) |
152
- | scheduler orchestration (admit/step/evict/join) | `tests/batch-scheduler.test.ts` | GATED | **KL-gated** teacher-forced (tol 1e-2) CPM; Gemma 12B greedy vs mlx-lm B=2 golden |
153
- | server wiring: Option-B engage, `/stats`, concurrent completion, SSE fan-out, serial-lane coexist (user-seed drain, no deadlock) | `tests/batch-serving.test.ts` | GATED (live in-process server) | **smoke** — coherence assertions, no numeric oracle |
154
- | prefill batched mask (training side) | `tests/train-batch{,-e2e}.test.ts` | FAST / gated | oracle (per-row loss == solo) |
155
-
156
- **Not tested anywhere:** row-failure containment (one row's error vs the
157
- batch), serial-lane starvation/fairness under sustained batchable load,
158
- KV admission (doesn't exist), batched Qwen3.5 (would fail, §1.3),
159
- ring-wrap **end-to-end** on a real Gemma forward (model-free math is gated;
160
- the long-context forward golden is the known open L1 follow-up),
161
- batched-lane usage-timing stats (currently hardwired zeros).
162
-
163
- ### 1.5 What the flags/docs currently say (accurate except one claim)
164
-
165
- `--batch <n>` (cli.ts:750-757; `--decode-concurrency` accepted as alias),
166
- `--kv-quant` interplay (Option B: unset+batch ⇒ bf16; explicit ⇒ those requests
167
- serial, warned at server.ts:1065-1070), `/stats.batch`
168
- {configured, batched, active_rows} (server.ts:1437-1441). server-config.md's
169
- "Execution modes", lane-picker table, compatibility matrix, compat-mode
170
- rationale, and known-limitations list are **verified accurate against code**
171
- — except the Qwen3.5 batching claim (D1) and it doesn't mention the curve/
172
- signal endpoints' lock bypass (D3, which is a bug to fix, not to document).
173
-
174
- ---
175
-
176
- ## 2. Code-quality assessment
177
-
178
- **Verdict:** the *primitives* are excellent — oracle-gated, well-commented,
179
- disposal-disciplined (`batched-mask.ts`, `batched-rotating.ts` are among the
180
- cleanest files in the repo). The *orchestration* (`batch-scheduler.ts`,
181
- ~360 lines, readable) is correct-by-gate but **naive as an engine**: it has
182
- none of the serial loop's hard-won loop hygiene, its failure domain is the
183
- whole batch, and the server grew a second mutual-exclusion domain that two
184
- endpoints silently don't participate in. Specific debt, ranked:
185
-
186
- - **D1 — Qwen3.5 batched = guaranteed request failure + false doc claim.**
187
- batch-scheduler.ts:105 + :200-201 vs qwen3-delta.ts:184 (`SSMCache`, no
188
- `temporalView`); server-config.md:151-153. Fix now: a cache-capability gate
189
- (mirror mlx-lm server.py:371-374) in `willBatch` — batchable iff every
190
- `makeCache()` element is `KVCache | RotatingKVCache` — plus the doc
191
- correction. The real SSM batch path is item (h), later. *(confidence 95)*
192
- **RESOLVED**: gate landed 2026-07-01; the full SSM batch path landed
193
- 2026-07-02 (batching-perf-path.md P5) — server-config's Qwen3.5 claim is
194
- true again.
195
-
196
- - **D2 — `#admit` materializes full-prompt logits.**
197
- batch-scheduler.ts:173-174 runs `logitsFromHidden(h)` on the **whole**
198
- `[1,Lp,H]` hidden, then slices the last position. The lm_head matmul
199
- computes `[1,Lp,V]` at eval — at Gemma's V=262k and an 8k prompt that is a
200
- ~4.3 GB bf16 transient *per admit* (plus the wasted GEMM). The serial path
201
- slices `hLast` FIRST (generate.ts:466-468) precisely to avoid this. One-line
202
- reorder + the gated suites re-run. *(confidence 95)*
203
-
204
- - **D3 — two mutual-exclusion domains; curve `/generate` and `/signal` bypass
205
- the gateway.** The old serialized queue (`enqueue`, server.ts:946-951) used
206
- to be THE GPU lock; generation moved to the gateway's `AsyncMutex`, but
207
- `/generate` (curve designer, server.ts:1663 — `enqueue(() =>
208
- runGeneration(...))`) and `/signal` (server.ts:1608 — `enqueue` + raw
209
- `model.forward`) still run GPU work (and `PromptCache.take/put`) under
210
- `enqueue` only ⇒ they can run **concurrently with batched decode steps**
211
- (GPU contention + the prompt-cache "single owner" invariant from
212
- parallel-slots.md broken). Adapter mount/unmount (server.ts:1578, 1589) sit
213
- on `enqueue` too — mount is registry-level, but unmount disposing adapter
214
- arrays concurrently with a serial-lane generation using them is the same
215
- class of race. The `runGeneration` doc comment "Must be called inside the
216
- queue" (server.ts:960) is stale. Fix: one lock — expose
217
- `gateway.runExclusive(fn)` and route curve/signal/adapter-unmount through
218
- it; fix the comment. *(confidence 90 on curve//signal; 80 on unmount)*
219
-
220
- - **D4 — the batched loop lacks the serial loop's pipelining + clearCache
221
- cadence** (= reports/kernel-perf-review-2026-07-01.json backlog #6, area 7
222
- verdict). Three sub-items, all in `#step`/`#admit`:
223
- 1. **No `asyncEval` pipelining**: `#step` builds one step's graph then
224
- synchronously reads all B tokens (`toFloat32`, batch-scheduler.ts:286)
225
- — every step eats a full dispatch+readback bubble. Serial overlaps step
226
- n's readback with step n+1's compute (generate.ts:482, 539); mlx-lm's
227
- `GenerationBatch._step` does exactly this batched (`mx.async_eval`
228
- next, `mx.eval` current — generate.py:1367-1373).
229
- 2. **`clearCache()` EVERY step** (batch-scheduler.ts:293) — trashes the
230
- buffer pool each token, so every step re-allocates its transients.
231
- Serial clears every 256 tokens (generate.ts:571); mlx-lm batched every
232
- 512 steps (generate.py:1778-1779).
233
- 3. **No prefill chunking in `#admit`** — the whole prompt goes through one
234
- forward (batch-scheduler.ts:172), so a long prompt is an unbounded
235
- transient (serial chunks at `prefillChunkSize` with per-chunk eval +
236
- clearCache, generate.ts:440-456; mlx-lm at `prefill_step_size=2048`).
237
- Worse, admits run to completion before the next `#step`, so **every
238
- running row stalls for the entire prefill of every joiner**
239
- (head-of-line blocking; mlx-lm interleaves one decode step with one
240
- prefill chunk per loop iteration — generate.py:1760-1844).
241
- *(confidence 90; perf claims need bench numbers per house rules)*
242
-
243
- - **D5 — whole-batch failure domain.** Any throw in `#step` — including a
244
- row's own `onToken` (awaited inline, batch-scheduler.ts:299; an SSE
245
- controller error qualifies) — is caught in `#drive` (:153-158) and
246
- **rejects every row and drops the entire batch KV**. mlx-lm removes the
247
- offending uid and keeps the batch (server.py:913-919). Also: `#admit` has
248
- no try/finally around the prefill, so `solo` caches + `ids` leak if the
249
- forward throws (:170-173); onToken should be wrapped per-row so one
250
- consumer's exception evicts one row. *(confidence 90)*
251
-
252
- - **D6 — serial-lane starvation under sustained batchable load.** The
253
- scheduler holds the mutex while `running ∪ pending ≠ ∅` and **keeps
254
- admitting** new batchable arrivals (batch-scheduler.ts:135-145); a queued
255
- serial-lane request (vision, adapter, seed…) waits for the batch to empty,
256
- which under continuous traffic is *never*. mlx-lm's drain sets
257
- `drain_batch=True` and **stops admitting** until the incompatible request
258
- has run (server.py:826-830). Fix: gateway signals "serial waiter present" →
259
- scheduler pauses admission (finish running rows, release, FIFO mutex hands
260
- over, then resume). Single-user appliance softens this, but agent fan-out
261
- is exactly the batching use case, so it's real. *(confidence 85)*
262
-
263
- - **D7 — zeroed stats + duplicated request-shape/sampling logic.**
264
- - Batched stats hardwire `prefillMs/decodeMs/…Tps = 0`
265
- (generation-gateway.ts:170) → SSE `usage` timing and any logging report
266
- zeros for batched requests; `cachedTokens` 0 is documented, the timings
267
- are just missing accounting.
268
- - `RequestShape` construction duplicated chat vs completions
269
- (server.ts:1813 vs 2085) — extract one `shapeFor()` helper.
270
- - `maxTokens` fallback drift: gateway `?? 512` (generation-gateway.ts:160),
271
- chat finish `?? 1024` (server.ts:1904), completions finish `?? 512`
272
- (server.ts:2098) — all dead in practice (server defaults 65_536 at
273
- :1116) but a trap for library callers; unify.
274
- - The `#emit` EOS/length/onToken contract mirrors `generate()` by comment
275
- only (batch-scheduler.ts:306-316) — drift risk; a shared contract test
276
- (same fixture through both lanes) would pin it.
277
- - Full-layer `filter` keeps stale left-padding (no `min_left_pad`
278
- shift-left, cf. cache.py:1027-1033) — width waste, not correctness.
279
-
280
- None of this contradicts the gates that exist: the numerics ARE oracle-proven.
281
- The debt is engine mechanics, failure domains, and the second lock.
282
-
283
- ---
284
-
285
- ## 3. Gap plan (implementation-ready, per item)
286
-
287
- ### (a) `extend` join op — stop re-merging the running batch
288
-
289
- **Port target:** mlx-lm `BatchKVCache.extend` (cache.py:1035-1078) and
290
- `BatchRotatingKVCache.extend` (cache.py:1369-1416). Semantics: right-justify
291
- both caches to a common `_idx`/width (one `mx.pad` each), concat on B, sum
292
- left-padding adjustments. One pad+concat per layer per join, vs today's
293
- per-row slice storm + full rebuild.
294
-
295
- **Design:** introduce **`BatchedKVCache`** (new file or in batched-mask.ts) —
296
- the full-attention twin of `BatchedRotatingCache`: owns
297
- `keys/values/leftPad/offset`, methods `merge` (subsumes `mergeKVRows`),
298
- `filter` (subsumes `filterKVRows` + adds the min-left-pad shift),
299
- **`extend`**, `extract(row)` (the per-row slice `#admit` currently inlines),
300
- `makeMask` + `ropeOffsetArr` (subsumes the per-step `BatchedDecodeMaskCache`
301
- wrapper rebuild and the scheduler's shared `#fullLeftPad` field —
302
- batch-scheduler.ts:93, a state smell). `BatchedRotatingCache.extend` follows
303
- the same pad-to-common-idx recipe in temporal order (both sides are kept
304
- temporal by our merge, so no dynamic-roll needed for the N=1-update scope —
305
- verify against the golden, don't assume). Scheduler `#admit` becomes:
306
- solo-prefill → `inners[layer].extend(soloRow)` — no re-merge, no wrapper churn.
307
- **Does not touch gemma4-base.ts** (in-flight ownership).
308
-
309
- **Oracle:** extend `scripts/gen-batched-dynamic-golden.py` to drive the join
310
- through mlx-lm's real `extend` (it already drives merge/extract/filter);
311
- assert the same {A,B}→+C→−A per-row trajectories token-for-token, CPM L1 +
312
- a Gemma variant for the rotating extend. Existing gated suites must stay
313
- green unchanged (numerically `extend ≡ re-merge`; the golden proves it).
314
-
315
- **Effort: M.**
316
-
317
- ### (b) Prompt-cache reuse under batching
318
-
319
- **mlx-lm behavior (the model to follow):** on insert,
320
- `prompt_cache.fetch_nearest_cache` → pass the pre-warmed per-sequence cache
321
- into the batch (server.py:751-767); on row finish, extract the row's cache and
322
- `insert_cache` it back keyed by all tokens (server.py:900-907).
323
-
324
- **Design (parallel-slots.md's sole-owner rule, kept):** all `PromptCache`
325
- take/put happen **inside the scheduler loop** (plus the serial lane, which
326
- already does — the two are mutex-exclusive, and D3's fix closes the remaining
327
- holes).
328
- - `#admit`: `promptCache.take(promptIds, "")` (batched lane is always
329
- adapter-less, so namespace ""); on hit, the entry's caches ARE the solo
330
- caches — forward only the uncached tail (the trim/`cachedTokens` logic
331
- exists in generate.ts; lift the prefix-trim helper rather than reimplement —
332
- memory: eval-reuse-serving-prompt-path). `BatchStats.cachedTokens` becomes
333
- real.
334
- - Row finish (`#finish`) or eviction: `extract(row)` (from item a) →
335
- `restoreState` into fresh solo `KVCache`/`RotatingKVCache`s →
336
- `promptCache.put(rowTokens, caches, "")`. Cost: O(S_row) copy per finished
337
- row, off the step hot path (do it after `#applyFilter`).
338
- - Byte interplay with (c): mirror mlx-lm's
339
- `prompt_cache.trim_to(total − activeBatchBytes)` (server.py:795-799) so the
340
- LRU + live batch share one ceiling.
341
-
342
- **Gate:** batch-serving test asserting `cached_tokens > 0` on an identical
343
- repeat under `--batch 2`; parity assertion that a warm-start row's trajectory
344
- equals the cold row's (same-B, bit-exact — prefix KV is identical by
345
- construction); serial suite untouched.
346
-
347
- **Effort: M–L** (the prefix-trim + rotating-cache restore corners are where
348
- the time goes; RotatingKVCache entries past the window are non-trimmable —
349
- mirror the serial path's existing handling).
350
-
351
- ### (c) KV-budget admission (B×S_max) + `--prompt-concurrency`
352
-
353
- **Fact-check first:** mlx-lm does NOT byte-budget admission — it caps counts
354
- (`--decode-concurrency` 32, `--prompt-concurrency` 8), optionally swaps KVCache
355
- → RotatingKVCache via `max_kv_size`, and byte-trims only the *prompt cache*
356
- against `--prompt-cache-bytes` minus live batch bytes. So byte-budget admission
357
- is OUR addition (rung 2 of parallel-slots.md), consistent with the byte-capped
358
- `PromptCache` precedent and the existing `fit()` admission (server.ts:1012).
359
-
360
- **Design:**
361
- - Projected row bytes = Σ_layers 2·bytes_bf16(H_kv·D·S_cap) where
362
- S_cap = min(promptLen+maxTokens, window) for rotating layers and
363
- promptLen+maxTokens for full layers (a `projectKvBytes(model, promptLen,
364
- maxTokens)` helper next to `fit()`, which already knows per-layer shapes).
365
- - Budget = explicit `--kv-budget <GB>` else derived: `fit()`'s usable bytes −
366
- weights − prompt-cache cap − headroom. Admission: admit while
367
- Σ projected(running+candidate) ≤ budget; otherwise leave in `#pending`
368
- (FIFO — no reordering, keeps fairness and determinism of arrival order).
369
- `maxTokens` default 65_536 makes naive projection admit ~1 row — so
370
- projection uses a decode-allowance term (e.g. min(maxTokens, 4096) beyond
371
- prompt) with the true backstop being re-check-on-growth: on each STEP-chunk
372
- cache growth, re-evaluate; if over budget, stop admitting (never evict a
373
- running row). Document the estimate honestly.
374
- - **`--prompt-concurrency`: accept for drop-in** (we already accept
375
- `--decode-concurrency`), mapped to "max joiners admitted per loop
376
- iteration" — which becomes meaningful exactly when (d)'s interleaved
377
- chunked admit lands. Until then accept + warn like `--decode-concurrency`'s
378
- semantic note in cli.ts:744-748. `/stats.batch` grows
379
- `{pending_rows, kv_bytes, kv_budget_bytes}`.
380
-
381
- **Gate:** unit tests on the projection math (pure); a gated live test that
382
- M oversized requests are admitted ≤ budget-implied width and all complete
383
- (queueing, not OOM). Perf numbers only via bench-serving-load on a clean
384
- machine (Josh-gated).
385
-
386
- **Effort: M.**
387
-
388
- ### (d) Pipelining + clearCache cadence + chunked/interleaved admit
389
- (the backlog-#6 transplant — pairs with fixing D2, D4)
390
-
391
- 1. **Slice-before-head in `#admit`** (D2): `hLast = h.slice(last)` →
392
- `logitsFromHidden(hLast)`. One-line; do first.
393
- 2. **Chunked prefill in `#admit`**: loop `prefillChunkSize` (reuse the serial
394
- constant) with `evalAll(cache.state)` + `clearCache()` per chunk
395
- (generate.ts:440-456 pattern).
396
- 3. **Interleave admit with steps**: `#drive` admits at most one prefill
397
- *chunk* (per joiner, up to `--prompt-concurrency` joiners) between decode
398
- steps instead of running each admit to completion — bounds running-row
399
- stall to one chunk. (This restructures `#admit` into a resumable
400
- per-joiner state; mlx-lm's `_currently_processing` is the reference,
401
- generate.py:1760-1844.)
402
- 4. **Pipelined decode**: mirror mlx-lm's `GenerationBatch._step`
403
- (generate.py:1320-1373): keep step n's sampled `[B]` token array on
404
- device, `asyncEvalAll` it, build step n+1's graph from the **unread**
405
- array, then read step n. Eviction moves after the read: rows evicted at
406
- step n have already contributed a step-n+1 KV write — harmless, `filter`
407
- drops the row (mlx-lm does exactly this). EOS rows' extra write is also
408
- how mlx-lm behaves, so parity holds by construction.
409
- 5. **clearCache cadence**: every 256 steps (serial's constant; mlx-lm uses
410
- 512 — either is defensible, pick 256 for house consistency and comment
411
- the divergence).
412
-
413
- **Gate:** numerics unchanged ⇒ the teacher-forced KL suite and the mlx-lm
414
- goldens must pass **unchanged** (pipelining is scheduling, not math);
415
- batch-serving smoke; A/B throughput via `scripts/bench-serving-load.ts`
416
- (quotable numbers clean-machine only).
417
-
418
- **Effort: M** (item 3 is the bulk; 1/2/5 are S alone).
419
-
420
- ### (e) Gate widening — which serial-only features can batch safely
421
-
422
- Ranked by value/risk. Each needs per-row state listed; each lands with its own
423
- oracle before the `willBatch`/`hasLogitsExtras` split changes.
424
-
425
- 1. **min_p + XTC — S, low risk, do first.** Pure per-row *samplers*; the
426
- batched lane already builds a per-request sampler closure
427
- (generation-gateway.ts:150-156 → `makeSampler(options)`), which already
428
- implements min_p/XTC for serial. Per-row state: none beyond the closure
429
- (XTC needs `xtcSpecialTokens`, already in options). Change: move
430
- `minP`/`xtc*` out of `hasLogitsExtras` into the batchable set. Gate:
431
- teacher-forced determinism (same seed ⇒ same tokens, batched == serial,
432
- per row — sampling happens on the `[1,V]` row slice so it's the SAME code
433
- path); note the seed-collision fix must land first for two same-ms hot
434
- rows to be meaningfully independent (in flight).
435
- 2. **logprobs / top_logprobs — M, low-medium risk.** Non-stream only
436
- (server.ts:1797-1805 keeps streams capture-free). The scheduler's sampler
437
- already computes `toLogprobs` per row; extend `RowSampler` to optionally
438
- return extras (the serial `sampleStep` extras shape, generate.ts) and have
439
- `#emit` pass them to `onToken` — the handler's `LogprobsCollector` then
440
- works unchanged. Per-row state: none (readback per emitted token, rows
441
- that asked for it only). mlx-lm computes logprobs for every batched row
442
- natively, so behavior-parity is clean.
443
- 3. **penalties (repetition/presence/frequency) + logit_bias — M, medium
444
- risk.** Needs per-row token history + per-row logits processors applied to
445
- the `[1,V]` slice before sampling — exactly mlx-lm's per-row
446
- `logits_processors` loop (generate.py:1338-1349, TokenBuffer). We have the
447
- processors (sampler.ts, serial); per-row state: a bounded history array
448
- (context sizes 20 by default) per row, filtered on eviction. logit_bias is
449
- stateless (easiest of the family) — could ship with tier 1 if the family
450
- gate is split per-feature. Gate: teacher-forced vs serial with identical
451
- forced trajectories (processors are deterministic given history).
452
- 4. **LoRA same-adapter-set batching — L, defer.** Design already decided
453
- (group-by-adapter + drain, parallel-slots.md Fallbacks); needs adapter-set
454
- as a batch-compatibility KEY in the scheduler, not just a boolean gate.
455
- Not before (a)–(d).
456
-
457
- Never batch: vision (offset-0 bidirectional prefill), user seed
458
- (reproducibility ⇒ solo — matches mlx-lm), explicit kv-quant (item f),
459
- DiffusionGemma (non-AR).
460
-
461
- ### (f) Quantized-KV batched — L3 novel extension (design sketch ONLY, deferred)
462
-
463
- No ancestor does it (mlx-lm's `to_quantized` raises on batch caches; optiq
464
- doesn't batch) ⇒ no bit-exact oracle, per the three-level fidelity tree this
465
- is KL + 6-task + benchmark gated. Sketch: `BatchedQuantizedKVCache` mirroring
466
- (a)'s `BatchedKVCache` over (packed, scales, biases) triples — merge/filter/
467
- extend are the same B-axis surgery per component; attention falls to
468
- `quantizedSdpaUnfused` with the `[B,1,N,S]` bool mask (the fused decode kernel
469
- requires `mask.mode===""` and the tiled path 2-D causal — both disqualified,
470
- known perf debt from the modes analysis). Value: memory-density (4-bit KV ×
471
- batching compound, ~4× rows per GB) on the 24 GB machine. **Do not start**
472
- until (a)–(d) are landed and a concrete density need exists; then it begins
473
- with a KL harness (batched-quant vs batched-bf16 at same B), never a
474
- trajectory diff.
475
-
476
- ### (g) Docs + flags
477
-
478
- - **server-config.md**: fix the Qwen3.5 batching claim (D1) — with the
479
- capability gate landed the honest row is "hybrid-cache models (Qwen3.5)
480
- route to the serial lane; batched SSM is a later port (mlx-lm ArraysCache
481
- is the oracle)". Update the known-limitations list as (a)–(e) land (each
482
- item literally deletes a numbered limitation: #1 prompt cache, #2
483
- admission, #5 extend). Add `--kv-budget`, `--prompt-concurrency`,
484
- `/stats.batch.{pending_rows,kv_bytes}` when they exist. Add one line
485
- documenting the auto-batch rejection ("mode, not load-fallback —
486
- determinism", the standing decision).
487
- - **parallel-slots.md**: refresh the STATUS block (S2 admission half, extend,
488
- prompt-cache reuse) to point HERE for the v2 execution plan; it remains the
489
- primitive-design record.
490
- - **`--batch` stays the UX.** No smarter default. `--decode-concurrency`
491
- alias kept; `--prompt-concurrency` accepted per (c). No new mode flags.
492
- - **STATUS.md / PLAN.md Phase 18**: check off items as they land (house
493
- rule: write conclusions down).
494
-
495
- ### (h) (surfaced by this audit, not in the original list) SSM batched path
496
-
497
- Port mlx-lm's ArraysCache batch ops for `SSMCache` (conv/recurrent state are
498
- `[B,…]` tensors; merge/filter/extend are trivial B-axis concat/take — no
499
- padding subtleties since there's no sequence axis; the mask is the harder bit:
500
- Qwen3.5's linear layers need an `ssm_mask` under left-padding). Oracle exists
501
- (mlx-lm batches these models). Schedule after (a); until then the capability
502
- gate (D1 fix) keeps Qwen servers correct-but-serial.
503
-
504
- ---
505
-
506
- ## 4. Sequencing
507
-
508
- Order minimizes risk (correctness first, then the engine, then features),
509
- respects in-flight work (seed fix in flight — untouched; gemma4-base.ts owned
510
- elsewhere — all new cache code goes in new/batched-* files), and gives every
511
- step a gate.
512
-
513
- | # | Item | Effort | Test gate | Notes |
514
- | --- | --- | --- | --- | --- |
515
- | 1 | ✅ **DONE 2026-07-01** — **Hotfix bundle**: D2 slice-before-head; D1 capability gate + server-config.md correction; D3 route curve/`/signal`(/adapter-unmount) through `gateway.runExclusive`; stale comment server.ts:960 | **S** | gated batch suites unchanged; fast suite; new unit: willBatch(Qwen)=false | no numerics change anywhere |
516
- | 2 | ✅ **DONE 2026-07-01** — **D5/D6 failure + fairness**: per-row onToken containment (evict row, keep batch); `#admit` try/finally; drain-on-serial-waiter | **S–M** | new gated test: throwing onToken evicts one row, siblings complete; drain test (serial completes under sustained batchable load) | mlx-lm `remove` semantics |
517
- | 3 | ✅ **DONE 2026-07-01** — **(d) engine hygiene**: chunked+interleaved admit, pipelined decode, clearCache cadence | **M** | KL suite + mlx-lm goldens UNCHANGED; bench-serving-load A/B (clean machine, Josh-gated for quotables) | closes perf-review backlog #6 |
518
- | 4 | **(a) `BatchedKVCache` + `extend`** (full + rotating) — **still open** | **M** | extended dynamic golden (mlx-lm `extend` driven); existing suites green | new files only — no gemma4-base.ts conflict |
519
- | 5 | ✅ **DONE 2026-07-03** (admission half; `--prompt-concurrency` still open) — **(c) KV-budget admission** — executed as grammar-spec-batching-integration.md Phase D | **M** | projection unit tests; gated queueing test (tests/batch-kv-budget.test.ts); `/stats` fields | depends on 3 (interleaved admit) for prompt-concurrency semantics |
520
- | 6 | **(b) prompt-cache reuse under batching** — **still open** | **M–L** | `cached_tokens>0` gated test; warm==cold row parity; serial suite | depends on 4 (`extract`) + 5 (byte interplay) |
521
- | 7 | **(e) gate widening**: ✅ e1 min_p/XTC + e3 penalties/logit_bias **DONE 2026-07-02** (per-row logits processors, batching-perf-path.md header item 2); e2 logprobs **still open** (routes serial) | **S+M+M** | per-feature: batched==serial teacher-forced/deterministic-seed tests | e1 waits for the seed fix to merge |
522
- | 8 | ✅ **DONE 2026-07-02** — **(h) SSM batched path** (Qwen3.5) — landed as batching-perf-path.md P5, token-exact vs mlx-lm B=2 | **M–L** | mlx-lm B=N golden for a Qwen3.5 model (ArraysCache oracle) | un-does the item-1 gate for Qwen; doc row flips back |
523
- | 9 | **(g) docs pass** | **S** | doc-vs-code re-audit (this file's §1 as the checklist) | rolling — update per landing, final sweep here |
524
- | 10 | **(f) quantized-KV batched** | **L** | KL + 6-task + density benchmark — design doc first | DEFERRED — needs a concrete density case |
525
-
526
- D7's small items (shared `shapeFor()`, `maxTokens` fallback unification, real
527
- batched timing stats, filter min-left-pad shift, shared emit-contract test)
528
- slot into whichever of steps 2–4 touches the neighboring code; none justify a
529
- standalone step.