mlx-bun 0.0.10 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/README.md +107 -32
  2. package/THIRD_PARTY_LICENSES.md +10 -0
  3. package/docs/archive/mlx-bun-lab-report.html +275 -0
  4. package/docs/archive/wwdc-mlx-bun.html +848 -0
  5. package/docs/design/audio-input-plan.md +356 -0
  6. package/docs/design/batching-perf-path.md +58 -35
  7. package/docs/design/batching-v2-plan.md +23 -9
  8. package/docs/design/bucketing-stage.md +2 -1
  9. package/docs/design/colibri-glm52-port.md +1479 -0
  10. package/docs/design/compat-cli-surface-design.md +4 -3
  11. package/docs/design/decode-speed-program.md +172 -0
  12. package/docs/design/docs-reorg-plan.md +1 -1
  13. package/docs/design/dspark-serving-program.md +371 -0
  14. package/docs/design/dspark-speculative-decoding.md +167 -5
  15. package/docs/design/faithful-l1-consolidation.md +310 -0
  16. package/docs/design/generic-model-support.md +8 -5
  17. package/docs/design/grammar-spec-batching-integration.md +301 -0
  18. package/docs/design/minicpm5-decode-megakernel.md +16 -11
  19. package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
  20. package/docs/design/omlx-adoption-map.md +14 -6
  21. package/docs/design/orpo-training.md +34 -30
  22. package/docs/design/paged-kv-cache.md +214 -0
  23. package/docs/design/parallel-slots.md +82 -49
  24. package/docs/design/parity-tier-dag.md +43 -33
  25. package/docs/design/pre-colibri-stabilization.md +329 -0
  26. package/docs/design/repo-cleanup-plan.md +189 -0
  27. package/docs/design/runtime-isolation.md +19 -3
  28. package/docs/design/segmented-backward-training.md +3 -2
  29. package/docs/design/ssd-kv-cold-tier.md +130 -0
  30. package/docs/design/structured-output.md +484 -0
  31. package/docs/design/the-dreaming-master-plan.md +2 -3
  32. package/docs/design/turboquant-kv.md +235 -0
  33. package/docs/design/unified-engine-frontier-plan.md +774 -0
  34. package/docs/design/web-chat-beat-matrix.md +315 -0
  35. package/docs/design/web-chat-redesign.md +1112 -0
  36. package/docs/investigations/colibri-oracle-pin.md +1284 -0
  37. package/docs/investigations/dspark-handoff.md +182 -16
  38. package/docs/investigations/indexshare-performance-spike.md +357 -0
  39. package/docs/investigations/lab-build-journal.md +2 -2
  40. package/docs/investigations/openwiki-evaluation.md +249 -0
  41. package/docs/planning/IDEAS.md +14 -0
  42. package/docs/planning/PRODUCT_ROADMAP.md +2 -3
  43. package/docs/planning/chat-ui-vision-memory-first.md +199 -0
  44. package/docs/planning/chat-ui-vision.md +50 -0
  45. package/docs/planning/memory-docs-and-dag-plan.md +15 -6
  46. package/docs/planning/release-notes-v0.0.11.md +239 -0
  47. package/docs/planning/release-notes-v0.0.12.md +80 -0
  48. package/docs/reference/cli.md +63 -18
  49. package/docs/reference/distribution.md +37 -6
  50. package/docs/reference/embedding.md +40 -16
  51. package/docs/reference/features-matrix.md +99 -0
  52. package/docs/reference/library-api.md +69 -20
  53. package/docs/reference/memory.md +26 -6
  54. package/docs/reference/models.md +60 -3
  55. package/docs/reference/orpo-quickstart.md +3 -4
  56. package/docs/reference/server-api.md +717 -24
  57. package/docs/reference/server-config.md +265 -129
  58. package/docs/reference/training.md +24 -16
  59. package/package.json +5 -3
  60. package/src/anthropic.ts +67 -44
  61. package/src/audio/conformer.ts +647 -0
  62. package/src/audio/decode.ts +133 -0
  63. package/src/audio/features.ts +202 -0
  64. package/src/audio/transcode.ts +67 -0
  65. package/src/chat-template.ts +155 -10
  66. package/src/cli.ts +483 -113
  67. package/src/config.ts +116 -24
  68. package/src/diffusion/diffusion-generate.ts +14 -8
  69. package/src/download.ts +143 -12
  70. package/src/eval/README.md +7 -6
  71. package/src/eval/ifeval.ts +63 -120
  72. package/src/eval/kl.ts +59 -3
  73. package/src/eval/quality-db.ts +2 -2
  74. package/src/eval/runner.ts +17 -8
  75. package/src/eval/tasks/ifeval.ts +167 -31
  76. package/src/expert-io.ts +391 -0
  77. package/src/expert-offload.ts +15 -2
  78. package/src/expert-residency.ts +1134 -0
  79. package/src/expert-usage.ts +415 -0
  80. package/src/flags.ts +13 -0
  81. package/src/generate.ts +438 -35
  82. package/src/grammar.ts +585 -0
  83. package/src/hf-push.ts +1 -1
  84. package/src/hub-rest.ts +242 -0
  85. package/src/index.ts +14 -2
  86. package/src/jobs/runner.ts +37 -18
  87. package/src/kv-store.ts +586 -93
  88. package/src/lora.ts +85 -14
  89. package/src/md-modules.d.ts +42 -0
  90. package/src/media-fetch.ts +426 -0
  91. package/src/memory/rest.ts +245 -0
  92. package/src/memory/vault.ts +74 -0
  93. package/src/mlx/array.ts +149 -19
  94. package/src/mlx/autograd.ts +53 -16
  95. package/src/mlx/checkpoint.ts +46 -13
  96. package/src/mlx/custom-vjp.ts +83 -35
  97. package/src/mlx/ffi.ts +27 -3
  98. package/src/mlx/metal-kernel.ts +7 -4
  99. package/src/mlx/ops.ts +153 -23
  100. package/src/mlx/turboquant-ops.ts +831 -0
  101. package/src/mlx/turboquant-tables.ts +237 -0
  102. package/src/model/batched-mask.ts +62 -1
  103. package/src/model/batched-quant.ts +206 -0
  104. package/src/model/batched-rotating-quant.ts +346 -0
  105. package/src/model/batched-rotating.ts +46 -1
  106. package/src/model/compiled-decode.ts +4 -15
  107. package/src/model/factory.ts +103 -6
  108. package/src/model/gemma4-base.ts +395 -39
  109. package/src/model/gemma4.ts +122 -33
  110. package/src/model/generated/gemma4-12b.ts +9 -13
  111. package/src/model/generated/gemma4-26b.ts +10 -5
  112. package/src/model/generated/gemma4-e4b.ts +14 -9
  113. package/src/model/glm52-atlas.ts +511 -0
  114. package/src/model/glm52-cache.ts +706 -0
  115. package/src/model/glm52-config.ts +262 -0
  116. package/src/model/glm52-container.ts +305 -0
  117. package/src/model/glm52-coupling.ts +388 -0
  118. package/src/model/glm52-dsa.ts +602 -0
  119. package/src/model/glm52-expert-layout.ts +253 -0
  120. package/src/model/glm52-layout.ts +152 -0
  121. package/src/model/glm52-memory.ts +473 -0
  122. package/src/model/glm52-mla.ts +786 -0
  123. package/src/model/glm52-moe.ts +223 -0
  124. package/src/model/glm52-pilot.ts +537 -0
  125. package/src/model/glm52-reference.ts +445 -0
  126. package/src/model/glm52-residency.ts +557 -0
  127. package/src/model/glm52-resident-weights.ts +135 -0
  128. package/src/model/glm52-streamed-experts.ts +453 -0
  129. package/src/model/glm52-streamed-metal.ts +610 -0
  130. package/src/model/glm52-weights.ts +503 -0
  131. package/src/model/glm52.ts +1153 -0
  132. package/src/model/minicpm5.ts +58 -102
  133. package/src/model/paged-kv.ts +253 -0
  134. package/src/model/qwen3-delta.ts +76 -15
  135. package/src/model/qwen3-moe.ts +404 -0
  136. package/src/model/qwen3.ts +9 -8
  137. package/src/model/qwen3_5.ts +88 -23
  138. package/src/model/support.ts +19 -1
  139. package/src/model/universal/dense.ts +40 -5
  140. package/src/model/universal/rope.ts +36 -21
  141. package/src/native/expert_io.c +523 -0
  142. package/src/native-pack.ts +6 -4
  143. package/src/pi-web.ts +1299 -82
  144. package/src/preflight.ts +27 -8
  145. package/src/prompt-cache.ts +293 -33
  146. package/src/quantize/quantizer.ts +16 -1
  147. package/src/registry.ts +57 -2
  148. package/src/responses.ts +62 -49
  149. package/src/serve/batch-scheduler.ts +1050 -78
  150. package/src/serve/generation-gateway.ts +252 -24
  151. package/src/serve/isolate.ts +417 -0
  152. package/src/serve/lane-registry.ts +44 -0
  153. package/src/serve/session-search.ts +326 -0
  154. package/src/server.ts +1701 -175
  155. package/src/spec/assistant-source.ts +150 -0
  156. package/src/spec/deepspec-source.ts +143 -0
  157. package/src/spec/dflash-source.ts +124 -0
  158. package/src/spec/dspark/ab-stats.ts +136 -0
  159. package/src/spec/dspark/calibration.ts +112 -0
  160. package/src/spec/dspark/deepspec-module.ts +801 -0
  161. package/src/spec/dspark/generate-dflash.ts +72 -20
  162. package/src/spec/dspark/loader.ts +23 -0
  163. package/src/spec/dspark/module-dflash.ts +278 -36
  164. package/src/spec/dspark/quantize-drafter.ts +91 -0
  165. package/src/spec/generate.ts +59 -25
  166. package/src/spec/glm52-mtp-source.ts +392 -0
  167. package/src/spec/ngram-source.ts +132 -0
  168. package/src/spec/serve-loop.ts +617 -0
  169. package/src/spec/source.ts +118 -0
  170. package/src/spec/two-model.ts +171 -0
  171. package/src/ssd-cache.ts +111 -35
  172. package/src/tokenizer.ts +288 -2
  173. package/src/tool-approvals.ts +131 -0
  174. package/src/tool-call.ts +438 -22
  175. package/src/train/flash-cce.ts +2 -2
  176. package/src/train/job.ts +0 -12
  177. package/src/train/lora-params.ts +17 -6
  178. package/src/train/trainer.ts +60 -30
  179. package/src/vision/prompt.ts +217 -48
  180. package/src/web/app.html +1197 -1791
  181. package/src/web/app.js +5694 -0
  182. package/src/web/icon.svg +13 -0
  183. package/src/web/manifest.webmanifest +24 -0
  184. package/src/web/src/adapters-panel.ts +288 -0
  185. package/src/web/src/api.ts +57 -0
  186. package/src/web/src/assistant.ts +485 -0
  187. package/src/web/src/chat.ts +1050 -0
  188. package/src/web/src/composer.ts +959 -0
  189. package/src/web/src/dataset.ts +132 -0
  190. package/src/web/src/finetune.ts +267 -0
  191. package/src/web/src/hub.ts +428 -0
  192. package/src/web/src/main.ts +98 -0
  193. package/src/web/src/markdown.ts +0 -0
  194. package/src/web/src/memory-panel.ts +572 -0
  195. package/src/web/src/model-picker.ts +159 -0
  196. package/src/web/src/palette.ts +346 -0
  197. package/src/web/src/protocol.ts +62 -0
  198. package/src/web/src/quantize.ts +167 -0
  199. package/src/web/src/rag.ts +274 -0
  200. package/src/web/src/sessions.ts +469 -0
  201. package/src/web/src/shell.ts +804 -0
  202. package/src/web/src/status.ts +300 -0
  203. package/src/web/src/ui-catalog.ts +92 -0
  204. package/src/web/sw.js +71 -0
  205. package/src/web/vendor/README.md +61 -0
  206. package/src/web/vendor/hljs-LICENSE +29 -0
  207. package/src/web/vendor/hljs-theme.css +59 -0
  208. package/src/web/vendor/hljs.js +26 -0
  209. package/src/web-tools.ts +54 -2
  210. package/src/weights.ts +82 -19
  211. package/src/model/fused-decode-kernel.ts +0 -312
  212. package/src/model/fused-geglu-kernel.ts +0 -198
  213. package/src/model/fused-mlp-kernel.ts +0 -169
  214. package/src/model/fused-swiglu-kernel.ts +0 -240
  215. package/src/model/steel-linear-kernel.ts +0 -96
  216. package/src/status-page.html +0 -497
