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.
- package/README.md +107 -32
- package/THIRD_PARTY_LICENSES.md +10 -0
- package/docs/archive/mlx-bun-lab-report.html +275 -0
- package/docs/archive/wwdc-mlx-bun.html +848 -0
- package/docs/design/audio-input-plan.md +356 -0
- package/docs/design/batching-perf-path.md +58 -35
- package/docs/design/batching-v2-plan.md +23 -9
- package/docs/design/bucketing-stage.md +2 -1
- package/docs/design/colibri-glm52-port.md +1479 -0
- package/docs/design/compat-cli-surface-design.md +4 -3
- package/docs/design/decode-speed-program.md +172 -0
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +371 -0
- package/docs/design/dspark-speculative-decoding.md +167 -5
- package/docs/design/faithful-l1-consolidation.md +310 -0
- package/docs/design/generic-model-support.md +8 -5
- package/docs/design/grammar-spec-batching-integration.md +301 -0
- package/docs/design/minicpm5-decode-megakernel.md +16 -11
- package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
- package/docs/design/omlx-adoption-map.md +14 -6
- package/docs/design/orpo-training.md +34 -30
- package/docs/design/paged-kv-cache.md +214 -0
- package/docs/design/parallel-slots.md +82 -49
- package/docs/design/parity-tier-dag.md +43 -33
- package/docs/design/pre-colibri-stabilization.md +329 -0
- package/docs/design/repo-cleanup-plan.md +189 -0
- package/docs/design/runtime-isolation.md +19 -3
- package/docs/design/segmented-backward-training.md +3 -2
- package/docs/design/ssd-kv-cold-tier.md +130 -0
- package/docs/design/structured-output.md +484 -0
- package/docs/design/the-dreaming-master-plan.md +2 -3
- package/docs/design/turboquant-kv.md +235 -0
- package/docs/design/unified-engine-frontier-plan.md +774 -0
- package/docs/design/web-chat-beat-matrix.md +315 -0
- package/docs/design/web-chat-redesign.md +1112 -0
- package/docs/investigations/colibri-oracle-pin.md +1284 -0
- package/docs/investigations/dspark-handoff.md +182 -16
- package/docs/investigations/indexshare-performance-spike.md +357 -0
- package/docs/investigations/lab-build-journal.md +2 -2
- package/docs/investigations/openwiki-evaluation.md +249 -0
- package/docs/planning/IDEAS.md +14 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -3
- package/docs/planning/chat-ui-vision-memory-first.md +199 -0
- package/docs/planning/chat-ui-vision.md +50 -0
- package/docs/planning/memory-docs-and-dag-plan.md +15 -6
- package/docs/planning/release-notes-v0.0.11.md +239 -0
- package/docs/planning/release-notes-v0.0.12.md +80 -0
- package/docs/reference/cli.md +63 -18
- package/docs/reference/distribution.md +37 -6
- package/docs/reference/embedding.md +40 -16
- package/docs/reference/features-matrix.md +99 -0
- package/docs/reference/library-api.md +69 -20
- package/docs/reference/memory.md +26 -6
- package/docs/reference/models.md +60 -3
- package/docs/reference/orpo-quickstart.md +3 -4
- package/docs/reference/server-api.md +717 -24
- package/docs/reference/server-config.md +265 -129
- package/docs/reference/training.md +24 -16
- package/package.json +5 -3
- package/src/anthropic.ts +67 -44
- package/src/audio/conformer.ts +647 -0
- package/src/audio/decode.ts +133 -0
- package/src/audio/features.ts +202 -0
- package/src/audio/transcode.ts +67 -0
- package/src/chat-template.ts +155 -10
- package/src/cli.ts +483 -113
- package/src/config.ts +116 -24
- package/src/diffusion/diffusion-generate.ts +14 -8
- package/src/download.ts +143 -12
- package/src/eval/README.md +7 -6
- package/src/eval/ifeval.ts +63 -120
- package/src/eval/kl.ts +59 -3
- package/src/eval/quality-db.ts +2 -2
- package/src/eval/runner.ts +17 -8
- package/src/eval/tasks/ifeval.ts +167 -31
- package/src/expert-io.ts +391 -0
- package/src/expert-offload.ts +15 -2
- package/src/expert-residency.ts +1134 -0
- package/src/expert-usage.ts +415 -0
- package/src/flags.ts +13 -0
- package/src/generate.ts +438 -35
- package/src/grammar.ts +585 -0
- package/src/hf-push.ts +1 -1
- package/src/hub-rest.ts +242 -0
- package/src/index.ts +14 -2
- package/src/jobs/runner.ts +37 -18
- package/src/kv-store.ts +586 -93
- package/src/lora.ts +85 -14
- package/src/md-modules.d.ts +42 -0
- package/src/media-fetch.ts +426 -0
- package/src/memory/rest.ts +245 -0
- package/src/memory/vault.ts +74 -0
- package/src/mlx/array.ts +149 -19
- package/src/mlx/autograd.ts +53 -16
- package/src/mlx/checkpoint.ts +46 -13
- package/src/mlx/custom-vjp.ts +83 -35
- package/src/mlx/ffi.ts +27 -3
- package/src/mlx/metal-kernel.ts +7 -4
- package/src/mlx/ops.ts +153 -23
- package/src/mlx/turboquant-ops.ts +831 -0
- package/src/mlx/turboquant-tables.ts +237 -0
- package/src/model/batched-mask.ts +62 -1
- package/src/model/batched-quant.ts +206 -0
- package/src/model/batched-rotating-quant.ts +346 -0
- package/src/model/batched-rotating.ts +46 -1
- package/src/model/compiled-decode.ts +4 -15
- package/src/model/factory.ts +103 -6
- package/src/model/gemma4-base.ts +395 -39
- package/src/model/gemma4.ts +122 -33
- package/src/model/generated/gemma4-12b.ts +9 -13
- package/src/model/generated/gemma4-26b.ts +10 -5
- package/src/model/generated/gemma4-e4b.ts +14 -9
- package/src/model/glm52-atlas.ts +511 -0
- package/src/model/glm52-cache.ts +706 -0
- package/src/model/glm52-config.ts +262 -0
- package/src/model/glm52-container.ts +305 -0
- package/src/model/glm52-coupling.ts +388 -0
- package/src/model/glm52-dsa.ts +602 -0
- package/src/model/glm52-expert-layout.ts +253 -0
- package/src/model/glm52-layout.ts +152 -0
- package/src/model/glm52-memory.ts +473 -0
- package/src/model/glm52-mla.ts +786 -0
- package/src/model/glm52-moe.ts +223 -0
- package/src/model/glm52-pilot.ts +537 -0
- package/src/model/glm52-reference.ts +445 -0
- package/src/model/glm52-residency.ts +557 -0
- package/src/model/glm52-resident-weights.ts +135 -0
- package/src/model/glm52-streamed-experts.ts +453 -0
- package/src/model/glm52-streamed-metal.ts +610 -0
- package/src/model/glm52-weights.ts +503 -0
- package/src/model/glm52.ts +1153 -0
- package/src/model/minicpm5.ts +58 -102
- package/src/model/paged-kv.ts +253 -0
- package/src/model/qwen3-delta.ts +76 -15
- package/src/model/qwen3-moe.ts +404 -0
- package/src/model/qwen3.ts +9 -8
- package/src/model/qwen3_5.ts +88 -23
- package/src/model/support.ts +19 -1
- package/src/model/universal/dense.ts +40 -5
- package/src/model/universal/rope.ts +36 -21
- package/src/native/expert_io.c +523 -0
- package/src/native-pack.ts +6 -4
- package/src/pi-web.ts +1299 -82
- package/src/preflight.ts +27 -8
- package/src/prompt-cache.ts +293 -33
- package/src/quantize/quantizer.ts +16 -1
- package/src/registry.ts +57 -2
- package/src/responses.ts +62 -49
- package/src/serve/batch-scheduler.ts +1050 -78
- package/src/serve/generation-gateway.ts +252 -24
- package/src/serve/isolate.ts +417 -0
- package/src/serve/lane-registry.ts +44 -0
- package/src/serve/session-search.ts +326 -0
- package/src/server.ts +1701 -175
- package/src/spec/assistant-source.ts +150 -0
- package/src/spec/deepspec-source.ts +143 -0
- package/src/spec/dflash-source.ts +124 -0
- package/src/spec/dspark/ab-stats.ts +136 -0
- package/src/spec/dspark/calibration.ts +112 -0
- package/src/spec/dspark/deepspec-module.ts +801 -0
- package/src/spec/dspark/generate-dflash.ts +72 -20
- package/src/spec/dspark/loader.ts +23 -0
- package/src/spec/dspark/module-dflash.ts +278 -36
- package/src/spec/dspark/quantize-drafter.ts +91 -0
- package/src/spec/generate.ts +59 -25
- package/src/spec/glm52-mtp-source.ts +392 -0
- package/src/spec/ngram-source.ts +132 -0
- package/src/spec/serve-loop.ts +617 -0
- package/src/spec/source.ts +118 -0
- package/src/spec/two-model.ts +171 -0
- package/src/ssd-cache.ts +111 -35
- package/src/tokenizer.ts +288 -2
- package/src/tool-approvals.ts +131 -0
- package/src/tool-call.ts +438 -22
- package/src/train/flash-cce.ts +2 -2
- package/src/train/job.ts +0 -12
- package/src/train/lora-params.ts +17 -6
- package/src/train/trainer.ts +60 -30
- package/src/vision/prompt.ts +217 -48
- package/src/web/app.html +1197 -1791
- package/src/web/app.js +5694 -0
- package/src/web/icon.svg +13 -0
- package/src/web/manifest.webmanifest +24 -0
- package/src/web/src/adapters-panel.ts +288 -0
- package/src/web/src/api.ts +57 -0
- package/src/web/src/assistant.ts +485 -0
- package/src/web/src/chat.ts +1050 -0
- package/src/web/src/composer.ts +959 -0
- package/src/web/src/dataset.ts +132 -0
- package/src/web/src/finetune.ts +267 -0
- package/src/web/src/hub.ts +428 -0
- package/src/web/src/main.ts +98 -0
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +572 -0
- package/src/web/src/model-picker.ts +159 -0
- package/src/web/src/palette.ts +346 -0
- package/src/web/src/protocol.ts +62 -0
- package/src/web/src/quantize.ts +167 -0
- package/src/web/src/rag.ts +274 -0
- package/src/web/src/sessions.ts +469 -0
- package/src/web/src/shell.ts +804 -0
- package/src/web/src/status.ts +300 -0
- package/src/web/src/ui-catalog.ts +92 -0
- package/src/web/sw.js +71 -0
- package/src/web/vendor/README.md +61 -0
- package/src/web/vendor/hljs-LICENSE +29 -0
- package/src/web/vendor/hljs-theme.css +59 -0
- package/src/web/vendor/hljs.js +26 -0
- package/src/web-tools.ts +54 -2
- package/src/weights.ts +82 -19
- package/src/model/fused-decode-kernel.ts +0 -312
- package/src/model/fused-geglu-kernel.ts +0 -198
- package/src/model/fused-mlp-kernel.ts +0 -169
- package/src/model/fused-swiglu-kernel.ts +0 -240
- package/src/model/steel-linear-kernel.ts +0 -96
- 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.
|
|
25
|
-
| `--prompt-cache` | GB | `
|
|
26
|
-
| `--
|
|
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
|
|
29
|
-
| `--
|
|
30
|
-
| `--
|
|
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
|
-
| `--
|
|
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
|
|
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`).
|
|
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
|
-
| `--
|
|
49
|
-
| `--fused-
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
the
|
|
60
|
-
|
|
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
|
|
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
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
bf16
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
| `
|
|
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`) |
|
|
147
|
-
|
|
|
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
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
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)* |
|
|
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 |
|
|
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` | ✅ | ✅
|
|
193
|
-
| `min_p` / `xtc_*` / `logit_bias` / presence+frequency penalties | ✅ | ✅
|
|
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
|
-
|
|
|
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
|
-
- **`--
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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.**
|
|
219
|
-
|
|
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
|
|
224
|
-
|
|
225
|
-
|
|
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.**
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
246
|
-
|
|
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
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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.
|