mlx-bun 0.0.10 → 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 (173) 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 +58 -35
  7. package/docs/design/batching-v2-plan.md +23 -9
  8. package/docs/design/bucketing-stage.md +2 -1
  9. package/docs/design/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 +14 -6
  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 +3 -2
  27. package/docs/design/ssd-kv-cold-tier.md +130 -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/dspark-handoff.md +182 -16
  35. package/docs/investigations/lab-build-journal.md +2 -2
  36. package/docs/investigations/openwiki-evaluation.md +249 -0
  37. package/docs/planning/IDEAS.md +14 -0
  38. package/docs/planning/PRODUCT_ROADMAP.md +2 -3
  39. package/docs/planning/chat-ui-vision-memory-first.md +199 -0
  40. package/docs/planning/chat-ui-vision.md +50 -0
  41. package/docs/planning/memory-docs-and-dag-plan.md +15 -6
  42. package/docs/planning/release-notes-v0.0.11.md +239 -0
  43. package/docs/reference/cli.md +35 -16
  44. package/docs/reference/distribution.md +13 -6
  45. package/docs/reference/embedding.md +35 -14
  46. package/docs/reference/features-matrix.md +97 -0
  47. package/docs/reference/library-api.md +30 -9
  48. package/docs/reference/memory.md +26 -6
  49. package/docs/reference/models.md +11 -1
  50. package/docs/reference/orpo-quickstart.md +3 -4
  51. package/docs/reference/server-api.md +634 -22
  52. package/docs/reference/server-config.md +250 -121
  53. package/docs/reference/training.md +14 -15
  54. package/package.json +5 -3
  55. package/src/anthropic.ts +13 -0
  56. package/src/audio/conformer.ts +647 -0
  57. package/src/audio/decode.ts +133 -0
  58. package/src/audio/features.ts +202 -0
  59. package/src/audio/transcode.ts +67 -0
  60. package/src/cli.ts +356 -105
  61. package/src/config.ts +49 -2
  62. package/src/download.ts +40 -0
  63. package/src/eval/README.md +7 -6
  64. package/src/eval/kl.ts +59 -3
  65. package/src/eval/quality-db.ts +2 -2
  66. package/src/eval/runner.ts +3 -3
  67. package/src/expert-offload.ts +15 -2
  68. package/src/flags.ts +13 -0
  69. package/src/generate.ts +330 -24
  70. package/src/grammar.ts +585 -0
  71. package/src/hub-rest.ts +242 -0
  72. package/src/kv-store.ts +442 -88
  73. package/src/lora.ts +8 -0
  74. package/src/md-modules.d.ts +42 -0
  75. package/src/media-fetch.ts +284 -0
  76. package/src/memory/rest.ts +245 -0
  77. package/src/memory/vault.ts +74 -0
  78. package/src/mlx/array.ts +106 -19
  79. package/src/mlx/autograd.ts +2 -2
  80. package/src/mlx/ffi.ts +27 -3
  81. package/src/mlx/metal-kernel.ts +7 -4
  82. package/src/mlx/ops.ts +153 -23
  83. package/src/mlx/turboquant-ops.ts +831 -0
  84. package/src/mlx/turboquant-tables.ts +237 -0
  85. package/src/model/batched-mask.ts +62 -1
  86. package/src/model/batched-quant.ts +206 -0
  87. package/src/model/batched-rotating-quant.ts +346 -0
  88. package/src/model/batched-rotating.ts +46 -1
  89. package/src/model/compiled-decode.ts +4 -15
  90. package/src/model/factory.ts +10 -4
  91. package/src/model/gemma4-base.ts +363 -39
  92. package/src/model/gemma4.ts +122 -33
  93. package/src/model/generated/gemma4-12b.ts +9 -13
  94. package/src/model/generated/gemma4-26b.ts +10 -5
  95. package/src/model/generated/gemma4-e4b.ts +14 -9
  96. package/src/model/minicpm5.ts +58 -102
  97. package/src/model/paged-kv.ts +253 -0
  98. package/src/model/qwen3-delta.ts +76 -15
  99. package/src/model/qwen3-moe.ts +404 -0
  100. package/src/model/qwen3.ts +9 -8
  101. package/src/model/qwen3_5.ts +88 -23
  102. package/src/model/support.ts +12 -1
  103. package/src/model/universal/dense.ts +40 -5
  104. package/src/model/universal/rope.ts +36 -21
  105. package/src/pi-web.ts +1299 -82
  106. package/src/preflight.ts +27 -8
  107. package/src/prompt-cache.ts +293 -33
  108. package/src/quantize/quantizer.ts +16 -1
  109. package/src/registry.ts +57 -2
  110. package/src/serve/batch-scheduler.ts +965 -72
  111. package/src/serve/generation-gateway.ts +214 -11
  112. package/src/serve/isolate.ts +417 -0
  113. package/src/serve/lane-registry.ts +44 -0
  114. package/src/serve/session-search.ts +326 -0
  115. package/src/server.ts +1380 -135
  116. package/src/spec/assistant-source.ts +150 -0
  117. package/src/spec/deepspec-source.ts +143 -0
  118. package/src/spec/dflash-source.ts +124 -0
  119. package/src/spec/dspark/ab-stats.ts +136 -0
  120. package/src/spec/dspark/calibration.ts +112 -0
  121. package/src/spec/dspark/deepspec-module.ts +801 -0
  122. package/src/spec/dspark/generate-dflash.ts +72 -20
  123. package/src/spec/dspark/loader.ts +23 -0
  124. package/src/spec/dspark/module-dflash.ts +278 -36
  125. package/src/spec/dspark/quantize-drafter.ts +91 -0
  126. package/src/spec/generate.ts +59 -25
  127. package/src/spec/ngram-source.ts +132 -0
  128. package/src/spec/serve-loop.ts +536 -0
  129. package/src/spec/source.ts +93 -0
  130. package/src/spec/two-model.ts +171 -0
  131. package/src/ssd-cache.ts +107 -35
  132. package/src/tokenizer.ts +288 -2
  133. package/src/tool-approvals.ts +131 -0
  134. package/src/tool-call.ts +408 -20
  135. package/src/train/flash-cce.ts +2 -2
  136. package/src/train/job.ts +0 -2
  137. package/src/train/trainer.ts +1 -4
  138. package/src/vision/prompt.ts +217 -48
  139. package/src/web/app.html +1184 -1789
  140. package/src/web/app.js +5667 -0
  141. package/src/web/icon.svg +13 -0
  142. package/src/web/manifest.webmanifest +24 -0
  143. package/src/web/src/adapters-panel.ts +288 -0
  144. package/src/web/src/api.ts +57 -0
  145. package/src/web/src/assistant.ts +485 -0
  146. package/src/web/src/chat.ts +1050 -0
  147. package/src/web/src/composer.ts +959 -0
  148. package/src/web/src/dataset.ts +132 -0
  149. package/src/web/src/finetune.ts +267 -0
  150. package/src/web/src/hub.ts +428 -0
  151. package/src/web/src/main.ts +98 -0
  152. package/src/web/src/markdown.ts +0 -0
  153. package/src/web/src/memory-panel.ts +572 -0
  154. package/src/web/src/model-picker.ts +159 -0
  155. package/src/web/src/palette.ts +346 -0
  156. package/src/web/src/protocol.ts +62 -0
  157. package/src/web/src/quantize.ts +167 -0
  158. package/src/web/src/rag.ts +274 -0
  159. package/src/web/src/sessions.ts +469 -0
  160. package/src/web/src/shell.ts +804 -0
  161. package/src/web/src/status.ts +252 -0
  162. package/src/web/src/ui-catalog.ts +92 -0
  163. package/src/web/sw.js +71 -0
  164. package/src/web/vendor/README.md +61 -0
  165. package/src/web/vendor/hljs-LICENSE +29 -0
  166. package/src/web/vendor/hljs-theme.css +59 -0
  167. package/src/web/vendor/hljs.js +26 -0
  168. package/src/model/fused-decode-kernel.ts +0 -312
  169. package/src/model/fused-geglu-kernel.ts +0 -198
  170. package/src/model/fused-mlp-kernel.ts +0 -169
  171. package/src/model/fused-swiglu-kernel.ts +0 -240
  172. package/src/model/steel-linear-kernel.ts +0 -96
  173. package/src/status-page.html +0 -497
