mlx-bun 0.0.10 → 0.0.12

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 (216) hide show
  1. package/README.md +107 -32
  2. package/THIRD_PARTY_LICENSES.md +10 -0
  3. package/docs/archive/mlx-bun-lab-report.html +275 -0
  4. package/docs/archive/wwdc-mlx-bun.html +848 -0
  5. package/docs/design/audio-input-plan.md +356 -0
  6. package/docs/design/batching-perf-path.md +58 -35
  7. package/docs/design/batching-v2-plan.md +23 -9
  8. package/docs/design/bucketing-stage.md +2 -1
  9. package/docs/design/colibri-glm52-port.md +1479 -0
  10. package/docs/design/compat-cli-surface-design.md +4 -3
  11. package/docs/design/decode-speed-program.md +172 -0
  12. package/docs/design/docs-reorg-plan.md +1 -1
  13. package/docs/design/dspark-serving-program.md +371 -0
  14. package/docs/design/dspark-speculative-decoding.md +167 -5
  15. package/docs/design/faithful-l1-consolidation.md +310 -0
  16. package/docs/design/generic-model-support.md +8 -5
  17. package/docs/design/grammar-spec-batching-integration.md +301 -0
  18. package/docs/design/minicpm5-decode-megakernel.md +16 -11
  19. package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
  20. package/docs/design/omlx-adoption-map.md +14 -6
  21. package/docs/design/orpo-training.md +34 -30
  22. package/docs/design/paged-kv-cache.md +214 -0
  23. package/docs/design/parallel-slots.md +82 -49
  24. package/docs/design/parity-tier-dag.md +43 -33
  25. package/docs/design/pre-colibri-stabilization.md +329 -0
  26. package/docs/design/repo-cleanup-plan.md +189 -0
  27. package/docs/design/runtime-isolation.md +19 -3
  28. package/docs/design/segmented-backward-training.md +3 -2
  29. package/docs/design/ssd-kv-cold-tier.md +130 -0
  30. package/docs/design/structured-output.md +484 -0
  31. package/docs/design/the-dreaming-master-plan.md +2 -3
  32. package/docs/design/turboquant-kv.md +235 -0
  33. package/docs/design/unified-engine-frontier-plan.md +774 -0
  34. package/docs/design/web-chat-beat-matrix.md +315 -0
  35. package/docs/design/web-chat-redesign.md +1112 -0
  36. package/docs/investigations/colibri-oracle-pin.md +1284 -0
  37. package/docs/investigations/dspark-handoff.md +182 -16
  38. package/docs/investigations/indexshare-performance-spike.md +357 -0
  39. package/docs/investigations/lab-build-journal.md +2 -2
  40. package/docs/investigations/openwiki-evaluation.md +249 -0
  41. package/docs/planning/IDEAS.md +14 -0
  42. package/docs/planning/PRODUCT_ROADMAP.md +2 -3
  43. package/docs/planning/chat-ui-vision-memory-first.md +199 -0
  44. package/docs/planning/chat-ui-vision.md +50 -0
  45. package/docs/planning/memory-docs-and-dag-plan.md +15 -6
  46. package/docs/planning/release-notes-v0.0.11.md +239 -0
  47. package/docs/planning/release-notes-v0.0.12.md +80 -0
  48. package/docs/reference/cli.md +63 -18
  49. package/docs/reference/distribution.md +37 -6
  50. package/docs/reference/embedding.md +40 -16
  51. package/docs/reference/features-matrix.md +99 -0
  52. package/docs/reference/library-api.md +69 -20
  53. package/docs/reference/memory.md +26 -6
  54. package/docs/reference/models.md +60 -3
  55. package/docs/reference/orpo-quickstart.md +3 -4
  56. package/docs/reference/server-api.md +717 -24
  57. package/docs/reference/server-config.md +265 -129
  58. package/docs/reference/training.md +24 -16
  59. package/package.json +5 -3
  60. package/src/anthropic.ts +67 -44
  61. package/src/audio/conformer.ts +647 -0
  62. package/src/audio/decode.ts +133 -0
  63. package/src/audio/features.ts +202 -0
  64. package/src/audio/transcode.ts +67 -0
  65. package/src/chat-template.ts +155 -10
  66. package/src/cli.ts +483 -113
  67. package/src/config.ts +116 -24
  68. package/src/diffusion/diffusion-generate.ts +14 -8
  69. package/src/download.ts +143 -12
  70. package/src/eval/README.md +7 -6
  71. package/src/eval/ifeval.ts +63 -120
  72. package/src/eval/kl.ts +59 -3
  73. package/src/eval/quality-db.ts +2 -2
  74. package/src/eval/runner.ts +17 -8
  75. package/src/eval/tasks/ifeval.ts +167 -31
  76. package/src/expert-io.ts +391 -0
  77. package/src/expert-offload.ts +15 -2
  78. package/src/expert-residency.ts +1134 -0
  79. package/src/expert-usage.ts +415 -0
  80. package/src/flags.ts +13 -0
  81. package/src/generate.ts +438 -35
  82. package/src/grammar.ts +585 -0
  83. package/src/hf-push.ts +1 -1
  84. package/src/hub-rest.ts +242 -0
  85. package/src/index.ts +14 -2
  86. package/src/jobs/runner.ts +37 -18
  87. package/src/kv-store.ts +586 -93
  88. package/src/lora.ts +85 -14
  89. package/src/md-modules.d.ts +42 -0
  90. package/src/media-fetch.ts +426 -0
  91. package/src/memory/rest.ts +245 -0
  92. package/src/memory/vault.ts +74 -0
  93. package/src/mlx/array.ts +149 -19
  94. package/src/mlx/autograd.ts +53 -16
  95. package/src/mlx/checkpoint.ts +46 -13
  96. package/src/mlx/custom-vjp.ts +83 -35
  97. package/src/mlx/ffi.ts +27 -3
  98. package/src/mlx/metal-kernel.ts +7 -4
  99. package/src/mlx/ops.ts +153 -23
  100. package/src/mlx/turboquant-ops.ts +831 -0
  101. package/src/mlx/turboquant-tables.ts +237 -0
  102. package/src/model/batched-mask.ts +62 -1
  103. package/src/model/batched-quant.ts +206 -0
  104. package/src/model/batched-rotating-quant.ts +346 -0
  105. package/src/model/batched-rotating.ts +46 -1
  106. package/src/model/compiled-decode.ts +4 -15
  107. package/src/model/factory.ts +103 -6
  108. package/src/model/gemma4-base.ts +395 -39
  109. package/src/model/gemma4.ts +122 -33
  110. package/src/model/generated/gemma4-12b.ts +9 -13
  111. package/src/model/generated/gemma4-26b.ts +10 -5
  112. package/src/model/generated/gemma4-e4b.ts +14 -9
  113. package/src/model/glm52-atlas.ts +511 -0
  114. package/src/model/glm52-cache.ts +706 -0
  115. package/src/model/glm52-config.ts +262 -0
  116. package/src/model/glm52-container.ts +305 -0
  117. package/src/model/glm52-coupling.ts +388 -0
  118. package/src/model/glm52-dsa.ts +602 -0
  119. package/src/model/glm52-expert-layout.ts +253 -0
  120. package/src/model/glm52-layout.ts +152 -0
  121. package/src/model/glm52-memory.ts +473 -0
  122. package/src/model/glm52-mla.ts +786 -0
  123. package/src/model/glm52-moe.ts +223 -0
  124. package/src/model/glm52-pilot.ts +537 -0
  125. package/src/model/glm52-reference.ts +445 -0
  126. package/src/model/glm52-residency.ts +557 -0
  127. package/src/model/glm52-resident-weights.ts +135 -0
  128. package/src/model/glm52-streamed-experts.ts +453 -0
  129. package/src/model/glm52-streamed-metal.ts +610 -0
  130. package/src/model/glm52-weights.ts +503 -0
  131. package/src/model/glm52.ts +1153 -0
  132. package/src/model/minicpm5.ts +58 -102
  133. package/src/model/paged-kv.ts +253 -0
  134. package/src/model/qwen3-delta.ts +76 -15
  135. package/src/model/qwen3-moe.ts +404 -0
  136. package/src/model/qwen3.ts +9 -8
  137. package/src/model/qwen3_5.ts +88 -23
  138. package/src/model/support.ts +19 -1
  139. package/src/model/universal/dense.ts +40 -5
  140. package/src/model/universal/rope.ts +36 -21
  141. package/src/native/expert_io.c +523 -0
  142. package/src/native-pack.ts +6 -4
  143. package/src/pi-web.ts +1299 -82
  144. package/src/preflight.ts +27 -8
  145. package/src/prompt-cache.ts +293 -33
  146. package/src/quantize/quantizer.ts +16 -1
  147. package/src/registry.ts +57 -2
  148. package/src/responses.ts +62 -49
  149. package/src/serve/batch-scheduler.ts +1050 -78
  150. package/src/serve/generation-gateway.ts +252 -24
  151. package/src/serve/isolate.ts +417 -0
  152. package/src/serve/lane-registry.ts +44 -0
  153. package/src/serve/session-search.ts +326 -0
  154. package/src/server.ts +1701 -175
  155. package/src/spec/assistant-source.ts +150 -0
  156. package/src/spec/deepspec-source.ts +143 -0
  157. package/src/spec/dflash-source.ts +124 -0
  158. package/src/spec/dspark/ab-stats.ts +136 -0
  159. package/src/spec/dspark/calibration.ts +112 -0
  160. package/src/spec/dspark/deepspec-module.ts +801 -0
  161. package/src/spec/dspark/generate-dflash.ts +72 -20
  162. package/src/spec/dspark/loader.ts +23 -0
  163. package/src/spec/dspark/module-dflash.ts +278 -36
  164. package/src/spec/dspark/quantize-drafter.ts +91 -0
  165. package/src/spec/generate.ts +59 -25
  166. package/src/spec/glm52-mtp-source.ts +392 -0
  167. package/src/spec/ngram-source.ts +132 -0
  168. package/src/spec/serve-loop.ts +617 -0
  169. package/src/spec/source.ts +118 -0
  170. package/src/spec/two-model.ts +171 -0
  171. package/src/ssd-cache.ts +111 -35
  172. package/src/tokenizer.ts +288 -2
  173. package/src/tool-approvals.ts +131 -0
  174. package/src/tool-call.ts +438 -22
  175. package/src/train/flash-cce.ts +2 -2
  176. package/src/train/job.ts +0 -12
  177. package/src/train/lora-params.ts +17 -6
  178. package/src/train/trainer.ts +60 -30
  179. package/src/vision/prompt.ts +217 -48
  180. package/src/web/app.html +1197 -1791
  181. package/src/web/app.js +5694 -0
  182. package/src/web/icon.svg +13 -0
  183. package/src/web/manifest.webmanifest +24 -0
  184. package/src/web/src/adapters-panel.ts +288 -0
  185. package/src/web/src/api.ts +57 -0
  186. package/src/web/src/assistant.ts +485 -0
  187. package/src/web/src/chat.ts +1050 -0
  188. package/src/web/src/composer.ts +959 -0
  189. package/src/web/src/dataset.ts +132 -0
  190. package/src/web/src/finetune.ts +267 -0
  191. package/src/web/src/hub.ts +428 -0
  192. package/src/web/src/main.ts +98 -0
  193. package/src/web/src/markdown.ts +0 -0
  194. package/src/web/src/memory-panel.ts +572 -0
  195. package/src/web/src/model-picker.ts +159 -0
  196. package/src/web/src/palette.ts +346 -0
  197. package/src/web/src/protocol.ts +62 -0
  198. package/src/web/src/quantize.ts +167 -0
  199. package/src/web/src/rag.ts +274 -0
  200. package/src/web/src/sessions.ts +469 -0
  201. package/src/web/src/shell.ts +804 -0
  202. package/src/web/src/status.ts +300 -0
  203. package/src/web/src/ui-catalog.ts +92 -0
  204. package/src/web/sw.js +71 -0
  205. package/src/web/vendor/README.md +61 -0
  206. package/src/web/vendor/hljs-LICENSE +29 -0
  207. package/src/web/vendor/hljs-theme.css +59 -0
  208. package/src/web/vendor/hljs.js +26 -0
  209. package/src/web-tools.ts +54 -2
  210. package/src/weights.ts +82 -19
  211. package/src/model/fused-decode-kernel.ts +0 -312
  212. package/src/model/fused-geglu-kernel.ts +0 -198
  213. package/src/model/fused-mlp-kernel.ts +0 -169
  214. package/src/model/fused-swiglu-kernel.ts +0 -240
  215. package/src/model/steel-linear-kernel.ts +0 -96
  216. package/src/status-page.html +0 -497
