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