package/src/cli.ts CHANGED
@@ -12,13 +12,12 @@
12
12
  import { existsSync } from "node:fs";
13
13
  import { basename, join, resolve } from "node:path";
14
14
  import { Registry } from "./registry";
15
- import { loadModelConfig } from "./config";
15
+ import { loadModelConfig, parseTurboQuantScheme, type TurboQuantScheme } from "./config";
16
16
  import { fit, skuMatrix, thisMachine } from "./fit";
17
17
  import { EvalDB } from "./evaldb";
18
18
  import pkg from "../package.json" with { type: "json" };
19
19
  import { renderHelp } from "./tui";
20
20
  import { isSupportedModelRecord } from "./model/support";
21
- import { perfKernelEnabled } from "./model/fused-decode-kernel";
22
21
  import type { JobEvent } from "./jobs/types";
23
22
 
24
23
  const argv = process.argv.slice(2);
@@ -36,7 +35,7 @@ Usage: mlx-bun [command] [options] (no command = serve)
36
35
 
37
36
  Commands:
38
37
  pi Launch a pi coding-agent session on a local model (the appliance path)
39
- serve Start the OpenAI/Anthropic-compatible server + status page
38
+ serve Start the OpenAI/Anthropic-compatible server + web app
40
39
  get Download a model from Hugging Face (resumable, verified)