@@ -21,19 +21,31 @@ the design rationale is in
21
21
  | --- | --- | --- | --- |
22
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
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 | Admission ceiling. Requests whose `prompt + max_tokens` exceed the budget's max safe context are **rejected with 400** (`type: memory_admission`) instead of risking an uncatchable GPU OOM. Also caps the mlx allocator (`mlx_set_memory_limit`) as defense in depth. **Decimal GB (×10⁹).** |
25
- | `--prompt-cache` | GB | `2` (≈2e9 bytes) | Byte cap for the prompt (prefix-reuse KV) cache. **Binary GiB (×2³⁰)** on input. Unset ⇒ the 2 GB default; an explicit `--prompt-cache 0` **disables** the cache (maxBytes 0 evicts every entry). |
26
- | `--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, is snapshotted after requests settle (debounced 1 s), and **survives restarts** — a long-context agent re-attach restores via zero-copy mmap instead of re-prefilling (measured 13.7k-token prefix: 12 s → 0.24 s TTFT; 0% decode overhead). Entries are keyed by model fingerprint **+ effective kv scheme** + tokenizer hash + adapter ns; incompatible/corrupt files self-quarantine. Serial lane only. Requires the RAM cache. |
24
+ | `--memory-budget` | GB | machine RAM × 0.75; GLM-5.2: min(25 GiB, physical RAM) | Admission ceiling. Requests whose `prompt + max_tokens` exceed the budget's max safe context are **rejected with 400** (`type: memory_admission`) instead of risking an uncatchable GPU OOM. GLM-5.2 runs its header-only exact process equation before opening resident weights or expert slabs, and caps MLX to that plan's allocator reserve. **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. |
27
29
  | `--ssd-cache-max` | GB | `32` | SSD tier byte cap (binary GiB); oldest-mtime entries evicted at the cap. |
28
- | `--ssd-cache-verify` | (bool) | off | Verify every tensor hash on restore. Reads all bytes eagerly (defeats lazy page fault-in) — integrity paranoia only; the header hash is always verified. |
29
- | `--batch` | n | `1` (serial) | Max concurrent requests batched through the mlx-lm-parity engine. `>1` switches the **whole server** into bf16 continuous batching — a *mode*, not a load fallback. See [Execution modes](#execution-modes-serial-vs---batch-n). `--decode-concurrency` is accepted for drop-in compatibility, but the semantics differ: in mlx_lm.server it caps per-BatchGenerator decode parallelism (default 32); in mlx-bun it enables continuous batching with this cap (mlx-bun's default is the optimized serial path). |
30
- | `--kv-quant` | `config`\|`off`\|`4`\|`8` | `config` serial / `off`(bf16) under `--batch N` | KV-cache quantization. `config` = per-layer `kv_config.json` (optiq parity); `off` = bf16; `4`/`8` = uniform bits (group 64, start 0). Under `--batch N`, an explicit value routes those requests to the **serial** lane (batched is bf16-only). |
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. |
31
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. |
32
- | `--thinking` | `true`\|`false` | model's own (false for CPM) | Server-wide default for the chat template's `enable_thinking` (MiniCPM5/CPM and Qwen3.5 hybrid reasoning). A request's `chat_template_kwargs.enable_thinking` overrides it. |
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`. |
33
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. |
34
45
  | `--top-p` | n ∈ [0,1] | `generation_config.json` | Server-wide top-p default (per-request `top_p` wins). |
