mlx-bun 0.0.9 → 0.0.11

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 (179) hide show
  1. package/README.md +66 -21
  2. package/THIRD_PARTY_LICENSES.md +1 -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 +255 -0
  7. package/docs/design/batching-v2-plan.md +23 -9
  8. package/docs/design/bucketing-stage.md +2 -1
  9. package/docs/design/compat-cli-surface-design.md +4 -3
  10. package/docs/design/decode-speed-program.md +172 -0
  11. package/docs/design/docs-reorg-plan.md +1 -1
  12. package/docs/design/dspark-serving-program.md +371 -0
  13. package/docs/design/dspark-speculative-decoding.md +167 -5
  14. package/docs/design/faithful-l1-consolidation.md +310 -0
  15. package/docs/design/generic-model-support.md +8 -5
  16. package/docs/design/grammar-spec-batching-integration.md +301 -0
  17. package/docs/design/minicpm5-decode-megakernel.md +16 -11
  18. package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
  19. package/docs/design/omlx-adoption-map.md +98 -0
  20. package/docs/design/orpo-training.md +34 -30
  21. package/docs/design/paged-kv-cache.md +214 -0
  22. package/docs/design/parallel-slots.md +82 -49
  23. package/docs/design/parity-tier-dag.md +43 -33
  24. package/docs/design/repo-cleanup-plan.md +189 -0
  25. package/docs/design/runtime-isolation.md +19 -3
  26. package/docs/design/segmented-backward-training.md +18 -2
  27. package/docs/design/ssd-kv-cold-tier.md +288 -0
  28. package/docs/design/structured-output.md +484 -0
  29. package/docs/design/the-dreaming-master-plan.md +2 -3
  30. package/docs/design/turboquant-kv.md +235 -0
  31. package/docs/design/unified-engine-frontier-plan.md +774 -0
  32. package/docs/design/web-chat-beat-matrix.md +315 -0
  33. package/docs/design/web-chat-redesign.md +1112 -0
  34. package/docs/investigations/decode-roofline-lookagain.md +92 -6
  35. package/docs/investigations/dspark-handoff.md +182 -16
  36. package/docs/investigations/kernel-perf-review-2026-07.md +105 -26
  37. package/docs/investigations/lab-build-journal.md +2 -2
  38. package/docs/investigations/openwiki-evaluation.md +249 -0
  39. package/docs/planning/IDEAS.md +14 -0
  40. package/docs/planning/PRODUCT_ROADMAP.md +2 -3
  41. package/docs/planning/chat-ui-vision-memory-first.md +199 -0
  42. package/docs/planning/chat-ui-vision.md +50 -0
  43. package/docs/planning/memory-docs-and-dag-plan.md +15 -6
  44. package/docs/planning/release-notes-v0.0.10.md +57 -0
  45. package/docs/planning/release-notes-v0.0.11.md +239 -0
  46. package/docs/reference/cli.md +35 -16
  47. package/docs/reference/distribution.md +13 -6
  48. package/docs/reference/embedding.md +35 -14
  49. package/docs/reference/features-matrix.md +97 -0
  50. package/docs/reference/library-api.md +30 -9
  51. package/docs/reference/memory.md +26 -6
  52. package/docs/reference/models.md +11 -1
  53. package/docs/reference/orpo-quickstart.md +3 -4
  54. package/docs/reference/server-api.md +634 -22
  55. package/docs/reference/server-config.md +251 -119
  56. package/docs/reference/training.md +20 -16
  57. package/package.json +5 -3
  58. package/src/anthropic.ts +13 -0
  59. package/src/audio/conformer.ts +647 -0
  60. package/src/audio/decode.ts +133 -0
  61. package/src/audio/features.ts +202 -0
  62. package/src/audio/transcode.ts +67 -0
  63. package/src/cli.ts +402 -107
  64. package/src/config.ts +49 -2
  65. package/src/download.ts +40 -0
  66. package/src/eval/README.md +7 -6
  67. package/src/eval/kl.ts +59 -3
  68. package/src/eval/quality-db.ts +2 -2
  69. package/src/eval/runner.ts +3 -3
  70. package/src/expert-offload.ts +15 -2
  71. package/src/flags.ts +13 -0
  72. package/src/generate.ts +330 -16
  73. package/src/grammar.ts +585 -0
  74. package/src/hub-rest.ts +242 -0
  75. package/src/kv-store.ts +618 -122
  76. package/src/lora.ts +8 -0
  77. package/src/md-modules.d.ts +42 -0
  78. package/src/media-fetch.ts +284 -0
  79. package/src/memory/rest.ts +245 -0
  80. package/src/memory/vault.ts +74 -0
  81. package/src/mlx/array.ts +127 -19
  82. package/src/mlx/autograd.ts +2 -2
  83. package/src/mlx/ffi.ts +27 -3
  84. package/src/mlx/metal-kernel.ts +7 -4
  85. package/src/mlx/ops.ts +179 -23
  86. package/src/mlx/turboquant-ops.ts +831 -0
  87. package/src/mlx/turboquant-tables.ts +237 -0
  88. package/src/model/batched-mask.ts +62 -1
  89. package/src/model/batched-quant.ts +206 -0
  90. package/src/model/batched-rotating-quant.ts +346 -0
  91. package/src/model/batched-rotating.ts +46 -1
  92. package/src/model/compiled-decode.ts +4 -15
  93. package/src/model/factory.ts +10 -4
  94. package/src/model/gemma4-base.ts +391 -34
  95. package/src/model/gemma4.ts +122 -33
  96. package/src/model/generated/gemma4-12b.ts +9 -13
  97. package/src/model/generated/gemma4-26b.ts +10 -5
  98. package/src/model/generated/gemma4-e4b.ts +14 -9
  99. package/src/model/minicpm5.ts +58 -102
  100. package/src/model/paged-kv.ts +253 -0
  101. package/src/model/qwen3-delta.ts +123 -15
  102. package/src/model/qwen3-moe.ts +404 -0
  103. package/src/model/qwen3.ts +9 -8
  104. package/src/model/qwen3_5.ts +99 -25
  105. package/src/model/support.ts +12 -1
  106. package/src/model/universal/dense.ts +40 -5
  107. package/src/model/universal/rope.ts +36 -21
  108. package/src/pi-web.ts +1299 -82
  109. package/src/preflight.ts +27 -8
  110. package/src/prompt-cache.ts +338 -32
  111. package/src/quantize/quantizer.ts +16 -1
  112. package/src/registry.ts +61 -3
  113. package/src/serve/batch-scheduler.ts +1001 -76
  114. package/src/serve/generation-gateway.ts +292 -30
  115. package/src/serve/isolate.ts +417 -0
  116. package/src/serve/lane-registry.ts +44 -0
  117. package/src/serve/session-search.ts +326 -0
  118. package/src/server.ts +1460 -105
  119. package/src/spec/assistant-source.ts +150 -0
  120. package/src/spec/deepspec-source.ts +143 -0
  121. package/src/spec/dflash-source.ts +124 -0
  122. package/src/spec/dspark/ab-stats.ts +136 -0
  123. package/src/spec/dspark/calibration.ts +112 -0
  124. package/src/spec/dspark/deepspec-module.ts +801 -0
  125. package/src/spec/dspark/generate-dflash.ts +72 -20
  126. package/src/spec/dspark/loader.ts +23 -0
  127. package/src/spec/dspark/module-dflash.ts +278 -36
  128. package/src/spec/dspark/quantize-drafter.ts +91 -0
  129. package/src/spec/generate.ts +59 -25
  130. package/src/spec/ngram-source.ts +132 -0
  131. package/src/spec/serve-loop.ts +536 -0
  132. package/src/spec/source.ts +93 -0
  133. package/src/spec/two-model.ts +171 -0
  134. package/src/ssd-cache.ts +285 -0
  135. package/src/tokenizer.ts +288 -2
  136. package/src/tool-approvals.ts +131 -0
  137. package/src/tool-call.ts +408 -20
  138. package/src/train/flash-cce.ts +29 -26
  139. package/src/train/job.ts +0 -2
  140. package/src/train/loss.ts +53 -11
  141. package/src/train/prefix-shared.ts +30 -7
  142. package/src/train/segmented.ts +150 -46
  143. package/src/train/trainer.ts +22 -15
  144. package/src/vision/prompt.ts +217 -48
  145. package/src/web/app.html +1184 -1789
  146. package/src/web/app.js +5667 -0
  147. package/src/web/icon.svg +13 -0
  148. package/src/web/manifest.webmanifest +24 -0
  149. package/src/web/src/adapters-panel.ts +288 -0
  150. package/src/web/src/api.ts +57 -0
  151. package/src/web/src/assistant.ts +485 -0
  152. package/src/web/src/chat.ts +1050 -0
  153. package/src/web/src/composer.ts +959 -0
  154. package/src/web/src/dataset.ts +132 -0
  155. package/src/web/src/finetune.ts +267 -0
  156. package/src/web/src/hub.ts +428 -0
  157. package/src/web/src/main.ts +98 -0
  158. package/src/web/src/markdown.ts +0 -0
  159. package/src/web/src/memory-panel.ts +572 -0
  160. package/src/web/src/model-picker.ts +159 -0
  161. package/src/web/src/palette.ts +346 -0
  162. package/src/web/src/protocol.ts +62 -0
  163. package/src/web/src/quantize.ts +167 -0
  164. package/src/web/src/rag.ts +274 -0
  165. package/src/web/src/sessions.ts +469 -0
  166. package/src/web/src/shell.ts +804 -0
  167. package/src/web/src/status.ts +252 -0
  168. package/src/web/src/ui-catalog.ts +92 -0
  169. package/src/web/sw.js +71 -0
  170. package/src/web/vendor/README.md +61 -0
  171. package/src/web/vendor/hljs-LICENSE +29 -0
  172. package/src/web/vendor/hljs-theme.css +59 -0
  173. package/src/web/vendor/hljs.js +26 -0
  174. package/src/model/fused-decode-kernel.ts +0 -312
  175. package/src/model/fused-geglu-kernel.ts +0 -198
  176. package/src/model/fused-mlp-kernel.ts +0 -169
  177. package/src/model/fused-swiglu-kernel.ts +0 -240
  178. package/src/model/steel-linear-kernel.ts +0 -96
  179. package/src/status-page.html +0 -497