package/src/cli.ts CHANGED
@@ -12,13 +12,20 @@
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
- import { isSupportedModelRecord } from "./model/support";
21
- import { perfKernelEnabled } from "./model/fused-decode-kernel";
20
+ import { isGlm52Config, isSupportedModelRecord } from "./model/support";
21
+ import {
22
+ GLM52_G5_ASPIRATIONAL_DECODE_TPS,
23
+ GLM52_G5_DEFAULT_MAX_GENERATION_TOKENS,
24
+ GLM52_G5_DEFAULT_PROCESS_LIMIT_BYTES,
25
+ GLM52_G5_DIRECT_ORACLE_WARM_DECODE_TPS,
26
+ GLM52_G5_MEASURED_WARM_DECODE_TPS,
27
+ planGlm52MemoryForArtifact,
28
+ } from "./model/glm52-memory";
22
29
  import type { JobEvent } from "./jobs/types";
23
30
 
24
31
  const argv = process.argv.slice(2);
@@ -36,7 +43,7 @@ Usage: mlx-bun [command] [options] (no command = serve)
36
43
 
37
44
  Commands:
38
45
  pi Launch a pi coding-agent session on a local model (the appliance path)
39
- serve Start the OpenAI/Anthropic-compatible server + status page
46
+ serve Start the OpenAI/Anthropic-compatible server + web app
40
47
  get Download a model from Hugging Face (resumable, verified)
41
48
  ls List downloaded models
42
49
  gc Reclaim superseded model snapshots + dead blobs from the HF cache
@@ -63,7 +70,7 @@ Options:
63
70
  Examples:
64
71
  mlx-bun # download (first run) + serve + open the chat UI
65
72
  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/
73
+ mlx-bun serve 12B # serve the 12B; web app at http://localhost:8080/
67
74
  mlx-bun train e4b --data ./prefs # ORPO LoRA fine-tune on {prompt,chosen,rejected} data
68
75
  mlx-bun get mlx-community/gemma-4-12B-it-OptiQ-4bit`;
69
76
 
@@ -78,7 +85,12 @@ const SERVER_FLAGS = `Server options:
78
85
  --memory-budget <GB> Admission-control memory budget; requests that
79
86
  cannot fit are rejected instead of crashing the
80
87
  GPU [default: machine RAM × 0.75, check-only]