35
46
  | `--top-k` | n ∈ [0,1e6] | `generation_config.json` | Server-wide top-k default (per-request `top_k` wins). |
36
- | `--max-tokens` | n | `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). |
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. |
37
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). |
38
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). |
39
51
  | `--hlg-width` | nats | `4` | HLG mid-region half-width (nats). Only meaningful with `--hlg-sampling on`. |
@@ -41,32 +53,34 @@ the design rationale is in
41
53
  | `--hlg-toe` | nats | `6` | HLG shadow rolloff scale (nats). Only meaningful with `--hlg-sampling on`. |
42
54
  | `--hlg-pivot-offset` | nats | `6` | HLG pivot point: nats below the top token. Only meaningful with `--hlg-sampling on`. |
43
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. |
44
- | `--l1` | (bool) | — | **Parity tier preset:** bit-for-bit IDENTICAL to mlx-lm (bf16 KV, unfused SDPA, no perf kernel). Sets the whole decode route; any explicit per-fork flag (`--kv-quant`/`--perf-kernel`/…) overrides the preset. See [docs/design/parity-tier-dag.md](../design/parity-tier-dag.md). |
45
- | `--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`). The perf kernel stays **off**: it is envelope-gated, not bit-exact (see `--l3`), and opting into it (`--l2 --perf-kernel on`) is an explicit choice that leaves the bare-tier guarantee. |
46
- | `--l3` | (bool) | — | **Parity tier preset:** best performance, no bit-exact oracle (KL + test gated). On the decode path L3 = L2 + the envelope-gated perf kernel; L3 also owns the no-oracle features (HLG sampler, expert offload). No tier given ⇒ the per-flag defaults below, which equal `--l3`. |
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`). |
47
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. |
48
- | `--perf-kernel` | on\|off | **on** | Fused quantized-KV decode-SDPA Metal kernel (`MLX_BUN_PERF_KERNEL`), the perf side of the compat A/B. **Not bit-exact** — envelope-gated (≥56/64 teacher-forced argmax vs the frozen compat trajectory, `tests/perf-kernel-oracle.test.ts`), so it is an **L3 lever**: on in `--l3`/no-tier default, **off in bare `--l1`/`--l2`**. Engages on quantized caches at decode. **Serial lane only.** |
49
- | `--fused-decode` | on\|off | off | Experimental: tile the quantized decode SDPA (`MLX_BUN_FUSED_DECODE`). **Serial lane only.** |
50
- | `--fused-sdpa` | on\|off | on | Fused SDPA path for quantized prefill/continuation (inverted env `MLX_BUN_NO_FUSED_SDPA`). **Serial lane only.** |
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.** |
51
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.** |
52
62
 