package/src/cli.ts CHANGED
@@ -9,14 +9,15 @@
9
9
  // mlx-bun evals recent benchmark runs
10
10
  // mlx-bun harness pi [--base-url <url>] [--remove] connect your own pi to the local server
11
11
 
12
+ import { existsSync } from "node:fs";
13
+ import { basename, join, resolve } from "node:path";
12
14
  import { Registry } from "./registry";
13
- import { loadModelConfig } from "./config";
15
+ import { loadModelConfig, parseTurboQuantScheme, type TurboQuantScheme } from "./config";
14
16
  import { fit, skuMatrix, thisMachine } from "./fit";
15
17
  import { EvalDB } from "./evaldb";
16
18
  import pkg from "../package.json" with { type: "json" };
17
19
  import { renderHelp } from "./tui";
18
20
  import { isSupportedModelRecord } from "./model/support";
19
- import { perfKernelEnabled } from "./model/fused-decode-kernel";
20
21
  import type { JobEvent } from "./jobs/types";
21
22
 
22
23
  const argv = process.argv.slice(2);
@@ -34,7 +35,7 @@ Usage: mlx-bun [command] [options] (no command = serve)
34
35
 
35
36
  Commands:
36
37
  pi Launch a pi coding-agent session on a local model (the appliance path)
37
- serve Start the OpenAI/Anthropic-compatible server + status page
38
+ serve Start the OpenAI/Anthropic-compatible server + web app
38
39
  get Download a model from Hugging Face (resumable, verified)
39
40
  ls List downloaded models
40
41
  gc Reclaim superseded model snapshots + dead blobs from the HF cache
@@ -61,29 +62,72 @@ Options:
61
62
  Examples:
62
63
  mlx-bun # download (first run) + serve + open the chat UI
63
64
  mlx-bun pi # start (download if needed) and chat with an agent
64
- mlx-bun serve 12B # serve the 12B; status page at http://localhost:8080/
65
+ mlx-bun serve 12B # serve the 12B; web app at http://localhost:8080/
65
66
  mlx-bun train e4b --data ./prefs # ORPO LoRA fine-tune on {prompt,chosen,rejected} data
66
67
  mlx-bun get mlx-community/gemma-4-12B-it-OptiQ-4bit`;
67
68
 
68
69
  const SERVER_FLAGS = `Server options:
70
+ --model <path|query> Explicit model override (mlx_lm.server spelling):
71
+ a directory containing config.json loads from that
72
+ path; anything else resolves like the positional
73
+ query. Wins over the positional and auto-pick.
69
74
  --host <addr> Interface to bind [default: 127.0.0.1, loopback
70
75
  only; use 0.0.0.0 to expose on your network]
71
76
  --port <n> Listen port [default: 8080]
72
77
  --memory-budget <GB> Admission-control memory budget; requests that
73
78
  cannot fit are rejected instead of crashing the
74
79
  GPU [default: machine RAM × 0.75, check-only]
