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