53
63
  The default host/port (`127.0.0.1:8080`) match `mlx_lm.server`, so running
54
64
  mlx-bun alongside the Python reference server needs an explicit `--port`.
55
65
 
56
- The five performance levers are A/B knobs whose defaults are the measured
57
- winners; flip them to compare. They are set as `MLX_BUN_*` env vars
58
- before the model loads, so they apply to `mlx-bun pi` too. They affect
59
- the **serial** decode path (`generate()`); the batched scheduler calls
60
- `model.forwardHidden` directly and so is unaffected by all of them — see
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
61
82
  [Levers that don't reach the batched lane](#--batch-n-is-compat-mode--perf-flags-dont-apply-by-design).
62
83
 
63
- > **Note — `--perf-kernel` default.** The code defaults it **on**
64
- > (`perfKernelEnabled()` returns true unless `MLX_BUN_PERF_KERNEL=0`, and
65
- > the ready card / `/stats` report it on). `STATUS.md` still lists a
66
- > "default flip" as pending; treat that as stale — the engaged default
67
- > today is on. (If the intent is genuinely off-until-clean-machine-pass,
68
- > the *code* is what needs changing, not this doc.)
69
-
70
84
  ## Per-request overrides
71
85
 
72
86
  Most quality knobs can be set per request in the chat body and override
@@ -88,44 +102,62 @@ These ride all three protocol surfaces (`/v1/chat/completions`,
88
102
  ## Environment levers
89
103
 
90
104
  The CLI flags above set these; you can also export them directly (e.g.
91
- for `bun scripts/serve.ts` or paired A/B harnesses). One has no CLI flag.
105
+ for paired A/B harnesses).
92
106
 
93
107
  | Env var | CLI flag | Default | Effect |
94
108
  | --- | --- | --- | --- |
95
109
  | `MLX_BUN_COMPILED_DECODE` | `--compiled-decode` | on (`!=="0"`) | Compiled decode graph replay. |
96
- | `MLX_BUN_PERF_KERNEL` | `--perf-kernel` | on (`!=="0"`) | Fused quantized-KV decode kernel (not bit-exact; perf A/B). |
97
- | `MLX_BUN_FUSED_DECODE` | `--fused-decode` | off (`==="1"`) | Tile quantized decode SDPA. |
98
- | `MLX_BUN_NO_FUSED_SDPA` | `--fused-sdpa` (inverted) | fused on | `=1` forces the stock unfused SDPA everywhere. |
99
- | `MLX_BUN_FUSED_GELU` | *(none)* | on (`!=="0"`) | Fused GeGLU MLP kernel. Bit-exact, so it stays on both lanes; env-only opt-out. |
110
+ | `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.) |
100
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.)
101
130
 
102
131
  ## Execution modes: serial vs. `--batch N`
103
132
 
104
- `--batch 1` (default) is the serialized single-queue path: one GPU, one
105
- generation at a time, prompt-cache prefix reuse, mixed-precision KV by
106
- default. Untouched and unconditional.
107
-
108
- `--batch N` (N>1) opts the **whole server** into a continuous-batching
109
- engine that is **bit-parity with `mlx_lm.server` at `B=N`**. It is a
110
- *mode switch*, not a load-dependent fallback (results must not depend on
111
- concurrency).
112
-
113
- Parity is the guarantee, so the batched lane runs in **compat mode**: it
114
- exposes **none** of the serial lane's perf flags and runs the plain
115
- bit-exact forward — the same path mlx-lm runs, never the optional
116
- parity-breaking kernels. This is the mechanism of the guarantee, not a
117
- missing feature (see
118
- [compat mode](#--batch-n-is-compat-mode--perf-flags-dont-apply-by-design)).
119
- Because mlx-lm's batched path is bf16 (its quantized batching is NYI),
120
- bf16 continuous batching **is** the drop-in:
121
-
122
- - **KV quant unset ⇒ bf16** so the batch path engages out of the box
123
- ("Option B"). The serial default stays mixed-precision (optiq parity).
124
- - **Explicit `--kv-quant config|4|8` ⇒** those requests route to the
125
- serial lane (batched is bf16-only; a startup warning is printed). With
126
- an explicit `--kv-quant`, *every* request carries a quant scheme, so
127
- **nothing batches** — `--batch N --kv-quant config` is effectively
128
- serial-with-quant. Omit `--kv-quant` to actually batch.
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.
139
+
140
+ `--batch 1` pins the strict serialized single-queue path: one generation
141
+ at a time, arrival-independent numerics (a request's bits never depend on
142
+ 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.
147
+
148
+ 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):
151
+
152
+ - **KV quant unset ⇒ bf16** — the batch path engages out of the box
153
+ (bf16 is the serial default too, since 2026-07-05).
154
+ - **`--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).
129
161
 