81
- --prompt-cache <GB> Prompt (KV) cache byte cap [default: 2 GB;
88
+ --kv-budget <GB> Aggregate KV budget across concurrent batch rows
89
+ (decimal GB): joiners whose projected KV
90
+ (prompt + max_tokens) would exceed it queue until
91
+ rows finish; a request over the budget alone is
92
+ rejected. Off unless set.
93
+ --prompt-cache <GB> Prompt (KV) cache byte cap [default: 8 GB;
82
94
  --prompt-cache 0 disables the cache]
83
95
  --ssd-cache <dir> SSD cold tier for the prompt cache: prefix KV
84
96
  spills to disk on eviction and persists across
@@ -86,18 +98,44 @@ const SERVER_FLAGS = `Server options:
86
98
  full re-prefill to a zero-copy mmap restore).
87
99
  Off unless set.
88
100
  --ssd-cache-max <GB> SSD tier byte cap [default: 32 GB]
101
+ --ssd-demote-idle <sec> Demote prompt-cache entries idle this long to the
102
+ SSD tier, freeing their GPU memory (prefixes stay
103
+ reachable via zero-copy restore). 0 disables.
104
+ [default: 300 when --ssd-cache is on]
89
105
  --ssd-cache-verify Verify tensor hashes on every restore (reads all
90
106
  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.
107
+ --isolate Run the inference engine as a CHILD process and
108
+ keep this process a pure UI/API proxy — the UI
109
+ stays instant under GPU load and survives engine
110
+ crashes (respawned). /ws/chat not proxied yet.
111
+ Requests naming another model (exact /v1/models
112
+ id) spawn/route to THAT model's engine child.
113
+ --model-pool <n> With --isolate: max RESIDENT model engines
114
+ [default: 1]. Over the cap, the least-recently-
115
+ used engine drains, demotes its caches to the
116
+ SSD tier, and exits — switching back respawns
117
+ it with all state restored from disk.
118
+ --unix <path> (internal) listen on a unix socket instead of a
119
+ TCP port — the engine half of --isolate.
120
+ --batch <n> Max concurrent requests decoded together
121
+ [default: 8]. A LONE request runs the exact
122
+ serial engine (same bits, same speed); the cap
123
+ engages only when concurrent requests arrive
124
+ (agent fan-out). 1 pins strict serial.
95
125
  --decode-concurrency is accepted as the
96
126
  mlx_lm.server alias (semantics differ; see
97
127
  docs/reference/server-config.md)
98
128
  --no-open Don't open the chat UI in your browser on start.
99
129
  By default an interactive terminal opens
100
130
  http://<host>:<port>/#/chat once the server is up.
131
+ --allow-private-media Let image_url/audio_url content parts fetch from
132
+ private/loopback/link-local hosts (NAS, another
133
+ LAN box). Blocked by default: a request's URL is
134
+ attacker-controlled input, and the default keeps
135
+ the server from being steered at cloud metadata
136
+ or LAN-internal services (SSRF). data: URLs are
137
+ always fine; the 10 s timeout and 64 MB response
138
+ cap apply either way.
101
139
 
102
140
  Model & quality:
103
141
  --adapter <dir> Mount a LoRA adapter at startup and use it as the
@@ -105,12 +143,53 @@ Model & quality:
105
143
  request's \`adapter\` field, incl. "none", wins).
106
144
  --adapter-path is accepted as the mlx_lm.server
107
145
  alias. Hot-swap via POST /v1/adapters unchanged.
146
+ --draft-model <query> Speculative decoding: a drafter proposes tokens
147
+ the main model verifies (exact results, faster
148
+ decode when drafts land). The artifact's kind is
149
+ auto-detected — a full same-tokenizer model
150
+ (mlx_lm.server parity), a Gemma "-assistant"
151
+ KV-borrowing drafter, or a DSpark checkpoint.
152
+ Serial lane only — with --batch N a mounted draft
153
+ routes every request serial, like mlx_lm.server.
154
+ --draft-kind <kind> Override draft-artifact detection:
155
+ two-model | assistant | dspark | deepspec | ngram
156
+ (deepspec = DeepSeek's released Gemma4DSparkModel
157
+ drafters; ngram = MODEL-FREE prompt lookup — no
158
+ --draft-model, drafts copied from the request's
159
+ own context, lossless) [default: auto]
160
+ --num-draft-tokens <n> Drafts per verify round [default: 3; ngram: 10;
161
+ DSpark pins to its trained block width]
162
+ --ngram-max <k> Prompt-lookup longest match window [default: 3]
163
+ --ngram-min <k> Prompt-lookup shortest match window [default: 1]
164
+ --mtp <on|off> GLM-5.2 checkpoint-native MTP. On by default;
165
+ routes requests through the exact serial verify
166
+ lane. off enables ordinary continuous batching.
167
+ --context-length <n> GLM-5.2 resource-plan context reservation
168
+ [default: 4096]
108
169
  --kv-quant <mode> KV cache quantization: config (per-layer
109
170
  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]
171
+ (bf16), 4 / 8 (uniform bits), or turbo[:k<bits>v
172
+ <bits>] (rotation-based TurboQuant — default
173
+ k8v3; kBits in {2,4,5,8}, vBits in {2,3,4,5,8};
174
+ docs/design/turboquant-kv.md)
175
+ [default: off — quantized KV measured 5-20% slower
176
+ decode than bf16 at ≤16k (on mlx-lm too); it buys
177
+ memory headroom, so opt in when context is tight.
178
+ The batched engine (--batch N) is bf16-only: an
179
+ explicit --kv-quant routes those requests serial.
180
+ turbo is solo-only in v1 (full-attention layers
181
+ only; sliding-window layers stay bf16)]
182
+ --paged-kv OPTIONAL vLLM-style paged KV storage (block pool +
183
+ gather before the stock SDPA; env MLX_BUN_PAGED_KV=1).
184
+ v1: serial only (pins --batch 1 unless --batch is
185
+ given), Gemma4-family, bf16 — refuses --batch N>1,
186
+ --kv-quant, --draft-model; bypasses the prompt
187
+ cache; runs uncompiled decode. Bit-exact with the
188
+ plain path; expect a small decode cost at batch=1
189
+ (the gather copy). docs/design/paged-kv-cache.md
190
+ [default: off]
191
+ --paged-kv-block-size <n> Tokens per KV block [default: 256 = the plain
192
+ cache's growth step]
114
193
  --thinking <true|false> Default for the chat template's enable_thinking
115
194
  variable (CPM and other hybrid-reasoning models);
116
195
  a request's chat_template_kwargs overrides it
@@ -124,7 +203,8 @@ Model & quality:
124
203
  [default: the model's generation_config.json]
125
204
  --max-tokens <n> Completion cap when a request omits max_tokens
126
205
  (mlx_lm.server flag; its default is 512 there)
127
- [default: 65536 chat / 512 raw completion]
206
+ [default: 128 GLM-5.2; otherwise 65536 chat /
207
+ 512 raw completion]
128
208
  --hlg-sampling on|off Piecewise tone-curve sampling (HLG): rolls off the
129
209
  top, boosts the mids, gentles the tail. Gain folds
130
210
  from --temperature. [default: off]
@@ -134,23 +214,28 @@ Model & quality:
134
214
  --hlg-toe <nats> HLG shadow rolloff scale [default: 6]
135
215
  --hlg-pivot-offset <nats> HLG pivot: nats below the top token [default: 6]
136
216
 
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)
217
+ Parity tier (an ALIAS for the per-fork flags below; each expands to the fastest
218
+ set of forks that still holds the guarantee — a per-fork flag overrides one):
219
+ --l1 Bit-for-bit IDENTICAL to mlx-lm (bf16 KV, compiled
220
+ activations — the faithful @mx.compile kernels).
221
+ No tier given = --l1 (decided 2026-07-05: the L1
222
+ faithful kernels match mlx-lm 1.00x on every model;
223
+ anything output-changing must beat that baseline in
224
+ a paired A/B to earn a default).
139
225
  --l2 Bit-for-bit IDENTICAL to mlx-optiq (quantized KV +
140
226
  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.
227
+ optiq-golden composition)
146
228
 
147
- Performance levers (A/B levers; defaults are the measured winners):
229
+ Kill switches (bit-exact A/B levers — each selects a slower same-parity path):
148
230
  --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]
231
+ --compiled-activations on|off
232
+ Compiled geglu/swiglu (mlx-lm's @mx.compile kernel —
233
+ bit-exact AND fewer dispatches). off = the uncompiled
234
+ composition (same parity, slower) [default: on]
235
+ --fused-sdpa on|off Fused SDPA path (quantized KV only) [default:
236
+ follows --kv-quant — on for config (the optiq
237
+ composition), off for uniform 4/8 (the mlx-lm
238
+ composition) and bf16]
154
239
  --force-wire Wire weights into memory at load
155
240
  --expert-offload MoE only: serve experts from a page-aligned file
156
241
  mmap (built on first use) — keeps the model out of
@@ -199,7 +284,7 @@ ${SERVER_FLAGS}
199
284
 
200
285
  Endpoints: /v1/chat/completions, /v1/completions, /v1/messages, /v1/responses,
201
286
  /v1/models, /v1/adapters, /health, /stats, /fit, /library, /downloads —
202
- status page at /, browser chat at /chat`,
287
+ web app at / (lands on chat) — /chat, /status, /quantize, /finetune, /dataset deep-link its views`,
203
288
 
204
289
  get: `mlx-bun get — download a model from Hugging Face
205
290
 
@@ -214,7 +299,9 @@ Options:
214
299
 
215
300
  Resumable (Range requests against partial blobs) and verified (sha256
216
301
  for LFS blobs). Re-running after an interruption continues where it
217
- stopped. Uses HF_TOKEN / hf auth login credentials when present.
302
+ stopped. Before payload transfer, complete blobs and partial prefixes are
303
+ credited and the target volume must fit the remainder plus a 1 GiB reserve.
304
+ Uses HF_TOKEN / hf auth login credentials when present.
218
305
  When upstream pushed a new revision, the previous snapshot stays on
219
306
  disk — \`mlx-bun gc\` reclaims it.`,
220
307
 
@@ -305,11 +392,11 @@ Options:
305
392
  --tokens <n> Tokens to decode per run [default: 256]
306
393
  --runs <n> Runs (median reported) [default: 3]
307
394
  --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]
395
+ --kv-quant <mode> config | off | 4 | 8 | turbo[:k<bits>v<bits>]
396
+ [default: off, the historical baseline]
310
397
 