41
40
  ls List downloaded models
42
41
  gc Reclaim superseded model snapshots + dead blobs from the HF cache
@@ -63,7 +62,7 @@ Options:
63
62
  Examples:
64
63
  mlx-bun # download (first run) + serve + open the chat UI
65
64
  mlx-bun pi # start (download if needed) and chat with an agent
66
- 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/
67
66
  mlx-bun train e4b --data ./prefs # ORPO LoRA fine-tune on {prompt,chosen,rejected} data
68
67
  mlx-bun get mlx-community/gemma-4-12B-it-OptiQ-4bit`;
69
68
 
@@ -78,7 +77,12 @@ const SERVER_FLAGS = `Server options:
78
77
  --memory-budget <GB> Admission-control memory budget; requests that
79
78
  cannot fit are rejected instead of crashing the
80
79
  GPU [default: machine RAM × 0.75, check-only]
81
- --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;
82
86
  --prompt-cache 0 disables the cache]
83
87
  --ssd-cache <dir> SSD cold tier for the prompt cache: prefix KV
84
88
  spills to disk on eviction and persists across
@@ -86,18 +90,44 @@ const SERVER_FLAGS = `Server options:
86
90
  full re-prefill to a zero-copy mmap restore).
87
91
  Off unless set.
88
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]
89
97
  --ssd-cache-verify Verify tensor hashes on every restore (reads all
90
98
  bytes eagerly — integrity paranoia only)
91
- --batch <n> Max concurrent requests batched through the
92
- mlx-lm-parity engine [default: 1 = serial].
93
- >1 opts the whole server into bf16 continuous
94
- batching (= mlx-lm B=N); see --kv-quant.
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.
95
117
  --decode-concurrency is accepted as the
96
118
  mlx_lm.server alias (semantics differ; see
97
119
  docs/reference/server-config.md)
98
120
  --no-open Don't open the chat UI in your browser on start.
99
121
  By default an interactive terminal opens
100
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.
101
131
 
102
132
  Model & quality:
103
133
  --adapter <dir> Mount a LoRA adapter at startup and use it as the
@@ -105,12 +135,48 @@ Model & quality:
105
135
  request's \`adapter\` field, incl. "none", wins).
106
136
  --adapter-path is accepted as the mlx_lm.server
107
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]
108
156
  --kv-quant <mode> KV cache quantization: config (per-layer
109
157
  kv_config.json when the model ships one), off
110
- (bf16), or 4 / 8 (uniform bits)
111
- [default: config; bf16 under --batch N — the
112
- batched engine is bf16-only, so an explicit
113
- --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]
114
180
  --thinking <true|false> Default for the chat template's enable_thinking
115
181
  variable (CPM and other hybrid-reasoning models);
116
182
  a request's chat_template_kwargs overrides it
@@ -134,23 +200,28 @@ Model & quality:
134
200
  --hlg-toe <nats> HLG shadow rolloff scale [default: 6]
135
201
  --hlg-pivot-offset <nats> HLG pivot: nats below the top token [default: 6]
136
202
 
137
- Parity tier (sets the whole decode route; a per-fork flag below overrides):
138
- --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).
139
211
  --l2 Bit-for-bit IDENTICAL to mlx-optiq (quantized KV +
140
212
  fused prefill SDPA, stock unfused decode — the
141
- optiq-golden composition; perf-kernel stays OFF)
142
- --l3 Best performance, no bit-exact oracle (KL + test
143
- gated; adds the envelope-gated perf kernel).
144
- No tier given = the per-flag defaults below,
145
- which equal --l3 today.
213
+ optiq-golden composition)
146
214
 
147
- 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):
148
216
  --compiled-decode on|off Compiled decode graphs [default: on]
149
- --perf-kernel on|off Fused quantized-KV decode-SDPA Metal kernel —
150
- NOT bit-exact, envelope-gated (an L3 lever; the
151
- perf side of the compat A/B) [default: on]
152
- --fused-decode on|off Fused-decode experiment lever [default: off]
153
- --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]
154
225
  --force-wire Wire weights into memory at load
155
226
  --expert-offload MoE only: serve experts from a page-aligned file
156
227
  mmap (built on first use) — keeps the model out of
@@ -199,7 +270,7 @@ ${SERVER_FLAGS}
199
270
 
200
271
  Endpoints: /v1/chat/completions, /v1/completions, /v1/messages, /v1/responses,
201
272
  /v1/models, /v1/adapters, /health, /stats, /fit, /library, /downloads —
202
- status page at /, browser chat at /chat`,
273
+ web app at / (lands on chat) — /chat, /status, /quantize, /finetune, /dataset deep-link its views`,
203
274
 
204
275
  get: `mlx-bun get — download a model from Hugging Face