130
162
  ### The lane picker (`GenerationGateway.willBatch`)
131
163
 
@@ -140,25 +172,27 @@ concurrently with each other.
140
172
  | --- | --- |
141
173
  | vision (image parts) | ❌ serial — needs offset-0 single-seq prefill + bidirectional image mask |
142
174
  | LoRA `adapter` (resolves to ≥1) | ❌ serial — `loraState.active` is one per-generation field; per-row adapters unsupported |
143
- | `repetition_penalty` | ❌ serial — per-row logits processors are a later refinement |
144
- | `min_p` / `xtc_*` / `logit_bias` / `presence_penalty` / `frequency_penalty` | ❌ serial — one gate for the whole sampler/processor family (safe v1; min_p/XTC are per-row samplers and could batch later) |
175
+ | `logprobs` / `top_logprobs` | ❌ serial — the batched sampler doesn't capture logprob arrays yet |
145
176
  | explicit `seed` | ❌ serial — reproducibility ⇒ solo (matches mlx-lm) |
146
- | KV quant active (explicit `--kv-quant`) | ❌ serial — batched is bf16-only in v1 |
147
- | hybrid-cache model (Qwen3.5 gated-DeltaNet) | ❌ serial — model-level cache-capability gate (see below) |
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) |
148
182
  | `temperature` / `top_p` / `top_k` | ✅ batches (each row samples with its own seed) |