311
- Performance levers (--compiled-decode, --perf-kernel, --fused-decode,
312
- --fused-sdpa) apply to the run — A/B by running twice.`,
398
+ Kill switches (--compiled-decode, --compiled-activations, --fused-sdpa)
399
+ apply to the run — A/B by running twice.`,
313
400
 
314
401
  embed: `mlx-bun embed — text embeddings from a local embedding model
315
402
 
@@ -358,9 +445,9 @@ Options:
358
445
  --top-k <n> Top-k sampling
359
446
  --seed <n> Sampler seed (reproducible runs)
360
447
 
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,
448
+ Decode-path levers mirror serve: --l1/--l2 and the per-fork flags
449
+ (--fused-sdpa/--compiled-decode/--compiled-activations/--kv-quant).
450
+ The tier's KV scheme applies here too (--l2 = the model's kv_config,
364
451
  same tokens as serve); with no tier/--kv-quant the KV cache stays bf16
365
452
  (the bit-exact mlx-lm greedy path). mlx-lm compat = --l1.
366
453
 
@@ -720,69 +807,96 @@ function openChatUi(url: string, hostPort: string): void {
720
807
  /** Server/runtime flags shared by every mode that loads a model
721
808
  * (serve, pi). Env levers are set here so they're in place before the
722
809
  * 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.
810
+ /** Resolve the decode ROUTE: a tier alias (--l1/--l2) sets the whole route,
811
+ * and an explicit per-fork flag (--kv-quant/--fused-sdpa/…) overrides the
812
+ * alias. Sets the decode env levers and returns the kv-quant mode. See
813
+ * docs/design/parity-tier-dag.md + unified-engine-frontier-plan.md.
727
814
  * Each tier is a GUARANTEE about which reference you reproduce bit-for-bit:
728
815
  * --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 } {
816
+ * (bf16 KV; THE naked default since 2026-07-05)
817
+ * --l2 bit-for-bit IDENTICAL to mlx-optiq — drop-in replacement for
818
+ * mlx-optiq: quantized KV per kv_config.json + fused N-tiled SDPA for
819
+ * L>1 + STOCK unfused L=1 decode (the optiq-golden composition,
820
+ * scripts/regen-kvq-goldens.ts + regen-mixed-kv-goldens.ts).
821
+ * There is no --l3: output-changing experiments live in the Lab (env flags
822
+ * with a bench + expiry) until one beats the L1 baseline in a paired A/B —
823
+ * docs/design/unified-engine-frontier-plan.md §6-7. */
824
+ function applyDecodeRoute(): { kvQuant?: "off" | "config" | number; turboQuant?: TurboQuantScheme } {
741
825
  const onOff = (name: string): boolean | null => {
742
826
  const v = opt(name); if (v == null) return null;
743
827
  if (v === "on" || v === "1" || v === "true") return true;
744
828
  if (v === "off" || v === "0" || v === "false") return false;
745
829
  console.error(`--${name} expects on|off (got "${v}")`); process.exit(1);
746
830
  };
747
- type Preset = { kv: "off" | "config"; perf: boolean; fusedSdpa: boolean; compiled: boolean; fusedDecode: boolean };
831
+ if (flag("l3")) {
832
+ console.error(
833
+ "--l3 was removed 2026-07-05: no output-changing kernel beat the L1 baseline " +
834
+ "in a paired A/B, so the tier had no content. Use --l2 for the optiq route, " +
835
+ "or the Lab (docs/design/unified-engine-frontier-plan.md) for experiments.",
836
+ );
837
+ process.exit(1);
838
+ }
839
+ type Preset = { kv: "off" | "config"; fusedSdpa: boolean; compiled: boolean; compiledAct: boolean };
748
840
  const TIERS: Record<string, Preset> = {
749
841
  // A tier is the GUARANTEE; within it we default to the FAST kernel that still
750
842
  // holds it, and expose the slow one as an opt-in. compiled-decode is BIT-EXACT
751
843
  // 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)
844
+ // EVERY tier (--compiled-decode off is the slow, same-tier opt-in). compiled
845
+ // ACTIVATIONS (geglu/swiglu) go through the SAME libmlx as mlx-lm's `@mx.compile`
846
+ // → same kernel, bit-exact AND faster → ON in every tier (--compiled-activations
847
+ // off is the slower same-parity opt-in). fused-sdpa matches OPTIQ bit-for-bit
848
+ // (tier-a goldens, fused-sdpa.test) and is quantized-KV only → it's the L2 bridge,
849
+ // a no-op on L1's bf16.
850
+ l1: { kv: "off", fusedSdpa: false, compiled: true, compiledAct: true }, // = mlx-lm bit-for-bit (bf16)
851
+ l2: { kv: "config", fusedSdpa: true, compiled: true, compiledAct: true }, // = mlx-optiq bit-for-bit
764
852
  };
765
- const tier = flag("l1") ? "l1" : flag("l2") ? "l2" : flag("l3") ? "l3" : null;
766
- const p = tier ? TIERS[tier]! : null;
853
+ // NAKED DEFAULT = L1 (decided 2026-07-05): with no tier flag you get the
854
+ // mlx-lm bit-for-bit route. The 2026-07-05 benchmark pass showed the L1
855
+ // faithful kernel set matches mlx-lm 1.00× on every model while every
856
+ // output-changing lever failed to beat it in a paired A/B (the losers were
857
+ // deleted the same day — Phase 1 of unified-engine-frontier-plan.md).
858
+ // Quantized KV costs 5–20% decode at ≤16k on BOTH stacks and pays only in
859
+ // memory/context headroom → explicit opt-in (--kv-quant / --l2).
860
+ const tier = flag("l2") ? "l2" : "l1";
861
+ const p = TIERS[tier]!;
862
+ const kv = opt("kv-quant"); // explicit --kv-quant overrides the tier
767
863
  const pick = (name: string, base: boolean | undefined): boolean | null => {
768
864
  const ex = onOff(name); return ex !== null ? ex : (base ?? null); // explicit flag wins, else tier preset
769
865
  };
770
866
  const set = (env: string, val: boolean | null, invert = false) => {
771
867
  if (val !== null) process.env[env] = (invert ? !val : val) ? "1" : "0";
772
868
  };
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
869
+ // Explicit --kv-quant picks the composition its ORACLE uses: config (the
870
+ // optiq mixed scheme) runs optiq's fused SDPA (the L2 golden composition);
871
+ // uniform 4/8 runs UNFUSED (mlx-lm's quantized_scaled_dot_product_attention
872
+ // — the bit-exact L1-eligible scheme). --fused-sdpa still overrides either.
873
+ const fusedSdpaBase = kv === "config" ? true : kv != null ? false : p.fusedSdpa;
874
+ set("MLX_BUN_COMPILED_DECODE", pick("compiled-decode", p.compiled));
875
+ set("MLX_BUN_NO_FUSED_SDPA", pick("fused-sdpa", fusedSdpaBase), true); // inverted env
876
+ // Compiled activations (the faithful geglu/swiglu — mlx-lm's @mx.compile kernel).
877
+ // One fork drives both env vars; qwen3/qwen3.5/universal compile unconditionally,
878
+ // so this toggles gemma geglu + minicpm5 swiglu (the only unfused-capable sites).
879
+ set("MLX_BUN_COMPILED_GEGLU", pick("compiled-activations", p.compiledAct));
880
+ set("MLX_BUN_COMPILED_SWIGLU", pick("compiled-activations", p.compiledAct));
778
881
  if (kv === "off") return { kvQuant: "off" };
779
882
  if (kv === "config") return { kvQuant: "config" };
780
883
  if (kv) {
884
+ // TurboQuant (docs/design/turboquant-kv.md): a distinct axis from the
885
+ // affine kvQuant above — dequantize-on-fetch, stock unfused ops.sdpa,
886
+ // no kvQuant value at all (mutually exclusive with kvBits/kvConfig).
887
+ let turboQuant: TurboQuantScheme | null;
888
+ try {
889
+ turboQuant = parseTurboQuantScheme(kv);
890
+ } catch (e) {
891
+ console.error((e as Error).message);
892
+ process.exit(1);
893
+ }
894
+ if (turboQuant) return { turboQuant };
781
895
  const bits = Number(kv);
782
- if (![4, 8].includes(bits)) { console.error(`--kv-quant expects config|off|4|8 (got "${kv}")`); process.exit(1); }
896
+ if (![4, 8].includes(bits)) { console.error(`--kv-quant expects config|off|4|8|turbo (got "${kv}")`); process.exit(1); }
783
897
  return { kvQuant: bits };
784
898
  }
785
- return p ? { kvQuant: p.kv } : {};
899
+ return { kvQuant: p.kv };
786
900
  }
787
901
 
788
902
  function serverRuntimeFlags(): { port: number; serverOptions: import("./server").ServerOptions } {
@@ -794,14 +908,26 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
794
908
  console.error(`--${name} expects on|off (got "${v}")`);
795
909
  process.exit(1);
796
910
  };
797
- const route = applyDecodeRoute(); // --l1/--l2/--l3 tier alias, with per-fork flags overriding
911
+ const route = applyDecodeRoute(); // --l1/--l2 tier alias, with per-fork flags overriding
798
912
  if (flag("force-wire")) process.env.MLX_BUN_FORCE_WIRE = "1";
913
+ // Remote image_url/audio_url media fetches block private/loopback/link-
914
+ // local destinations by default (SSRF — src/media-fetch.ts); this is the
915
+ // LAN-hosts escape hatch.
916
+ if (flag("allow-private-media")) process.env.MLX_BUN_ALLOW_PRIVATE_MEDIA = "1";
799
917
 
800
918
  const serverOptions: import("./server").ServerOptions = {};
801
919
  const budgetGB = Number(opt("memory-budget", "0"));
802
920
  if (budgetGB > 0) serverOptions.memoryBudgetBytes = budgetGB * 1e9;
921
+ // Aggregate KV budget across batch rows (batching-perf-path P3 admission).
922
+ const kvBudgetGB = Number(opt("kv-budget", "0"));
923
+ if (kvBudgetGB > 0) serverOptions.kvBudgetBytes = kvBudgetGB * 1e9;
803
924
  const pcRaw = opt("prompt-cache"); // null if absent; an explicit value (incl. 0) wins → `--prompt-cache 0` DISABLES the cache
804
925
  if (pcRaw !== null) serverOptions.promptCacheBytes = Math.max(0, Number(pcRaw)) * 2 ** 30;
926
+ // Engine-child mode (runtime-isolation.md): listen on a unix socket
927
+ // instead of TCP — the isolated parent proxies here. Internal flag,
928
+ // spawned by --isolate; usable directly for socket-level integration.
929
+ const unixRaw = opt("unix");
930
+ if (unixRaw !== null) serverOptions.unixSocket = unixRaw;
805
931
  // SSD cold tier (docs/design/ssd-kv-cold-tier.md) — off unless a dir is given.
806
932
  const ssdDir = opt("ssd-cache");
807
933
  if (ssdDir !== null) {
@@ -809,13 +935,21 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
809
935
  const maxRaw = opt("ssd-cache-max");
810
936
  if (maxRaw !== null) serverOptions.ssdCacheMaxBytes = Math.max(1, Number(maxRaw)) * 2 ** 30;
811
937
  if (flag("ssd-cache-verify")) serverOptions.ssdCacheVerify = true;
938
+ const demoteRaw = opt("ssd-demote-idle");
939
+ if (demoteRaw !== null) serverOptions.ssdDemoteIdleSec = Math.max(0, Number(demoteRaw));
940
+ } else {
941
+ // Refuse-loudly house rule: sub-flags of an absent tier must not vanish.
942
+ for (const f of ["ssd-cache-max", "ssd-demote-idle"] as const)
943
+ if (opt(f) !== null) console.warn(`--${f} has no effect without --ssd-cache — ignored`);
944
+ if (flag("ssd-cache-verify"))
945
+ console.warn("--ssd-cache-verify has no effect without --ssd-cache — ignored");
812
946
  }
813
947
  // --batch N: max concurrent requests batched through the mlx-lm-parity
814
948
  // engine (N=1 = today's serial path). --decode-concurrency is accepted for
815
949
  // drop-in compatibility with mlx_lm.server, but the semantics differ: there
816
950
  // 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).
951
+ // caps continuous batching (default 8; `--batch 1` pins the serial path —
952
+ // the unified engine is bit-parity with it and costs ~nothing at B=1).
819
953
  const batchRaw = opt("batch") ?? opt("decode-concurrency");
820
954
  if (batchRaw !== null) {
821
955
  const n = Number(batchRaw);
@@ -826,6 +960,19 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
826
960
  serverOptions.batch = n;
827
961
  }
828
962
  if (route.kvQuant !== undefined) serverOptions.kvQuant = route.kvQuant;
963
+ if (route.turboQuant !== undefined) serverOptions.turboQuant = route.turboQuant;
964
+ // --paged-kv (env: MLX_BUN_PAGED_KV=1): OPTIONAL vLLM-style block-pool KV
965
+ // storage, default off (docs/design/paged-kv-cache.md). Serial-only in v1:
966
+ // with no explicit --batch, pin --batch 1 (the default is 8); an explicit
967
+ // --batch N>1 is refused loudly by createServer rather than downgraded.
968
+ if (flag("paged-kv") || process.env.MLX_BUN_PAGED_KV === "1") {
969
+ const bsRaw = opt("paged-kv-block-size");
970
+ serverOptions.pagedKv = bsRaw !== null ? { blockSize: Number(bsRaw) } : {};
971
+ if (batchRaw === null) {
972
+ serverOptions.batch = 1;
973
+ console.log("[paged-kv] serial-only in v1 — pinning --batch 1");
974
+ }
975
+ }
829
976
  // Bind loopback unless asked otherwise (mlx_lm.server parity); --host
830
977
  // 0.0.0.0 is the explicit opt-in for LAN exposure. The chat-UI open and
831
978
  // `mlx-bun pi` attach both go through localhost, so loopback-only is
@@ -876,6 +1023,39 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
876
1023
  return { port: Number(opt("port", "8080")), serverOptions };
877
1024
  }
878
1025
 
1026
+ /** Direct Colibri runtime defaults shared by `serve` and the embedded `pi`
1027
+ * server. Other model families ignore this load-only option block. */
1028
+ function glm52RuntimeFlags(
1029
+ serverOptions: import("./server").ServerOptions,
1030
+ ): import("./model/factory").Glm52RuntimeOpenOptions {
1031
+ const mtpRaw = opt("mtp");
1032
+ let enableMtp: boolean | undefined;
1033
+ if (mtpRaw !== null) {
1034
+ if (["on", "1", "true"].includes(mtpRaw)) enableMtp = true;
1035
+ else if (["off", "0", "false"].includes(mtpRaw)) enableMtp = false;
1036
+ else {
1037
+ console.error(`--mtp expects on|off (got "${mtpRaw}")`);
1038
+ process.exit(1);
1039
+ }
1040
+ }
1041
+ const contextRaw = opt("context-length");
1042
+ let contextTokens: number | undefined;
1043
+ if (contextRaw !== null) {
1044
+ contextTokens = Number(contextRaw);
1045
+ if (!Number.isSafeInteger(contextTokens) || contextTokens < 1) {
1046
+ console.error(`--context-length expects an integer >= 1 (got "${contextRaw}")`);
1047
+ process.exit(1);
1048
+ }
1049
+ }
1050
+ return {
1051
+ memoryBudgetBytes: serverOptions.memoryBudgetBytes,
1052
+ batchSize: serverOptions.batch ?? 8,
1053
+ maxGenerationTokens: serverOptions.defaultMaxTokens ?? 128,
1054
+ ...(contextTokens !== undefined ? { contextTokens } : {}),
1055
+ ...(enableMtp !== undefined ? { enableMtp } : {}),
1056
+ };
1057
+ }
1058
+
879
1059
  /** `--adapter <dir>` (alias `--adapter-path`, mlx_lm.server's spelling):
880
1060
  * mount a LoRA adapter at startup — the same machinery as POST /v1/adapters —
881
1061
  * and make it the default for requests that don't select one (a request's
@@ -904,11 +1084,11 @@ async function mountStartupAdapter(
904
1084
 
905
1085
  /** One-line summary of the active runtime levers for the ready card. */
906
1086
  function runtimeSummary(o: import("./server").ServerOptions): string {
907
- const kv = o.kvQuant === "off" ? "off" : typeof o.kvQuant === "number" ? `kv${o.kvQuant}` : "config";
1087
+ const kv = o.turboQuant ? `turbo-k${o.turboQuant.kBits}v${o.turboQuant.vBits}`
1088
+ : o.kvQuant === "off" ? "off" : typeof o.kvQuant === "number" ? `kv${o.kvQuant}` : "config";
908
1089
  const lever = (env: string, dflt: string) => process.env[env] ?? dflt;
909
1090
  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" : "") +
1091
+ (lever("MLX_BUN_COMPILED_GEGLU", "1") === "0" ? " · compiled-activations off" : "") +
912
1092
  (o.batch && o.batch > 1 ? ` · batch ${o.batch}` : "") +
913
1093
  (o.defaultThinking !== undefined ? ` · thinking ${o.defaultThinking ? "on" : "off"}` : "") +
914
1094
  (o.defaultTemperature !== undefined ? ` · temp ${o.defaultTemperature}` : "") +
@@ -958,7 +1138,7 @@ async function resolveModelAuto(query: string | null): Promise<{ m: import("./re
958
1138
  await reg.scan();
959
1139
  if (recRepo !== starterRepo) {
960
1140
  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")}`);