205
276
 
@@ -305,11 +376,11 @@ Options:
305
376
  --tokens <n> Tokens to decode per run [default: 256]
306
377
  --runs <n> Runs (median reported) [default: 3]
307
378
  --prompt-tokens <n> Pad the prompt to ~n tokens (long-context decode)
308
- --kv-quant <mode> config | off | 4 | 8 [default: off, the
309
- historical baseline]
379
+ --kv-quant <mode> config | off | 4 | 8 | turbo[:k<bits>v<bits>]
380
+ [default: off, the historical baseline]
310
381
 
311
- Performance levers (--compiled-decode, --perf-kernel, --fused-decode,
312
- --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.`,
313
384
 
314
385
  embed: `mlx-bun embed — text embeddings from a local embedding model
315
386
 
@@ -358,9 +429,9 @@ Options:
358
429
  --top-k <n> Top-k sampling
359
430
  --seed <n> Sampler seed (reproducible runs)
360
431
 
361
- Decode-path levers mirror serve: --l1/--l2/--l3 and the per-fork flags
362
- (--perf-kernel/--fused-sdpa/--compiled-decode/--fused-decode/--kv-quant).
363
- 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,
364
435
  same tokens as serve); with no tier/--kv-quant the KV cache stays bf16
365
436
  (the bit-exact mlx-lm greedy path). mlx-lm compat = --l1.
366
437
 