149
183
  | `stop` sequences | ✅ batches (per-row `StopMatcher` in the onToken closure) |
150
184
  | `tools` / `tool_choice` | ✅ batches (per-row tool router; decode-layer parse) |
151
185
  | `--thinking` / `enable_thinking` | ✅ batches (template-render concern, lane-independent) |
152
- | multi-turn / long prompt | ✅ batches, but **no prompt-cache reuse** (`cached_tokens=0`) |
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 |
153
187
 
154
- Full-attention (CPM) and sliding-window (Gemma) models batch — the
155
- scheduler assembles each layer's cache by attention type. Hybrid
156
- gated-DeltaNet models (Qwen3.5) **route serial**: their `SSMCache` has no
157
- dynamic-B merge/filter, so the gateway's cache-capability gate (the
158
- mirror of mlx-lm server.py's all-caches-have-`merge` check) sends every
159
- request down the serial lane with the standard drain behavior. A batched
160
- SSM path is planned (mlx-lm's `ArraysCache` is the oracle) — see
161
- [batching-v2-plan.md](../design/batching-v2-plan.md) item (h).
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).
162
196
 
163
197
  A non-batchable request **drains** the batch: while it waits, the
164
198
  scheduler stops admitting new rows, finishes the running ones, and
@@ -180,20 +214,26 @@ inside it per the table above).
180
214
 
