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,86 +1,316 @@
1
1
  # Server configuration
2
2
 
3
- Every start flag for `mlx-bun serve` (and `mlx-bun pi`, which shares the
4
- same flag set), what each does, its default, and — the part that bites —
5
- **which combinations compose and which don't**. For the request/response
6
- wire format (chat/messages/responses bodies, SSE grammar, tool calling,
7
- adapters admin) see [server-api.md](./server-api.md); this doc is about
8
- *starting* the server.
9
-
10
- The flags are parsed in `serverRuntimeFlags()`
11
- ([src/cli.ts](../../src/cli.ts)); the runtime knobs live in
12
- `ServerOptions` ([src/server.ts](../../src/server.ts)). The two execution
13
- lanes (serial vs. `--batch N`) and the lane picker are in
14
- [src/serve/generation-gateway.ts](../../src/serve/generation-gateway.ts);
15
- the design rationale is in
16
- [docs/design/parallel-slots.md](../design/parallel-slots.md).
3
+ The canonical reference for **every start flag, every `MLX_BUN_*`
4
+ environment variable, and every serving feature** of `mlx-bun serve` (and
5
+ `mlx-bun pi`, which shares the flag set). Each default here is read from
6
+ the code, not from memory: flags are parsed by `serverRuntimeFlags()` /
7
+ `applyDecodeRoute()` in [src/cli.ts](../../src/cli.ts) (the `SERVER_FLAGS`
8
+ help block is the same list), runtime knobs land in `ServerOptions`
9
+ ([src/server.ts](../../src/server.ts)), environment reads go through
10
+ [src/runtime-config.ts](../../src/runtime-config.ts), and the `--isolate`
11
+ proxy lives in [src/serve/isolate.ts](../../src/serve/isolate.ts). For the
12
+ request/response wire format see [server-api.md](./server-api.md); for
13
+ CLI verbs see [cli.md](./cli.md).
14
+
15
+ The server runs generation through method-neutral sessions. A model binding
16
+ supplies execution planning, its supported methods, media preparation and
17
+ diagnostics. Stop/tool parsing runs directly in session delivery, so it can stop
18
+ generation before another token is produced. This adds no architecture flag.
19
+
20
+ Sections: [Start flags](#start-flags) · [`--isolate` semantics](#--isolate-semantics)
21
+ · [Per-request overrides](#per-request-overrides) · [Environment variables](#environment-variables)
22
+ · [Execution modes](#execution-modes-serial-vs---batch-n) · [Compatibility matrix](#compatibility-matrix)
23
+ · [Fidelity tiers](#fidelity-tiers-and-the-decode-route---l1----l2) · [Feature matrix](#feature-matrix)
24
+ · [Performance & recipes](#performance-characteristics--recipes) · [`GET /stats`](#observability--get-stats).
17
25
 
18
26
  ## Start flags
19
27
 
20
- | Flag | Arg | Default | What it does |
21
- | --- | --- | --- | --- |
22
- | `--host` | addr | `127.0.0.1` | Interface to bind. Loopback-only by default (mlx_lm.server parity); pass `--host 0.0.0.0` to expose the server on your network. |
23
- | `--port` | n | `8080` | Listen port. A pre-flight probe refuses to start if the port is already serving. |
24
- | `--memory-budget` | GB | machine RAM × 0.75; GLM-5.2: min(25 GiB, physical RAM) | Admission ceiling. Generic budgeted serving rejects requests whose `prompt + max_tokens` exceed the safe context with **400** (`type: memory_admission`) instead of risking an uncatchable GPU OOM. GLM-5.2 runs its exact process equation before opening resident weights or expert slabs and caps MLX to the plan's allocator reserve; because its context is fixed up front, a broad client `max_tokens` upper bound is capped to the remaining planned context, while a prompt that leaves no generation slot is still rejected. **Decimal GB (×10⁹).** |
25
- | `--prompt-cache` | GB | `8` | RAM prefix-KV cache (byte-capped LRU; `0` disables). **PREFIX SHARING (2026-07-05): serves are NON-CONSUMING** — a hit hands out zero-copy clones and leaves the donor entry intact, so N agents (or a brand-new session) sharing a system prompt all reuse ONE prefill without destroying each other's conversation entries (the old consume-and-trim take cannibalized the donor). Extended entries supersede their prefix-ancestors (when trimmable), so a conversation stays one entry. Works on BOTH lanes and tiers over the SSD store (Layer 0). |
26
- | `--isolate` | (bool) | off | Runtime isolation ([runtime-isolation.md](../design/runtime-isolation.md)): the inference engine runs as a CHILD process on a unix socket; this process stays a pure UI/API reverse proxy — instant under any GPU load, survives engine crashes (auto-respawn, in-flight requests get a clean 502). Measured cost ≈0 (−0.4% tok/s, +2 ms TTFT, per-token SSE granularity preserved). `/ws/chat` is not proxied yet (501). `/engine` reports child pid/restarts. |
27
- | `--model-pool` | n | `1` | With `--isolate`: max RESIDENT model engines. Requests naming another model (**exact `/v1/models` id** — fuzzy strings keep mlx-lm's ignored-field semantics) spawn/route to that model's own engine child (SPAWN-OVERLAP: the new model loads while the old one keeps serving; nobody's stream is interrupted). Over the cap, the LRU engine drains, **demotes its prompt cache to the SSD tier**, and exits — switching back respawns it with state restored from disk. Measured (M1 Max, cpm5⇄qwen0.8b, pool 1): switch 1.5 s, switch-back 1.2 s with `cached_tokens` 103/104 — the conversation survived the eviction. The pool drives this eviction via `POST /admin/drain` — unix-socket-only (never exposed on the public TCP listener) — which quiesces the gateway and demotes the whole prompt cache to the SSD tier before eviction, responding `{ drained: true, demotions: N }`. |
28
- | `--ssd-cache` | dir | off | SSD cold tier under the prompt cache ([docs/design/ssd-kv-cold-tier.md](../design/ssd-kv-cold-tier.md)): prefix KV spills to disk on RAM eviction AND idle demotion, is snapshotted after requests settle (debounced 1 s; the flush is idle-gated per tensor so it never taxes an active decode — `MLX_BUN_SSD_WRITEBEHIND=0` disables), and **survives restarts** — a long-context agent re-attach restores via a bounded streamed copy instead of re-prefilling (measured 13.7k-token prefix: 12 s → ~0.25 s TTFT; 0% decode overhead; restore transient = live entry + one tensor, no mapping outlives the load). Entries are keyed by model fingerprint **+ effective kv scheme** + tokenizer hash + adapter ns; incompatible/corrupt files self-quarantine. **Layer 0 (2026-07-05): the tiering lives inside PromptCache.take(), so BOTH lanes restore from disk — batch-scheduler joiners included — and every put (batch rows too) schedules the write-behind snapshot.** Requires the RAM cache. |
29
- | `--ssd-cache-max` | GB | `32` | SSD tier byte cap (binary GiB); oldest-mtime entries evicted at the cap. |
30
- | `--ssd-cache-verify` | (bool) | off | Verify every tensor hash on restore before it is copied in (roughly doubles restore reads) — integrity paranoia only; the header hash is always verified. |
31
- | `--ssd-demote-idle` | sec | `300` (with `--ssd-cache`) | Idle demotion (Layer 0): prompt-cache entries unused this long spill to the SSD tier and **free their GPU memory** — unified memory drains between agent bursts while every prefix stays reachable (the next hit restores via the bounded streamed copy, ~0.25 s for a 13.7k-token entry vs a 12 s re-prefill). Swept only when the engine is fully idle (never drains a running batch). `0` disables. |
32
- | `--batch` | n | `8` | Max concurrent requests decoded together. **Default flipped 1→8 (2026-07-05)** after GATE-B1-SPEED: a LONE request through the unified engine IS the serial engine — adopted serial-class caches, compiled decode, prompt cache + SSD restore, byte-identical output, 0.992–0.996 paired decode ratios — so the cap only changes behavior when concurrent requests actually arrive (the agentic sub-agent workload: 4–8 coding agents against one local server). `--batch 1` pins strict serial (arrival-independent numerics). See [Execution modes](#execution-modes-serial-vs---batch-n). `--decode-concurrency` is accepted for drop-in compatibility (mlx_lm.server's cap, default 32). |
33
- | `--kv-quant` | `config`\|`off`\|`4`\|`8`\|`turbo[:k<bits>v<bits>]` | `off` (bf16) | KV-cache quantization. **Default flipped to bf16 2026-07-05** (naked = L1): quantized KV measured 5–20% slower decode than bf16 at ≤16k on every model — on mlx-lm too — so it pays only in **memory headroom** (e.g. ~1.3 GB on the 12B @16k) and is an explicit opt-in (`--kv-quant …` or `--l2`, whose preset passes `config`). `off` = bf16 (L1). `4`/`8` = **uniform** bits (group 64, start 0) — the scheme mlx-lm exposes as `--kv-bits`/`--kv-group-size`/`--quantized-kv-start`. In the L1 kernel set (`--l1 --kv-quant 8` = fused-sdpa off) our unfused quantized SDPA is **op-for-op identical** to mlx-lm's `quantized_scaled_dot_product_attention`, so uniform-quant is **bit-exact L1** (the fused-sdpa-ON path is optiq-aligned). `config` = **per-layer mixed-precision** from `kv_config.json` — optiq-only, no mlx-lm analog → **L2**. `turbo[:k<bits>v<bits>]` = **TurboQuant** ([docs/design/turboquant-kv.md](../design/turboquant-kv.md)): rotation-based KV quantization (asymmetric-affine int8/int4/int2/int5 keys, FWHT + Lloyd-Max values) — default `k8v3` (2.56× compression at head_dim 128); `kBits` ∈ {2,4,5,8}, `vBits` ∈ {2,3,4,5,8}. A separate axis from the scheme above (mutually exclusive with `config`/`4`/`8`); v1 is dequantize-on-fetch (stock `ops.sdpa`, no fused kernel) and **full-attention layers only** — sliding-window layers stay bf16 (one-time warning, never a throw). Under `--batch N`, a per-layer `config` scheme **batches** — full-attention layers since Phase 3.1 (MiniCPM5), rotating/sliding layers since milestone 2 (gemma's whole kv_config), so every shipped `kv_config.json` now batches. The scheduler applies the mixed scheme per row, gated bit-exact per row vs the serial composition (`tests/batched-kv-quant-parity.test.ts` — compositions no other stack ships). Uniform `4`/`8` still routes those requests serial (quantizedKvStart threshold semantics). **`turbo` is solo-only, unconditionally** — `TurboQuantKVCache` is a novel `Cache` implementation (no merge/filter/temporalView), so it's excluded from the batch scheduler by construction; `GenerationGateway.willBatch` also refuses it explicitly (belt + braces — both layers exist on purpose). |
34
- | `--paged-kv` | (bool) | off | **OPTIONAL vLLM-style paged KV cache** ([docs/design/paged-kv-cache.md](../design/paged-kv-cache.md)): full-attention layers store K/V in fixed-size **block pools** (host-side block table, `ops.takeAxis` gather back to contiguous before the stock SDPA — no new attention math, no fused kernel in v1). Env: `MLX_BUN_PAGED_KV=1`. **Gated bit-exact vs the plain path** (`tests/paged-kv-parity.test.ts`: identical single-forward logits + identical 48-token greedy trajectory on the 12B). **v1 scope: serial batch=1, Gemma4-family, bf16** — with no explicit `--batch` the CLI pins `--batch 1`; the server **refuses** (fails startup, never silently downgrades) `--batch N>1`, any `--kv-quant`, and `--draft-model`; sliding-window layers keep the rotating scheme (mixed paged-full + rotating-sliding is the supported shape). Media (vision/audio) and LoRA-adapter **requests** are scoped OUT per request — they run the plain cache path (v1 non-goal cells), never a 400. Paged requests **bypass the prompt cache** (fresh caches per request; `--ssd-cache` sees nothing) and run **uncompiled decode** (a data-dependent block-list length can't shapeless-replay). Expect a small decode cost at batch=1 — the per-step gather copy buys nothing until the batched/CoW follow-ups; this ships the correctness-proven abstraction, honestly labeled. Pool sized from prompt+`max_tokens` at request setup; exhaustion is a typed error (accounting tripwire), never truncation. |
35
- | `--paged-kv-block-size` | n | `256` | Tokens per KV block (`--paged-kv` only). 256 = the plain cache's growth step, so v1's allocation granularity is a permutation of today's into reusable slots, not a new tuning axis. |
36
- | `--adapter` | dir | none | Mount a LoRA adapter at startup (same machinery as `POST /v1/adapters`; the adapter id is the directory's basename) and make it the **default** for requests that send no `adapter` field. A request's explicit `adapter` — including `"none"` — always wins, and hot-swap via `/v1/adapters` is unchanged. `--adapter-path` is accepted as the mlx_lm.server-named alias. A bad adapter fails startup loudly rather than silently serving the base model. This is the flag `mlx-bun train`'s completion message points at. |
37
- | `--draft-model` | path/query | none | **Speculative decoding**: a drafter proposes tokens the main model verifies in one forward — exact results, faster decode when drafts land. Resolves like the main model. The artifact's **kind is auto-detected**: a full same-tokenizer model (mlx_lm.server parity — L1: token-for-token vs mlx-lm's spec path; tokenizer-family mismatch fails startup, upstream silently accepts ~0%), a Gemma `-assistant` KV-borrowing drafter (L2 vs optiq spec_generate; ~1.09× γ=1 on 12B, no training), a locally-trained **DSpark** checkpoint (`dspark.json`), or one of **DeepSeek's released DSpark drafters** (DeepSpec `Gemma4DSparkModel` config stamp, e.g. `deepseek-ai/dspark_gemma4_12b_block7` for the 12B — oracle: DeepSpec's reference at temperature 0, token-for-token). All four share ONE serve loop. A confidence-calibrated DSpark checkpoint additionally self-schedules its draft length per round (threshold pruning — fewer wasted verify positions; uncalibrated checkpoints draft fixed-length). Mounting a draft routes **every** request to the serial lane (upstream `is_batchable = draft is None`). Pays on slow targets (12B+); fast small models lose to the draft overhead. Composes with structured output (the constrained verify walk). Prompt-cache reuse is bypassed on the spec path (v1). Telemetry: `usage.speculation` (`drafted`/`accepted`/`targetCalls`). |
38
- | `--draft-kind` | `two-model`\|`assistant`\|`dspark`\|`deepspec`\|`ngram` | auto | Override the draft-artifact kind detection (`dspark.json` → dspark; `Gemma4DSparkModel` architecture stamp → deepspec; `*_assistant` config → assistant; else two-model). **`ngram` is never auto-detected — it has no artifact**: MODEL-FREE prompt lookup (drafts copied from the request's own prompt+generation when its trailing k-gram occurred earlier in the stream; port of prompt-lookup decoding / vLLM's `ngram` proposer). Mount it ALONE — `--draft-kind ngram` with a `--draft-model` is refused, as is any other kind without one. Zero weights/memory, lossless by the same verify (gated: serve-loop output token-identical to non-spec greedy on e4b, tests/spec-ngram.test.ts); a no-match round degrades to one plain target step. Best on agentic/RAG/code-edit traffic that re-emits context spans. |
39
- | `--num-draft-tokens` | n | `3` (`ngram`: `10`) | Drafts per verify round (mlx_lm.server's default; `mlx_lm.generate`'s is 2). A DSpark draft pins this to its trained block width (`cfg.gamma`); ngram drafting is free so its default is the reference implementation's 10. |
40
- | `--ngram-max` / `--ngram-min` | k | `3` / `1` | `--draft-kind ngram` only: longest/shortest trailing k-gram searched for a match (the reference values — longest first, first occurrence wins). |
41
- | `--mtp` | `on`\|`off` | on for GLM-5.2 | Mount the checkpoint-native GLM MTP row as the server's drafter. It uses the bounded auxiliary expert tier and exact serial verify loop (`usage.lane: "serial+spec"`). `off` removes the draft so ordinary GLM requests can use continuous batching; per-row batched MTP is post-release. Mutually exclusive with an explicit `--draft-model`/`--draft-kind`. |
42
- | `--context-length` | tokens | `4096` for GLM-5.2 | Context reserved by GLM's header-only resource equation. The same value becomes the request-admission ceiling and is reported in `/stats.glm52`; an impossible plan fails before committing model memory. |
43
- | `--thinking` | `true`\|`false` | model's own (false for CPM and GLM-5.2) | Server-wide default for the chat template's `enable_thinking` (MiniCPM5/CPM, Qwen3.5, and GLM-5.2). Full precedence: an explicit request `chat_template_kwargs.enable_thinking` always wins; else `reasoning_effort` (`"none"` → off, any other level → on) if the request sent one; else this server default; else the model's own default. GLM renders disabled thinking as `<think></think>` and enabled thinking as an open `<think>` generation primer plus `Reasoning Effort: Max`. |
44
- | `--temperature` | n ∈ [0,5] | `generation_config.json` | Server-wide sampling default. Per-request `temperature` still wins; the browser chat (sends none) inherits this. `--temp` is accepted as an alias (mlx_lm.server compat); explicit `--temperature` wins if both are given. **Migration note:** mlx_lm.server's `--temp` *default* is `0.0` (unset-temperature requests are greedy there); mlx-bun falls back to the model's `generation_config.json`, then `0.7` — pass `--temp 0` for mlx-lm's behavior. |
45
- | `--top-p` | n ∈ [0,1] | `generation_config.json` | Server-wide top-p default (per-request `top_p` wins). |
46
- | `--top-k` | n ∈ [0,1e6] | `generation_config.json` | Server-wide top-k default (per-request `top_k` wins). |
47
- | `--max-tokens` | n | GLM-5.2: `128`; otherwise `65536` chat / `512` raw completion | Completion cap when a request omits `max_tokens` (mlx_lm.server flag; its default there is 512 — `--max-tokens 512` reproduces mlx_lm.server exactly). For GLM this is also reserved by the pre-open resource equation and must fit inside `--context-length`. |
48
- | `--allow-private-media` | (bool) | off | Let `image_url`/`audio_url` content parts fetch from **private/loopback/link-local** hosts (a NAS, another LAN box). Off by default: a request's URL is attacker-controlled input, so remote media fetches (`src/media-fetch.ts`) refuse non-http(s) schemes and destinations in private/loopback/link-local/CGNAT ranges — including hosts that *resolve* there and every **redirect hop** (each hop re-passes the whole policy) — which keeps the server from being steered at cloud metadata (`169.254.169.254`) or LAN-internal services (SSRF). Independent of the flag, every remote fetch has a **10 s wall-clock timeout** and a **64 MB response cap** (streaming-enforced, not just Content-Length); violations surface as clean `400`s (`prompt build failed: …`). `data:` URLs decode locally and are never policy-checked. |
49
- | `--no-open` | (bool) | off | Skip the automatic browser open on start. By default an interactive terminal session opens `http://<host>:<port>/#/chat` once the server is ready; pass this flag to suppress it (e.g. headless or non-TTY environments already skip it). |
50
- | `--hlg-sampling` | `on`\|`off` | off | Piecewise tone-curve (HLG) sampling: rolls off the top-token region, boosts the mids, gentles the tail. The overall gain folds from `--temperature`. See [docs/design/hlg-sampling.md](../design/hlg-sampling.md). |
51
- | `--hlg-width` | nats | `4` | HLG mid-region half-width (nats). Only meaningful with `--hlg-sampling on`. |
52
- | `--hlg-shoulder` | nats | `4` | HLG highlight rolloff scale (nats). Only meaningful with `--hlg-sampling on`. |
53
- | `--hlg-toe` | nats | `6` | HLG shadow rolloff scale (nats). Only meaningful with `--hlg-sampling on`. |
54
- | `--hlg-pivot-offset` | nats | `6` | HLG pivot point: nats below the top token. Only meaningful with `--hlg-sampling on`. |
55
- | `--expert-offload` | (bool) | off | **MoE models only.** Serve experts from a page-aligned file mmap (built on first use). Keeps the model out of memory pressure — physical footprint ≈ active params. Ignored with a warning on dense models. Bit-exact with the resident path. |
56
- | `--l1` | (bool) | — | **Parity tier ALIAS:** bit-for-bit IDENTICAL to mlx-lm (bf16 KV, compiled decode + compiled activations — the faithful `@mx.compile` geglu/swiglu). **No tier given ⇒ `--l1`** (decided 2026-07-05: the L1 faithful kernel set matches mlx-lm 1.00× on every model and no output-changing lever has beaten that baseline in a paired A/B — each is opt-in until it does). Expands to the fastest set of per-fork flags that still holds the guarantee; any explicit per-fork flag (`--kv-quant`/`--fused-sdpa`/`--compiled-decode`/`--compiled-activations`) overrides one. See [docs/design/faithful-l1-consolidation.md](../design/faithful-l1-consolidation.md) and [parity-tier-dag.md](../design/parity-tier-dag.md). |
57
- | `--l2` | (bool) | — | **Parity tier preset:** bit-for-bit IDENTICAL to mlx-optiq (quantized KV per `kv_config.json` + fused N-tiled prefill SDPA + **stock unfused decode** — the composition the optiq goldens track, `scripts/regen-kvq-goldens.ts`). |
58
- | `--compiled-decode` | on\|off | on | Replay the per-step decode graph in C++ (`MLX_BUN_COMPILED_DECODE`). Bit-exact A/B lever. **Serial lane only** (see note below). **Gemma4-dense only** — LoRA, MoE, and non-Gemma4 models (MiniCPM5 / Qwen3.5) run eager; an unsupported step falls back to eager for the rest of that generation. |
59
- | `--compiled-activations` | on\|off | on | Route the geglu/swiglu activation through mlx-lm's `@mx.compile` closure (`MLX_BUN_COMPILED_GEGLU` + `MLX_BUN_COMPILED_SWIGLU`) — the **faithful** kernel: same libmlx graph as mlx-lm → **bit-exact** AND one dispatch instead of ~9. `off` = the uncompiled composition (same L1 parity, slower). Toggles gemma geglu + MiniCPM5 swiglu; qwen3/qwen3.5/universal compile unconditionally. |
60
- | `--fused-sdpa` | on\|off | follows `--kv-quant` | Fused SDPA path for quantized prefill/continuation (inverted env `MLX_BUN_NO_FUSED_SDPA`). Defaults to the composition its oracle uses: **on** under `--kv-quant config` (the optiq-golden composition), **off** under uniform `4`/`8` (mlx-lm's `quantized_scaled_dot_product_attention`, the bit-exact L1-eligible scheme) and bf16 (no-op there). **Serial lane only.** |
61
- | `--force-wire` | (bool) | off | Wire weights into memory for the whole generation (`MLX_BUN_FORCE_WIRE`). Near-ceiling models (e.g. 26B) need it. **Serial lane only.** |
28
+ How flags are parsed (`src/cli.ts` `opt`/`flag`/`positional`):
29
+
30
+ - A value flag is `--name value` (space-separated; `--name=value` is not
31
+ recognized). A value flag with no following token takes its default.
32
+ - **Unknown flags are silently ignored** — there is no unknown-flag error.
33
+ mlx_lm.server's `--kv-group-size` and `--quantized-kv-start` are in this
34
+ class: `src/serve/isolate.ts` lists them as value-taking so the engine
35
+ argv builder does not mistake their value for a model positional, but
36
+ `serve` never reads them (uniform `--kv-quant 4|8` is fixed at group
37
+ size 64, start 0).
38
+ - Aliases: `--adapter-path` = `--adapter`; `--temp` = `--temperature`
39
+ (explicit `--temperature` wins); `--decode-concurrency` = `--batch`
40
+ (`--batch` wins); `--query`/`-q` = the positional model query.
41
+ - `mlx-bun pi` consumes the same flag set; when it attaches to an
42
+ already-running server every server-shaping flag except `--port` is
43
+ reported as ignored (the running server keeps its configuration).
44
+
45
+ **Lane/tier column:** `serial` = the strict single-queue executor;
46
+ `batch` = the continuous scheduler (`--batch N`); `both` = lane-independent.
47
+ L1 = bit-exact vs mlx-lm, L2 = bit-exact vs mlx-optiq, Lab = no external
48
+ oracle. See [Fidelity tiers](#fidelity-tiers-and-the-decode-route---l1----l2).
49
+
50
+ ### Model selection
51
+
52
+ | Flag | Arg | Default | Lane/tier | What it does |
53
+ | --- | --- | --- | --- | --- |
54
+ | `[query]` (positional) | registry query | auto-pick | both | First non-flag token after `serve`: a registry query (`"12B"`, `"e4b"`, a repo substring). Omitted: serves e4b when downloaded, else the largest downloaded model that fits; a fresh install downloads a small starter model first, then e4b in the background. |
55
+ | `--model` | path \| query | none | both | mlx_lm.server's spelling, and the **highest-precedence** selector: a directory containing `config.json` loads straight from that path (`scanSnapshot`, no registry lookup — the bench/CI shape; an HF-cache path recovers its repo id from `models--org--name/snapshots/`); anything else resolves like the positional query. Precedence: `--model` > positional > `--query` > auto-pick. Under `--isolate`, the parent resolves the model and pins `--model <path>` on the engine child. |
56
+ | `--query` / `-q` | registry query | none | both | Same as the positional query (lowest-precedence explicit selector). `-q` is the `mlx-bun pi` spelling. |
57
+
58
+ ### Network
59
+
60
+ | Flag | Arg | Default | Lane/tier | What it does |
61
+ | --- | --- | --- | --- | --- |
62
+ | `--host` | addr | `127.0.0.1` | both | Interface to bind. Loopback-only by default (mlx_lm.server parity); `--host 0.0.0.0` exposes the server on your network. The browser-open uses `localhost` when the bind is `0.0.0.0`/`::`. |
63
+ | `--port` | n | `8080` | both | Listen port. Before loading weights the CLI probes `http://localhost:<port>/v1` and refuses to start if something is already serving there (skipped under `--unix`). |
64
+ | `--unix` | socket path | none | both | **Internal.** Listen on a unix domain socket instead of TCP — the engine half of `--isolate` (`ServerOptions.unixSocket`; `hostname`/port are ignored, stale socket files are unlinked before bind). Usable directly for socket-level integration. |
65
+
66
+ ### Memory, admission, and caches
67
+
68
+ The RAM prompt-cache limit is an upper bound, not extra GPU memory beyond
69
+ the model's budget. Serial requests finish outstanding SSD snapshots before
70
+ generation. When live allocations leave too little room for prefill and KV
71
+ growth, older RAM entries demote to SSD and remain reusable there. Serial and
72
+ continuous execution check headroom after acquiring cached state, under the
73
+ GPU lease. Qwen rechecks between prefill layers; serial decode rechecks every
74
+ 256 tokens. These checks inspect materialized allocations; a deferred graph
75
+ can still exceed available memory when evaluated.
76
+ If cache demotion cannot provide enough headroom, the request fails and
77
+ retains its in-flight checkpoint. The configured SSD cap is unchanged.
78
+ With `--ssd-cache`, an exact recurrent-cache hit persists its boundary before
79
+ transferring the live state to the request. This keeps that boundary reusable
80
+ after the recurrent state advances. Already-durable boundaries need no write;
81
+ a failed required store retains the cache entry and refuses the transfer.
82
+
83
+ | Flag | Arg | Default | Lane/tier | What it does |
84
+ | --- | --- | --- | --- | --- |
85
+ | `--memory-budget` | GB (decimal, ×10⁹) | machine RAM × `WIRED_FRACTION` (0.75, `src/fit.ts`); GLM-5.2: min(25 GiB, physical RAM) | both (per request) | Admission ceiling. A request whose prompt fits is always admitted: a broad client `max_tokens` is capped to the room left under the safe context; only a prompt that leaves no generation slot is rejected with **400** (`type: memory_admission`) — the alternative is an uncatchable GPU OOM. The safe-context ceiling bills the active `--kv-quant` scheme at its true bytes/element (`turbo` is billed bf16, conservatively). A budget too small for any context logs a warning and serves anyway. GLM-5.2 runs its header-only resource equation against this ceiling before opening any resident weights or expert slabs. Not an aggregate cap across batch rows — see `--kv-budget`. |
86
+ | `--kv-budget` | GB (decimal) | off | batch | Aggregate KV budget across concurrently admitted batch rows: a joiner whose projected KV (prompt + `max_tokens`, window-capped) would exceed it **queues** until rows finish; a request over the budget alone is rejected. Without it, N large-context rows can collectively exceed memory. Reported in `/stats.batch.kv_budget_bytes`. |
87
+ | `--prompt-cache` | GB (binary GiB) | `8` | both | RAM prefix-KV cache (byte-capped LRU). `--prompt-cache 0` disables it. Ordinary hits are **non-consuming**: a hit hands out zero-copy clones and leaves the donor entry intact, so N agents sharing a system prompt reuse one prefill. Exact Qwen hybrid hits containing recurrent SSM state transfer the entry to the request and put the extended state back afterward, avoiding an extra resident recurrent snapshot. Batch-lane joiners `take()` the longest usable prefix at admission; a row that finishes without ever merging `put()`s its caches back (merged rows' entries age out). |
88
+ | `--ssd-cache` | dir | off | both | SSD cold tier under the prompt cache ([docs/design/kv-cache.md](../design/kv-cache.md)): prefix KV spills to disk on RAM eviction and idle demotion, is snapshotted after requests settle (debounced, each tensor step owns the generation lock so it cannot overlap inference; `MLX_BUN_SSD_WRITEBEHIND=0` disables), and **survives restarts**. Entries are keyed by model fingerprint + effective KV scheme + backend numerical identity + tokenizer hash + adapter namespace. MLX includes its runtime version and GPU architecture; older numerical identities use separate directories. `SIGINT`/`SIGTERM` drains active requests and flushes dirty snapshots before exit (`MLX_BUN_SHUTDOWN_TIMEOUT_MS`); `POST /admin/cache/flush` is the explicit boundary. Requires the RAM cache. |
89
+ | `--ssd-cache-max` | GB (binary GiB; `0` = unlimited) | unlimited | both | Optional SSD tier byte cap; oldest-mtime entries are evicted only when a positive cap is configured. Warns and is ignored without `--ssd-cache`. |
90
+ | `--ssd-cache-verify` | (bool) | off | both | Verify every tensor hash on restore (reads all bytes eagerly, defeating lazy fault-in) — integrity paranoia only; the header hash is always verified. Warns and is ignored without `--ssd-cache`. |
91
+ | `--ssd-demote-idle` | seconds | `300` with `--ssd-cache`, else off | both | Prompt-cache entries unused this long spill to the SSD tier and free their GPU memory; the next hit restores them. Swept only when the engine is fully idle. `0` disables. Warns and is ignored without `--ssd-cache`. |
92
+ | `--generation-checkpoint` | output tokens | off | serial | Atomically snapshots an in-flight generation every N emitted tokens. Repeating the identical request after a restart replays the saved assistant prefix and continues from its already-sampled next token. Requires `--ssd-cache` and `--batch 1`; completed generations remove their checkpoint, while client cancellation preserves the latest checkpoint. |
93
+
94
+ Generation checkpoint identity includes stop strings, sampling/KV policy, the
95
+ resolved execution method, compiled/grammar policy, and artifact/implementation
96
+ identity. Changing any of these starts a new completion. Version-2/3 generation keys are not
97
+ selected for automatic resume. Prefix-cache files retain their state codec
98
+ format, but automatic reuse also requires the model binding's numerical
99
+ identity. MLX runtime or GPU architecture changes select a fresh cache
100
+ directory and leave the previous directory intact. A different backend codec
101
+ identity refuses incompatible files. Adapter namespaces include the mounted weight contents and
102
+ scale, so a changed adapter starts with a fresh prefix and checkpoint.
103
+ Native media and grammar preparation waits for the generation execution lease
104
+ and retains a reservation through completion. At most one media preparation and
105
+ `--batch` grammar preparations can be retained. Generation and preparation
106
+ queues each allow 64 waiting requests; overflow returns `429` before a response
107
+ stream opens, or a terminal stream error after it opens. Disconnects release
108
+ queued reservations and completed preparation resources.
109
+
110
+ Disconnected serial requests leave the admission queue immediately. Active AR
111
+ requests check cancellation between prefill chunks and decode steps; a native
112
+ operation already running completes before that boundary.
113
+
114
+ ### Runtime isolation
115
+
116
+ | Flag | Arg | Default | Lane/tier | What it does |
117
+ | --- | --- | --- | --- | --- |
118
+ | `--isolate` | (bool) | off | both | Run the inference engine as a **child process** on a unix socket while this process owns CPU application routes and proxies inference — instant under any GPU load, survives engine crashes (auto-respawn). Full semantics in [`--isolate` semantics](#--isolate-semantics). |
119
+ | `--model-pool` | n (≥1) | `1` | both | With `--isolate`: max **resident** model engines. A request whose `model` field is an **exact** `/v1/models` id spawns/routes to that model's own engine child (spawn-overlap: the new model loads while the old keeps serving). Over the cap the least-recently-used engine is drained (`POST /admin/drain` over its socket), demotes its prompt cache to the SSD tier, and exits; switching back respawns it with state restored from disk. Without `--isolate` the flag warns and is ignored. |
120
+
121
+ ### Scheduling
122
+
123
+ | Flag | Arg | Default | Lane/tier | What it does |
124
+ | --- | --- | --- | --- | --- |
125
+ | `--batch` | n (integer ≥ 1) | `8` | batch / L1 | Max concurrent requests decoded together. A lone admitted request uses the B=1 fast path (adopted serial-class caches, compiled decode, prompt cache + SSD restore — byte-identical to serial); a batch layout only exists once a second request arrives. `--batch 1` pins the strict serial executor (arrival-independent numerics — use it for golden regeneration). `--decode-concurrency` is accepted as the mlx_lm.server alias, but its semantics differ there (per-`BatchGenerator` decode parallelism, default 32). See [Execution modes](#execution-modes-serial-vs---batch-n). |
126
+
127
+ ### KV cache
128
+
129
+ | Flag | Arg | Default | Lane/tier | What it does |
130
+ | --- | --- | --- | --- | --- |
131
+ | `--kv-quant` | `config` \| `off` \| `4` \| `8` \| `turbo[:k<bits>v<bits>]` | `off` (bf16) | see row | KV-cache quantization. `off` = bf16 (the L1 default: quantized KV measured 5–20% slower decode than bf16 at ≤16k on both stacks, so it is an opt-in that buys **memory headroom**). `4`/`8` = **uniform** bits (group 64, start 0 — mlx-lm's `--kv-bits` scheme); with fused-sdpa off (the default for uniform) our unfused quantized SDPA is op-for-op mlx-lm's `quantized_scaled_dot_product_attention`, so uniform is **bit-exact L1**; uniform requests route **serial**. `config` = **per-layer mixed precision** from the model's `kv_config.json` — optiq-only → **L2**; per-layer configs **batch** on every shipped model (full-attention and rotating layers; gated bit-exact per row vs the serial composition). `turbo[:k<bits>v<bits>]` = **TurboQuant** ([docs/design/turboquant.md](../design/turboquant.md)): rotation-based KV quantization, default `k8v3`; `kBits` ∈ {2,4,5,8}, `vBits` ∈ {2,3,4,5,8}; a separate axis (mutually exclusive with `config`/`4`/`8`), v1 is dequantize-on-fetch via stock `ops.sdpa`, **full-attention layers only** (sliding-window layers stay bf16 with a one-time warning), refuses head dims outside `TURBOQUANT_HEAD_DIMS`, and is **solo-only** — `GenerationGateway.place` refuses continuous scheduling for it unconditionally. Any explicit `--kv-quant` overrides the tier preset. |
132
+ | `--paged-kv` | (bool) | off | serial / gated bit-exact | **Optional vLLM-style paged KV cache** ([docs/design/kv-cache.md](../design/kv-cache.md)): full-attention layers store K/V in fixed-size block pools (host-side block table, gather back to contiguous before the stock SDPA; no new attention math). Env mirror `MLX_BUN_PAGED_KV=1`. With no explicit `--batch` the CLI pins `--batch 1`. `createServer` **refuses** (fails startup, never downgrades) `--batch N>1`, any `--kv-quant` (incl. turbo), `--draft-model`, and non-Gemma4 models. Media and adapter requests run the plain cache path per request. Paged requests bypass the prompt cache and run uncompiled decode; pool exhaustion is a typed error, never truncation. Gated bit-exact vs the plain path (`tests/parity/paged-kv-parity.test.ts`). |
133
+ | `--paged-kv-block-size` | n | `256` | serial | Tokens per KV block (`--paged-kv` only); 256 = the plain cache's growth step. |
134
+
135
+ ### Adapters and speculative decoding
136
+
137
+ Qwen MTP has an experimental paired RAM prefix cache. Set
138
+ `MLX_BUN_MTP_PROMPT_CACHE=1` to retain one aligned target/draft prefill snapshot
139
+ within the prompt cache's byte budget. A hit transfers both states together;
140
+ different targets, KV policies or non-extending histories miss. The cache is
141
+ RAM-only and does not persist across restarts. `usage.cached_tokens` reports
142
+ the reused target prefix. The default remains off.
143
+
144
+ `MLX_BUN_QWEN_SPEC_KV4=1` qualifies Qwen uniform affine KV4 with
145
+ `quantizedKvStart=0` for speculative execution. Recurrent state and the MTP
146
+ cache keep their original precision. TurboQuant and per-layer KV remain
147
+ incompatible. This composition has diagnostic coverage; final combined,
148
+ pressure and quiet-machine acceptance remain open.
149
+
150
+ For controlled experiments, `MLX_BUN_RD_PREFILL_CHUNK` sets the speculative
151
+ target prefill chunk, default 2048, unless the library caller provides
152
+ `prefillChunkSize`. `MLX_BUN_RD_CONTEXT_LIMIT` caps physical context admission
153
+ without enlarging it. Both require positive integers; the context cap is unset
154
+ by default. These are benchmark controls, not changes to the published model
155
+ profile or sampling policy.
156
+
157
+ | Flag | Arg | Default | Lane/tier | What it does |
158
+ | --- | --- | --- | --- | --- |
159
+ | `--adapter` | dir | none | serial (adapter requests route serial) | Mount a LoRA adapter at startup (same machinery as `POST /v1/adapters`; the id is the directory basename) and make it the default for requests without an `adapter` field. A request's explicit `adapter` — including `"none"` — wins; hot-swap via `/v1/adapters` is unchanged. A bad adapter fails startup. Alias `--adapter-path`. |
160
+ | `--draft-model` | path \| query | none | serial (all requests) / per-drafter oracle | **Speculative decoding**: a drafter proposes tokens the target verifies in one forward — exact results, faster decode when drafts land. Resolves like the main model. Kind is auto-detected: a full same-tokenizer model (mlx_lm.server parity, L1 token-for-token; tokenizer-family mismatch fails startup), a Gemma `-assistant` KV-borrowing drafter (L2 vs optiq `spec_generate`), a locally trained **DSpark** checkpoint (`dspark.json`), or a released DeepSpec `Gemma4DSparkModel` drafter. Mounting a draft routes **every** request serial (upstream `is_batchable = draft is None`). Composes with structured output; ordinary prompt-cache reuse is bypassed on the spec path; Qwen MTP can opt into paired RAM prefixes with `MLX_BUN_MTP_PROMPT_CACHE=1`. Telemetry: `usage.speculation`. |
161
+ | `--draft-kind` | `two-model` \| `assistant` \| `dspark` \| `deepspec` \| `mtp` \| `ngram` | auto | serial | Override drafter detection. `mtp` = a native multi-token-prediction head split from the target's release (`*_mtp` model_type; shares the target's embeddings/lm-head, defaults `--num-draft-tokens` to its trained `block_size − 1`, rolls DeltaNet caches back by snapshot/replay on partial rejects); `mtp` alone mounts the companion bundled at `<model>/mtp/` when present. Qwen MTP companions accept dense or affine-quantized projections using their checkpoint metadata; changing the draft can change acceptance and speed. `ngram` = **model-free prompt lookup** (drafts copied from the request's own prompt+generation; port of prompt-lookup decoding / vLLM's `ngram` proposer) — mount it **alone** (`--draft-kind ngram` with a `--draft-model` is refused, as is any other kind without one); lossless by the same verify, a no-match round degrades to one plain target step. Any other value fails startup. |
162
+ | `--num-draft-tokens` | n (integer ≥ 1) | `3` (`ngram`: `10`; DSpark: pinned ≤ its trained `gamma`) | serial | Drafts per verify round (mlx_lm.server's default; `mlx_lm.generate`'s is 2). |
163
+ | `--ngram-max` / `--ngram-min` | k (integer ≥ 1) | `3` / `1` | serial | `--draft-kind ngram` only: longest/shortest trailing k-gram searched (longest first, first occurrence wins). `--ngram-min` > `--ngram-max` fails startup; either flag without `ngram` warns and is ignored. |
164
+ | `--mtp` | `on` \| `off` | `on` for GLM-5.2 | serial+spec | GLM-5.2 checkpoint-native MTP row as the server's drafter, using the bounded auxiliary expert tier and the exact serial verify loop (`usage.lane: "serial+spec"`). `off` removes the draft so ordinary GLM requests can use continuous batching. Other model families ignore it. |
165
+ | `--context-length` | tokens (integer ≥ 1) | `4096` (GLM-5.2) | both | Context reserved by GLM-5.2's header-only resource equation; also the request-admission ceiling, reported in `/stats.glm52`. An impossible plan fails before committing model memory. Ignored by other families. |
166
+
167
+ ### Sampling and template defaults
168
+
169
+ | Flag | Arg | Default | Lane/tier | What it does |
170
+ | --- | --- | --- | --- | --- |
171
+ | `--thinking` | `true`\|`false` (also `on`/`off`/`1`/`0`) | model's own (`false` for MiniCPM5) | both | Server-wide default for the chat template's `enable_thinking`. Precedence (`resolveEnableThinking`): explicit request `chat_template_kwargs.enable_thinking` → request `reasoning_effort` (`"none"` = off, any other level = on) → this flag → the model's default. With no explicit temperature, a no-think turn is capped at 0.7 while a think turn keeps the model's configured temperature. |
172
+ | `--temperature` | n ∈ [0, 5] | `generation_config.json`, else `0.7` | both | Server-wide sampling default; a per-request `temperature` wins; the browser chat (sends none) inherits it. Alias `--temp` (mlx_lm.server spelling — note its *default* there is `0.0`; pass `--temp 0` for that behavior). |
173
+ | `--top-p` | n ∈ [0, 1] | `generation_config.json`, else `0` (off) | both | Server-wide top-p default (per-request `top_p` wins). |
174
+ | `--top-k` | n ∈ [0, 1e6] | `generation_config.json`, else `0` (off) | both | Server-wide top-k default (per-request `top_k` wins). |
175
+ | `--max-tokens` | n ∈ [1, 1e7] | GLM-5.2: `128` (memory-plan reservation); otherwise none — a request that omits `max_tokens` generates until EOS or the admitted context is exhausted (admission clamps to `maxSafeContext − promptLen`; an invented cap ahead of that could only stop work that would have succeeded) | both | Completion cap when a request omits `max_tokens` (mlx_lm.server flag). DEVIATION when unset: `mlx_lm.server` stops a defaulted request at 512 — `--max-tokens 512` reproduces it. For GLM-5.2 the value is also reserved by the pre-open resource equation and must fit inside `--context-length`. Note: `max_tokens` also feeds `--kv-budget` row projections (prompt + cap), so a defaulted request reserves worst-case and batches conservatively — clients that state `max_tokens` pack tighter. |
176
+ | `--hlg-sampling` | `on`\|`off` | off | serial / Lab | Piecewise tone-curve (HLG) sampling: rolls off the top-token region, boosts the mids, gentles the tail; the gain folds from `--temperature`. Design: [docs/archive/hlg-sampling.md](../archive/hlg-sampling.md). |
177
+ | `--hlg-width` | nats ∈ [0, 100] | `4` | serial | HLG mid-region half-width. Only with `--hlg-sampling on`. |
178
+ | `--hlg-shoulder` | nats ∈ [0, 100] | `4` | serial | HLG highlight rolloff scale. Only with `--hlg-sampling on`. |
179
+ | `--hlg-toe` | nats ∈ [0, 100] | `6` | serial | HLG shadow rolloff scale. Only with `--hlg-sampling on`. |
180
+ | `--hlg-pivot-offset` | nats ∈ [0, 100] | `6` | serial | HLG pivot: nats below the top token. Only with `--hlg-sampling on`. |
181
+
182
+ ### Media and UX
183
+
184
+ | Flag | Arg | Default | Lane/tier | What it does |
185
+ | --- | --- | --- | --- | --- |
186
+ | `--allow-private-media` | (bool) | off | serial (media requests) | Let `image_url`/`audio_url`/`video_url` parts fetch from **private/loopback/link-local** hosts. Off by default: a request's URL is attacker-controlled, so `src/media-fetch.ts` refuses non-http(s) schemes and private/loopback/link-local/CGNAT destinations — including hosts that *resolve* there and every redirect hop (SSRF guard). Independent of the flag, every remote fetch has a 10 s timeout and a 64 MB streaming-enforced cap; violations are clean `400`s. `data:` URLs are never policy-checked. Env mirror `MLX_BUN_ALLOW_PRIVATE_MEDIA=1`. |
187
+ | `--no-open` | (bool) | off | both | Skip the automatic browser open. By default an interactive TTY opens `http://<host>:<port>/#/chat` once the server is ready; non-TTY runs never open. Parent-only under `--isolate`. |
188
+
189
+ ### Parity tier and kill switches
190
+
191
+ | Flag | Arg | Default | Lane/tier | What it does |
192
+ | --- | --- | --- | --- | --- |
193
+ | `--l1` | (bool) | **the default** | serial route / L1 | Tier alias: bit-for-bit identical to mlx-lm — bf16 KV, compiled decode, compiled activations, fused-sdpa off. No tier flag ⇒ `--l1` (decision 2026-07-05). Any per-fork flag below overrides one preset value. |
194
+ | `--l2` | (bool) | — | serial route / L2 | Tier alias: bit-for-bit identical to mlx-optiq — `--kv-quant config` + fused N-tiled prefill SDPA + stock unfused decode (the composition the optiq goldens track). |
195
+ | `--l3` | — | — | — | **Removed 2026-07-05.** Passing it exits with a pointer to `--l2` / the Lab. |
196
+ | `--compiled-decode` | `on`\|`off` | on | serial, and batch at B=1 / L1 | Replay the per-step decode graph in C++ (`MLX_BUN_COMPILED_DECODE`). Bit-exact A/B lever. Gemma4-dense only; LoRA, MoE, and other families run eager, and an unsupported step falls back to eager for the rest of that generation. |
197
+ | `--compiled-activations` | `on`\|`off` | on | both / L1 | Route geglu/swiglu through mlx-lm's `@mx.compile` closure (`MLX_BUN_COMPILED_GEGLU` + `MLX_BUN_COMPILED_SWIGLU`). MiniCPM5 uses compiled SwiGLU for decode and prefill. `off` selects the uncompiled composition and can change prefill numerics on MLX 0.32.2. Toggles gemma geglu + MiniCPM5 swiglu; qwen3/qwen3.5/universal compile unconditionally. |
198
+ | `--fused-sdpa` | `on`\|`off` | follows `--kv-quant`: on for `config`, off for `4`/`8` and bf16 | serial / L2 | Fused SDPA for quantized-KV prefill/continuation (inverted env `MLX_BUN_NO_FUSED_SDPA`). Defaults to the composition its oracle uses. No-op on bf16. |
199
+ | `--force-wire` | (bool) | off | serial | Wire weights into memory for the whole generation (`MLX_BUN_FORCE_WIRE=1`). Near-ceiling models need it. The batch scheduler bypasses `generate()`'s wired scope. |
200
+ | `--expert-offload` | (bool) | off | serial / Lab | **MoE only.** Serve experts from a page-aligned file mmap (`<model>/.mlx-bun-offload`, built on first use) — physical footprint ≈ active params. Dense models log "ignored". Bit-exact with the resident path. Env mirror: `MLX_BUN_EXPERT_OFFLOAD=<dir>`. |
62
201
 
63
202
  The default host/port (`127.0.0.1:8080`) match `mlx_lm.server`, so running
64
- mlx-bun alongside the Python reference server needs an explicit `--port`.
65
-
66
- The remaining levers are bit-exact A/B knobs; **the naked default is the
67
- L1 set** (2026-07-05 decision: an output-changing lever earns a default
68
- only by beating the L1 faithful baseline in a paired A/B, and none did —
69
- the 2026-07-05 pass measured the custom fused-decode at 1.00×, fused-gelu
70
- at +0–1%, the flash perf-kernel at 0.62–0.93× on e4b, and quantized KV
71
- 5–20% *slower* decode than bf16 at ≤16k on both stacks; those losing
72
- kernels and their flags were **deleted** the same day — Phase 1 of
73
- [unified-engine-frontier-plan.md](../design/unified-engine-frontier-plan.md).
74
- New output-changing experiments live in the **Lab**: env-flagged, with a
75
- bench + expiry, until one beats the L1 baseline in a paired A/B). Flip
76
- the survivors to compare. They are set as `MLX_BUN_*` env vars before the
77
- model loads, so they apply to `mlx-bun pi` too. They affect the
78
- **serial** decode path (`generate()`); the batched scheduler calls
79
- `model.forwardHidden` directly and is unaffected by all of them except
80
- `--compiled-decode`, which also covers a lone batch-lane request (Phase
81
- 3.2) — see
203
+ mlx-bun alongside the Python reference needs an explicit `--port`.
204
+
205
+ ## Reproducing mlx_lm.server
206
+
207
+ Numeric parity (same tokens in → same logits, bit-for-bit) is unconditional
208
+ — no flag buys it back and none is needed. **Behavior policy** (defaults,
209
+ caps, sampling fallbacks) is mlx-bun's own; mlx_lm.server's policy is one
210
+ configuration of it:
211
+
212
+ ```sh
213
+ mlx-bun serve <model> --temp 0 --top-p 0 --top-k 0 --max-tokens 512 --batch 1
214
+ ```
215
+
216
+ - `--temp 0 --top-p 0 --top-k 0` — mlx_lm.server does not read
217
+ `generation_config.json` (we inject its sampling defaults, optiq-style);
218
+ its unset-request defaults are temperature 0.0, top-p/top-k off.
219
+ - `--max-tokens 512` — its cap when a request omits `max_tokens` (ours is
220
+ none: run to EOS or the admitted context). Note a defaulted mlx-bun run's
221
+ first 512 tokens are byte-identical to mlx-lm's capped run — the cap only
222
+ moves the stopping point and `finish_reason`.
223
+ - `--batch 1` — strict serial, arrival-independent numerics. (`--batch N`
224
+ is itself bit-parity with mlx-lm at B=N; pin 1 for golden regeneration.)
225
+ - Already matching without flags: host/port, bf16 KV (`--kv-quant off`),
226
+ the L1 kernel preset (`--l1` is the default), logprobs caps, error text.
227
+
228
+ The kill switches are bit-exact A/B levers; **the naked default is the L1
229
+ set** (2026-07-05: an output-changing lever earns a default only by beating
230
+ the L1 baseline in a paired A/B, and none did — the losing kernels were
231
+ deleted, Phase 1 of
232
+ [unified-engine-frontier-plan.md](../design/unified-engine-frontier-plan.md)).
233
+ They are installed as `MLX_BUN_*` runtime values before the model loads,
234
+ so they apply to `mlx-bun pi` too. They affect the **serial** decode path;
235
+ the batch scheduler drives `model.forwardHidden` directly and is unaffected
236
+ except by `--compiled-decode` at B=1 — see
82
237
  [Levers that don't reach the batched lane](#--batch-n-is-compat-mode--perf-flags-dont-apply-by-design).
83
238
 
239
+ ## `--isolate` semantics
240
+
241
+ Design and measurements: [docs/reference/server-config.md](./server-config.md)
242
+ (P1 single engine and P2 model pool landed). What the flag does, from
243
+ `src/serve/isolate.ts` and the `serve` handler in `src/cli.ts`:
244
+
245
+ - **Process layout.** The parent binds the TCP `--host`/`--port` and makes
246
+ zero MLX calls. It re-execs itself as `serve` with the same argv minus
247
+ the **parent-only flags** (`--isolate`, `--port`, `--host`, `--model-pool`,
248
+ `--no-open`), the model selectors replaced by a pinned `--model <path>`,
249
+ plus `--unix <socket>` (socket under the OS temp dir, named by parent
250
+ pid). Every other flag and every exported `MLX_BUN_*` variable reaches
251
+ the engine child unchanged (`env: process.env`); the child's stdout/stderr
252
+ are inherited, so load progress prints in your terminal.
253
+ - **Readiness.** The parent polls the child's `/health` over the socket
254
+ (up to 15 minutes — large models take a while) and prints "engine ready
255
+ pid N". Requests arriving earlier wait on that readiness.
256
+ - **Application state.** The web shell, job submissions, records, progress streams, settings and
257
+ Responses conversation history live in the CPU-only parent, so worker death or model eviction does not remove
258
+ them. Native inspection/merge/export and generation still execute in workers.
259
+ WebSocket chat retains its existing unsupported status under isolation.
260
+ - **Proxying.** Remaining HTTP requests are forwarded over the unix
261
+ socket (hop-by-hop headers stripped, SSE streams through, a client
262
+ abort aborts the proxied fetch so the engine sees the disconnect).
263
+ `GET /engine` answers from the parent:
264
+ `{ isolated: true, pid, restarts, socket, response_store, pool?: { resident, default } }`.
265
+ Worker fields describe the currently resident default worker and are `null`
266
+ while it is evicted; inspection does not load a model. A recreated worker has
267
+ its own restart count.
268
+ `/ws/chat` is **not proxied** — `501` with a pointer to run without
269
+ `--isolate` for the web chat UI.
270
+ - **Crashes.** A child exit (uncatchable Metal OOM/SIGTRAP included) is
271
+ respawned automatically (`restarts` increments), with at most three restarts
272
+ in a rolling 60-second window. Exhausting that budget leaves the host
273
+ unavailable until restarted. An engine that dies within 10 s of spawning
274
+ waits 5 s before the retry. In-flight requests
275
+ get `502 { error: { type: "engine_unavailable" } }`; bodyless `GET`/`HEAD`
276
+ requests are retried once after the respawn. A request whose client
277
+ disconnected answers `499`.
278
+ - **Model pool (`--model-pool n`).** `POST` bodies on `/v1/chat/completions`,
279
+ `/v1/completions`, `/v1/messages`, `/v1/responses`, `/v1/embeddings` are
280
+ buffered to read `model`. An **exact** registry id (`/v1/models` id)
281
+ spawns/routes to that model's own child; anything else — empty, fuzzy,
282
+ `"gpt-4"`-style — rides the default engine (mlx-lm's ignored-field
283
+ semantics). Eviction over the cap: `POST /admin/drain` on the victim's
284
+ socket (gateway quiesce + demote its prompt cache to the SSD tier),
285
+ then stop; `/admin/drain` is unix-socket-only, never on the TCP
286
+ listener. Cold model starts are serialized through a bounded queue while the
287
+ resident model keeps serving; concurrent switches cannot evict a newly loaded
288
+ worker before its pending request is returned. `--model-pool` is clamped to ≥ 1.
289
+ - **Managed GPU jobs.** A FIFO parent coordinator waits for active worker
290
+ responses, then holds a native execution lease inside every resident worker
291
+ before spawning a quantization/finetuning job. Startup, restart and eviction
292
+ participate in admission too. Job exit releases those leases. Dataset jobs use
293
+ the same loopback completion API and keep their progress in the parent.
294
+ - **Shutdown.** `SIGINT`/`SIGTERM` cancels queued jobs, stops the active GPU job,
295
+ waits for in-process task cleanup, then closes workers and unlinks sockets.
296
+ Worker close also waits for pending startup admission and failed startup
297
+ attempts to release their execution leases, including before a child exists.
298
+
299
+ ## Near-ceiling models on small machines (24 GB)
300
+
301
+ A model whose weights approach the default macOS GPU wired ceiling (~75%
302
+ of RAM) loads and serves short prompts, but a long prefill can exceed the
303
+ ceiling and die with an **uncatchable** Metal OOM. `mlx-bun serve` checks
304
+ this at startup (model bytes > 80% of `maxRecommendedWorkingSetSize` while
305
+ `iogpu.wired_limit_mb` is at its default) and prints the remedy:
306
+
307
+ ```
308
+ sudo sysctl iogpu.wired_limit_mb=<RAM_MB - ~2500> # resets on reboot
309
+ ```
310
+
311
+ For very long contexts on such machines, additionally prefer `--kv-quant`
312
+ (e.g. `turbo`) — the prefill transient scales with chunk size × context.
313
+
84
314
  ## Per-request overrides
85
315
 
86
316
  Most quality knobs can be set per request in the chat body and override
@@ -90,331 +320,433 @@ the server-wide default. Precedence, highest first:
90
320
  `min_p`, `xtc_probability`/`xtc_threshold`, `logit_bias`,
91
321
  `repetition_penalty`, `presence_penalty`/`frequency_penalty` (+ their
92
322
  `*_context_size` windows), `max_tokens`/`max_completion_tokens`, `stop`,
93
- `chat_template_kwargs.enable_thinking`, `adapter`, `tools`)
94
- 2. the matching `--temperature`/`--top-p`/`--top-k`/`--thinking` server default
323
+ `chat_template_kwargs.enable_thinking`, `reasoning_effort`, `adapter`,
324
+ `tools`)
325
+ 2. the matching `--temperature`/`--top-p`/`--top-k`/`--thinking`/`--max-tokens`
326
+ server default
95
327
  3. the model's `generation_config.json`
96
- 4. the built-in fallback (temp 0.7, top-p/top-k 0 = off)
328
+ 4. the built-in fallback (temperature 0.7, top-p/top-k 0 = off)
97
329
 
98
330
  These ride all three protocol surfaces (`/v1/chat/completions`,
99
331
  `/v1/messages`, `/v1/responses`) because each funnels through one
100
- `handleChat`. Full field list: [server-api.md](./server-api.md).
332
+ chat stage (`src/serve/chat-stage.ts`). Full field list: [server-api.md](./server-api.md).
333
+
334
+ ## Environment variables
101
335
 
102
- ## Environment levers
336
+ All `MLX_BUN_*` variables are captured once at process start into an
337
+ immutable snapshot (`src/runtime-config.ts`); CLI flags install overrides
338
+ into the same snapshot before the model loads. Boolean levers read the
339
+ literal strings `"1"`/`"0"` — `flagOn` treats any other value as unset.
340
+ Under `--isolate` the whole environment is inherited by the engine child.
103
341
 
104
- The CLI flags above set these; you can also export them directly (e.g.
105
- for paired A/B harnesses).
342
+ ### Serving levers (flag-backed and lane kill switches)
106
343
 
107
344
  | Env var | CLI flag | Default | Effect |
108
345
  | --- | --- | --- | --- |
109
- | `MLX_BUN_COMPILED_DECODE` | `--compiled-decode` | on (`!=="0"`) | Compiled decode graph replay. |
346
+ | `MLX_BUN_COMPILED_DECODE` | `--compiled-decode` | on (`"0"` disables) | Compiled decode graph replay (serial lane; batch lane at B=1). |
347
+ | `MLX_BUN_EARLY_FIRST_TOKEN` | — | off (`=1`) | Serial/native generation yields token zero before constructing the next decode step. This can reduce first visible output latency when token zero contains visible text. Later decode remains pipelined. Serial fill, grammar, checkpoint resume and single-token budgets retain their existing order. A native consumer stopping at the first yield leaves a prompt-only cache entry with matching usage. Reusing that cache with a longer suffix can change prefill shapes and subsequent numerics; reconciling this boundary remains a promotion gate. The continuous scheduler also yields after preparation creates its first active row when no other request is queued, allowing prepared output to flush before decode. It then rechecks cancellation, admission and shutdown; queued short admissions still group together. The setting is captured by generation and the batch runtime. Experimental pending broader cached/pressure and quiet-machine acceptance. |
348
+ | `MLX_BUN_TURBOQUANT_FUSED_DECODE` | — | off (`=1`) | Experimental packed K/V decode fusion for an existing `--kv-quant turbo:...` cache, captured when each cache is created. A shared Metal operation unpacks keys and values, applies the existing key zero/scale and Lloyd-Max value scale, and preserves the codec's eager or deferred inverse rotation. It accepts supported bit widths, head dimensions 64/128/256/512 and 32/64-element groups with fp16/bf16/f32 metadata. Eager k8v3 with B1/H4, head dimension 256, fp16 metadata, group32 and at least 8192 cached tokens also fuses inverse rotation; other shapes keep the existing rotation path. Unsupported inputs, CPU streams and shapeless traces retain ordinary operations. Quantization, stored cache format and serving eligibility are unchanged. Joint-decoder Qwen native/serial and repeated long-context HTTP gates pass. The inverse operation passes both integrated Qwen model gates and six HTTP pairs per quant. MiniCPM/Gemma deferred-consumer serving gates also pass. Combined settings, pressure and quiet M4 Pro acceptance remain. |
110
349
  | `MLX_BUN_NO_FUSED_SDPA` | `--fused-sdpa` (inverted) | follows `--kv-quant` | `=1` forces the stock unfused SDPA everywhere. |
111
- | `MLX_BUN_COMPILED_GEGLU` | `--compiled-activations` | on (`!=="0"`) | Gemma geglu via mlx-lm's `@mx.compile` closure — the faithful default (bit-exact vs mlx-lm, one kernel). `=0` → uncompiled composition (same parity, slower). |
112
- | `MLX_BUN_COMPILED_SWIGLU` | `--compiled-activations` | on (`!=="0"`) | `mx.compile`'d SwiGLU (`silu(gate)·up` → one kernel) on MiniCPM5 decode (M=1), porting mlx-lm's `activations.py`. Bit-exact (passes the exact logit-parity gate), both lanes. +5.5% CPM5 decode. (qwen3/qwen3.5/universal compile swiglu unconditionally, independent of this flag.) |
113
- | `MLX_BUN_FORCE_WIRE` | `--force-wire` | off (`==="1"`) | Wire weights for the generation. |
114
- | `MLX_BUN_PAGED_KV` | `--paged-kv` | off (`==="1"`) | Paged KV cache (see the flag row above — v1: serial, Gemma4, bf16; refusals + prompt-cache bypass apply identically). |
115
- | `MLX_BUN_ALLOW_PRIVATE_MEDIA` | `--allow-private-media` | off (`==="1"`) | Permit `image_url`/`audio_url` fetches to private/loopback/link-local hosts (the SSRF guard's LAN escape hatch — see the flag row above; timeout + size cap still apply). |
116
- | `MLX_BUN_PREFILL_TAIL_SPLIT` | — | on (`!=="0"`) | Oracle prefill convention (mlx-lm `generate_step` and its server's batched engine): drain the prompt only to len−1, then compute step-0 logits from a separate **L=1 forward of the last prompt token**. Both lanes. **The SPEC lane (`--draft-model`) follows its own oracle's shape under the same flag** (mlx-lm `speculative_generate_step`, re-anchored 2026-07-07): both target AND draft drain to len−1 and there is **no separate step-0 at all** — the un-drained last prompt token heads the first verify window. Gated live: token-for-token vs the oracle venv incl. a knife-edge cell (tests/spec-serve.test.ts "L1 knife-edge"). (The standalone optiq-oracled assistant loop `specGenerate` deliberately keeps FULL-prompt prefill — optiq's own convention.) `=0` restores the pre-2026-07-07 full-final-chunk convention everywhere (A/B lever + kill switch) — that convention is ulp-different in bf16 at step 0 AND in the last prompt token's stored KV, which flips near-tie greedy streams vs mlx-lm (the 2026-07-07 12B completion-probe divergence). |
117
- | `MLX_BUN_GRAMMAR` | — | on (`!=="0"`) | `=0` disables structured-output mask compilation. Requested constraints still take the documented graceful-degrade route: chat prompt injection plus a `Warning` header; raw completions emit the header only. |
118
- | `MLX_BUN_GRAMMAR_JUMP` | — | **off** (`==="1"`) | Jump-forward decoding for structured output (SGLang's technique via xgrammar's `findJumpForwardString`): when the grammar forces a unique continuation (JSON punctuation/keys; longest under `any_whitespace:false` fixed formatting), the SERIAL lane emits its retokenized ids with ONE multi-token forward instead of one masked forward per token. Lossless in STRING space and always grammar-valid, but the token stream (and, conditioning on it, content after a forced span) can legally differ from an unjumped run — no oracle, hence opt-in. Excluded automatically when either `logprobs` or `top_logprobs` is requested, because jumped tokens have no sampled distribution row; SentencePiece-family tokenizers whose raw encode can't reproduce a mid-stream span simply never jump (guard degrades to normal decode). The batch lane's per-row grammar (`#stepGrammar`) does not jump yet. Tests: tests/grammar-jump.test.ts (contract + Llama-3.2-1B e2e). |
119
- | `MLX_BUN_SSD_WRITEBEHIND` | — | on (`!=="0"`) | `--ssd-cache`'s debounced write-behind snapshot (restart survival). `=0` disables it entirely — the paired-A/B lever + kill switch for the 2026-07-07 idle-gating fix (eviction/demotion spills still write). The flush is **idle-gated**: every per-tensor step (blocking GPU sync + `writeSync`) waits for the engine to go idle and pauses when a request arrives mid-flush, so durability work never taxes an active decode (pre-fix: a ~16k entry's flush overlapping cached ctx repeats cost e4b ~9% decode@ctx vs mlx-lm). The snapshot timer also never holds batch admission — while rows are active it re-arms instead of grabbing the exclusive. |
120
- | `MLX_BUN_SSD_SPILL_QUEUE_GB` | — | 2 | Byte cap on the write-behind queue's PENDING spill clones (they pin the evicted entries' GPU memory until the idle-gated flush runs). Over cap, the oldest queued spill drops — its clones free immediately and the entry becomes a future cache miss (never a wrong result). `=0` keeps only the newest + in-flight clone pinned (the soft cap never drops the item just enqueued). `/stats.ssd_cache` reports `pending_spills` / `pending_spill_bytes` / `dropped_spills`. Note: there is deliberately **no shutdown flush** — exiting under traffic loses queued write-behind entries (best-effort tier; cost = one re-prefill). |
121
- | `MLX_BUN_BATCH_NO_PIPELINE` | — | off (`==="1"`) | Disables the batch lane's pipelined token readback (the A/B lever for the shipped pipelining, like its peers `MLX_BUN_BATCH_EXTEND` / `MLX_BUN_BATCH_SSM` / `MLX_BUN_BATCH_VEC_SAMPLE`). Numerically equivalent, slower steps. |
122
- | `MLX_BUN_DSPARK_MINCONF` | — | checkpoint-driven | Overrides the DSpark/DeepSpec draft scheduler's minimum-confidence threshold (Alg-1 draft-length pruning). Spec lane only; no CLI flag — a calibration/A/B lever (see [dspark-speculative-decoding.md](../design/dspark-speculative-decoding.md)). |
123
- | `MLX_BUN_EXPERT_OFFLOAD` | `--expert-offload` | off (`==="1"`) | Env mirror of the flag for direct/library runs (scripts that never parse serve flags). |
124
-
125
- (`MLX_BUN_PERF_KERNEL`, `MLX_BUN_FUSED_GELU`, `MLX_BUN_FUSED_DECODE`,
126
- `MLX_BUN_FUSED_SWIGLU*`, and `MLX_BUN_CPM5_FAITHFUL` were deleted
127
- 2026-07-05 with their kernels — see
128
- [unified-engine-frontier-plan.md](../design/unified-engine-frontier-plan.md);
129
- exporting them now does nothing.)
350
+ | `MLX_BUN_COMPILED_GEGLU` | `--compiled-activations` | on (`"0"` disables) | Gemma geglu via mlx-lm's `@mx.compile` closure. `=0` → uncompiled composition (same parity, slower). |
351
+ | `MLX_BUN_COMPILED_SWIGLU` | `--compiled-activations` | on (`!== "0"`) | Compiled SwiGLU on MiniCPM5 decode and prefill, matching mlx-lm. An enclosing compiled graph owns its own fusion. qwen3/qwen3.5/universal compile unconditionally. |
352
+ | `MLX_BUN_FORCE_WIRE` | `--force-wire` | off (`=1`) | Wire weights for the generation. |
353
+ | `MLX_BUN_TRELLIS` | — | `kernel` (`=expand`) | Packed trellis-coded weights (`mode: "trellis"` modules, Q2b — design: `docs/design/turboquant.md`). `kernel` serves them through the Metal decode kernels (M≤4 matvec; larger M expands one tensor to bf16 and runs a stock matmul). `=expand` decodes every trellis tensor at LOAD into 8-bit g64 affine (+~4 GiB at 27B, the eval-carrier numerics) and serves it through the stock quantized path — the fallback for a machine where the kernels lose. |
354
+ | `MLX_BUN_TRELLIS_VARIANT` | — | `6` | Trellis kernel variant (see [Q2b experiments](../design/turboquant.md)): `6` = code computed inline × reciprocal, weight served as f32 code×scale; `1` adds a residual step and bf16 rounding to reproduce the fake-quant artifact's weights. `0`/`2`/`3`/`4`/`5` are bench-only decoder variants. Experimental `7`–`13` retain variant-6 decode values and tune work assignment; `11` tiles eligible short axis-1 prefills, `12` adds split-K axis-0 prefill at M=5..8, and `13` also vectorizes remaining bf16 expansion for k2/k3/k4, T=256, L=12. On MLX 0.32.2/M3+ the axis-1 M5..15 path instead uses direct packed decoding with native wide-matvec arithmetic when Qwen's RMSNorm establishes aligned row-contiguous inputs; callers without that layout proof use native expansion/matmul. Qwen27B additionally selects an integer codebook for its interleaved k3 down projection at M=3/4 with bf16 activations; other calls retain the computed decoder. Prefill rounds weights to the activation dtype. |
355
+ | `MLX_BUN_TRELLIS_ASYNC_EXPAND` | — | off (`=1`) | Experimental variant-13 expansion scheduling. Submit an expanded projection asynchronously while MLX active allocation is below 75% of the device's recommended working set; retain blocking evaluation above it. Other variants are unchanged. Uses the execution's runtime-policy snapshot and preserves the caller's layer barriers. The threshold is not a total-memory cap. Qwen27B integrated native, repeated serial/continuous HTTP and both saved-agent pressure gates pass on M4 Pro 24 GB. Broader-model, combined-optimization and quiet M4 Pro acceptance remain. |
356
+ | `MLX_BUN_PAGED_KV` | `--paged-kv` | off (`=1`) | Paged KV cache; the same refusals and prompt-cache bypass as the flag. |
357
+ | `MLX_BUN_ALLOW_PRIVATE_MEDIA` | `--allow-private-media` | off (`=1`) | Permit media fetches to private/loopback/link-local hosts (timeout + size cap still apply). |
358
+ | `MLX_BUN_EXPERT_OFFLOAD` | `--expert-offload` | off | `=<dir>` — the path of a built expert-offload file, activated at module load (`src/expert-offload.ts`) for scripts and library runs that never parse serve flags. The CLI flag builds the file and activates it itself. |
359
+ | `MLX_BUN_PREFILL_TAIL_SPLIT` | — | on (`"0"` disables) | Oracle prefill convention: drain the prompt to len−1, then compute step-0 logits from a separate L=1 forward of the last prompt token (mlx-lm `generate_step` and its batched engine). Both lanes. The spec lane follows its own oracle's shape under the same flag (mlx-lm `speculative_generate_step`: target and draft drain to len−1, no separate step 0). `=0` restores the full-final-chunk convention everywhere — ulp-different at step 0, flips near-tie greedy streams vs mlx-lm. |
360
+ | `MLX_BUN_GRAMMAR` | — | on (`"0"` disables) | `=0` disables structured-output mask compilation; requested constraints take the graceful-degrade route (chat prompt injection + a `Warning` header; raw completions header only). |
361
+ | `MLX_BUN_TOKEN_MASK` | `host` \| `metal` | `host` | Experimental grammar-mask implementation. `metal` uploads packed bits and applies the same additive mask in a compiled shared Metal kernel. Captured when the grammar controller is created; serving performance gates remain open. |
362
+ | `MLX_BUN_GRAMMAR_JUMP` | — | **off** (`=1`) | Jump-forward decoding for structured output (SGLang's technique via xgrammar's `findJumpForwardString`): when the grammar forces a unique continuation, the **serial** lane emits its retokenized ids with one multi-token forward. Lossless in string space and always grammar-valid, but the token stream can legally differ from an unjumped run — no oracle, hence opt-in. Skipped when `logprobs`/`top_logprobs` is requested; SentencePiece-family tokenizers that can't reproduce a mid-stream span never jump. The batch lane does not jump. |
363
+ | `MLX_BUN_FILL` | — | **off** (`=strict` \| `=echo`) | **Token fast-forwarding** — lookup, not speculation. Spans of the assistant turn that the request's `tools` + the chat template already DETERMINE (tool-call open scaffold, the rest of a tool name after its first disambiguating token, a sole-required-key skeleton) are appended as committed context in bounded model forwards, and sampling resumes after them. No draft, no verify, no rollback — an injected token is context, indistinguishable to the model from one it sampled. Rows are compiled per request by diffing probe renderings of the model's OWN template, always sliced from a rendering carrying the REAL tool names and schema keys (so a span can never split a merged token such as Qwen3.5's `=get`), so a template that does not render `tool_calls` yields no rows and no fill. Key and call-close assertions require a closed object schema with exactly one required property and no pattern properties; optional/additional arguments stay available. If no property is required, the compiler preserves the empty-argument choice. Template-derived rows require a fresh request-local parser context. Tool-only output after closed reasoning qualifies; quoted/fenced examples, incomplete delimiters and ambiguous literals decline. Requests with more than 32 tools decline template-derived fills. Held-out identity coverage remains open. **Serial lane only** (it never forces a request off the batch lane) and refused for: fixed `seed`, `logprobs`/`top_logprobs`, structured output (grammar owns forced tokens), image/audio/video prompts, a mounted draft model, quantized/TurboQuant KV, and sliding-window models (one warning). Injection bypasses the sampler — a behavior-policy deviation at `temperature > 0` — hence opt-in. `=echo` additionally arms the **echo index** (Lab tier): spans copied from earlier in the same session, held under policy `verify` — the engine appends them in the same single forward, reads the argmax already in that forward's logits at every span position (free, no extra pass), keeps the prefix the model agrees with, and rewinds the rest through the same cache contract the spec lane's rounds use. A wrong echo costs a rewound forward, never a wrong token. Strict rows stay policy `assert` (no readback, no checkpoint). Telemetry: `usage.fill`. |
364
+ | `MLX_BUN_FILL_MAX_SPAN` | — | `32` (floor 2) | Hard cap on one injected span. |
365
+ | `MLX_BUN_FILL_APPEND_CHUNK_SIZE` | — | `0` (model limit) | Execution chunk cap for assert-policy fill, captured by each FillSession. Zero selects the model's qualified limit; a positive value can lower that limit. The Qwen 27B text path on `applegpu_g16s`, validated on the M4 Pro, supports up to four positions with supported affine projections and optional Trellis MLPs. It rechecks the limit after each chunk and splits at MLX's attention arithmetic boundaries to preserve one-token numerics. Other configurations use one position per forward. The engine commits the complete span before emission and resumes sampling after it, with no intermediate vocabulary heads or verification. Verify-policy proposals keep a single forward for recurrent rollback. The injected-token cap is unchanged. Library callers can set `FillSession`'s `appendChunkSize` option. Held-out, broader-model and quiet M4 Pro acceptance remain. |
366
+ | `MLX_BUN_FILL_K` | — | `8` (floor 2) | Echo anchor length: the k-gram that must match before a copied span is proposed (the corpus study's token-level threshold). |
367
+ | `MLX_BUN_FILL_CANDIDATES` | — | `24` | Echo bucket-scan cap — the NEAREST occurrences of the anchor win. A span extends only while every one of them agrees; the first disagreement (a **branch point**) ends it. |
368
+ | `MLX_BUN_FILL_INDEX_MAX` | — | `131072` (floor 1024) | Token cap on the growing echo index; past it the index freezes rather than growing. |
369
+ | `MLX_BUN_FILL_TRACE` | — | off (`=1` \| `=<file.jsonl>`) | `=1`: assert the fill cache-alignment invariant (`cache offset == prompt + forwarded`) on both sides of every append, and log each event. `=<file.jsonl>`: additionally append one record per proposal — proposed ids/text next to what the model's own logits said at every span position (position 0 = the in-flight sample, then argmax), for both policies; under `assert` that readback is trace-only. `bun scripts/fill.ts trace <file>` prints the list and per-position agreement. |
370
+ | `MLX_BUN_GRAMMAR_BATCH` | — | on (`"0"` forces serial) | `=0` routes grammar requests to the serial lane instead of per-row batch matchers (A/B + kill switch). |
371
+ | `MLX_BUN_BATCH_SSM` | — | on (`"0"` forces serial) | `=0` excludes SSMCache (Qwen3.5 gated-DeltaNet hybrids) from the batch capability gate → those models route serial. |
372
+ | `MLX_BUN_BATCH_EXTEND` | — | on (`"0"` reverts) | Joining rows append to the running batch's KV in one pad+concat (mlx-lm `BatchKVCache.extend`). `=0` reverts to whole-batch re-merge (numerically equivalent, O(B·S)). |
373
+ | `MLX_BUN_BATCH_VEC_SAMPLE` | — | on (`"0"` reverts) | Vectorized greedy batch sampling; `=0` falls back to per-row sampling (bit-equal A/B). |
374
+ | `MLX_BUN_BATCH_NO_PIPELINE` | — | off (`=1`) | Read each batch step's tokens synchronously instead of pipelined (A/B lever; numerically equivalent, slower). Read once at module load. |
375
+ | `MLX_BUN_SSD_WRITEBEHIND` | — | on (`"0"` disables) | `--ssd-cache`'s debounced write-behind snapshot (restart survival). Each tensor flush owns the generation lock, and requests can run between tensors. `=0` disables it; eviction and demotion spills still write. |
376
+ | `MLX_BUN_SSD_SPILL_QUEUE_GB` | — | `2` (GiB; any finite value ≥ 0) | Byte cap on pending write-behind spill clones (they pin evicted entries' GPU memory until the generation-locked flush runs). Over cap the oldest queued spill drops and frees immediately; the durability coordinator keeps its snapshot dirty so an explicit or shutdown flush can retry. `=0` keeps only the newest + in-flight clone pinned. Counters in `/stats.ssd_cache`. |
377
+ | `MLX_BUN_SHUTDOWN_TIMEOUT_MS` | — | `120000` (any finite value > 0) | Maximum time `serve` gives active requests plus the SSD durability flush after `SIGINT`/`SIGTERM`; on timeout it logs the remaining snapshot/spill counters and exits. |
378
+ | `MLX_BUN_DSPARK_MINCONF` | — | checkpoint-driven | Overrides the DSpark/DeepSpec draft scheduler's minimum-confidence threshold (draft-length pruning). Spec lane only ([docs/design/speculative-decoding.md](../design/speculative-decoding.md)). |
379
+ | `MLX_BUN_MEMORY_BATCH` | — | `1` (integer ≥ 1) | Row width for `mlx-bun memory` model calls. 1 = serial (batching measured 1.7–1.9× slower on the heterogeneous extract/chunk workload — [docs/design/dreaming-nightly-pipeline.md](../design/dreaming-nightly-pipeline.md)). |
380
+
381
+ ### Paths and tooling
382
+
383
+ | Env var | Default | Effect |
384
+ | --- | --- | --- |
385
+ | `MLX_BUN_LIBMLXC` | resolved: beside the executable → native-pack cache (`~/Library/Caches/mlx-bun/native-v<ver>-<arch>/`) → Homebrew | Explicit path to `libmlxc.dylib`; its directory is then the native runtime dir (`src/mlx/ffi.ts`, `src/native-pack.ts`). |
386
+ | `MLX_BUN_NATIVE_PACK_URL` | the GitHub release asset for `NATIVE_PACK_VERSION` | Override the native runtime pack download URL (first-run fetch, sha256-verified). |
387
+ | `MLX_BUN_FRAME_EXTRACT` | resolved: beside the executable → native-pack cache → `dist-native/` → compile from `src/native` | Explicit path to the `mlx-bun-frame-extract` AVFoundation sidecar used for video input. |
388
+ | `MLX_BUN_EXPERT_IO_DYLIB` | resolved: beside the executable → native-pack cache → `dist-native/` | Explicit path to `libmlx_bun_expert_io.dylib` (GLM-5.2 streamed expert I/O). |
389
+ | `MLX_BUN_WIKI` | `~/.mlx-bun/wiki` | Root of the memory vault (`~` expands). |
390
+ | `MLX_BUN_JOBS_DB` / `MLX_BUN_JOBS_DIR` | the `JobStore` defaults | Job-store SQLite path and logs dir. The job runner sets both on the subprocess it spawns so the child opens the same store; override for tests. |
391
+ | `MLX_BUN_CALIBRATION_JSONL` | bundled optiq mix in the oracle venv | Explicit calibration corpus for `mlx-bun convert`/quantize. |
392
+ | `MLX_BUN_ORACLE_PYTHON` | `/Users/joshrossi/Code/mlx-lm/.venv/bin/python` (the reference box; machine-specific, never re-commit) | Oracle venv interpreter for evals that shell out to Python (HumanEval). |
393
+ | `MLX_BUN_EVAL_DATA` | `~/.cache/mlx-bun/eval-data` | Directory of exported eval `.jsonl` datasets. |
394
+
395
+ ### Diagnostics and tracing (off unless set)
396
+
397
+ | Env var | Value | Effect |
398
+ | --- | --- | --- |
399
+ | `MLX_BUN_P2R_TRACE` | `=1` | Per-request prompt→response phase trace (admission wait, prefill, token-zero, …) for `/v1/chat/completions` and `/v1/completions`; records print to stderr as JSON lines. The trace id is `x-mlx-bun-trace-id` when the request sends it. |
400
+ | `MLX_BUN_P2R_SYNC` | `=1` (with `MLX_BUN_P2R_TRACE`) | Attribution mode: synchronizes the GPU at phase boundaries so each phase is charged its own work (MLX is lazy — without it, unsubmitted work lands in the next phase). Slows the traced request. |
401
+ | `MLX_BUN_LANE_DEBUG` | `=1` | Logs each request's scheduling placement (`mechanism` + shape) to stderr. |
402
+ | `MLX_BUN_BATCH_STEP_TRACE` | `=1` | Per-step phase timing in the batch scheduler (build / read / emit / gap), read once at module load; summarized by `stepTraceReport()`. |
403
+ | `MLX_BUN_GRAMMAR_DEBUG` | `=1` | Logs per-step grammar row state in the batch scheduler. |
404
+ | `MLX_BUN_PREFILL_MEM_LOG` | `=1` | Logs active/peak memory after each serial prefill chunk. |
405
+ | `MLX_BUN_EXPERT_TRACE` | `=<path>` | Records every MoE router decision as JSONL to that path (adds a per-call GPU→host sync — a measurement tool, not a serving path). |
406
+ | `MLX_BUN_PI_DEBUG` | any non-empty | Extra `[pi-web]` logging (prompt fingerprint, tool/memory surface) for the web-chat pi session. |
407
+ | `MLX_BUN_EVAL_DEBUG` | `=1` | HumanEval: pipe the sandbox's stderr and print failures. |
408
+
409
+ ### Eval and training
410
+
411
+ | Env var | Default | Effect |
412
+ | --- | --- | --- |
413
+ | `MLX_BUN_EVAL_THINK` | off (`=1`) | Evaluate with `enable_thinking` on (default off for parity with optiq's published non-thinking numbers). |
414
+ | `MLX_BUN_EVAL_KV_QUANT` | off (`=1`) | Generate through the model's `kv_config.json` quantized KV during evals (default full-precision KV, matching optiq's eval). An explicit `--kv-quant` on `mlx-bun eval` wins. |
415
+ | `MLX_BUN_MMLU_FROZEN` / `MLX_BUN_GSM8K_FROZEN` / `MLX_BUN_HUMANEVAL_FROZEN` / `MLX_BUN_IFEVAL_FROZEN` / `MLX_BUN_BFCL_FROZEN` / `MLX_BUN_HASHHOP_FROZEN` | on (`"0"` reverts) | Use the frozen oracle-exported problem sets (parity with the reference numbers); `=0` falls back to our own sampling of the full set. |
416
+ | `MLX_BUN_TRAIN_ATTN` | `ops.sdpa` (unset) | `=flash` selects the hand-rolled O(L)-memory flash kernel for training attention (~30× slower). **Refused for Gemma models** (e4b SIGTRAPs at seq ≥ 2048); MiniCPM5 allowed. |
417
+ | `MLX_BUN_FLASH_MIN_M` | `1024` | With a flash-CCE head requested, rows shorter than this take the exact fused head instead; `=0` always honors flash. |
418
+ | `MLX_BUN_MEM_LOG` | off (`=1`) | Trainer per-step memory logging. |
419
+
420
+ `MLX_BUN_CCE_*` (flash-CCE kernel variant selectors and epsilons in
421
+ `src/train/flash-cce.ts`) and `MLX_BUN_SEG_*` (segmented-backward head and
422
+ memory probes in `src/train/segmented.ts`) are intentionally undocumented
423
+ developer knobs; `MLX_BUN_EVAL_KV_ARM_NOOP` is an inert placeholder the KL
424
+ harness flips.
425
+
426
+ ### Removed
427
+
428
+ `MLX_BUN_PERF_KERNEL`, `MLX_BUN_FUSED_GELU`, `MLX_BUN_FUSED_DECODE`,
429
+ `MLX_BUN_FUSED_SWIGLU*`, `MLX_BUN_CPM5_FAITHFUL`, and the `--l3` tier were
430
+ deleted 2026-07-05 with their kernels
431
+ ([unified-engine-frontier-plan.md](../design/unified-engine-frontier-plan.md));
432
+ exporting the variables now does nothing, and `--l3` exits with a pointer.
130
433
 
131
434
  ## Execution modes: serial vs. `--batch N`
132
435
 
133
- Batching is **concurrency-driven** (default cap 8, flipped 2026-07-05): a
134
- lone request runs the exact serial engine — its caches are ADOPTED
135
- serial-class objects, compiled decode replays, the prompt cache and SSD
136
- tier serve it — and only a second concurrent request causes a batch
137
- layout to exist. "How many requests you send" is the batching decision;
138
- the flag is just the cap.
436
+ Scheduling is **concurrency-driven** (default cap 8): a lone scheduler
437
+ request uses the B=1 fast path — its caches are adopted serial-class
438
+ objects, compiled decode replays, and the prompt cache and SSD tier serve
439
+ it. Only a second concurrent request causes a batch layout to exist. The
440
+ flag declares the concurrency cap; active rows select B=1 or B=N inside
441
+ the scheduler.
139
442
 
140
443
  `--batch 1` pins the strict serialized single-queue path: one generation
141
444
  at a time, arrival-independent numerics (a request's bits never depend on
142
445
  what else was in flight). Pin it for golden regeneration and
143
- reproducibility work. Note the trade is real but small: batched rows
144
- carry bf16 left-pad reduction-order noise vs their solo runs (calibrated
145
- per model in the gated suites) — a request that never shares a step is
146
- bit-identical either way.
446
+ reproducibility work. Batched rows carry bf16 left-pad reduction-order
447
+ noise vs their solo runs (calibrated per model in the gated suites) — a
448
+ request that never shares a step is bit-identical either way.
147
449
 
148
450
  The batched engine is **bit-parity with `mlx_lm.server` at `B=N`** per
149
- row for bf16, and per-row oracle-gated for the quantized compositions
150
- (Phase 3.1 + milestone 2 — compositions mlx-lm does not ship):
451
+ row for bf16, and per-row oracle-gated for the quantized compositions:
151
452
 
152
- - **KV quant unset ⇒ bf16** — the batch path engages out of the box
153
- (bf16 is the serial default too, since 2026-07-05).
453
+ - **KV quant unset ⇒ bf16** — the batch path engages out of the box.
154
454
  - **`--kv-quant config`** (per-layer mixed precision) **batches** on
155
- every shipped model — full-attention layers since Phase 3.1 (cpm5),
156
- rotating layers since milestone 2 (gemma) — applied per row, gated
157
- bit-exact/KL-0 for unpadded rows vs the serial composition.
158
- - **`--kv-quant 4|8`** (uniform bits) routes those requests to the
159
- serial lane (quantizedKvStart threshold semantics; a startup warning
160
- is printed).
161
-
162
- ### The lane picker (`GenerationGateway.willBatch`)
163
-
164
- Under `--batch N`, each request is routed per-request. It joins the
165
- batch only if **all** of these hold; otherwise it drains the batch and
166
- runs solo (mlx-lm's `_is_batchable` behavior). The two lanes are
167
- mutually exclusive on the GPU (one `AsyncMutex`), so a serial-fallback
168
- request never runs alongside a batched step — but batched requests run
169
- concurrently with each other.
170
-
171
- | Request property | Batches? |
455
+ every shipped model — full-attention and rotating layers — applied per
456
+ row, gated bit-exact for unpadded rows vs the serial composition.
457
+ - **`--kv-quant 4|8`** (uniform bits) and **`turbo`** route those
458
+ requests to the serial lane.
459
+
460
+ ### Scheduling declaration (`GenerationGateway.place`)
461
+
462
+ With a cap greater than one, `GenerationGateway.place()` freezes one
463
+ scheduling mechanism for the exact resolved request shape: `continuous`
464
+ admits it to the scheduler (which then picks B=1 or B=N from active rows);
465
+ `serial` preserves the strict executor. The support check never removes,
466
+ downgrades, or substitutes MTP, KV schemes, TurboQuant, grammar, adapters,
467
+ or sampling — a composition the scheduler does not implement drains it and
468
+ runs unchanged serially. The two mechanisms are mutually exclusive on the
469
+ GPU (one `AsyncMutex`).
470
+
471
+ | Request property | Continuous scheduler? |
172
472
  | --- | --- |
173
- | vision (image parts) | ❌ serial — needs offset-0 single-seq prefill + bidirectional image mask |
174
- | LoRA `adapter` (resolves to ≥1) | ❌ serial — `loraState.active` is one per-generation field; per-row adapters unsupported |
175
- | `logprobs` / `top_logprobs` | ❌ serial — the batched sampler doesn't capture logprob arrays yet |
473
+ | vision / audio / video parts | ❌ serial — offset-0 single-sequence prefill + media masks |
474
+ | LoRA `adapter` (resolves to ≥1) | ❌ serial — one active adapter per generation |
475
+ | `logprobs` / `top_logprobs` | ❌ serial — the batched sampler doesn't capture logprob arrays |
176
476
  | explicit `seed` | ❌ serial — reproducibility ⇒ solo (matches mlx-lm) |
177
- | KV quant active (explicit `--kv-quant`) | ✅ batches for per-layer `config` schemes — full-attention layers (Phase 3.1) AND rotating layers (milestone 2: gemma's kv_config; unpadded row gated KL-0 vs serial); uniform bits → serial |
178
- | `--kv-quant turbo` (TurboQuant) | ❌ serial, unconditionally — solo-only in v1 (novel `Cache` class, no batched merge/filter/temporalView; belt-and-braces refusal in `willBatch` on top of the automatic capability-gate exclusion) |
179
- | `--draft-model` mounted | ❌ serial, server-wide — speculation is a B=1 latency mode (upstream `is_batchable = draft is None`) |
180
- | `repetition_penalty` / `min_p` / `xtc_*` / `logit_bias` / presence+frequency penalties | ✅ batches — per-row logits processors over a per-row device-side history (since 2026-07-02; some models — Qwen3.5 — ship a *default* repetition penalty, which used to route everything serial) |
181
- | structured output (`response_format` / `guided_*`) | ✅ batches — per-row grammar matchers driven by the scheduler (`MLX_BUN_GRAMMAR_BATCH=0` forces serial) |
477
+ | KV quant active | ✅ batches for per-layer `config` schemes; uniform bits → serial |
478
+ | `--kv-quant turbo` | ❌ serial, unconditionally (novel `Cache` class, no batched merge/filter; explicit refusal in placement on top of the capability gate) |
479
+ | `--draft-model` mounted | ❌ serial, server-wide (`is_batchable = draft is None`) |
480
+ | `repetition_penalty` / `min_p` / `xtc_*` / `logit_bias` / presence+frequency penalties | ✅ batches — per-row logits processors over per-row device-side history (Qwen3.5 ships a *default* repetition penalty, so this is load-bearing) |
481
+ | structured output (`response_format` / `guided_*`) | ✅ batches — per-row grammar matchers (`MLX_BUN_GRAMMAR_BATCH=0` forces serial) |
182
482
  | `temperature` / `top_p` / `top_k` | ✅ batches (each row samples with its own seed) |
183
- | `stop` sequences | ✅ batches (per-row `StopMatcher` in the onToken closure) |
184
- | `tools` / `tool_choice` | ✅ batches (per-row tool router; decode-layer parse) |
185
- | `--thinking` / `enable_thinking` | ✅ batches (template-render concern, lane-independent) |
186
- | multi-turn / long prompt | ✅ batches **with prompt-cache reuse** (Phase 3.2): a joiner restores the longest usable cached prefix and prefills only the suffix (`cached_tokens` reported); a request that finishes without ever sharing the batch puts its caches back. **Prefix sharing (2026-07-05): serves are non-consuming clones**, so concurrent agents with a shared system prompt all reuse one prefill and nobody's entry is destroyed. A row that merges with others is not re-put (its entry ages out) — heavy concurrency reduces hit rate, never correctness |
187
-
188
- **Which models batch:** full-attention (CPM), sliding-window (Gemma),
189
- hybrid gated-DeltaNet (Qwen3.5 — the SSM batched path, token-exact vs the
190
- mlx-lm B=2 oracle, landed 2026-07-02; `MLX_BUN_BATCH_SSM=0` reverts it to
191
- serial routing), and **plain full-attention Tier-0 universal archs**
192
- (Llama etc. — per-row RoPE gated token-exact vs mlx-lm B=2, 2026-07-03).
193
- Still serial by the model-level capability gate: gemma2-family and
194
- sliding-window *universal* archs (unvalidated cells) and DiffusionGemma
195
- (non-autoregressive).
196
-
197
- A non-batchable request **drains** the batch: while it waits, the
198
- scheduler stops admitting new rows, finishes the running ones, and
199
- releases the GPU so the serial request runs (mlx-lm's `drain_batch`);
200
- admission then resumes. So a steady stream of batchable traffic cannot
201
- starve a serial-lane request.
202
-
203
- `--batch N` is a **mode switch, not a load-dependent fallback**:
204
- auto-batching "when >1 request arrives" was considered and rejected —
205
- an idle vs. loaded server would produce different numerics for the same
206
- request, breaking determinism and the drop-in-for-`mlx_lm.server`
207
- promise.
483
+ | `stop` sequences | ✅ batches (per-row `StopMatcher`) |
484
+ | `tools` / `tool_choice` | ✅ batches (per-row tool router) |
485
+ | `--thinking` / `enable_thinking` | ✅ batches (template-render concern) |
486
+ | multi-turn / long prompt | ✅ batches with prompt-cache reuse: a joiner restores the longest usable cached prefix and prefills only the suffix (`cached_tokens` reported) |
487
+
488
+ **Which models use continuous scheduling** (`#modelCachesBatchable`):
489
+ full-attention (CPM), sliding-window (Gemma), hybrid gated-DeltaNet
490
+ (Qwen3.5 — `MLX_BUN_BATCH_SSM=0` reverts to serial routing), and plain
491
+ full-attention Tier-0 universal archs (Llama etc., gated token-exact vs
492
+ mlx-lm B=2). Still serial by the capability gate: gemma2-family
493
+ (`maskArray`) and sliding-window *universal* archs (unvalidated cells) and
494
+ DiffusionGemma (non-autoregressive).
495
+
496
+ A request requiring the serial mechanism **drains** the scheduler: while
497
+ it waits, the scheduler stops admitting new rows, finishes the running
498
+ ones, and releases the GPU (mlx-lm's `drain_batch`); admission then
499
+ resumes, so batchable traffic cannot starve a serial-lane request.
500
+ Placement is not load-dependent: a request admitted to the continuous
501
+ scheduler stays there whether it is the only row or one of many.
208
502
 
209
503
  ## Compatibility matrix
210
504
 
211
- How each option behaves in each mode. "serial" = `--batch 1`; "`--batch N`"
212
- = the batched mode (a given request may still take the serial *lane*
213
- inside it per the table above).
505
+ "serial" = `--batch 1`; `--batch N` permits continuous scheduling, though a
506
+ declared composition may still require the serial mechanism as shown above.
214
507
 
215
508
  | Option | serial (`--batch 1`) | `--batch N` (N>1) |
216
509
  | --- | --- | --- |
217
510
  | `--kv-quant config` | ✅ applied to all requests | ✅ batches where the loaded cache capability supports the per-layer scheme; otherwise routes serial |
218
511
  | `--kv-quant 4`/`8` | ✅ applied to all requests | ⚠️ uniform-threshold semantics route requests serial |
219
- | `--kv-quant turbo[:k<bits>v<bits>]` | ✅ applied to all requests | ⚠️ applied, but forces **all** requests to the serial lane (no batching) |
220
- | `--kv-quant off` | ✅ bf16 | ✅ bf16 (same as the implicit batch default) |
221
- | *(kv-quant unset)* | bf16 (the L1 default) | **bf16** (Option B) — incl. serial-lane fallback requests |
222
- | `--memory-budget` | ✅ per-request admission | ✅ per-request admission — but **not aggregate** across rows (see limitations) |
223
- | `--prompt-cache` | ✅ prefix reuse | ✅ prefix reuse on BOTH lanes (Phase 3.2): batch joiners `take()` at admission; lone never-merged rows `put()` back on finish (merged rows' entries age out) |
512
+ | `--kv-quant turbo[:k<bits>v<bits>]` | ✅ applied to all requests | ⚠️ applied, but forces **all** requests to the serial lane |
513
+ | `--kv-quant off` / unset | ✅ bf16 (the L1 default) | ✅ bf16 |
514
+ | `--paged-kv` | ✅ (pinned here by default) | ❌ startup refusal |
515
+ | `--memory-budget` | ✅ per-request admission | ✅ per-request admission — not aggregate (use `--kv-budget`) |
516
+ | `--kv-budget` | n/a | ✅ aggregate queue/reject across rows |
517
+ | `--prompt-cache` / `--ssd-cache` | ✅ prefix reuse + SSD restore | ✅ on both lanes: joiners `take()` at admission; never-merged rows `put()` back |
224
518
  | `--temperature`/`--top-p`/`--top-k` | ✅ | ✅ (per-row) |
225
519
  | `--thinking` | ✅ | ✅ |
226
- | vision request | ✅ | ✅ via serial lane (in bf16 under Option B) |
520
+ | vision / audio / video request | ✅ | ✅ via serial lane |
227
521
  | LoRA `adapter` | ✅ | ✅ via serial lane |
228
- | `repetition_penalty` | ✅ | ✅ (batches — per-row logits processors, since 2026-07-02) |
229
- | `min_p` / `xtc_*` / `logit_bias` / presence+frequency penalties | ✅ | ✅ (batches — same per-row processors) |
522
+ | `repetition_penalty` / `min_p` / `xtc_*` / `logit_bias` / presence+frequency | ✅ | ✅ (batches — per-row processors) |
230
523
  | `seed` | ✅ | ✅ via serial lane |
231
524
  | `tools` / `stop` | ✅ | ✅ (batches) |
232
525
  | structured output (`response_format`/`guided_*`) | ✅ (mask in the decode loop) | ✅ (batches; per-row matchers) |
233
- | `--draft-model` | ✅ spec decode (grammar composes) | ⚠️ mounts, but routes **every** request serial — spec and batching are different modes |
526
+ | `--draft-model` / `--draft-kind` | ✅ spec decode (grammar composes) | ⚠️ mounts, but routes **every** request serial |
234
527
  | GLM `--mtp on` | ✅ native MTP spec decode | ⚠️ default-on MTP routes every request serial+spec; `--mtp off` exposes ordinary GLM batching |
235
- | `--compiled-decode` | ✅ (serial decode route) | ✅ at **B=1 only** (Phase 3.2): a lone request's adopted serial-class caches replay the same compiled step, same kill switch; B>1 steps run the plain graph |
236
- | `--fused-sdpa`/`--force-wire` | ✅ (serial decode route) | **n/a — compat mode, no perf flags by design** |
528
+ | `--compiled-decode` | ✅ | ✅ at **B=1 only**: a lone request's adopted serial-class caches replay the same compiled step; B>1 steps run the plain graph |
529
+ | `--fused-sdpa` / `--force-wire` | ✅ (serial decode route) | n/a — compat mode, no perf flags by design |
237
530
 
238
531
  ### `--batch N` is compat mode — perf flags don't apply by design
239
532
 
240
533
  The bit-parity guarantee (mlx-lm B=N) is the *whole point* of `--batch N`,
241
- and it requires running the plain forward path. So the batched lane
242
- deliberately exposes **no** perf knobs: the scheduler
534
+ and it requires running the plain forward path. The scheduler
243
535
  ([batch-scheduler.ts](../../src/serve/batch-scheduler.ts)) drives the
244
536
  model through `forwardHidden`/`logitsFromHidden` directly (not
245
- `generate()`), running the same bit-exact kernels mlx-lm runs — never the
246
- optional, parity-breaking ones. Flagging the batched lane would defeat
247
- the guarantee, so it's intentionally not wired.
248
-
249
- - **`--compiled-decode`** — engages at **B=1 only** (Phase 3.2,
250
- adopt-don't-copy): a lone request's caches stay serial-class, so the
251
- scheduler replays the serial engine's compiled step — bit-exact by the
252
- same gate, `MLX_BUN_COMPILED_DECODE=0` is the same kill switch. The
253
- moment a second row joins, steps run the plain batched graph.
254
- - **`--fused-sdpa`** — never engages in the batched lane (the fused
255
- quantized-KV SDPA is an L2 serial-lane composition). See the
256
- validation matrix in [parallel-slots.md](../design/parallel-slots.md).
257
- - **`--force-wire`** — doesn't wire (the scheduler bypasses `generate()`'s
258
- wired scope). A model that needs wiring for speed (e.g. 26B: 8.6 → 32.3
259
- tok/s wired) runs at mlx-lm-equivalent unwired speed under `--batch N`;
260
- in practice such a model has no headroom for B>1 KV anyway.
261
- - **Always-on bit-exact kernels still run.** The compiled activations
262
- (mlx-lm's own `@mx.compile` geglu/swiglu) are bit-exact with the
263
- spelled-out MLP, so they stay on in both lanes without breaking
264
- parity — "compat mode" means *no parity-breaking optionality*, not
265
- -O0.
266
-
267
- The serial lane is where the decode route lives: `--compiled-decode` /
268
- `--fused-sdpa` / `--kv-quant` (and the `--l2` optiq composition) engage
269
- there.
537
+ `generate()`), so:
538
+
539
+ - **`--compiled-decode`** engages at **B=1 only** (adopt-don't-copy: the
540
+ lone request's caches stay serial-class, so the scheduler replays the
541
+ serial engine's compiled step — same kill switch). A second row ⇒ the
542
+ plain batched graph.
543
+ - **`--fused-sdpa`** never engages in the batched lane (an L2 serial-lane
544
+ composition).
545
+ - **`--force-wire`** doesn't wire (the scheduler bypasses `generate()`'s
546
+ wired scope).
547
+ - **Always-on bit-exact kernels still run.** The compiled activations are
548
+ bit-exact with the spelled-out MLP, so they stay on in both lanes —
549
+ "compat mode" means *no parity-breaking optionality*, not -O0.
270
550
 
271
551
  ## Known limitations under `--batch N`
272
552
 
273
- These are deliberate v1 scope, not bugs — but they change behavior, so
274
- know them:
553
+ Deliberate v1 scope, not bugs:
275
554
 
276
555
  1. **Prompt cache on the SPEC path is bypassed.** A `--draft-model`
277
- server re-prefills every request; the target+draft cache-entry
278
- composition is designed in mlx-lm-tool-parity-plan §7.6, not built.
279
- (Batched rows DO reuse the prompt cache since Phase 3.2 — see the
280
- lane table above and `/stats` `submitted_rows`.)
281
- 2. **Aggregate admission is opt-in.** `--memory-budget` checks each
282
- request against single-sequence max-safe-context; the AGGREGATE cap
283
- across N concurrent rows is `--kv-budget <GB>` (landed 2026-07-03):
284
- over-budget joiners queue until rows finish, a request over the budget
285
- alone rejects. Without `--kv-budget`, N large-context rows can still
286
- collectively exceed memory (uncatchable GPU OOM) — set it when running
287
- `--batch N` near the machine's limit.
288
- 3. **Short-context only.** Verified pre-ring-wrap (rows < the 1024
289
- sliding window). Long-context (context > window) batched decode is a
290
- separate validation.
291
- 4. **bf16 by default — by contract; mixed-KV batching beyond it.**
292
- mlx-lm's batched path *is* bf16, so bf16 batching is exactly what
293
- mlx-lm-parity means. Phase 3.1 adds batched per-layer MIXED KV for
294
- all-full-attention configs — a beyond-mlx-lm composition, verified
295
- per row against the optiq oracle instead (not a
296
- shortcoming). Going further — batched + mixed-precision KV quant — is
297
- novel territory with no mlx-lm/optiq oracle, so it's a deferred,
298
- KL-gated extension.
299
- 5. **`extend` join (landed 2026-07-03).** A joining request appends to
300
- the running batch's full-attention KV in one pad+concat (mlx-lm
301
- `BatchKVCache.extend` semantics, token-exact vs its oracle);
302
- `MLX_BUN_BATCH_EXTEND=0` reverts to the old whole-batch re-merge
303
- (numerically equivalent, O(B·S)). Sliding-window layers still re-merge
304
- on join (rotating-extend is a follow-up).
556
+ server re-prefills every request (the target+draft cache-entry
557
+ composition is designed in mlx-lm-tool-parity-plan §7.6, not built).
558
+ 2. **Aggregate admission is opt-in** via `--kv-budget`; without it N
559
+ large-context rows can collectively exceed memory.
560
+ 3. **Short-context only.** Verified pre-ring-wrap (rows < the 1024 sliding
561
+ window); long-context batched decode is a separate validation.
562
+ 4. **bf16 by contract; mixed-KV batching beyond it.** mlx-lm's batched
563
+ path *is* bf16. Per-layer `config` batching is a beyond-mlx-lm
564
+ composition verified per row against the optiq oracle. Batched
565
+ uniform/turbo KV has no oracle and is deferred.
566
+ 5. **`extend` join** appends a joining request to the running batch's
567
+ full-attention KV in one pad+concat (`MLX_BUN_BATCH_EXTEND=0` reverts
568
+ to whole-batch re-merge); sliding-window layers still re-merge on join.
305
569
 
306
570
  ## Fidelity tiers and the decode route (`--l1` / `--l2`)
307
571
 
308
- The tiers are **correctness contracts**, and each flag is an alias for a
309
- decode-route preset (`applyDecodeRoute()` in [src/cli.ts](../../src/cli.ts));
310
- any per-fork flag (`--kv-quant`, `--compiled-decode`,
311
- `--compiled-activations`, `--fused-sdpa`)
312
- overrides its tier's preset.
572
+ The tiers are **correctness contracts**; each flag is an alias for a
573
+ decode-route preset (`applyDecodeRoute()` in `src/cli.ts`), and any
574
+ per-fork flag (`--kv-quant`, `--compiled-decode`, `--compiled-activations`,
575
+ `--fused-sdpa`) overrides its tier's preset.
313
576
 
314
577
  | Tier | Contract | KV | Kernels | Verified against |
315
578
  | --- | --- | --- | --- | --- |
316
- | `--l1` | mlx-lm **bit-for-bit** | bf16 | compiled-decode + compiled activations (faithful geglu/swiglu) | mlx-lm goldens (per machine) |
317
- | `--l2` | mlx-optiq **bit-for-bit** | mixed-precision (`kv_config.json`) | + fused-SDPA (matches optiq exactly) | optiq goldens |
318
- | *(none)* | **= `--l1`** | bf16 | compiled-decode + compiled activations | mlx-lm goldens |
319
-
320
- Compiled-decode is on in **every** tier (proven bit-exact with uncompiled —
321
- free speed, not a fidelity trade).
322
-
323
- **There is no `--l3` tier anymore** (removed 2026-07-05; passing `--l3`
324
- errors out with a pointer). The tier had no content: every kernel it
325
- gated lost its paired A/B against the L1 baseline and was deleted. Its
326
- role is replaced by the **Lab** — output-changing experiments run as env
327
- flags with a bench + expiry, and one graduates only by beating the L1
328
- baseline in a paired A/B. See
329
- [docs/design/unified-engine-frontier-plan.md](../design/unified-engine-frontier-plan.md).
330
-
331
- **KV precision is a separate axis from the tier's kernels.** Each tier row shows its
332
- *default* KV scheme, but KV is not what defines the tier. mlx-lm supports **uniform**
333
- quantized KV (`--kv-bits`), and `--l1 --kv-quant 8` is a **bit-exact L1** config: with
334
- fused-sdpa off (the L1 default) our quantized decode runs `quantizedSdpaUnfused`,
335
- which is op-for-op identical to mlx-lm's `quantized_scaled_dot_product_attention`
336
- (`mlx_lm/models/base.py`) — same `mx.quantized_matmul` ×2 + `mx.softmax(precise=True)`
337
- + `where(…, finfo.min)`. bf16 is just the simplest L1 default. The **fused / N-tiled**
338
- quantized path (fused-sdpa ON — the `--kv-quant config` / `--l2` composition) is the
339
- **optiq**-aligned one. Only the **per-layer mixed-precision** `config` scheme is
340
- optiq-only (→ L2).
579
+ | `--l1` | mlx-lm **bit-for-bit** | bf16 | compiled decode + compiled activations, fused-sdpa off | mlx-lm goldens (per machine) |
580
+ | `--l2` | mlx-optiq **bit-for-bit** | mixed precision (`kv_config.json`) | + fused SDPA (matches optiq exactly) | optiq goldens |
581
+ | *(none)* | **= `--l1`** | bf16 | as `--l1` | mlx-lm goldens |
582
+ | **Lab** | no external oracle | — | env-flagged experiments with a bench + expiry; graduate only by beating L1 in a paired A/B | validity / KL / eval gates |
583
+
584
+ Compiled decode is on in **every** tier (proven bit-exact with uncompiled —
585
+ free speed, not a fidelity trade). **There is no `--l3` tier** (removed
586
+ 2026-07-05; its role is the Lab).
587
+
588
+ **KV precision is a separate axis from the tier's kernels.** `--l1
589
+ --kv-quant 8` is a bit-exact L1 config: with fused-sdpa off our quantized
590
+ decode runs `quantizedSdpaUnfused`, op-for-op mlx-lm's
591
+ `quantized_scaled_dot_product_attention` (`mlx_lm/models/base.py`). Only
592
+ the per-layer `config` scheme is optiq-only (→ L2).
341
593
 
342
594
  **Where each feature sits:** batching = L1-class (mlx-lm B=N parity);
343
- speculative decoding = L1-class (token-for-token vs mlx-lm's spec path);
344
- structured output = L2-class (oMLX oracle; masking doesn't touch the
345
- numerics of valid tokens, so it composes with any tier); grammar × spec
346
- together, HLG sampling, expert offload, and (future) batched quantized KV
347
- are **no-oracle (Lab-gated)** — no ancestor runtime does them, so they're
348
- gated by validity/KL/quality instead of bit-parity.
595
+ two-model speculative decoding = L1-class; structured output = L2-class
596
+ (oMLX oracle; masking doesn't touch the numerics of valid tokens, so it
597
+ composes with any tier); grammar × spec, HLG sampling, expert offload,
598
+ TurboQuant's cache class, jump-forward decoding, and batched
599
+ uniform/turbo KV are **no-oracle (Lab-gated)**.
600
+
601
+ ## Feature matrix
602
+
603
+ Everything mlx-bun serves, with its default, lane, fidelity tier, and knob.
604
+
605
+ ### Serving & performance
606
+
607
+ | Feature | Default | Lane | Tier | Knob |
608
+ | --- | --- | --- | --- | --- |
609
+ | OpenAI chat completions (`/v1/chat/completions`, SSE) | on | both | — | — |
610
+ | Raw text completions (`/v1/completions`) | on | both | — | — |
611
+ | Anthropic Messages (`/v1/messages`) | on | both | — | — |
612
+ | OpenAI Responses shim (`/v1/responses`) | on | both | — | — |
613
+ | Continuous batching (mlx-lm parity at the same B) | on, cap 8 | batch | L1 | `--batch <n>` (`1` pins serial) |
614
+ | Prompt cache (prefix KV reuse) | on, 8 GiB | both | — | `--prompt-cache <GB>` (`0` = off) |
615
+ | SSD KV cold tier (survives eviction + restarts; GLM compressed MLA/DSA state included) | off | both | — | `--ssd-cache <dir>` (+ `--ssd-cache-max`, `--ssd-cache-verify`, `--ssd-demote-idle`, `--generation-checkpoint`) |
616
+ | Runtime isolation (crash-isolated engine child behind a proxy parent) | off | both | — | `--isolate` |
617
+ | Model pool (LRU-capped resident engines under `--isolate`) | 1 | both | — | `--model-pool <n>` |
618
+ | Mixed-precision KV (`kv_config.json`, optiq's scheme) | off | serial + batch (per-layer configs batch) | L2 | `--kv-quant config`, `--l2` |
619
+ | Uniform quantized KV (mlx-lm's `--kv-bits` scheme) | off | serial | L1 | `--kv-quant 4\|8` |
620
+ | TurboQuant KV (rotation-based: affine keys + FWHT/Lloyd-Max values) | off | serial only (solo) | Lab (codec oracle-backed vs vllm-metal; cache class unvalidated) | `--kv-quant turbo[:k<bits>v<bits>]` (default `k8v3`) |
621
+ | Compiled decode (bit-exact graph replay) | on | serial, batch at B=1 | L1/L2 | `--compiled-decode on\|off` |
622
+ | Compiled activations (mlx-lm's `@mx.compile` geglu/swiglu) | on | both | L1 | `--compiled-activations on\|off` |
623
+ | Fused SDPA (optiq-exact quantized-KV attention) | follows `--kv-quant` | serial | L2 | `--fused-sdpa on\|off` |
624
+ | Paged KV (vLLM-style block pool, gather before the stock SDPA) | off | serial (pins `--batch 1`) | gated bit-exact vs plain `KVCache` | `--paged-kv`, `--paged-kv-block-size <n>` |
625
+ | Speculative decoding (two-model / Gemma `-assistant` / DSpark / DeepSpec / native MTP head) | off | serial (forces all-serial) | per-drafter oracle (L1 two-model, L2 assistant, DeepSpec reference, Lab DSpark) | `--draft-model`, `--draft-kind`, `--num-draft-tokens` |
626
+ | GLM-5.2 checkpoint-native MTP | on for GLM-5.2 | serial+spec | oracle trajectory + synthetic HTTP gate | `--mtp on\|off` |
627
+ | Model-free prompt-lookup speculation (vLLM `ngram` port) | off | serial (forces all-serial) | lossless by verify (gated vs non-spec greedy) | `--draft-kind ngram`, `--ngram-max`, `--ngram-min`, `--num-draft-tokens` (default 10) |
628
+ | Memory admission (never GPU-OOM) | on (RAM × 0.75) | both | — | `--memory-budget <GB>` |
629
+ | Aggregate KV admission for batch rows | off | batch | — | `--kv-budget <GB>` |
630
+ | Expert offload (MoE experts on mmap) | off | serial | Lab | `--expert-offload` |
631
+ | Extend-join (O(1) batch admission) | on | batch | L1 (mlx-lm `extend`) | `MLX_BUN_BATCH_EXTEND=0` |
632
+ | Vectorized greedy batch sampling | on | batch | bit-equal A/B | `MLX_BUN_BATCH_VEC_SAMPLE=0` |
633
+ | Pipelined batch token readback | on | batch | numerically equivalent | `MLX_BUN_BATCH_NO_PIPELINE=1` |
634
+
635
+ ### Request features
636
+
637
+ | Feature | Default | Lane | Tier | Knob |
638
+ | --- | --- | --- | --- | --- |
639
+ | Structured output (`response_format` json_object/json_schema) | on | both | L2 (oMLX) | request field; `MLX_BUN_GRAMMAR=0` kills |
640
+ | Structured-output jump-forward (grammar-forced spans in one forward) | off | serial only | Lab | `MLX_BUN_GRAMMAR_JUMP=1` |
641
+ | Token fast-forwarding for tool calls (template-determined spans in one forward) | off | serial only | opt-in; identity tested on covered fixtures, parser/held-out gates remain (`tests/parity/fill-strict.test.ts`) | `MLX_BUN_FILL=strict` |
642
+ | Echo injection (session self-copy spans, verified against the same forward's logits) | off | serial only | Lab (paired A/B on task success + wall clock before any default) | `MLX_BUN_FILL=echo`, `MLX_BUN_FILL_K`, `MLX_BUN_FILL_CANDIDATES`, `MLX_BUN_FILL_INDEX_MAX` |
643
+ | `guided_grammar` (EBNF) / `guided_regex`¹ / `guided_choice` / `structured_outputs` | on | both | L2 | request fields |
644
+ | Structured output × speculative decoding | on when both active | serial | Lab | — |
645
+ | Quantized KV × speculative decoding | KV scheme wins by default. Qwen uniform KV4 with start=0 can opt into speculation with `MLX_BUN_QWEN_SPEC_KV4=1`; TurboQuant and per-layer schemes remain incompatible. | serial | experimental, default off | `MLX_BUN_QWEN_SPEC_KV4=1` with `--kv-quant 4` |
646
+ | Tool calling (Gemma sentinel / CPM+Qwen XML / GLM `arg_key`+`arg_value`) + `role:"tool"` loops | on | both | — | request `tools` |
647
+ | Vision (`image_url`; PNG/JPEG/HEIC/AVIF/WebP/TIFF/GIF/BMP) | on for models with a tower; SSRF guard on remote URLs | serial | L1/L2 | `--allow-private-media` |
648
+ | Video input (`video_url`/`video`; AVFoundation sidecar, 2 fps, ≤768 frames, 256 MB body cap; never with audio) | on for Qwen3.5-family | serial | mlx-vlm oracle | `--allow-private-media`, `MLX_BUN_FRAME_EXTRACT` |
649
+ | Audio input (`input_audio`/`audio`/`audio_url`; WAV native, mp3/m4a/flac/ogg/aiff via CoreAudio; ≤30 s per clip; mixes with images) | on for models with `audio_config` + sidecar tower (e4b) | serial³ | L2 (greedy stream exact vs optiq's internal model) | `--allow-private-media` |
650
+ | LoRA adapters (mount at start / hot-swap) | off | serial | — | `--adapter <dir>`, `POST /v1/adapters` |
651
+ | Sampling: temperature / top-p / top-k / min-p / XTC / logit_bias / presence+frequency+repetition penalties | per request | both | L1 (mlx-lm-faithful) | request fields / server defaults |
652
+ | `logprobs` / `top_logprobs` | off | serial | L1 | request fields |
653
+ | Fixed `seed` reproducibility | off | serial | — | request field |
654
+ | Thinking-mode control (hybrid-reasoning models) | model default | both | — | `--thinking`, `chat_template_kwargs`, `reasoning_effort` |
655
+ | Stop sequences / streaming / usage accounting | on | both | — | request fields |
656
+ | HLG tone-curve sampling | off | serial | Lab | `--hlg-sampling on` |
657
+ | Spec-decode telemetry (`usage.speculation`) | on with a draft | serial | — | — |
658
+ | Token-fast-forwarding telemetry (`usage.fill`) | on with `MLX_BUN_FILL=strict` | serial | — | — |
659
+ | Per-turn lane telemetry (`usage.lane`: serial / serial+spec / batched) | on | both | — | — |
660
+
661
+ ### Model coverage (per-model validated cells)
662
+
663
+ | Family | Serial | Batch | Notes |
664
+ | --- | --- | --- | --- |
665
+ | MiniCPM5 (cpm5) | ✅ L1/L2 | ✅ | the starter model |
666
+ | Gemma 4 (1B/e4b/12B/26B, + vision e4b/12B, + audio e4b) | ✅ L1/L2 | ✅ | sliding+full interleaved; MoE 26B |
667
+ | Qwen3.5 (gated-DeltaNet hybrid) | ✅ L1/L2 | ✅ (SSM path) | `MLX_BUN_BATCH_SSM=0` reverts |
668
+ | Qwen3.8-27B (same qwen3_5 graph) | ✅ L1 | ✅ (SSM path) | native MTP head via `--draft-model`/`--draft-kind mtp` (lossless-gated; slower on a quiet box — opt-in); images and video serve (mlx-vlm oracle) |
669
+ | GLM-5.2 / Colibri | ✅ chat/text, Messages, Responses, SSE, tools, grammar, logprobs | ✅ compressed MLA/DSA scheduler | native MTP defaults to serial+spec; `--mtp off` enables batching; embeddings, vision/audio, adapters, training unsupported |
670
+ | DiffusionGemma-26B (non-autoregressive) | ✅ (own engine) | — serial always | first bit-exact non-AR port |
671
+ | Tier-0 universal (llama/qwen2/qwen3/olmo2/…, 11 archs) | ✅ L1 | ✅ plain full-attention archs² | gemma2-family / sliding-window universal → serial |
672
+
673
+ ¹ `guided_regex` accepts the regex∩EBNF subset today (no `\d`/anchors — those degrade to prompt injection).
674
+ ² Gated token-exact vs mlx-lm B=2 (static + dynamic join/leave) on Llama-3.2-3B.
675
+ ³ Audio is a capability neither ancestor serves (mlx-lm strips the tower; optiq never wires it into its serve frontend). Serial by design; prompt cache skipped.
676
+
677
+ Beyond serving, the same binary does training (`mlx-bun train`),
678
+ quantization (`convert`, `fuse`), embeddings, local memory (`mlx-bun
679
+ memory`), the pi agent (`mlx-bun pi`), the registry/`fit`/`gc`, HF
680
+ `upload`, and the web Model Hub — see [cli.md](./cli.md).
349
681
 
350
682
  ## Performance characteristics & recipes
351
683
 
352
684
  Reference numbers are from this project's dev machines (loaded-machine
353
- numbers are directional only; `benchmarks/RESULTS.md` holds the quotable
354
- set, and `scripts/bench-feature-matrix.ts` measures the six composition
355
- cells in one run).
356
-
357
- - **Serial is the fastest single stream** — prompt cache, mixed-precision
358
- KV, compiled decode. The batch lane at ONE live row is ~25% slower
359
- (cpm5 ~149 vs 193 tok/s: no prompt cache, no kv-quant, wrapper
360
- overhead), which is why serial stays the default.
685
+ numbers are directional only; [benchmarks.md](./benchmarks.md) holds the
686
+ quotable set, and `scripts/bench-matrix.ts features` measures the
687
+ composition cells in one run).
688
+
689
+ - **A lone request is the serial engine** — prompt cache, compiled
690
+ decode, and SSD restore all apply at B=1 under the default cap.
361
691
  - **`--batch N` wins under concurrency** — cpm5 `--batch 4`: ~349 tok/s
362
- aggregate vs ~173 serial-queued, TTFT 2–3× better; Llama-3B at just
363
- B=2: 1.7× aggregate, TTFT 765→162 ms.
692
+ aggregate vs ~173 serial-queued, TTFT 2–3× better; Llama-3B at B=2:
693
+ 1.7× aggregate, TTFT 765→162 ms.
364
694
  - **Spec pays only on slow targets.** One accepted draft = one skipped
365
- target forward, so value scales with target step cost: 12B ≈ 1.09× at
366
- γ=1 (measured); fast small targets LOSE (e4b 0.78×; 3B smoke: spec
367
- 61.5 vs serial 81.5 tok/s). Draft for 12B+, skip below.
368
- - **Structured output is ~free** (<1% serial; the batch lane drops to
369
- read-before-build scheduling while a grammar row is live — bounded,
370
- ~0.1 ms/step class).
371
- - **`--ssd-cache` has 0% decode overhead** — pure TTFT/restart win; no
372
- reason to leave it off on a persistent server.
695
+ target forward: 12B ≈ 1.09× at γ=1; fast small targets lose (e4b
696
+ 0.78×). Draft for 12B+, skip below.
697
+ - **Structured output is ~free** (<1% serial; bounded ~0.1 ms/step class
698
+ in the batch lane).
699
+ - **`--ssd-cache` has 0% decode overhead** — pure TTFT/restart win.
373
700
  - **Quantized-KV prefill pays a scheme-intrinsic tax at long context** —
374
- chunked prefill converts each chunk's KV at the chunk boundary, so
375
- every later chunk attends against the already-quantized prefix
376
- (quantize-on-write; the same streaming conversion optiq serve uses —
377
- `maybeQuantizeKv` in `src/generate.ts` is the port). That cost is by
378
- design, not an implementation gap: ~30% prefill throughput vs bf16 at
379
- ~16k where the config quantizes every cache (the 1B starter measured
380
- −33% in the 2026-07-06 serve pass), single-digit % on the
381
- sliding-window gemmas (rotating caches stay window-bounded). The lever
382
- is upstream — mlx's `quantized_matmul` kernels (the split-K work,
383
- [ml-explore/mlx#3120](https://github.com/ml-explore/mlx/pull/3120);
384
- status in [decode-speed-program.md](../design/decode-speed-program.md)
385
- lever 2) — not this codebase.
701
+ chunked prefill converts each chunk's KV at the boundary, so later
702
+ chunks attend against the quantized prefix (the same streaming
703
+ conversion optiq serve uses): ~30% prefill throughput vs bf16 at ~16k
704
+ where the config quantizes every cache, single-digit % on the
705
+ sliding-window gemmas. The lever is upstream (mlx `quantized_matmul`,
706
+ [decode-speed-program.md](../design/decode-speed-program.md) lever 2).
386
707
 
387
708
  **Recipes:**
388
709
 
389
710
  - *Single-user agent/chat (the default use):*
390
- `mlx-bun serve <model> --ssd-cache <dir>` — L1 serial, prompt cache +
391
- SSD tier. On 12B+ add `--draft-model <small-same-tokenizer>`.
711
+ `mlx-bun serve <model> --ssd-cache <dir>` — L1, prompt cache + SSD tier.
712
+ On 12B+ add `--draft-model <small-same-tokenizer>`.
392
713
  - *Several clients at once (throughput):*
393
- `mlx-bun serve <model> --batch 4 --ssd-cache <dir>` — don't set
394
- `--kv-quant` (it would just un-batch everything), don't mount a draft.
395
- - *Reproducibility:* bare / `--l1` (≡ mlx-lm), `--l1 --batch N`
396
- (≡ mlx-lm B=N), `--l2` (≡ optiq).
397
- - *Memory-tight big model:* serial + `--kv-quant config|4|8` +
714
+ `mlx-bun serve <model> --batch 4 --ssd-cache <dir> --kv-budget <GB>` —
715
+ don't set uniform/turbo `--kv-quant` (it un-batches everything), don't
716
+ mount a draft.
717
+ - *UI must never lag / survive engine crashes:* add `--isolate`
718
+ (`--model-pool 2` to keep two models resident).
719
+ - *Reproducibility:* bare / `--l1` (≡ mlx-lm), `--l1 --batch 1` (strict
720
+ serial), `--l2` (≡ optiq).
721
+ - *Memory-tight big model:* `--kv-quant config|4|8|turbo` +
398
722
  `--memory-budget <GB>` + `--ssd-cache <dir>`; MoE adds
399
- `--expert-offload`. Inherently the serial recipe until batched
400
- quantized KV lands.
723
+ `--expert-offload`. Uniform/turbo make this the serial recipe.
401
724
 
402
725
  **The two exclusions to remember:** batching excludes uniform/TurboQuant KV
403
- (per-layer `kv_config.json` does compose with batching), and spec excludes
404
- prompt-cache reuse (v1 bypass; the §7.6 composition is the fix).
726
+ (per-layer `kv_config.json` does compose), and spec excludes prompt-cache
727
+ reuse (v1 bypass).
405
728
 
406
729
  ## Observability — `GET /stats`
407
730
 
408
731
  The live config and batch state. The canonical field-by-field snippet
409
- lives in [server-api.md](server-api.md#get-stats) (kept in lockstep with
410
- the code — this section only explains how to read it); highlights:
411
- `prompt_cache` (8 GB default cap), `kv_quant.mode` (incl. `turbo kXvY`),
412
- the conditional `ssd_cache` block, and `batch`
732
+ lives in [server-api.md](server-api.md#get-stats); highlights:
733
+ `prompt_cache` (cap), `kv_quant.mode` (incl. `turbo kXvY`), the
734
+ conditional `ssd_cache` block (pending/dropped/failed spill counters), the
735
+ conditional `glm52` block, and `batch`
413
736
  (`mode`/`active_rows`/`pending_rows`/`submitted_rows`/`kv_bytes`/
414
737
  `kv_budget_bytes`).
415
738
 
416
739
  `batch.mode` is the truthful configured/capability state: `off` for
417
740
  `--batch 1`, `serial` when a larger configured cap cannot batch the loaded
418
- model's cache layout, and `batch` when the model is admitted. `batch.batched`
419
- is the compatibility boolean for `mode == "batch"`; `active_rows` reports the
420
- instantaneous live rows.
741
+ model's cache layout, and `batch` when the model is admitted.
742
+ `batch.batched` is the compatibility boolean for `mode == "batch"`;
743
+ `active_rows` is the instantaneous live-row count. Under `--isolate`,
744
+ `GET /engine` on the parent reports the child pid, restart count, socket,
745
+ and pool residency.
746
+
747
+ Managed quantization and finetuning jobs wait for the serving gateway to drain,
748
+ then hold its execution lease until their subprocess exits. Generation and native
749
+ preparation wait during that job. Shutdown cancels queued jobs and terminates the
750
+ host's active job before releasing its lease. Resident model weights remain
751
+ loaded. Under isolation, the parent coordinates all of its model-pool workers.
752
+ This controls execution, not memory capacity or independently launched servers.