@@ -720,69 +791,96 @@ function openChatUi(url: string, hostPort: string): void {
720
791
  /** Server/runtime flags shared by every mode that loads a model
721
792
  * (serve, pi). Env levers are set here so they're in place before the
722
793
  * generate/compiled-decode modules read them. */
723
- /** Resolve the decode ROUTE: a tier alias (--l1/--l2/--l3) sets the whole route,
724
- * and an explicit per-fork flag (--perf-kernel/--kv-quant/…) overrides the alias.
725
- * The tier IS the intent; the forks are the escape hatch. Sets the decode env
726
- * 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.
727
798
  * Each tier is a GUARANTEE about which reference you reproduce bit-for-bit:
728
799
  * --l1 bit-for-bit IDENTICAL to mlx-lm — drop-in replacement for mlx-lm
729
- * --l2 bit-for-bit IDENTICAL to mlx-optiq — drop-in replacement for mlx-optiq.
730
- * The optiq-golden decode composition (scripts/regen-kvq-goldens.ts) is
731
- * fused N-tiled SDPA for L>1 + STOCK unfused L=1 decode, so L2 = fused-sdpa
732
- * ON, perf-kernel OFF. The perf kernel is an mlx-bun ORIGINAL Metal kernel,
733
- * envelope-gated (≥56/64 teacher-forced argmax vs OUR OWN frozen compat
734
- * trajectory, tests/perf-kernel-oracle.test.ts) — an L3 node, never the
735
- * bare-L2 default. (Commit f1bf5cc put it in L2 claiming the goldens track
736
- * it; they don't — reverted 2026-07-01 with the evidence above.)
737
- * --l3 best performance, NO bit-exact guarantee — correct, but gated by KL +
738
- * tests, because mlx-optiq has no analogy for what L3 does.
739
- * L2 ⊂ L3 (L3 = L2 + perf kernel). L1 is a separate target (bf16, unfused). */
740
- 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 } {
741
809
  const onOff = (name: string): boolean | null => {
742
810
  const v = opt(name); if (v == null) return null;
743
811
  if (v === "on" || v === "1" || v === "true") return true;
744
812
  if (v === "off" || v === "0" || v === "false") return false;
745
813
  console.error(`--${name} expects on|off (got "${v}")`); process.exit(1);
746
814
  };
747
- 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 };
748
824
  const TIERS: Record<string, Preset> = {
749
825
  // A tier is the GUARANTEE; within it we default to the FAST kernel that still
750
826
  // holds it, and expose the slow one as an opt-in. compiled-decode is BIT-EXACT
751
827
  // with uncompiled (compiled-decode.test: on==off) → free speed, so it's ON in
752
- // EVERY tier (--compiled-decode off is the slow, same-tier opt-in). fused-sdpa
753
- // matches OPTIQ bit-for-bit (tier-a goldens, fused-sdpa.test) and is
754
- // quantized-KV only → it's the L2 bridge, a no-op on L1's bf16. perf-kernel is
755
- // NOT: it's an original flash-decoding kernel, envelope-gated against our own
756
- // frozen trajectory (perf-kernel-oracle.test) — bare --l2 must ship the
757
- // optiq-golden decode composition (stock unfused L=1), so perf-kernel is
758
- // L3/explicit-only (--perf-kernel on opts a tier into it by choice). This is
759
- // the decode-axis difference between L2 and L3; L3 also owns the no-oracle
760
- // FEATURES (HLG sampler, expert offload, batched mixed-precision).
761
- l1: { kv: "off", perf: false, fusedSdpa: false, compiled: true, fusedDecode: false }, // = mlx-lm bit-for-bit (bf16)
762
- l2: { kv: "config", perf: false, fusedSdpa: true, compiled: true, fusedDecode: false }, // = mlx-optiq bit-for-bit
763
- 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
764
836
  };
765
- const tier = flag("l1") ? "l1" : flag("l2") ? "l2" : flag("l3") ? "l3" : null;
766
- 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
767
847
  const pick = (name: string, base: boolean | undefined): boolean | null => {
768
848
  const ex = onOff(name); return ex !== null ? ex : (base ?? null); // explicit flag wins, else tier preset
769
849
  };
770
850
  const set = (env: string, val: boolean | null, invert = false) => {
771
851
  if (val !== null) process.env[env] = (invert ? !val : val) ? "1" : "0";
772
852
  };
773
- set("MLX_BUN_COMPILED_DECODE", pick("compiled-decode", p?.compiled));
774
- set("MLX_BUN_PERF_KERNEL", pick("perf-kernel", p?.perf));
775
- set("MLX_BUN_FUSED_DECODE", pick("fused-decode", p?.fusedDecode));
776
- set("MLX_BUN_NO_FUSED_SDPA", pick("fused-sdpa", p?.fusedSdpa), true); // inverted env
777
- 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));
778
865
  if (kv === "off") return { kvQuant: "off" };
779
866
  if (kv === "config") return { kvQuant: "config" };
780
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 };
781
879
  const bits = Number(kv);
782
- 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); }
783
881
  return { kvQuant: bits };
784
882
  }
785
- return p ? { kvQuant: p.kv } : {};
883
+ return { kvQuant: p.kv };
786
884
  }
787
885
 
788
886
  function serverRuntimeFlags(): { port: number; serverOptions: import("./server").ServerOptions } {
@@ -794,14 +892,26 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
794
892
  console.error(`--${name} expects on|off (got "${v}")`);
795
893
  process.exit(1);
796
894
  };
797
- 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
798
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";
799
901
 
800
902
  const serverOptions: import("./server").ServerOptions = {};
801
903
  const budgetGB = Number(opt("memory-budget", "0"));
802
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;
803
908
  const pcRaw = opt("prompt-cache"); // null if absent; an explicit value (incl. 0) wins → `--prompt-cache 0` DISABLES the cache