181
215
  | Option | serial (`--batch 1`) | `--batch N` (N>1) |
182
216
  | --- | --- | --- |
183
- | `--kv-quant config`/`4`/`8` | ✅ applied to all requests | ⚠️ applied, but forces **all** requests to the serial lane (no batching) |
217
+ | `--kv-quant config` | ✅ applied to all requests | ✅ batches where the loaded cache capability supports the per-layer scheme; otherwise routes serial |
218
+ | `--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) |
184
220
  | `--kv-quant off` | ✅ bf16 | ✅ bf16 (same as the implicit batch default) |
185
- | *(kv-quant unset)* | mixed-precision `config` | **bf16** (Option B) — incl. serial-lane fallback requests |
221
+ | *(kv-quant unset)* | bf16 (the L1 default) | **bf16** (Option B) — incl. serial-lane fallback requests |
186
222
  | `--memory-budget` | ✅ per-request admission | ✅ per-request admission — but **not aggregate** across rows (see limitations) |
187
- | `--prompt-cache` | ✅ prefix reuse | ⚠️ bypassed for batched requests (`cached_tokens=0`); serial-lane requests still reuse |
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) |
188
224
  | `--temperature`/`--top-p`/`--top-k` | ✅ | ✅ (per-row) |
189
225
  | `--thinking` | ✅ | ✅ |
190
226
  | vision request | ✅ | ✅ via serial lane (in bf16 under Option B) |
191
227
  | LoRA `adapter` | ✅ | ✅ via serial lane |
192
- | `repetition_penalty` | ✅ | ✅ via serial lane |
193
- | `min_p` / `xtc_*` / `logit_bias` / presence+frequency penalties | ✅ | ✅ 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) |
194
230
  | `seed` | ✅ | ✅ via serial lane |
195
231
  | `tools` / `stop` | ✅ | ✅ (batches) |
196
- | `--compiled-decode`/`--perf-kernel`/`--fused-*`/`--force-wire` | ✅ (serial perf tree) | **n/a — compat mode, no perf flags by design** |
232
+ | 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 |
234
+ | 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** |
197
237
 
198
238
  ### `--batch N` is compat mode — perf flags don't apply by design
199
239
 
@@ -206,79 +246,175 @@ model through `forwardHidden`/`logitsFromHidden` directly (not
206
246
  optional, parity-breaking ones. Flagging the batched lane would defeat
207
247
  the guarantee, so it's intentionally not wired.
208
248
 
209
- - **`--perf-kernel` / `--fused-decode` / `--compiled-decode`** — never
210
- engage in the batched lane. They diverge from the -O0 reference (or, for
211
- the quantized-KV kernels, are moot since batched is bf16). They're the
212
- L3 perf row, a separate KL-gated path — see the validation matrix in
213
- [parallel-slots.md](../design/parallel-slots.md).
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).
214
257
  - **`--force-wire`** — doesn't wire (the scheduler bypasses `generate()`'s
215
258
  wired scope). A model that needs wiring for speed (e.g. 26B: 8.6 → 32.3
216
259
  tok/s wired) runs at mlx-lm-equivalent unwired speed under `--batch N`;
217
260
  in practice such a model has no headroom for B>1 KV anyway.
218
- - **Always-on bit-exact kernels still run.** Fused GeGLU is bit-exact with
219
- the spelled-out MLP, so it stays on in both lanes without breaking
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
220
264
  parity — "compat mode" means *no parity-breaking optionality*, not
221
265
  -O0.
222
266
 
223
- The serial lane is where the perf/optimization tree lives: `--perf-kernel`
224
- / `--fused-*` / `--compiled-decode` engage there, and the
225
- mixed-precision-KV (optiq) default applies there.
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.
226
270
 
227
271
  ## Known limitations under `--batch N`
228
272
 
229
273
  These are deliberate v1 scope, not bugs — but they change behavior, so
230
274
  know them:
231
275
 
232
- 1. **Prompt cache bypassed.** Batched requests solo-prefill every row;
233
- `cached_tokens=0`. Wiring `PromptCache` into the scheduler is a
234
- follow-up.
235
- 2. **Admission is per-request, not aggregate.** `--memory-budget` checks
236
- each request against single-sequence max-safe-context, but N
237
- concurrent rows can collectively exceed the budget (the `B×S_max`
238
- KV-budget admission is TODO). With a tight budget and several
239
- large-context requests, the allocator cap is the only backstop — and
240
- a true GPU OOM here is uncatchable. Size the budget with headroom for
241
- N rows, or keep N small.
276
+ 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.
242
288
  3. **Short-context only.** Verified pre-ring-wrap (rows < the 1024
243
289
  sliding window). Long-context (context > window) batched decode is a
244
290
  separate validation.
245
- 4. **bf16 only — by contract.** mlx-lm's batched path *is* bf16, so
246
- bf16-only batching is exactly what mlx-lm-parity means (not a
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
247
296
  shortcoming). Going further — batched + mixed-precision KV quant — is
248
297
  novel territory with no mlx-lm/optiq oracle, so it's a deferred,
249
298
  KL-gated extension.
250
- 5. **`extend` join not yet used.** A joining request re-merges the whole
251
- batch (O(B·S)); mlx-lm's keep-the-running-batch `extend` is a later
252
- optimization (numerically equivalent).
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).
305
+
306
+ ## Fidelity tiers and the decode route (`--l1` / `--l2`)
307
+
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.
313
+
314
+ | Tier | Contract | KV | Kernels | Verified against |
315
+ | --- | --- | --- | --- | --- |
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).
341
+
342
+ **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.
349
+
350
+ ## Performance characteristics & recipes
351
+
352
+ 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.
361
+ - **`--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.
364
+ - **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.
373
+ - **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.
386
+
387
+ **Recipes:**
388
+
389
+ - *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>`.
392
+ - *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` +
398
+ `--memory-budget <GB>` + `--ssd-cache <dir>`; MoE adds
399
+ `--expert-offload`. Inherently the serial recipe until batched
400
+ quantized KV lands.
401
+
402
+ **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).
253
405
 