75
- --prompt-cache <GB> Prompt (KV) cache byte cap [default: 2 GB;
80
+ --kv-budget <GB> Aggregate KV budget across concurrent batch rows
81
+ (decimal GB): joiners whose projected KV
82
+ (prompt + max_tokens) would exceed it queue until
83
+ rows finish; a request over the budget alone is
84
+ rejected. Off unless set.
85
+ --prompt-cache <GB> Prompt (KV) cache byte cap [default: 8 GB;
76
86
  --prompt-cache 0 disables the cache]
77
- --batch <n> Max concurrent requests batched through the
78
- mlx-lm-parity engine [default: 1 = serial].
79
- >1 opts the whole server into bf16 continuous
80
- batching (= mlx-lm B=N); see --kv-quant.
87
+ --ssd-cache <dir> SSD cold tier for the prompt cache: prefix KV
88
+ spills to disk on eviction and persists across
89
+ restarts (long-context agent TTFT drops from a
90
+ full re-prefill to a zero-copy mmap restore).
91
+ Off unless set.
92
+ --ssd-cache-max <GB> SSD tier byte cap [default: 32 GB]
93
+ --ssd-demote-idle <sec> Demote prompt-cache entries idle this long to the
94
+ SSD tier, freeing their GPU memory (prefixes stay
95
+ reachable via zero-copy restore). 0 disables.
96
+ [default: 300 when --ssd-cache is on]
97
+ --ssd-cache-verify Verify tensor hashes on every restore (reads all
98
+ bytes eagerly — integrity paranoia only)
99
+ --isolate Run the inference engine as a CHILD process and
100
+ keep this process a pure UI/API proxy — the UI
101
+ stays instant under GPU load and survives engine
102
+ crashes (respawned). /ws/chat not proxied yet.
103
+ Requests naming another model (exact /v1/models
104
+ id) spawn/route to THAT model's engine child.
105
+ --model-pool <n> With --isolate: max RESIDENT model engines
106
+ [default: 1]. Over the cap, the least-recently-
107
+ used engine drains, demotes its caches to the
108
+ SSD tier, and exits — switching back respawns
109
+ it with all state restored from disk.
110
+ --unix <path> (internal) listen on a unix socket instead of a
111
+ TCP port — the engine half of --isolate.
112
+ --batch <n> Max concurrent requests decoded together
113
+ [default: 8]. A LONE request runs the exact
114
+ serial engine (same bits, same speed); the cap
115
+ engages only when concurrent requests arrive
116
+ (agent fan-out). 1 pins strict serial.
81
117
  --decode-concurrency is accepted as the
82
118
  mlx_lm.server alias (semantics differ; see
83
119
  docs/reference/server-config.md)
84
120
  --no-open Don't open the chat UI in your browser on start.
85
121
  By default an interactive terminal opens
86
122
  http://<host>:<port>/#/chat once the server is up.
123
+ --allow-private-media Let image_url/audio_url content parts fetch from
124
+ private/loopback/link-local hosts (NAS, another
125
+ LAN box). Blocked by default: a request's URL is
126
+ attacker-controlled input, and the default keeps
127
+ the server from being steered at cloud metadata
128
+ or LAN-internal services (SSRF). data: URLs are
129
+ always fine; the 10 s timeout and 64 MB response
130
+ cap apply either way.
87
131
 
88
132
  Model & quality:
89
133
  --adapter <dir> Mount a LoRA adapter at startup and use it as the
@@ -91,12 +135,48 @@ Model & quality:
91
135
  request's \`adapter\` field, incl. "none", wins).
92
136
  --adapter-path is accepted as the mlx_lm.server
93
137
  alias. Hot-swap via POST /v1/adapters unchanged.
138
+ --draft-model <query> Speculative decoding: a drafter proposes tokens
139
+ the main model verifies (exact results, faster
140
+ decode when drafts land). The artifact's kind is
141
+ auto-detected — a full same-tokenizer model
142
+ (mlx_lm.server parity), a Gemma "-assistant"
143
+ KV-borrowing drafter, or a DSpark checkpoint.
144
+ Serial lane only — with --batch N a mounted draft
145
+ routes every request serial, like mlx_lm.server.
146
+ --draft-kind <kind> Override draft-artifact detection:
147
+ two-model | assistant | dspark | deepspec | ngram
148
+ (deepspec = DeepSeek's released Gemma4DSparkModel
149
+ drafters; ngram = MODEL-FREE prompt lookup — no
150
+ --draft-model, drafts copied from the request's
151
+ own context, lossless) [default: auto]
152
+ --num-draft-tokens <n> Drafts per verify round [default: 3; ngram: 10;
153
+ DSpark pins to its trained block width]
154
+ --ngram-max <k> Prompt-lookup longest match window [default: 3]
155
+ --ngram-min <k> Prompt-lookup shortest match window [default: 1]
94
156
  --kv-quant <mode> KV cache quantization: config (per-layer
95
157
  kv_config.json when the model ships one), off
96
- (bf16), or 4 / 8 (uniform bits)
97
- [default: config; bf16 under --batch N — the
98
- batched engine is bf16-only, so an explicit
99
- --kv-quant routes those requests to the serial path]
158
+ (bf16), 4 / 8 (uniform bits), or turbo[:k<bits>v
159
+ <bits>] (rotation-based TurboQuant — default
160
+ k8v3; kBits in {2,4,5,8}, vBits in {2,3,4,5,8};
161
+ docs/design/turboquant-kv.md)
162
+ [default: off — quantized KV measured 5-20% slower
163
+ decode than bf16 at ≤16k (on mlx-lm too); it buys
164
+ memory headroom, so opt in when context is tight.
165
+ The batched engine (--batch N) is bf16-only: an
166
+ explicit --kv-quant routes those requests serial.
167
+ turbo is solo-only in v1 (full-attention layers
168
+ only; sliding-window layers stay bf16)]
169
+ --paged-kv OPTIONAL vLLM-style paged KV storage (block pool +
170
+ gather before the stock SDPA; env MLX_BUN_PAGED_KV=1).
171
+ v1: serial only (pins --batch 1 unless --batch is
172
+ given), Gemma4-family, bf16 — refuses --batch N>1,
173
+ --kv-quant, --draft-model; bypasses the prompt
174
+ cache; runs uncompiled decode. Bit-exact with the
175
+ plain path; expect a small decode cost at batch=1
176
+ (the gather copy). docs/design/paged-kv-cache.md
177
+ [default: off]
178
+ --paged-kv-block-size <n> Tokens per KV block [default: 256 = the plain
179
+ cache's growth step]
100
180
  --thinking <true|false> Default for the chat template's enable_thinking
101
181
  variable (CPM and other hybrid-reasoning models);
102
182
  a request's chat_template_kwargs overrides it
@@ -120,23 +200,28 @@ Model & quality:
120
200
  --hlg-toe <nats> HLG shadow rolloff scale [default: 6]
121
201
  --hlg-pivot-offset <nats> HLG pivot: nats below the top token [default: 6]
122
202
 
123
- Parity tier (sets the whole decode route; a per-fork flag below overrides):
124
- --l1 Bit-for-bit IDENTICAL to mlx-lm (bf16 KV, unfused)
203
+ Parity tier (an ALIAS for the per-fork flags below; each expands to the fastest
204
+ set of forks that still holds the guarantee — a per-fork flag overrides one):
205
+ --l1 Bit-for-bit IDENTICAL to mlx-lm (bf16 KV, compiled
206
+ activations — the faithful @mx.compile kernels).
207
+ No tier given = --l1 (decided 2026-07-05: the L1
208
+ faithful kernels match mlx-lm 1.00x on every model;
209
+ anything output-changing must beat that baseline in
210
+ a paired A/B to earn a default).
125
211
  --l2 Bit-for-bit IDENTICAL to mlx-optiq (quantized KV +
126
212
  fused prefill SDPA, stock unfused decode — the
127
- optiq-golden composition; perf-kernel stays OFF)
128
- --l3 Best performance, no bit-exact oracle (KL + test
129
- gated; adds the envelope-gated perf kernel).
130
- No tier given = the per-flag defaults below,
131
- which equal --l3 today.
213
+ optiq-golden composition)
132
214
 
133
- Performance levers (A/B levers; defaults are the measured winners):
215
+ Kill switches (bit-exact A/B levers — each selects a slower same-parity path):
134
216
  --compiled-decode on|off Compiled decode graphs [default: on]
135
- --perf-kernel on|off Fused quantized-KV decode-SDPA Metal kernel —
136
- NOT bit-exact, envelope-gated (an L3 lever; the
137
- perf side of the compat A/B) [default: on]
138
- --fused-decode on|off Fused-decode experiment lever [default: off]
139
- --fused-sdpa on|off Fused SDPA path [default: on]
217
+ --compiled-activations on|off
218
+ Compiled geglu/swiglu (mlx-lm's @mx.compile kernel —
219
+ bit-exact AND fewer dispatches). off = the uncompiled
220
+ composition (same parity, slower) [default: on]
221
+ --fused-sdpa on|off Fused SDPA path (quantized KV only) [default:
222
+ follows --kv-quant — on for config (the optiq
223
+ composition), off for uniform 4/8 (the mlx-lm
224
+ composition) and bf16]
140
225
  --force-wire Wire weights into memory at load
141
226
  --expert-offload MoE only: serve experts from a page-aligned file
142
227
  mmap (built on first use) — keeps the model out of
@@ -185,7 +270,7 @@ ${SERVER_FLAGS}
185
270
 
186
271
  Endpoints: /v1/chat/completions, /v1/completions, /v1/messages, /v1/responses,
187
272
  /v1/models, /v1/adapters, /health, /stats, /fit, /library, /downloads —
188
- status page at /, browser chat at /chat`,
273
+ web app at / (lands on chat) — /chat, /status, /quantize, /finetune, /dataset deep-link its views`,
189
274
 
190
275
  get: `mlx-bun get — download a model from Hugging Face
191
276
 
@@ -285,14 +370,17 @@ and runs the mlx-lm/optiq comparison legs.
285
370
  positional) [default: auto-pick]
286
371
 
287
372
  Options:
373
+ --model <path|query> Explicit model override: a directory containing
374
+ config.json loads from that path; anything else
375
+ resolves like the positional query. Wins over both.
288
376
  --tokens <n> Tokens to decode per run [default: 256]
289
377
  --runs <n> Runs (median reported) [default: 3]
290
378
  --prompt-tokens <n> Pad the prompt to ~n tokens (long-context decode)
291
- --kv-quant <mode> config | off | 4 | 8 [default: off, the
292
- historical baseline]
379
+ --kv-quant <mode> config | off | 4 | 8 | turbo[:k<bits>v<bits>]
380
+ [default: off, the historical baseline]
293
381
 
294
- Performance levers (--compiled-decode, --perf-kernel, --fused-decode,
295
- --fused-sdpa) apply to the run — A/B by running twice.`,
382
+ Kill switches (--compiled-decode, --compiled-activations, --fused-sdpa)
383
+ apply to the run — A/B by running twice.`,
296
384
 
297
385
  embed: `mlx-bun embed — text embeddings from a local embedding model
298
386
 
@@ -341,9 +429,9 @@ Options:
341
429
  --top-k <n> Top-k sampling
342
430
  --seed <n> Sampler seed (reproducible runs)
343
431
 
344
- Decode-path levers mirror serve: --l1/--l2/--l3 and the per-fork flags
345
- (--perf-kernel/--fused-sdpa/--compiled-decode/--fused-decode/--kv-quant).
346
- The tier's KV scheme applies here too (--l2/--l3 = the model's kv_config,
432
+ Decode-path levers mirror serve: --l1/--l2 and the per-fork flags
433
+ (--fused-sdpa/--compiled-decode/--compiled-activations/--kv-quant).
434
+ The tier's KV scheme applies here too (--l2 = the model's kv_config,
347
435
  same tokens as serve); with no tier/--kv-quant the KV cache stays bf16