804
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;
805
915
  // SSD cold tier (docs/design/ssd-kv-cold-tier.md) — off unless a dir is given.
806
916
  const ssdDir = opt("ssd-cache");
807
917
  if (ssdDir !== null) {
@@ -809,13 +919,21 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
809
919
  const maxRaw = opt("ssd-cache-max");
810
920
  if (maxRaw !== null) serverOptions.ssdCacheMaxBytes = Math.max(1, Number(maxRaw)) * 2 ** 30;
811
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");
812
930
  }
813
931
  // --batch N: max concurrent requests batched through the mlx-lm-parity
814
932
  // engine (N=1 = today's serial path). --decode-concurrency is accepted for
815
933
  // drop-in compatibility with mlx_lm.server, but the semantics differ: there
816
934
  // it caps per-BatchGenerator decode parallelism (default 32); in mlx-bun it
817
- // enables continuous batching with this cap (the default is the optimized
818
- // 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).
819
937
  const batchRaw = opt("batch") ?? opt("decode-concurrency");
820
938
  if (batchRaw !== null) {
821
939
  const n = Number(batchRaw);
@@ -826,6 +944,19 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
826
944
  serverOptions.batch = n;
827
945
  }
828
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
+ }
829
960
  // Bind loopback unless asked otherwise (mlx_lm.server parity); --host
830
961
  // 0.0.0.0 is the explicit opt-in for LAN exposure. The chat-UI open and
831
962
  // `mlx-bun pi` attach both go through localhost, so loopback-only is