1141
+ console.log(` ${style.dim("it becomes the default next run · progress in the web app's Status view · resumable")}`);
962
1142
  downloadModel(recRepo)
963
1143
  .then(async () => {
964
1144
  await new Registry().scan();
@@ -1201,9 +1381,69 @@ switch (cmd) {
1201
1381
  if (reg.list().length === 0) await reg.scan();
1202
1382
  const m = reg.resolve(query);
1203
1383
  const config = await loadModelConfig(m.path);
1204
- const ctx = Number(opt("ctx", "8192"));
1205
- const r = fit(config, m.sizeBytes, ctx, thisMachine(), undefined, m.expertsBytes);
1206
1384
  const { box, table, style, h1, gradient } = await import("./tui");
1385
+ const glm = isGlm52Config(config);
1386
+ const ctx = Number(opt("ctx", glm ? "4096" : "8192"));
1387
+ if (glm) {
1388
+ const machine = thisMachine();
1389
+ let plan;
1390
+ try {
1391
+ plan = await planGlm52MemoryForArtifact(m.path, {
1392
+ machineBytes: machine.ramBytes,
1393
+ processLimitBytes: Math.min(
1394
+ GLM52_G5_DEFAULT_PROCESS_LIMIT_BYTES,
1395
+ machine.ramBytes,
1396
+ ),
1397
+ contextTokens: ctx,
1398
+ maxGenerationTokens: Math.min(
1399
+ ctx,
1400
+ GLM52_G5_DEFAULT_MAX_GENERATION_TOKENS,
1401
+ ),
1402
+ batchSize: 1,
1403
+ enableMtp: true,
1404
+ });
1405
+ } catch (error) {
1406
+ console.error(
1407
+ `GLM-5.2 resource plan refused: ` +
1408
+ `${error instanceof Error ? error.message : String(error)}`,
1409
+ );
1410
+ process.exitCode = 1;
1411
+ break;
1412
+ }
1413
+ const gib = (bytes: number): string => `${(bytes / 2 ** 30).toFixed(2)} GiB`;
1414
+ const li = plan.lineItems;
1415
+ const kvBytes = li.targetKvBytes + li.mtpKvBytes;
1416
+ const otherBytes = plan.plannedProcessBytes - li.residentWeightsBytes -
1417
+ li.mainExpertSlabBytes - li.mtpExpertSlabBytes - kvBytes;
1418
+ h1("will it fit?");
1419
+ console.log(` ${style.bold(m.repoId)} ${style.dim(`@ ${ctx.toLocaleString()} context · streamed GLM plan`)}`);
1420
+ console.log();
1421
+ box([
1422
+ `artifact on disk ${gib(m.sizeBytes).padStart(10)} ${style.dim("(streamed, not resident)")}`,
1423
+ `resident weights ${gib(li.residentWeightsBytes).padStart(10)}`,
1424
+ `main expert slab ${gib(li.mainExpertSlabBytes).padStart(10)}`,
1425
+ `MTP expert slab ${gib(li.mtpExpertSlabBytes).padStart(10)}`,
1426
+ `target + MTP KV ${gib(kvBytes).padStart(10)}`,
1427
+ `runtime reserves ${gib(otherBytes).padStart(10)}`,
1428
+ `process plan ${gib(plan.plannedProcessBytes).padStart(10)} ${style.dim(`of ${gib(plan.processLimitBytes)}`)} ${style.green(style.bold("FITS"))}`,
1429
+ `macOS headroom ${gib(plan.machineHeadroomBytes).padStart(10)}`,
1430
+ "",
1431
+ `max safe context ${style.bold(plan.contextTokens.toLocaleString())} tokens`,
1432
+ `max generation ${style.bold(plan.maxGenerationTokens.toLocaleString())} tokens`,
1433
+ `measured warm ${gradient(`${GLM52_G5_MEASURED_WARM_DECODE_TPS.toFixed(3)} tok/s`)} ${style.dim("(quality-preserving, MTP on)")}`,
1434
+ `direct oracle ${GLM52_G5_DIRECT_ORACLE_WARM_DECODE_TPS.toFixed(2)} tok/s`,
1435
+ `aspirational ${GLM52_G5_ASPIRATIONAL_DECODE_TPS.toFixed(1)} tok/s ${style.dim("(not a fit prediction or release gate)")}`,
1436
+ ]);
1437
+ if (flag("skus")) {
1438
+ console.log(style.dim(
1439
+ " GLM-5.2's direct-container preset is validated on the M1 Max 32 GB; " +
1440
+ "the generic resident-weight SKU projection does not apply.",
1441
+ ));
1442
+ }
1443
+ console.log();
1444
+ break;
1445
+ }
1446
+ const r = fit(config, m.sizeBytes, ctx, thisMachine(), undefined, m.expertsBytes);
1207
1447
  h1("will it fit?");
1208
1448
  console.log(` ${style.bold(m.repoId)} ${style.dim(`@ ${ctx.toLocaleString()} context · this machine`)}`);
1209
1449
  console.log();
@@ -1248,7 +1488,7 @@ switch (cmd) {
1248
1488
  console.log(style.dim(" no command given — starting the server · mlx-bun --help for all commands"));
1249
1489
  const rt = serverRuntimeFlags();
1250
1490
  // Friendly collision check before loading gigabytes of weights.
1251
- {
1491
+ if (!rt.serverOptions.unixSocket) {
1252
1492
  const { probeServer } = await import("./harness-pi");
1253
1493
  const running = await probeServer(`http://localhost:${rt.port}/v1`);
1254
1494
  if (running) {
@@ -1258,6 +1498,71 @@ switch (cmd) {
1258
1498
  process.exit(1);
1259
1499
  }
1260
1500
  }
1501
+ // Runtime isolation (--isolate, runtime-isolation.md): THIS process
1502
+ // becomes a pure reactor — it binds the TCP port and reverse-proxies to
1503
+ // an engine CHILD (the full server on a unix socket, spawned from the
1504
+ // same argv minus parent-only flags). Zero MLX calls in the parent: the
1505
+ // UI stays instant under any GPU load, and an engine crash respawns
1506
+ // without taking the session down.
1507
+ if (!flag("isolate") && opt("model-pool") !== null)
1508
+ console.warn("--model-pool has no effect without --isolate (child-per-model pool) — ignored");
1509
+ if (flag("isolate")) {
1510
+ const { startProxyServer, engineArgvForModel, defaultSocketPath } = await import("./serve/isolate");
1511
+ const sock = defaultSocketPath();
1512
+ // Re-spawn ourselves: compiled binary = execPath alone (argv[1] is a
1513
+ // $bunfs virtual path); dev = bun + the script path.
1514
+ const selfArgv = process.argv[1] && !process.argv[1].includes("$bunfs")
1515
+ ? [process.execPath, process.argv[1]]
1516
+ : [process.execPath];
1517
+ const rawArgs = process.argv.slice(2);
1518
+ // Resolve the DEFAULT model in the parent (registry lookup — no
1519
+ // weights load) so the pool has its key and the child gets a pinned
1520
+ // --model path instead of re-running fuzzy resolution.
1521
+ const { m: m0 } = await resolveModelAuto(opt("model") ?? positional(0) ?? opt("query"));
1522
+ // STRICT resolver for the pool (P2): only EXACT /v1/models ids
1523
+ // switch models. Fuzzy matching here would hijack drop-in clients
1524
+ // that send model:"gpt-4"-style strings (which must keep today's
1525
+ // ignored-field semantics and ride the default engine).
1526
+ const { Registry } = await import("./registry");
1527
+ const poolResolve = (query: string): { repoId: string; path: string } | null => {
1528
+ if (query === m0.repoId) return { repoId: m0.repoId, path: m0.path };
1529
+ try {
1530
+ const reg = new Registry();
1531
+ try {
1532
+ const hit = reg.listCanonical().find((r) => r.repoId === query);
1533
+ return hit ? { repoId: hit.repoId, path: hit.path } : null;
1534
+ } finally {
1535
+ reg.close();
1536
+ }
1537
+ } catch {
1538
+ return null;
1539
+ }
1540
+ };
1541
+ const poolMaxRaw = opt("model-pool");
1542
+ const poolMax = Math.max(1, Number(poolMaxRaw ?? 1) || 1);
1543
+ const { engine, pool } = startProxyServer({
1544
+ port: rt.port,
1545
+ ...(rt.serverOptions.hostname ? { hostname: rt.serverOptions.hostname } : {}),
1546
+ engine: {
1547
+ argv: [...selfArgv, ...engineArgvForModel(rawArgs, sock, m0.path)],
1548
+ socketPath: sock,
1549
+ },
1550
+ pool: {
1551
+ rawArgs, selfArgv, poolMax,
1552
+ resolve: poolResolve,
1553
+ defaultKey: m0.repoId,
1554
+ },
1555
+ });
1556
+ const shownHost0 = rt.serverOptions.hostname ?? "localhost";
1557
+ console.log(style.dim(` isolated mode: engine child starting (socket ${sock})`));
1558
+ console.log(` ${style.green("●")} ${style.bold("proxy up")} http://${shownHost0}:${rt.port} ${style.dim(`· ${m0.repoId} loading behind it · model pool ${poolMax}`)}`);
1559
+ const onExit = () => { pool?.stopAll(); engine.stop(); process.exit(0); };
1560
+ process.on("SIGINT", onExit);
1561
+ process.on("SIGTERM", onExit);
1562
+ await engine.ready;
1563
+ console.log(` ${style.green("●")} ${style.bold("engine ready")} ${style.dim(`pid ${engine.pid}`)}`);
1564
+ await new Promise(() => {}); // serve until killed
1565
+ }
1261
1566
  // --model <path-or-query> is the explicit override (mlx_lm.server's
1262
1567
  // spelling — previously accepted-but-IGNORED here, which silently
1263
1568
  // auto-picked the wrong model). A directory with config.json loads
@@ -1285,10 +1590,66 @@ switch (cmd) {
1285
1590
  sOff.done(`experts mmap'd ${style.dim(dir)} ${style.dim("· phys_footprint ≈ core")}`);
1286
1591
  }
1287
1592
  }
1593
+ // Speculative decoding (mlx_lm.server parity): --draft-model <path|query>
1594
+ // resolves like the main model; --num-draft-tokens defaults to 3 (the
1595
+ // server's default upstream — mlx_lm.generate's is 2). Serial-lane-only:
1596
+ // with --batch N, a mounted draft routes every request serial (upstream
1597
+ // is_batchable = draft is None).
1598
+ const draftQuery = opt("draft-model");
1599
+ let draftModelDir: string | undefined;
1600
+ if (draftQuery !== null) {
1601
+ const { m: dm } = await resolveModelAuto(draftQuery);
1602
+ draftModelDir = dm.path;
1603
+ }
1604
+ const numDraftRaw = opt("num-draft-tokens");
1605
+ const numDraftTokens = numDraftRaw !== null ? Number(numDraftRaw) : undefined;
1606
+ if (numDraftTokens !== undefined && (!Number.isInteger(numDraftTokens) || numDraftTokens < 1)) {
1607
+ console.error(`--num-draft-tokens expects an integer >= 1 (got "${numDraftRaw}")`);
1608
+ process.exit(1);
1609
+ }
1610
+ const draftKindRaw = opt("draft-kind");
1611
+ const draftKind = (draftKindRaw ?? undefined) as
1612
+ | "dspark" | "deepspec" | "assistant" | "two-model" | "ngram" | undefined;
1613
+ if (draftKind !== undefined && !["dspark", "deepspec", "assistant", "two-model", "ngram"].includes(draftKind)) {
1614
+ console.error(`--draft-kind expects two-model|assistant|dspark|deepspec|ngram (got "${draftKindRaw}")`);
1615
+ process.exit(1);
1616
+ }
1617
+ // Prompt-lookup window bounds (ngram kind only; validated here so a typo
1618
+ // fails the launch, not the first request).
1619
+ const ngramInt = (name: string): number | undefined => {
1620
+ const raw = opt(name);
1621
+ if (raw === null) return undefined;
1622
+ const v = Number(raw);
1623
+ if (!Number.isInteger(v) || v < 1) {
1624
+ console.error(`--${name} expects an integer >= 1 (got "${raw}")`);
1625
+ process.exit(1);
1626
+ }
1627
+ return v;
1628
+ };
1629
+ const ngramMax = ngramInt("ngram-max");
1630
+ const ngramMin = ngramInt("ngram-min");
1631
+ if (ngramMax !== undefined && ngramMin !== undefined && ngramMin > ngramMax) {
1632
+ console.error(`--ngram-min (${ngramMin}) must be <= --ngram-max (${ngramMax})`);
1633
+ process.exit(1);
1634
+ }
1635
+ if ((ngramMax !== undefined || ngramMin !== undefined) && draftKind !== "ngram")
1636
+ console.warn("--ngram-max/--ngram-min only apply with --draft-kind ngram — ignored");
1288
1637
  const sLoad = step("loading weights");
1289
1638
  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`)}`);
1639
+ const ctx = await loadContext(m.path, m.repoId, {
1640
+ memoryBudgetBytes: rt.serverOptions.memoryBudgetBytes,
1641
+ glm: glm52RuntimeFlags(rt.serverOptions),
1642
+ ...(draftModelDir || draftKind === "ngram"
1643
+ ? {
1644
+ ...(draftModelDir ? { draftModelDir } : {}),
1645
+ numDraftTokens,
1646
+ ...(draftKind ? { draftKind } : {}),
1647
+ ...(ngramMax !== undefined ? { ngramMax } : {}),
1648
+ ...(ngramMin !== undefined ? { ngramMin } : {}),
1649
+ }
1650
+ : {}),
1651
+ });
1652
+ 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
1653
  await mountStartupAdapter(ctx, rt.serverOptions);
1293
1654
  const server = createServer(ctx, rt.port, { ...rt.serverOptions, owner: "serve" });
1294
1655
  const shownHost = rt.serverOptions.hostname ?? "localhost";
@@ -1321,7 +1682,7 @@ switch (cmd) {
1321
1682
  // three entry points (raw / OpenAI API / chat UI) differ only in how params
1322
1683
  // are POPULATED; here every param is explicit. Decode-path levers mirror
1323
1684
  // serve so you can pin the route (mlx-lm compat = --l1).
1324
- // --l1/--l2/--l3 + per-fork overrides set the decode env levers; the
1685
+ // --l1/--l2 + per-fork overrides set the decode env levers; the
1325
1686
  // returned KV scheme is applied below so `generate --l2` runs the same
1326
1687
  // quantized-KV route (and produces the same tokens) as `serve --l2`.
1327
1688
  const route = applyDecodeRoute();
@@ -1330,18 +1691,24 @@ switch (cmd) {
1330
1691
  if (!prompt) {
1331
1692
  console.error('usage: mlx-bun generate [query] --prompt "…" [--raw] [--max-tokens N]');
1332
1693
  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');
1694
+ console.error(' decode path: --l1 (= mlx-lm compat, the default) / --l2, or per-fork flags');
1334
1695
  process.exit(1);
1335
1696
  }
1336
1697
  const { loadTaskModel, generateText } = await import("./eval/runner");
1337
- const tm = await loadTaskModel(positional(0) ?? opt("query") ?? "");
1698
+ const maxTokens = Number(opt("max-tokens", "256"));
1699
+ const tm = await loadTaskModel(
1700
+ positional(0) ?? opt("query") ?? "",
1701
+ undefined,
1702
+ { maxGenerationTokens: maxTokens, enableMtp: false },
1703
+ );
1338
1704
  // Resolve the tier/--kv-quant KV scheme exactly the way serve does
1339
1705
  // (server.ts kvScheme): "config" = the model's kv_config.json (absent →
1340
1706
  // bf16, same silent fallback as serve), N = uniform bits from decode
1341
1707
  // start. Unset (no tier, no --kv-quant) keeps generate's historical
1342
1708
  // default: bf16 bit-exact greedy (generateText's parity path).
1343
1709
  const kvScheme =
1344
- route.kvQuant === "off" ? {}
1710
+ route.turboQuant ? { turboQuant: route.turboQuant, quantizedKvStart: 0 }
1711
+ : route.kvQuant === "off" ? {}
1345
1712
  : route.kvQuant === "config"
1346
1713
  ? (tm.config.kvQuant?.length ? { kvConfig: tm.config.kvQuant } : {})
1347
1714
  : typeof route.kvQuant === "number"
@@ -1349,7 +1716,7 @@ switch (cmd) {
1349
1716
  : undefined;
1350
1717
  const num = (n: string): number | undefined => { const v = opt(n); return v == null ? undefined : Number(v); };
1351
1718
  const text = await generateText(tm, prompt, {
1352
- maxTokens: Number(opt("max-tokens", "256")),
1719
+ maxTokens,
1353
1720
  useChat: !flag("raw"),
1354
1721
  ...(kvScheme !== undefined ? { kvScheme } : {}),
1355
1722
  sampler: { temperature: num("temperature") ?? num("temp"), topP: num("top-p"), topK: num("top-k"), seed: num("seed") },
@@ -1427,7 +1794,7 @@ switch (cmd) {
1427
1794
  case "benchmark": {
1428
1795
  const { banner, step, box, style } = await import("./tui");
1429
1796
  banner(pkg.version);
1430
- serverRuntimeFlags(); // env levers (--compiled-decode, --perf-kernel, ...) apply to the run
1797
+ serverRuntimeFlags(); // env levers (--compiled-decode, --fused-sdpa, ...) apply to the run
1431
1798
  const { m, picked } = await resolveModelAuto(opt("model") ?? positional(0) ?? opt("query"));
1432
1799
  const tokens = Number(opt("tokens", "256"));
1433
1800
  const runs = Number(opt("runs", "3"));
@@ -1462,7 +1829,16 @@ switch (cmd) {
1462
1829
  const rendered = template.render([{ role: "user", content: userMsg }]);
1463
1830
  const ids = tok.encode(rendered);
1464
1831
  const promptIds = ids[0] === ids[1] && ids[0] === tok.bosTokenId ? ids.slice(1) : ids;
1465
- const kvOptions = kvMode === "config"
1832
+ let turboScheme: TurboQuantScheme | null;
1833
+ try {
1834
+ turboScheme = parseTurboQuantScheme(kvMode);
1835
+ } catch (e) {
1836
+ console.error((e as Error).message);
1837
+ process.exit(1);
1838
+ }
1839
+ const kvOptions = turboScheme
1840
+ ? { turboQuant: turboScheme, quantizedKvStart: 0 }
1841
+ : kvMode === "config"
1466
1842
  ? (() => {
1467
1843
  if (!config.kvQuant?.length) { console.error("model has no kv_config.json (--kv-quant config)"); process.exit(1); }
1468
1844
  return { kvConfig: config.kvQuant };
@@ -1516,7 +1892,7 @@ switch (cmd) {
1516
1892
  `decode ${style.green(style.bold(`${median(decodes).toFixed(1)} tok/s`))} ${style.dim(`median of ${runs} · [${decodes.map((d) => d.toFixed(1)).join(", ")}]`)}`,
1517
1893
  `prefill ${style.bold(`${median(prefills).toFixed(0)} tok/s`)} ${style.dim(`· ${lastStats.promptTokens} prompt tokens`)}`,
1518
1894
  `peak mem ${style.bold(gb(peak))} ${style.dim("(generation only)")}`,
1519
- `levers ${style.dim(runtimeSummary({ kvQuant: kvMode === "off" ? "off" : kvMode === "config" ? undefined : Number(kvMode) }))}`,
1895
+ `levers ${style.dim(runtimeSummary(turboScheme ? { turboQuant: turboScheme } : { kvQuant: kvMode === "off" ? "off" : kvMode === "config" ? undefined : Number(kvMode) }))}`,
1520
1896
  "",
1521
1897
  style.dim("recorded to the eval DB — see mlx-bun evals"),
1522
1898
  ]);
@@ -1616,14 +1992,17 @@ switch (cmd) {
1616
1992
  // (src/pi-terminal.ts) — pi is bundled, nothing to install. Users who
1617
1993
  // already run their own pi connect it with `mlx-bun harness pi`.
1618
1994
  const OURS_VAL = new Set([
1619
- "--query", "-q", "--model", "--port", "--host", "--memory-budget", "--prompt-cache",
1620
- "--ssd-cache", "--ssd-cache-max", "--kv-quant",
1995
+ "--query", "-q", "--model", "--port", "--host", "--memory-budget", "--kv-budget", "--prompt-cache",
1996
+ "--ssd-cache", "--ssd-cache-max", "--ssd-demote-idle", "--kv-quant", "--unix",
1621
1997
  "--batch", "--decode-concurrency", "--adapter", "--adapter-path",
1622
- "--compiled-decode", "--perf-kernel", "--fused-decode", "--fused-sdpa", "--thinking",
1998
+ "--draft-model", "--num-draft-tokens", "--draft-kind", "--ngram-max", "--ngram-min",
1999
+ "--compiled-decode", "--compiled-activations", "--fused-sdpa", "--thinking",
1623
2000
  "--temperature", "--temp", "--top-p", "--top-k", "--max-tokens",
2001
+ "--mtp", "--context-length",
1624
2002
  "--hlg-sampling", "--hlg-width", "--hlg-shoulder", "--hlg-toe", "--hlg-pivot-offset",
2003
+ "--paged-kv-block-size",
1625
2004
  ]);
1626
- const OURS_BOOL = new Set(["--force-wire", "--expert-offload", "--no-open", "--ssd-cache-verify", "--l1", "--l2", "--l3"]);
2005
+ const OURS_BOOL = new Set(["--force-wire", "--expert-offload", "--no-open", "--ssd-cache-verify", "--l1", "--l2", "--l3", "--isolate", "--paged-kv"]);
1627
2006
  const passthrough: string[] = [];
1628
2007
  for (let i = 1; i < argv.length; i++) {
1629
2008
  const a = argv[i]!;
@@ -1685,7 +2064,10 @@ switch (cmd) {
1685
2064
  const { createServer, loadContext } = await import("./server");
1686
2065
  const sLoad = step(`loading ${m.repoId}`);
1687
2066
  const t0 = performance.now();
1688
- const ctx = await loadContext(m.path, m.repoId, { memoryBudgetBytes: rt.serverOptions.memoryBudgetBytes });
2067
+ const ctx = await loadContext(m.path, m.repoId, {
2068
+ memoryBudgetBytes: rt.serverOptions.memoryBudgetBytes,
2069
+ glm: glm52RuntimeFlags(rt.serverOptions),
2070
+ });
1689
2071
  await mountStartupAdapter(ctx, rt.serverOptions);
1690
2072
  createServer(ctx, port, { ...rt.serverOptions, owner: "pi-session" });
1691
2073
  startedServer = true;
@@ -1813,14 +2195,7 @@ switch (cmd) {
1813
2195
 
1814
2196
  const { m, picked } = await resolveModelAuto(query);
1815
2197
 
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
2198
  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
2199
 
1825
2200
  const modelTag = isGemma ? "e4b" : "cpm5";
1826
2201
  const adapter = opt("adapter") ?? `${process.env.HOME}/.cache/mlx-bun/mlx-bun-finetunes/${method}-${modelTag}`;
@@ -1874,7 +2249,7 @@ switch (cmd) {
1874
2249
  if (!ds.ok) { console.error(`dataset: ${ds.error}`); process.exit(1); }
1875
2250
 
1876
2251
  const planLines = [
1877
- `${style.green("●")} ${style.bold(`train ${method}`)} ${style.dim(`· ${m.repoId}${picked ? " (auto-picked)" : ""}${isGemma ? " · e4b env set" : ""}`)}`,
2252
+ `${style.green("●")} ${style.bold(`train ${method}`)} ${style.dim(`· ${m.repoId}${picked ? " (auto-picked)" : ""}${isGemma ? " · e4b defaults" : ""}`)}`,
1878
2253
  "",
1879
2254
  `data ${style.bold(`${ds.n_train} train`)}${ds.n_valid ? ` · ${ds.n_valid} valid` : ""} ${style.dim(`· format ${ds.format}`)}`,
1880
2255
  `loop ${style.dim(`iters ${iters} · lr ${cfg.learning_rate} · rank ${cfg.rank} · scale ${cfg.scale} · seq ${seq} · batch ${cfg.batch_size}`)}`,
@@ -2258,11 +2633,6 @@ switch (cmd) {
2258
2633
  if (reg.list().length === 0) await reg.scan();
2259
2634
  modelDir = reg.resolve(modelArg).path;
2260
2635
  }
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
2636
  const sLoad = step(`loading ${modelDir}`);
2267
2637
  const { Weights } = await import("./weights");
2268
2638
  const { createModel } = await import("./model/factory");