348
436
  (the bit-exact mlx-lm greedy path). mlx-lm compat = --l1.
349
437
 
@@ -703,69 +791,96 @@ function openChatUi(url: string, hostPort: string): void {
703
791
  /** Server/runtime flags shared by every mode that loads a model
704
792
  * (serve, pi). Env levers are set here so they're in place before the
705
793
  * generate/compiled-decode modules read them. */
706
- /** Resolve the decode ROUTE: a tier alias (--l1/--l2/--l3) sets the whole route,
707
- * and an explicit per-fork flag (--perf-kernel/--kv-quant/…) overrides the alias.
708
- * The tier IS the intent; the forks are the escape hatch. Sets the decode env
709
- * levers and returns the kv-quant mode. See docs/design/parity-tier-dag.md.
794
+ /** Resolve the decode ROUTE: a tier alias (--l1/--l2) sets the whole route,
795
+ * and an explicit per-fork flag (--kv-quant/--fused-sdpa/…) overrides the
796
+ * alias. Sets the decode env levers and returns the kv-quant mode. See
797
+ * docs/design/parity-tier-dag.md + unified-engine-frontier-plan.md.
710
798
  * Each tier is a GUARANTEE about which reference you reproduce bit-for-bit:
711
799
  * --l1 bit-for-bit IDENTICAL to mlx-lm — drop-in replacement for mlx-lm
712
- * --l2 bit-for-bit IDENTICAL to mlx-optiq — drop-in replacement for mlx-optiq.
713
- * The optiq-golden decode composition (scripts/regen-kvq-goldens.ts) is
714
- * fused N-tiled SDPA for L>1 + STOCK unfused L=1 decode, so L2 = fused-sdpa
715
- * ON, perf-kernel OFF. The perf kernel is an mlx-bun ORIGINAL Metal kernel,
716
- * envelope-gated (≥56/64 teacher-forced argmax vs OUR OWN frozen compat
717
- * trajectory, tests/perf-kernel-oracle.test.ts) — an L3 node, never the
718
- * bare-L2 default. (Commit f1bf5cc put it in L2 claiming the goldens track
719
- * it; they don't — reverted 2026-07-01 with the evidence above.)
720
- * --l3 best performance, NO bit-exact guarantee — correct, but gated by KL +
721
- * tests, because mlx-optiq has no analogy for what L3 does.
722
- * L2 ⊂ L3 (L3 = L2 + perf kernel). L1 is a separate target (bf16, unfused). */
723
- function applyDecodeRoute(): { kvQuant?: "off" | "config" | number } {
800
+ * (bf16 KV; THE naked default since 2026-07-05)
801
+ * --l2 bit-for-bit IDENTICAL to mlx-optiq — drop-in replacement for
802
+ * mlx-optiq: quantized KV per kv_config.json + fused N-tiled SDPA for
803
+ * L>1 + STOCK unfused L=1 decode (the optiq-golden composition,
804
+ * scripts/regen-kvq-goldens.ts + regen-mixed-kv-goldens.ts).
805
+ * There is no --l3: output-changing experiments live in the Lab (env flags
806
+ * with a bench + expiry) until one beats the L1 baseline in a paired A/B —
807
+ * docs/design/unified-engine-frontier-plan.md §6-7. */
808
+ function applyDecodeRoute(): { kvQuant?: "off" | "config" | number; turboQuant?: TurboQuantScheme } {
724
809
  const onOff = (name: string): boolean | null => {
725
810
  const v = opt(name); if (v == null) return null;
726
811
  if (v === "on" || v === "1" || v === "true") return true;
727
812
  if (v === "off" || v === "0" || v === "false") return false;
728
813
  console.error(`--${name} expects on|off (got "${v}")`); process.exit(1);
729
814
  };
730
- type Preset = { kv: "off" | "config"; perf: boolean; fusedSdpa: boolean; compiled: boolean; fusedDecode: boolean };
815
+ if (flag("l3")) {
816
+ console.error(
817
+ "--l3 was removed 2026-07-05: no output-changing kernel beat the L1 baseline " +
818
+ "in a paired A/B, so the tier had no content. Use --l2 for the optiq route, " +
819
+ "or the Lab (docs/design/unified-engine-frontier-plan.md) for experiments.",
820
+ );
821
+ process.exit(1);
822
+ }
823
+ type Preset = { kv: "off" | "config"; fusedSdpa: boolean; compiled: boolean; compiledAct: boolean };
731
824
  const TIERS: Record<string, Preset> = {
732
825
  // A tier is the GUARANTEE; within it we default to the FAST kernel that still
733
826
  // holds it, and expose the slow one as an opt-in. compiled-decode is BIT-EXACT
734
827
  // with uncompiled (compiled-decode.test: on==off) → free speed, so it's ON in
735
- // EVERY tier (--compiled-decode off is the slow, same-tier opt-in). fused-sdpa
736
- // matches OPTIQ bit-for-bit (tier-a goldens, fused-sdpa.test) and is
737
- // quantized-KV only → it's the L2 bridge, a no-op on L1's bf16. perf-kernel is
738
- // NOT: it's an original flash-decoding kernel, envelope-gated against our own
739
- // frozen trajectory (perf-kernel-oracle.test) — bare --l2 must ship the
740
- // optiq-golden decode composition (stock unfused L=1), so perf-kernel is
741
- // L3/explicit-only (--perf-kernel on opts a tier into it by choice). This is
742
- // the decode-axis difference between L2 and L3; L3 also owns the no-oracle
743
- // FEATURES (HLG sampler, expert offload, batched mixed-precision).
744
- l1: { kv: "off", perf: false, fusedSdpa: false, compiled: true, fusedDecode: false }, // = mlx-lm bit-for-bit (bf16)
745
- l2: { kv: "config", perf: false, fusedSdpa: true, compiled: true, fusedDecode: false }, // = mlx-optiq bit-for-bit
746
- l3: { kv: "config", perf: true, fusedSdpa: true, compiled: true, fusedDecode: false }, // best perf (envelope-gated decode)
828
+ // EVERY tier (--compiled-decode off is the slow, same-tier opt-in). compiled
829
+ // ACTIVATIONS (geglu/swiglu) go through the SAME libmlx as mlx-lm's `@mx.compile`
830
+ // → same kernel, bit-exact AND faster → ON in every tier (--compiled-activations
831
+ // off is the slower same-parity opt-in). fused-sdpa matches OPTIQ bit-for-bit
832
+ // (tier-a goldens, fused-sdpa.test) and is quantized-KV only → it's the L2 bridge,
833
+ // a no-op on L1's bf16.
834
+ l1: { kv: "off", fusedSdpa: false, compiled: true, compiledAct: true }, // = mlx-lm bit-for-bit (bf16)
835
+ l2: { kv: "config", fusedSdpa: true, compiled: true, compiledAct: true }, // = mlx-optiq bit-for-bit
747
836
  };
748
- const tier = flag("l1") ? "l1" : flag("l2") ? "l2" : flag("l3") ? "l3" : null;
749
- const p = tier ? TIERS[tier]! : null;
837
+ // NAKED DEFAULT = L1 (decided 2026-07-05): with no tier flag you get the
838
+ // mlx-lm bit-for-bit route. The 2026-07-05 benchmark pass showed the L1
839
+ // faithful kernel set matches mlx-lm 1.00× on every model while every
840
+ // output-changing lever failed to beat it in a paired A/B (the losers were
841
+ // deleted the same day — Phase 1 of unified-engine-frontier-plan.md).
842
+ // Quantized KV costs 5–20% decode at ≤16k on BOTH stacks and pays only in
843
+ // memory/context headroom → explicit opt-in (--kv-quant / --l2).
844
+ const tier = flag("l2") ? "l2" : "l1";
845
+ const p = TIERS[tier]!;
846
+ const kv = opt("kv-quant"); // explicit --kv-quant overrides the tier
750
847
  const pick = (name: string, base: boolean | undefined): boolean | null => {
751
848
  const ex = onOff(name); return ex !== null ? ex : (base ?? null); // explicit flag wins, else tier preset
752
849
  };
753
850
  const set = (env: string, val: boolean | null, invert = false) => {
754
851
  if (val !== null) process.env[env] = (invert ? !val : val) ? "1" : "0";
755
852
  };
756
- set("MLX_BUN_COMPILED_DECODE", pick("compiled-decode", p?.compiled));
757
- set("MLX_BUN_PERF_KERNEL", pick("perf-kernel", p?.perf));
758
- set("MLX_BUN_FUSED_DECODE", pick("fused-decode", p?.fusedDecode));
759
- set("MLX_BUN_NO_FUSED_SDPA", pick("fused-sdpa", p?.fusedSdpa), true); // inverted env
760
- const kv = opt("kv-quant"); // explicit --kv-quant overrides the tier
853
+ // Explicit --kv-quant picks the composition its ORACLE uses: config (the
854
+ // optiq mixed scheme) runs optiq's fused SDPA (the L2 golden composition);
855
+ // uniform 4/8 runs UNFUSED (mlx-lm's quantized_scaled_dot_product_attention
856
+ // — the bit-exact L1-eligible scheme). --fused-sdpa still overrides either.
857
+ const fusedSdpaBase = kv === "config" ? true : kv != null ? false : p.fusedSdpa;
858
+ set("MLX_BUN_COMPILED_DECODE", pick("compiled-decode", p.compiled));
859
+ set("MLX_BUN_NO_FUSED_SDPA", pick("fused-sdpa", fusedSdpaBase), true); // inverted env
860
+ // Compiled activations (the faithful geglu/swiglu — mlx-lm's @mx.compile kernel).
861
+ // One fork drives both env vars; qwen3/qwen3.5/universal compile unconditionally,
862
+ // so this toggles gemma geglu + minicpm5 swiglu (the only unfused-capable sites).
863
+ set("MLX_BUN_COMPILED_GEGLU", pick("compiled-activations", p.compiledAct));
864
+ set("MLX_BUN_COMPILED_SWIGLU", pick("compiled-activations", p.compiledAct));
761
865
  if (kv === "off") return { kvQuant: "off" };
762
866
  if (kv === "config") return { kvQuant: "config" };
763
867
  if (kv) {
868
+ // TurboQuant (docs/design/turboquant-kv.md): a distinct axis from the
869
+ // affine kvQuant above — dequantize-on-fetch, stock unfused ops.sdpa,
870
+ // no kvQuant value at all (mutually exclusive with kvBits/kvConfig).
871
+ let turboQuant: TurboQuantScheme | null;
872
+ try {
873
+ turboQuant = parseTurboQuantScheme(kv);
874
+ } catch (e) {
875
+ console.error((e as Error).message);
876
+ process.exit(1);
877
+ }
878
+ if (turboQuant) return { turboQuant };
764
879
  const bits = Number(kv);
765
- if (![4, 8].includes(bits)) { console.error(`--kv-quant expects config|off|4|8 (got "${kv}")`); process.exit(1); }
880
+ if (![4, 8].includes(bits)) { console.error(`--kv-quant expects config|off|4|8|turbo (got "${kv}")`); process.exit(1); }
766
881
  return { kvQuant: bits };
767
882
  }
768
- return p ? { kvQuant: p.kv } : {};
883
+ return { kvQuant: p.kv };
769
884
  }
770
885
 
771
886
  function serverRuntimeFlags(): { port: number; serverOptions: import("./server").ServerOptions } {
@@ -777,20 +892,48 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
777
892
  console.error(`--${name} expects on|off (got "${v}")`);
778
893
  process.exit(1);
779
894
  };
780
- const route = applyDecodeRoute(); // --l1/--l2/--l3 tier alias, with per-fork flags overriding
895
+ const route = applyDecodeRoute(); // --l1/--l2 tier alias, with per-fork flags overriding
781
896
  if (flag("force-wire")) process.env.MLX_BUN_FORCE_WIRE = "1";
897
+ // Remote image_url/audio_url media fetches block private/loopback/link-
898
+ // local destinations by default (SSRF — src/media-fetch.ts); this is the
899
+ // LAN-hosts escape hatch.
900
+ if (flag("allow-private-media")) process.env.MLX_BUN_ALLOW_PRIVATE_MEDIA = "1";
782
901
 
783
902
  const serverOptions: import("./server").ServerOptions = {};
784
903
  const budgetGB = Number(opt("memory-budget", "0"));
785
904
  if (budgetGB > 0) serverOptions.memoryBudgetBytes = budgetGB * 1e9;
905
+ // Aggregate KV budget across batch rows (batching-perf-path P3 admission).
906
+ const kvBudgetGB = Number(opt("kv-budget", "0"));
907
+ if (kvBudgetGB > 0) serverOptions.kvBudgetBytes = kvBudgetGB * 1e9;
786
908
  const pcRaw = opt("prompt-cache"); // null if absent; an explicit value (incl. 0) wins → `--prompt-cache 0` DISABLES the cache
787
909
  if (pcRaw !== null) serverOptions.promptCacheBytes = Math.max(0, Number(pcRaw)) * 2 ** 30;
910
+ // Engine-child mode (runtime-isolation.md): listen on a unix socket
911
+ // instead of TCP — the isolated parent proxies here. Internal flag,
912
+ // spawned by --isolate; usable directly for socket-level integration.
913
+ const unixRaw = opt("unix");
914
+ if (unixRaw !== null) serverOptions.unixSocket = unixRaw;
915
+ // SSD cold tier (docs/design/ssd-kv-cold-tier.md) — off unless a dir is given.
916
+ const ssdDir = opt("ssd-cache");
917
+ if (ssdDir !== null) {
918
+ serverOptions.ssdCacheDir = ssdDir;
919
+ const maxRaw = opt("ssd-cache-max");
920
+ if (maxRaw !== null) serverOptions.ssdCacheMaxBytes = Math.max(1, Number(maxRaw)) * 2 ** 30;
921
+ if (flag("ssd-cache-verify")) serverOptions.ssdCacheVerify = true;
922
+ const demoteRaw = opt("ssd-demote-idle");
923
+ if (demoteRaw !== null) serverOptions.ssdDemoteIdleSec = Math.max(0, Number(demoteRaw));
924
+ } else {
925
+ // Refuse-loudly house rule: sub-flags of an absent tier must not vanish.
926
+ for (const f of ["ssd-cache-max", "ssd-demote-idle"] as const)
927
+ if (opt(f) !== null) console.warn(`--${f} has no effect without --ssd-cache — ignored`);
928
+ if (flag("ssd-cache-verify"))
929
+ console.warn("--ssd-cache-verify has no effect without --ssd-cache — ignored");
930
+ }
788
931
  // --batch N: max concurrent requests batched through the mlx-lm-parity
789
932
  // engine (N=1 = today's serial path). --decode-concurrency is accepted for
790
933
  // drop-in compatibility with mlx_lm.server, but the semantics differ: there
791
934
  // it caps per-BatchGenerator decode parallelism (default 32); in mlx-bun it
792
- // enables continuous batching with this cap (the default is the optimized
793
- // serial path).
935
+ // caps continuous batching (default 8; `--batch 1` pins the serial path —
936
+ // the unified engine is bit-parity with it and costs ~nothing at B=1).
794
937
  const batchRaw = opt("batch") ?? opt("decode-concurrency");
795
938
  if (batchRaw !== null) {
796
939
  const n = Number(batchRaw);
@@ -801,6 +944,19 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
801
944
  serverOptions.batch = n;
802
945
  }
803
946
  if (route.kvQuant !== undefined) serverOptions.kvQuant = route.kvQuant;
947
+ if (route.turboQuant !== undefined) serverOptions.turboQuant = route.turboQuant;
948
+ // --paged-kv (env: MLX_BUN_PAGED_KV=1): OPTIONAL vLLM-style block-pool KV
949
+ // storage, default off (docs/design/paged-kv-cache.md). Serial-only in v1:
950
+ // with no explicit --batch, pin --batch 1 (the default is 8); an explicit
951
+ // --batch N>1 is refused loudly by createServer rather than downgraded.
952
+ if (flag("paged-kv") || process.env.MLX_BUN_PAGED_KV === "1") {
953
+ const bsRaw = opt("paged-kv-block-size");
954
+ serverOptions.pagedKv = bsRaw !== null ? { blockSize: Number(bsRaw) } : {};
955
+ if (batchRaw === null) {
956
+ serverOptions.batch = 1;
957
+ console.log("[paged-kv] serial-only in v1 — pinning --batch 1");
958
+ }
959
+ }
804
960
  // Bind loopback unless asked otherwise (mlx_lm.server parity); --host
805
961
  // 0.0.0.0 is the explicit opt-in for LAN exposure. The chat-UI open and
806
962
  // `mlx-bun pi` attach both go through localhost, so loopback-only is
@@ -879,11 +1035,11 @@ async function mountStartupAdapter(
879
1035
 
880
1036
  /** One-line summary of the active runtime levers for the ready card. */
881
1037
  function runtimeSummary(o: import("./server").ServerOptions): string {
882
- const kv = o.kvQuant === "off" ? "off" : typeof o.kvQuant === "number" ? `kv${o.kvQuant}` : "config";
1038
+ const kv = o.turboQuant ? `turbo-k${o.turboQuant.kBits}v${o.turboQuant.vBits}`
1039
+ : o.kvQuant === "off" ? "off" : typeof o.kvQuant === "number" ? `kv${o.kvQuant}` : "config";
883
1040
  const lever = (env: string, dflt: string) => process.env[env] ?? dflt;
884
1041
  return `kv-quant ${kv} · compiled-decode ${lever("MLX_BUN_COMPILED_DECODE", "1") === "1" ? "on" : "off"}` +
885
- ` · perf-kernel ${perfKernelEnabled() ? "on" : "off"}` +
886
- (lever("MLX_BUN_FUSED_DECODE", "0") === "1" ? " · fused-decode on" : "") +
1042
+ (lever("MLX_BUN_COMPILED_GEGLU", "1") === "0" ? " · compiled-activations off" : "") +
887
1043
  (o.batch && o.batch > 1 ? ` · batch ${o.batch}` : "") +
888
1044
  (o.defaultThinking !== undefined ? ` · thinking ${o.defaultThinking ? "on" : "off"}` : "") +
889
1045
  (o.defaultTemperature !== undefined ? ` · temp ${o.defaultTemperature}` : "") +
@@ -893,8 +1049,21 @@ function runtimeSummary(o: import("./server").ServerOptions): string {
893
1049
 
894
1050
  /** Shared model resolution: explicit query wins; otherwise e4b (the default
895
1051
  * model everywhere) if it's downloaded, else the largest supported model that
896
- * fits this machine. Downloads the recommended model first on a fresh install. */
1052
+ * fits this machine. Downloads the recommended model first on a fresh install.
1053
+ * A query that is a DIRECTORY containing config.json loads directly via
1054
+ * scanSnapshot (no registry dependency) — the `--model <path>` bench/CI shape. */
897
1055
  async function resolveModelAuto(query: string | null): Promise<{ m: import("./registry").ModelRecord; picked: boolean }> {
1056
+ if (query && existsSync(join(query, "config.json"))) {
1057
+ const { scanSnapshot } = await import("./registry");
1058
+ const dir = resolve(query);
1059
+ // Recover the repo id from an HF-cache path (…/models--org--name/snapshots/<rev>);
1060
+ // otherwise the directory basename is the display id.
1061
+ const hf = /models--([^/]+)--([^/]+)\/snapshots\//.exec(dir);
1062
+ const repoId = hf ? `${hf[1]}/${hf[2]}` : basename(dir);
1063
+ const m = await scanSnapshot(dir, repoId);
1064
+ if (!m) throw new Error(`${dir} has config.json but no weights (*.safetensors)`);
1065
+ return { m, picked: false };
1066
+ }
898
1067
  const reg = new Registry();
899
1068
  if (reg.list().length === 0) await reg.scan();
900
1069
  if (query) return { m: reg.resolve(query), picked: false };
@@ -920,7 +1089,7 @@ async function resolveModelAuto(query: string | null): Promise<{ m: import("./re
920
1089
  await reg.scan();
921
1090
  if (recRepo !== starterRepo) {
922
1091
  console.log(` ${style.dim(`downloading ${recRepo} (recommended for this Mac) in the background —`)}`);
923
- console.log(` ${style.dim("it becomes the default next run · progress on the status page (/downloads) · resumable")}`);
1092
+ console.log(` ${style.dim("it becomes the default next run · progress in the web app's Status view · resumable")}`);
924
1093
  downloadModel(recRepo)
925
1094
  .then(async () => {
926
1095
  await new Registry().scan();
@@ -1210,7 +1379,7 @@ switch (cmd) {
1210
1379
  console.log(style.dim(" no command given — starting the server · mlx-bun --help for all commands"));
1211
1380
  const rt = serverRuntimeFlags();
1212
1381
  // Friendly collision check before loading gigabytes of weights.
1213
- {
1382
+ if (!rt.serverOptions.unixSocket) {
1214
1383
  const { probeServer } = await import("./harness-pi");
1215
1384
  const running = await probeServer(`http://localhost:${rt.port}/v1`);
1216
1385
  if (running) {
@@ -1220,7 +1389,77 @@ switch (cmd) {
1220
1389
  process.exit(1);
1221
1390
  }
1222
1391
  }
1223
- const { m, picked } = await resolveModelAuto(positional(0) ?? opt("query"));
1392
+ // Runtime isolation (--isolate, runtime-isolation.md): THIS process
1393
+ // becomes a pure reactor — it binds the TCP port and reverse-proxies to
1394
+ // an engine CHILD (the full server on a unix socket, spawned from the
1395
+ // same argv minus parent-only flags). Zero MLX calls in the parent: the
1396
+ // UI stays instant under any GPU load, and an engine crash respawns
1397
+ // without taking the session down.
1398
+ if (!flag("isolate") && opt("model-pool") !== null)
1399
+ console.warn("--model-pool has no effect without --isolate (child-per-model pool) — ignored");
1400
+ if (flag("isolate")) {
1401
+ const { startProxyServer, engineArgvForModel, defaultSocketPath } = await import("./serve/isolate");
1402
+ const sock = defaultSocketPath();
1403
+ // Re-spawn ourselves: compiled binary = execPath alone (argv[1] is a
1404
+ // $bunfs virtual path); dev = bun + the script path.
1405
+ const selfArgv = process.argv[1] && !process.argv[1].includes("$bunfs")
1406
+ ? [process.execPath, process.argv[1]]
1407
+ : [process.execPath];
1408
+ const rawArgs = process.argv.slice(2);
1409
+ // Resolve the DEFAULT model in the parent (registry lookup — no
1410
+ // weights load) so the pool has its key and the child gets a pinned
1411
+ // --model path instead of re-running fuzzy resolution.
1412
+ const { m: m0 } = await resolveModelAuto(opt("model") ?? positional(0) ?? opt("query"));
1413
+ // STRICT resolver for the pool (P2): only EXACT /v1/models ids
1414
+ // switch models. Fuzzy matching here would hijack drop-in clients
1415
+ // that send model:"gpt-4"-style strings (which must keep today's
1416
+ // ignored-field semantics and ride the default engine).
1417
+ const { Registry } = await import("./registry");
1418
+ const poolResolve = (query: string): { repoId: string; path: string } | null => {
1419
+ if (query === m0.repoId) return { repoId: m0.repoId, path: m0.path };
1420
+ try {
1421
+ const reg = new Registry();
1422
+ try {
1423
+ const hit = reg.listCanonical().find((r) => r.repoId === query);
1424
+ return hit ? { repoId: hit.repoId, path: hit.path } : null;
1425
+ } finally {
1426
+ reg.close();
1427
+ }
1428
+ } catch {
1429
+ return null;
1430
+ }
1431
+ };
1432
+ const poolMaxRaw = opt("model-pool");
1433
+ const poolMax = Math.max(1, Number(poolMaxRaw ?? 1) || 1);
1434
+ const { engine, pool } = startProxyServer({
1435
+ port: rt.port,
1436
+ ...(rt.serverOptions.hostname ? { hostname: rt.serverOptions.hostname } : {}),
1437
+ engine: {
1438
+ argv: [...selfArgv, ...engineArgvForModel(rawArgs, sock, m0.path)],
1439
+ socketPath: sock,
1440
+ },
1441
+ pool: {
1442
+ rawArgs, selfArgv, poolMax,
1443
+ resolve: poolResolve,
1444
+ defaultKey: m0.repoId,
1445
+ },
1446
+ });
1447
+ const shownHost0 = rt.serverOptions.hostname ?? "localhost";
1448
+ console.log(style.dim(` isolated mode: engine child starting (socket ${sock})`));
1449
+ console.log(` ${style.green("●")} ${style.bold("proxy up")} http://${shownHost0}:${rt.port} ${style.dim(`· ${m0.repoId} loading behind it · model pool ${poolMax}`)}`);
1450
+ const onExit = () => { pool?.stopAll(); engine.stop(); process.exit(0); };
1451
+ process.on("SIGINT", onExit);
1452
+ process.on("SIGTERM", onExit);
1453
+ await engine.ready;
1454
+ console.log(` ${style.green("●")} ${style.bold("engine ready")} ${style.dim(`pid ${engine.pid}`)}`);
1455
+ await new Promise(() => {}); // serve until killed
1456
+ }
1457
+ // --model <path-or-query> is the explicit override (mlx_lm.server's
1458
+ // spelling — previously accepted-but-IGNORED here, which silently
1459
+ // auto-picked the wrong model). A directory with config.json loads
1460
+ // straight from that path; anything else resolves like the positional
1461
+ // fuzzy query. Precedence: --model > positional > --query > auto-pick.
1462
+ const { m, picked } = await resolveModelAuto(opt("model") ?? positional(0) ?? opt("query"));
1224
1463
  const sFit = step("assessing fit");
1225
1464
  const config = await loadModelConfig(m.path);
1226
1465
  const report = fit(config, m.sizeBytes, 8192, undefined, undefined, m.expertsBytes);
@@ -1242,10 +1481,65 @@ switch (cmd) {
1242
1481
  sOff.done(`experts mmap'd ${style.dim(dir)} ${style.dim("· phys_footprint ≈ core")}`);
1243
1482
  }
1244
1483
  }
1484
+ // Speculative decoding (mlx_lm.server parity): --draft-model <path|query>
1485
+ // resolves like the main model; --num-draft-tokens defaults to 3 (the
1486
+ // server's default upstream — mlx_lm.generate's is 2). Serial-lane-only:
1487
+ // with --batch N, a mounted draft routes every request serial (upstream
1488
+ // is_batchable = draft is None).
1489
+ const draftQuery = opt("draft-model");
1490
+ let draftModelDir: string | undefined;
1491
+ if (draftQuery !== null) {
1492
+ const { m: dm } = await resolveModelAuto(draftQuery);
1493
+ draftModelDir = dm.path;
1494
+ }
1495
+ const numDraftRaw = opt("num-draft-tokens");
1496
+ const numDraftTokens = numDraftRaw !== null ? Number(numDraftRaw) : undefined;
1497
+ if (numDraftTokens !== undefined && (!Number.isInteger(numDraftTokens) || numDraftTokens < 1)) {
1498
+ console.error(`--num-draft-tokens expects an integer >= 1 (got "${numDraftRaw}")`);
1499
+ process.exit(1);
1500
+ }
1501
+ const draftKindRaw = opt("draft-kind");
1502
+ const draftKind = (draftKindRaw ?? undefined) as
1503
+ | "dspark" | "deepspec" | "assistant" | "two-model" | "ngram" | undefined;
1504
+ if (draftKind !== undefined && !["dspark", "deepspec", "assistant", "two-model", "ngram"].includes(draftKind)) {
1505
+ console.error(`--draft-kind expects two-model|assistant|dspark|deepspec|ngram (got "${draftKindRaw}")`);
1506
+ process.exit(1);
1507
+ }
1508
+ // Prompt-lookup window bounds (ngram kind only; validated here so a typo
1509
+ // fails the launch, not the first request).
1510
+ const ngramInt = (name: string): number | undefined => {
1511
+ const raw = opt(name);
1512
+ if (raw === null) return undefined;
1513
+ const v = Number(raw);
1514
+ if (!Number.isInteger(v) || v < 1) {
1515
+ console.error(`--${name} expects an integer >= 1 (got "${raw}")`);
1516
+ process.exit(1);
1517
+ }
1518
+ return v;
1519
+ };
1520
+ const ngramMax = ngramInt("ngram-max");
1521
+ const ngramMin = ngramInt("ngram-min");
1522
+ if (ngramMax !== undefined && ngramMin !== undefined && ngramMin > ngramMax) {
1523
+ console.error(`--ngram-min (${ngramMin}) must be <= --ngram-max (${ngramMax})`);
1524
+ process.exit(1);
1525
+ }
1526
+ if ((ngramMax !== undefined || ngramMin !== undefined) && draftKind !== "ngram")
1527
+ console.warn("--ngram-max/--ngram-min only apply with --draft-kind ngram — ignored");
1245
1528
  const sLoad = step("loading weights");
1246
1529
  const t0 = performance.now();
1247
- const ctx = await loadContext(m.path, m.repoId, { memoryBudgetBytes: rt.serverOptions.memoryBudgetBytes });
1248
- sLoad.done(`weights loaded ${style.dim(`in ${(performance.now() - t0).toFixed(0)} ms`)}`);
1530
+ const ctx = await loadContext(m.path, m.repoId, {
1531
+ memoryBudgetBytes: rt.serverOptions.memoryBudgetBytes,
1532
+ ...(draftModelDir || draftKind === "ngram"
1533
+ ? {
1534
+ ...(draftModelDir ? { draftModelDir } : {}),
1535
+ numDraftTokens,
1536
+ ...(draftKind ? { draftKind } : {}),
1537
+ ...(ngramMax !== undefined ? { ngramMax } : {}),
1538
+ ...(ngramMin !== undefined ? { ngramMin } : {}),
1539
+ }
1540
+ : {}),
1541
+ });
1542
+ sLoad.done(`weights loaded ${style.dim(`in ${(performance.now() - t0).toFixed(0)} ms`)}${draftModelDir ? style.dim(` · draft: ${draftModelDir.split("/").filter(Boolean).at(-1)}`) : draftKind === "ngram" ? style.dim(" · draft: ngram (prompt lookup)") : ""}`);
1249
1543
  await mountStartupAdapter(ctx, rt.serverOptions);
1250
1544
  const server = createServer(ctx, rt.port, { ...rt.serverOptions, owner: "serve" });
1251
1545
  const shownHost = rt.serverOptions.hostname ?? "localhost";
@@ -1278,7 +1572,7 @@ switch (cmd) {
1278
1572
  // three entry points (raw / OpenAI API / chat UI) differ only in how params
1279
1573
  // are POPULATED; here every param is explicit. Decode-path levers mirror
1280
1574
  // serve so you can pin the route (mlx-lm compat = --l1).
1281
- // --l1/--l2/--l3 + per-fork overrides set the decode env levers; the
1575
+ // --l1/--l2 + per-fork overrides set the decode env levers; the
1282
1576
  // returned KV scheme is applied below so `generate --l2` runs the same
1283
1577
  // quantized-KV route (and produces the same tokens) as `serve --l2`.
1284
1578
  const route = applyDecodeRoute();
@@ -1287,7 +1581,7 @@ switch (cmd) {
1287
1581
  if (!prompt) {
1288
1582
  console.error('usage: mlx-bun generate [query] --prompt "…" [--raw] [--max-tokens N]');
1289
1583
  console.error(' sampling: --temperature N (alias --temp) --top-p N --top-k N --seed N');
1290
- console.error(' decode path: --l1 (= mlx-lm compat) / --l2 / --l3, or per-fork flags');
1584
+ console.error(' decode path: --l1 (= mlx-lm compat, the default) / --l2, or per-fork flags');
1291
1585
  process.exit(1);
1292
1586
  }
1293
1587
  const { loadTaskModel, generateText } = await import("./eval/runner");
@@ -1298,7 +1592,8 @@ switch (cmd) {
1298
1592
  // start. Unset (no tier, no --kv-quant) keeps generate's historical
1299
1593
  // default: bf16 bit-exact greedy (generateText's parity path).
1300
1594
  const kvScheme =
1301
- route.kvQuant === "off" ? {}
1595
+ route.turboQuant ? { turboQuant: route.turboQuant, quantizedKvStart: 0 }
1596
+ : route.kvQuant === "off" ? {}
1302
1597
  : route.kvQuant === "config"
1303
1598
  ? (tm.config.kvQuant?.length ? { kvConfig: tm.config.kvQuant } : {})
1304
1599
  : typeof route.kvQuant === "number"
@@ -1384,8 +1679,8 @@ switch (cmd) {
1384
1679
  case "benchmark": {
1385
1680
  const { banner, step, box, style } = await import("./tui");
1386
1681
  banner(pkg.version);
1387
- serverRuntimeFlags(); // env levers (--compiled-decode, --perf-kernel, ...) apply to the run
1388
- const { m, picked } = await resolveModelAuto(positional(0) ?? opt("query"));
1682
+ serverRuntimeFlags(); // env levers (--compiled-decode, --fused-sdpa, ...) apply to the run
1683
+ const { m, picked } = await resolveModelAuto(opt("model") ?? positional(0) ?? opt("query"));
1389
1684
  const tokens = Number(opt("tokens", "256"));
1390
1685
  const runs = Number(opt("runs", "3"));
1391
1686
  const promptTokens = Number(opt("prompt-tokens", "0"));
@@ -1419,7 +1714,16 @@ switch (cmd) {
1419
1714
  const rendered = template.render([{ role: "user", content: userMsg }]);
1420
1715
  const ids = tok.encode(rendered);
1421
1716
  const promptIds = ids[0] === ids[1] && ids[0] === tok.bosTokenId ? ids.slice(1) : ids;
1422
- const kvOptions = kvMode === "config"
1717
+ let turboScheme: TurboQuantScheme | null;
1718
+ try {
1719
+ turboScheme = parseTurboQuantScheme(kvMode);
1720
+ } catch (e) {
1721
+ console.error((e as Error).message);
1722
+ process.exit(1);
1723
+ }
1724
+ const kvOptions = turboScheme
1725
+ ? { turboQuant: turboScheme, quantizedKvStart: 0 }
1726
+ : kvMode === "config"
1423
1727
  ? (() => {
1424
1728
  if (!config.kvQuant?.length) { console.error("model has no kv_config.json (--kv-quant config)"); process.exit(1); }
1425
1729
  return { kvConfig: config.kvQuant };
@@ -1473,7 +1777,7 @@ switch (cmd) {
1473
1777
  `decode ${style.green(style.bold(`${median(decodes).toFixed(1)} tok/s`))} ${style.dim(`median of ${runs} · [${decodes.map((d) => d.toFixed(1)).join(", ")}]`)}`,
1474
1778
  `prefill ${style.bold(`${median(prefills).toFixed(0)} tok/s`)} ${style.dim(`· ${lastStats.promptTokens} prompt tokens`)}`,
1475
1779
  `peak mem ${style.bold(gb(peak))} ${style.dim("(generation only)")}`,
1476
- `levers ${style.dim(runtimeSummary({ kvQuant: kvMode === "off" ? "off" : kvMode === "config" ? undefined : Number(kvMode) }))}`,
1780
+ `levers ${style.dim(runtimeSummary(turboScheme ? { turboQuant: turboScheme } : { kvQuant: kvMode === "off" ? "off" : kvMode === "config" ? undefined : Number(kvMode) }))}`,
1477
1781
  "",
1478
1782
  style.dim("recorded to the eval DB — see mlx-bun evals"),
1479
1783
  ]);
@@ -1573,13 +1877,16 @@ switch (cmd) {
1573
1877
  // (src/pi-terminal.ts) — pi is bundled, nothing to install. Users who
1574
1878
  // already run their own pi connect it with `mlx-bun harness pi`.
1575
1879
  const OURS_VAL = new Set([
1576
- "--query", "-q", "--port", "--host", "--memory-budget", "--prompt-cache", "--kv-quant",
1880
+ "--query", "-q", "--model", "--port", "--host", "--memory-budget", "--kv-budget", "--prompt-cache",
1881
+ "--ssd-cache", "--ssd-cache-max", "--ssd-demote-idle", "--kv-quant", "--unix",
1577
1882
  "--batch", "--decode-concurrency", "--adapter", "--adapter-path",
1578
- "--compiled-decode", "--perf-kernel", "--fused-decode", "--fused-sdpa", "--thinking",
1883
+ "--draft-model", "--num-draft-tokens", "--draft-kind", "--ngram-max", "--ngram-min",
1884
+ "--compiled-decode", "--compiled-activations", "--fused-sdpa", "--thinking",
1579
1885
  "--temperature", "--temp", "--top-p", "--top-k", "--max-tokens",
1580
1886
  "--hlg-sampling", "--hlg-width", "--hlg-shoulder", "--hlg-toe", "--hlg-pivot-offset",
1887
+ "--paged-kv-block-size",
1581
1888
  ]);
1582
- const OURS_BOOL = new Set(["--force-wire", "--expert-offload", "--no-open", "--l1", "--l2", "--l3"]);
1889
+ const OURS_BOOL = new Set(["--force-wire", "--expert-offload", "--no-open", "--ssd-cache-verify", "--l1", "--l2", "--l3", "--isolate", "--paged-kv"]);
1583
1890
  const passthrough: string[] = [];
1584
1891
  for (let i = 1; i < argv.length; i++) {
1585
1892
  const a = argv[i]!;
@@ -1769,14 +2076,7 @@ switch (cmd) {
1769
2076
 
1770
2077
  const { m, picked } = await resolveModelAuto(query);
1771
2078
 
1772
- // Detect Gemma/e4b and set its required training env flags BEFORE the
1773
- // trainer is imported (perfKernelEnabled / fused-gelu read them lazily at
1774
- // forward time, so setting them here — like the launcher — takes effect).
1775
2079
  const isGemma = (await Bun.file(`${m.path}/config.json`).text()).toLowerCase().includes("gemma");
1776
- if (isGemma) {
1777
- process.env.MLX_BUN_PERF_KERNEL ??= "0";
1778
- process.env.MLX_BUN_FUSED_GELU ??= "0";
1779
- }
1780
2080
 
1781
2081
  const modelTag = isGemma ? "e4b" : "cpm5";
1782
2082
  const adapter = opt("adapter") ?? `${process.env.HOME}/.cache/mlx-bun/mlx-bun-finetunes/${method}-${modelTag}`;
@@ -1830,7 +2130,7 @@ switch (cmd) {
1830
2130
  if (!ds.ok) { console.error(`dataset: ${ds.error}`); process.exit(1); }
1831
2131
 
1832
2132
  const planLines = [
1833
- `${style.green("●")} ${style.bold(`train ${method}`)} ${style.dim(`· ${m.repoId}${picked ? " (auto-picked)" : ""}${isGemma ? " · e4b env set" : ""}`)}`,
2133
+ `${style.green("●")} ${style.bold(`train ${method}`)} ${style.dim(`· ${m.repoId}${picked ? " (auto-picked)" : ""}${isGemma ? " · e4b defaults" : ""}`)}`,
1834
2134
  "",
1835
2135
  `data ${style.bold(`${ds.n_train} train`)}${ds.n_valid ? ` · ${ds.n_valid} valid` : ""} ${style.dim(`· format ${ds.format}`)}`,
1836
2136
  `loop ${style.dim(`iters ${iters} · lr ${cfg.learning_rate} · rank ${cfg.rank} · scale ${cfg.scale} · seq ${seq} · batch ${cfg.batch_size}`)}`,
@@ -2214,11 +2514,6 @@ switch (cmd) {
2214
2514
  if (reg.list().length === 0) await reg.scan();
2215
2515
  modelDir = reg.resolve(modelArg).path;
2216
2516
  }
2217
- // Gemma/e4b: same forward env flags the trainer sets (full-sequence pass).
2218
- if ((await Bun.file(`${modelDir}/config.json`).text()).toLowerCase().includes("gemma")) {
2219
- process.env.MLX_BUN_PERF_KERNEL ??= "0";
2220
- process.env.MLX_BUN_FUSED_GELU ??= "0";
2221
- }
2222
2517
  const sLoad = step(`loading ${modelDir}`);
2223
2518
  const { Weights } = await import("./weights");
2224
2519
  const { createModel } = await import("./model/factory");