@@ -904,11 +1035,11 @@ async function mountStartupAdapter(
904
1035
 
905
1036
  /** One-line summary of the active runtime levers for the ready card. */
906
1037
  function runtimeSummary(o: import("./server").ServerOptions): string {
907
- 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";
908
1040
  const lever = (env: string, dflt: string) => process.env[env] ?? dflt;
909
1041
  return `kv-quant ${kv} · compiled-decode ${lever("MLX_BUN_COMPILED_DECODE", "1") === "1" ? "on" : "off"}` +
910
- ` · perf-kernel ${perfKernelEnabled() ? "on" : "off"}` +
911
- (lever("MLX_BUN_FUSED_DECODE", "0") === "1" ? " · fused-decode on" : "") +
1042
+ (lever("MLX_BUN_COMPILED_GEGLU", "1") === "0" ? " · compiled-activations off" : "") +
912
1043
  (o.batch && o.batch > 1 ? ` · batch ${o.batch}` : "") +
913
1044
  (o.defaultThinking !== undefined ? ` · thinking ${o.defaultThinking ? "on" : "off"}` : "") +
914
1045
  (o.defaultTemperature !== undefined ? ` · temp ${o.defaultTemperature}` : "") +
@@ -958,7 +1089,7 @@ async function resolveModelAuto(query: string | null): Promise<{ m: import("./re
958
1089
  await reg.scan();
959
1090
  if (recRepo !== starterRepo) {
960
1091
  console.log(` ${style.dim(`downloading ${recRepo} (recommended for this Mac) in the background —`)}`);
961
- 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")}`);
962
1093
  downloadModel(recRepo)
963
1094
  .then(async () => {
964
1095
  await new Registry().scan();
@@ -1248,7 +1379,7 @@ switch (cmd) {
1248
1379
  console.log(style.dim(" no command given — starting the server · mlx-bun --help for all commands"));
1249
1380
  const rt = serverRuntimeFlags();
1250
1381
  // Friendly collision check before loading gigabytes of weights.
1251
- {
1382
+ if (!rt.serverOptions.unixSocket) {
1252
1383
  const { probeServer } = await import("./harness-pi");
1253
1384
  const running = await probeServer(`http://localhost:${rt.port}/v1`);
1254
1385
  if (running) {
@@ -1258,6 +1389,71 @@ switch (cmd) {
1258
1389
  process.exit(1);
1259
1390
  }
1260
1391
  }
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
+ }
1261
1457
  // --model <path-or-query> is the explicit override (mlx_lm.server's
1262
1458
  // spelling — previously accepted-but-IGNORED here, which silently
1263
1459
  // auto-picked the wrong model). A directory with config.json loads
@@ -1285,10 +1481,65 @@ switch (cmd) {
1285
1481
  sOff.done(`experts mmap'd ${style.dim(dir)} ${style.dim("· phys_footprint ≈ core")}`);
1286
1482
  }
1287
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");
1288
1528
  const sLoad = step("loading weights");
1289
1529
  const t0 = performance.now();
1290
- const ctx = await loadContext(m.path, m.repoId, { memoryBudgetBytes: rt.serverOptions.memoryBudgetBytes });
1291
- 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)") : ""}`);
1292
1543
  await mountStartupAdapter(ctx, rt.serverOptions);
1293
1544
  const server = createServer(ctx, rt.port, { ...rt.serverOptions, owner: "serve" });
1294
1545
  const shownHost = rt.serverOptions.hostname ?? "localhost";
@@ -1321,7 +1572,7 @@ switch (cmd) {
1321
1572
  // three entry points (raw / OpenAI API / chat UI) differ only in how params
1322
1573
  // are POPULATED; here every param is explicit. Decode-path levers mirror
1323
1574
  // serve so you can pin the route (mlx-lm compat = --l1).
1324
- // --l1/--l2/--l3 + per-fork overrides set the decode env levers; the
1575
+ // --l1/--l2 + per-fork overrides set the decode env levers; the
1325
1576
  // returned KV scheme is applied below so `generate --l2` runs the same
1326
1577
  // quantized-KV route (and produces the same tokens) as `serve --l2`.
1327
1578
  const route = applyDecodeRoute();
@@ -1330,7 +1581,7 @@ switch (cmd) {
1330
1581
  if (!prompt) {
1331
1582
  console.error('usage: mlx-bun generate [query] --prompt "…" [--raw] [--max-tokens N]');
1332
1583
  console.error(' sampling: --temperature N (alias --temp) --top-p N --top-k N --seed N');
1333
- 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');
1334
1585
  process.exit(1);
1335
1586
  }
1336
1587
  const { loadTaskModel, generateText } = await import("./eval/runner");
@@ -1341,7 +1592,8 @@ switch (cmd) {
1341
1592
  // start. Unset (no tier, no --kv-quant) keeps generate's historical
1342
1593
  // default: bf16 bit-exact greedy (generateText's parity path).
1343
1594
  const kvScheme =
1344
- route.kvQuant === "off" ? {}
1595
+ route.turboQuant ? { turboQuant: route.turboQuant, quantizedKvStart: 0 }
1596
+ : route.kvQuant === "off" ? {}
1345
1597
  : route.kvQuant === "config"
1346
1598
  ? (tm.config.kvQuant?.length ? { kvConfig: tm.config.kvQuant } : {})
1347
1599
  : typeof route.kvQuant === "number"
@@ -1427,7 +1679,7 @@ switch (cmd) {
1427
1679
  case "benchmark": {
1428
1680
  const { banner, step, box, style } = await import("./tui");
1429
1681
  banner(pkg.version);
1430
- serverRuntimeFlags(); // env levers (--compiled-decode, --perf-kernel, ...) apply to the run
1682
+ serverRuntimeFlags(); // env levers (--compiled-decode, --fused-sdpa, ...) apply to the run
1431
1683
  const { m, picked } = await resolveModelAuto(opt("model") ?? positional(0) ?? opt("query"));
1432
1684
  const tokens = Number(opt("tokens", "256"));
1433
1685
  const runs = Number(opt("runs", "3"));
@@ -1462,7 +1714,16 @@ switch (cmd) {
1462
1714
  const rendered = template.render([{ role: "user", content: userMsg }]);
1463
1715
  const ids = tok.encode(rendered);
1464
1716
  const promptIds = ids[0] === ids[1] && ids[0] === tok.bosTokenId ? ids.slice(1) : ids;
1465
- 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"
1466
1727
  ? (() => {
1467
1728
  if (!config.kvQuant?.length) { console.error("model has no kv_config.json (--kv-quant config)"); process.exit(1); }
1468
1729
  return { kvConfig: config.kvQuant };
@@ -1516,7 +1777,7 @@ switch (cmd) {
1516
1777
  `decode ${style.green(style.bold(`${median(decodes).toFixed(1)} tok/s`))} ${style.dim(`median of ${runs} · [${decodes.map((d) => d.toFixed(1)).join(", ")}]`)}`,
1517
1778
  `prefill ${style.bold(`${median(prefills).toFixed(0)} tok/s`)} ${style.dim(`· ${lastStats.promptTokens} prompt tokens`)}`,
1518
1779
  `peak mem ${style.bold(gb(peak))} ${style.dim("(generation only)")}`,
1519
- `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) }))}`,
1520
1781
  "",
1521
1782
  style.dim("recorded to the eval DB — see mlx-bun evals"),
1522
1783
  ]);
@@ -1616,14 +1877,16 @@ switch (cmd) {
1616
1877
  // (src/pi-terminal.ts) — pi is bundled, nothing to install. Users who
1617
1878
  // already run their own pi connect it with `mlx-bun harness pi`.
1618
1879
  const OURS_VAL = new Set([
1619
- "--query", "-q", "--model", "--port", "--host", "--memory-budget", "--prompt-cache",
1620
- "--ssd-cache", "--ssd-cache-max", "--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",
1621
1882
  "--batch", "--decode-concurrency", "--adapter", "--adapter-path",
1622
- "--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",
1623
1885
  "--temperature", "--temp", "--top-p", "--top-k", "--max-tokens",
1624
1886
  "--hlg-sampling", "--hlg-width", "--hlg-shoulder", "--hlg-toe", "--hlg-pivot-offset",
1887
+ "--paged-kv-block-size",
1625
1888
  ]);
1626
- const OURS_BOOL = new Set(["--force-wire", "--expert-offload", "--no-open", "--ssd-cache-verify", "--l1", "--l2", "--l3"]);
1889
+ const OURS_BOOL = new Set(["--force-wire", "--expert-offload", "--no-open", "--ssd-cache-verify", "--l1", "--l2", "--l3", "--isolate", "--paged-kv"]);
1627
1890
  const passthrough: string[] = [];
1628
1891
  for (let i = 1; i < argv.length; i++) {
1629
1892
  const a = argv[i]!;
@@ -1813,14 +2076,7 @@ switch (cmd) {
1813
2076
 
1814
2077
  const { m, picked } = await resolveModelAuto(query);
1815
2078
 
1816
- // Detect Gemma/e4b and set its required training env flags BEFORE the
1817
- // trainer is imported (perfKernelEnabled / fused-gelu read them lazily at
1818
- // forward time, so setting them here — like the launcher — takes effect).
1819
2079
  const isGemma = (await Bun.file(`${m.path}/config.json`).text()).toLowerCase().includes("gemma");
1820
- if (isGemma) {
1821
- process.env.MLX_BUN_PERF_KERNEL ??= "0";
1822
- process.env.MLX_BUN_FUSED_GELU ??= "0";
1823
- }
1824
2080
 
1825
2081
  const modelTag = isGemma ? "e4b" : "cpm5";
1826
2082
  const adapter = opt("adapter") ?? `${process.env.HOME}/.cache/mlx-bun/mlx-bun-finetunes/${method}-${modelTag}`;
@@ -1874,7 +2130,7 @@ switch (cmd) {
1874
2130
  if (!ds.ok) { console.error(`dataset: ${ds.error}`); process.exit(1); }
1875
2131
 
1876
2132
  const planLines = [
1877
- `${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" : ""}`)}`,
1878
2134
  "",
1879
2135
  `data ${style.bold(`${ds.n_train} train`)}${ds.n_valid ? ` · ${ds.n_valid} valid` : ""} ${style.dim(`· format ${ds.format}`)}`,
1880
2136
  `loop ${style.dim(`iters ${iters} · lr ${cfg.learning_rate} · rank ${cfg.rank} · scale ${cfg.scale} · seq ${seq} · batch ${cfg.batch_size}`)}`,
@@ -2258,11 +2514,6 @@ switch (cmd) {
2258
2514
  if (reg.list().length === 0) await reg.scan();
2259
2515
  modelDir = reg.resolve(modelArg).path;
2260
2516
  }
2261
- // Gemma/e4b: same forward env flags the trainer sets (full-sequence pass).
2262
- if ((await Bun.file(`${modelDir}/config.json`).text()).toLowerCase().includes("gemma")) {
2263
- process.env.MLX_BUN_PERF_KERNEL ??= "0";
2264
- process.env.MLX_BUN_FUSED_GELU ??= "0";
2265
- }
2266
2517
  const sLoad = step(`loading ${modelDir}`);
2267
2518
  const { Weights } = await import("./weights");
2268
2519
  const { createModel } = await import("./model/factory");