254
406
  ## Observability — `GET /stats`
255
407
 
256
- The live config and batch state:
257
-
258
- ```jsonc
259
- {
260
- "server": { "owner": "serve" | "pi-session" | "embedded", "model": "...", "started_at": 0 },
261
- "prompt_cache": { "entries": 0, "bytes": 0, "max_bytes": 2000000000, "hits": 0, "misses": 0 },
262
- "response_store": { "entries": 0, "bytes": 0, "max_bytes": 33554432, "ttl_ms": 3600000 },
263
- "kv_quant": {
264
- "mode": "mixed (kv_config.json)" | "uniform-kv8" | "bf16",
265
- "layers": { "kv4": 8, "bf16": 40 },
266
- "attention": { "global": 10, "sliding_window": 38 }
267
- },
268
- "admission": {
269
- "max_safe_context": 0, // requests above this 400
270
- "memory_budget_bytes": null, // explicit budget, or null = machine default
271
- "usable_bytes": 0, "weights_bytes": 0
272
- },
273
- "batch": {
274
- "configured": 1, // the --batch N value
275
- "batched": false, // batching enabled (N>1) for this server
276
- "active_rows": 0 // rows currently decoding in the batch
277
- }
278
- }
279
- ```
280
-
281
- `batch.batched` reflects only whether `--batch N` (N>1) is configured;
282
- with an explicit `--kv-quant` it can read `true` while `active_rows`
283
- never exceeds 1 (every request routes serial). `active_rows` is the
284
- honest signal of whether anything is actually batching.
408
+ 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`
413
+ (`mode`/`active_rows`/`pending_rows`/`submitted_rows`/`kv_bytes`/
414
+ `kv_budget_bytes`).
415
+
416
+ `batch.mode` is the truthful configured/capability state: `off` for
417
+ `--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.