mlx-bun 0.0.8 → 0.0.10
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 +112 -20
- package/docs/design/batching-perf-path.md +232 -0
- package/docs/design/batching-v2-plan.md +515 -0
- package/docs/design/bucketing-stage.md +196 -0
- package/docs/design/compat-cli-surface-design.md +11 -11
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dreaming-nightly-pipeline.md +291 -0
- package/docs/design/dspark-speculative-decoding.md +106 -0
- package/docs/design/generic-model-support.md +381 -0
- package/docs/design/memory-inference-path.md +422 -0
- package/docs/design/memory-synthesis.md +287 -0
- package/docs/design/minicpm5-decode-megakernel.md +484 -0
- package/docs/design/mlx-lm-tool-parity-plan.md +535 -0
- package/docs/design/omlx-adoption-map.md +90 -0
- package/docs/design/orpo-dynamic-lambda.md +41 -0
- package/docs/design/orpo-training.md +35 -5
- package/docs/design/parallel-slots.md +12 -1
- package/docs/design/parity-tier-dag.md +13 -5
- package/docs/design/segmented-backward-training.md +15 -0
- package/docs/design/spec-decode-larger-targets.md +229 -0
- package/docs/design/ssd-kv-cold-tier.md +158 -0
- package/docs/design/tauri-desktop-app.md +2 -2
- package/docs/design/the-dreaming-handoff.md +141 -0
- package/docs/design/the-dreaming-master-plan.md +446 -0
- package/docs/design/write-pipeline-entity.md +468 -0
- package/docs/investigations/curve-bisector-routing.md +1 -1
- package/docs/investigations/decode-roofline-lookagain.md +348 -0
- package/docs/investigations/dspark-handoff.md +58 -0
- package/docs/investigations/kernel-perf-review-2026-07.md +171 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -2
- package/docs/planning/curve-sampler-research-plan.md +331 -0
- package/docs/planning/memory-docs-and-dag-plan.md +185 -0
- package/docs/planning/pi-integration-review.md +178 -0
- package/docs/planning/release-notes-v0.0.10.md +57 -0
- package/docs/planning/release-notes-v0.0.9.md +122 -0
- package/docs/planning/web-ui-pass-plan.md +213 -0
- package/docs/planning/website-readme-pass-plan.md +224 -0
- package/docs/reference/cli.md +255 -0
- package/docs/reference/embedding.md +7 -2
- package/docs/reference/library-api.md +22 -3
- package/docs/reference/memory.md +5 -5
- package/docs/reference/models.md +137 -0
- package/docs/reference/orpo-quickstart.md +6 -0
- package/docs/reference/server-api.md +187 -9
- package/docs/reference/server-config.md +43 -9
- package/docs/reference/training.md +31 -12
- package/package.json +1 -1
- package/src/anthropic.ts +22 -0
- package/src/assets/curve-designer.html +1 -1
- package/src/chat-template.ts +25 -7
- package/src/cli.ts +1077 -90
- package/src/config.ts +4 -1
- package/src/download.ts +99 -8
- package/src/embed.ts +77 -0
- package/src/eval/perplexity.ts +164 -0
- package/src/eval/runner.ts +15 -6
- package/src/eval/tasks/bfcl.ts +1 -1
- package/src/generate.ts +149 -18
- package/src/harness-pi.ts +43 -17
- package/src/index.ts +2 -0
- package/src/kv-store.ts +240 -98
- package/src/lora.ts +2 -2
- package/src/memory/article.ts +558 -0
- package/src/memory/chunk-validation.ts +150 -0
- package/src/memory/chunk.ts +305 -12
- package/src/memory/cluster.ts +344 -17
- package/src/memory/coverage.ts +134 -0
- package/src/memory/crosslink.ts +0 -0
- package/src/memory/db.ts +184 -45
- package/src/memory/entity.ts +203 -0
- package/src/memory/gate.ts +187 -0
- package/src/memory/history.ts +152 -0
- package/src/memory/ingest.ts +324 -11
- package/src/memory/model.ts +321 -0
- package/src/memory/normalize.ts +671 -0
- package/src/memory/parse.ts +22 -0
- package/src/memory/pipeline.ts +242 -30
- package/src/memory/prompts.ts +42 -0
- package/src/memory/query.ts +334 -0
- package/src/memory/reconcile.ts +734 -0
- package/src/memory/redirect.ts +108 -0
- package/src/memory/reindex.ts +325 -0
- package/src/memory/resolve.ts +307 -0
- package/src/memory/route.ts +272 -0
- package/src/memory/schedule.ts +6 -4
- package/src/memory/stages.ts +774 -0
- package/src/memory/synthesize.ts +1629 -20
- package/src/memory/tools.ts +446 -96
- package/src/memory/vault.ts +462 -35
- package/src/memory/wikify.ts +1057 -16
- package/src/mlx/array.ts +21 -0
- package/src/mlx/ffi.ts +3 -0
- package/src/mlx/ops.ts +72 -0
- package/src/model/compiled-decode.ts +43 -8
- package/src/model/factory.ts +29 -8
- package/src/model/flash-attention.ts +4 -2
- package/src/model/fused-decode-kernel.ts +14 -7
- package/src/model/fused-mlp-kernel.ts +169 -0
- package/src/model/fused-swiglu-kernel.ts +240 -0
- package/src/model/gemma4-base.ts +96 -7
- package/src/model/gemma4.ts +33 -2
- package/src/model/minicpm5.ts +106 -8
- package/src/model/qwen3-delta.ts +48 -1
- package/src/model/qwen3.ts +276 -0
- package/src/model/qwen3_5.ts +11 -2
- package/src/model/steel-linear-kernel.ts +96 -0
- package/src/model/support.ts +38 -8
- package/src/model/universal/archs.ts +332 -0
- package/src/model/universal/dense.ts +576 -0
- package/src/model/universal/modules.ts +163 -0
- package/src/model/universal/rope.ts +366 -0
- package/src/pi-provider.ts +1 -1
- package/src/pi-terminal.ts +29 -8
- package/src/pi-web.ts +19 -2
- package/src/prompt-cache.ts +55 -9
- package/src/quantize/config-writer.ts +1 -1
- package/src/registry.ts +219 -8
- package/src/responses.ts +23 -0
- package/src/sampler.ts +220 -9
- package/src/serve/batch-scheduler.ts +307 -83
- package/src/serve/generation-gateway.ts +156 -29
- package/src/server.ts +746 -73
- package/src/spec/drafter.ts +81 -49
- package/src/spec/dspark/data-dflash.ts +156 -0
- package/src/spec/dspark/data.ts +201 -0
- package/src/spec/dspark/generate-dflash.ts +193 -0
- package/src/spec/dspark/generate.ts +290 -0
- package/src/spec/dspark/loss.ts +213 -0
- package/src/spec/dspark/module-dflash.ts +361 -0
- package/src/spec/dspark/module.ts +540 -0
- package/src/spec/dspark/sample.ts +112 -0
- package/src/spec/generate.ts +40 -19
- package/src/ssd-cache.ts +213 -0
- package/src/status-page.html +1 -1
- package/src/tokenizer.ts +7 -0
- package/src/train/flash-cce.ts +81 -23
- package/src/train/fuse.ts +243 -0
- package/src/train/index.ts +3 -0
- package/src/train/job.ts +8 -0
- package/src/train/loss.ts +341 -40
- package/src/train/prefix-shared.ts +62 -11
- package/src/train/segmented.ts +215 -55
- package/src/train/trainer.ts +112 -38
- package/src/web/skills/memory/SKILL.md +40 -17
package/src/cli.ts
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
// mlx-bun get <org/repo> [--revision main] resumable verified download
|
|
5
5
|
// mlx-bun scan index the HF cache
|
|
6
6
|
// mlx-bun ls [--vision] [--max-size 10GB] [query]
|
|
7
|
-
// mlx-bun fit <query> [--ctx
|
|
8
|
-
// mlx-bun serve [query] [--port
|
|
7
|
+
// mlx-bun fit <query> [--ctx 8192] [--skus]
|
|
8
|
+
// mlx-bun serve [query] [--port 8080] [--memory-budget GB]
|
|
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
15
|
import { loadModelConfig } from "./config";
|
|
14
16
|
import { fit, skuMatrix, thisMachine } from "./fit";
|
|
@@ -37,12 +39,20 @@ Commands:
|
|
|
37
39
|
serve Start the OpenAI/Anthropic-compatible server + status page
|
|
38
40
|
get Download a model from Hugging Face (resumable, verified)
|
|
39
41
|
ls List downloaded models
|
|
42
|
+
gc Reclaim superseded model snapshots + dead blobs from the HF cache
|
|
40
43
|
fit Will a model fit this machine? Memory + speed assessment
|
|
41
44
|
scan Re-index the Hugging Face cache
|
|
42
45
|
harness Connect your own pi install to the local mlx-bun server
|
|
43
46
|
train Fine-tune a LoRA adapter (SFT / DPO / ORPO) on your data
|
|
47
|
+
train-watch Live dashboard for a training run (tails metrics.jsonl)
|
|
48
|
+
fuse Merge a LoRA adapter into the base weights (standalone model)
|
|
49
|
+
convert Quantize an HF model into a local MLX snapshot
|
|
50
|
+
upload Push a local model directory to the Hugging Face Hub
|
|
51
|
+
perplexity Perplexity of a model over a local text/JSONL dataset
|
|
44
52
|
memory Set up + manage your local AI's personal wiki memory
|
|
53
|
+
generate One-shot raw text generation from a local model (no server)
|
|
45
54
|
benchmark Measure decode/prefill speed of OUR stack on this machine
|
|
55
|
+
embed Text embeddings from a local embedding model (Qwen3-Embedding)
|
|
46
56
|
evals Show recent benchmark runs (all stacks)
|
|
47
57
|
help Show help for a command (also: mlx-bun <command> --help)
|
|
48
58
|
|
|
@@ -53,27 +63,48 @@ Options:
|
|
|
53
63
|
Examples:
|
|
54
64
|
mlx-bun # download (first run) + serve + open the chat UI
|
|
55
65
|
mlx-bun pi # start (download if needed) and chat with an agent
|
|
56
|
-
mlx-bun serve 12B # serve the 12B; status page at http://localhost:
|
|
66
|
+
mlx-bun serve 12B # serve the 12B; status page at http://localhost:8080/
|
|
57
67
|
mlx-bun train e4b --data ./prefs # ORPO LoRA fine-tune on {prompt,chosen,rejected} data
|
|
58
68
|
mlx-bun get mlx-community/gemma-4-12B-it-OptiQ-4bit`;
|
|
59
69
|
|
|
60
70
|
const SERVER_FLAGS = `Server options:
|
|
61
|
-
--
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
--model <path|query> Explicit model override (mlx_lm.server spelling):
|
|
72
|
+
a directory containing config.json loads from that
|
|
73
|
+
path; anything else resolves like the positional
|
|
74
|
+
query. Wins over the positional and auto-pick.
|
|
75
|
+
--host <addr> Interface to bind [default: 127.0.0.1, loopback
|
|
76
|
+
only; use 0.0.0.0 to expose on your network]
|
|
77
|
+
--port <n> Listen port [default: 8080]
|
|
64
78
|
--memory-budget <GB> Admission-control memory budget; requests that
|
|
65
79
|
cannot fit are rejected instead of crashing the
|
|
66
80
|
GPU [default: machine RAM × 0.75, check-only]
|
|
67
|
-
--prompt-cache <GB> Prompt (KV) cache byte cap [default: 2 GB
|
|
81
|
+
--prompt-cache <GB> Prompt (KV) cache byte cap [default: 2 GB;
|
|
82
|
+
--prompt-cache 0 disables the cache]
|
|
83
|
+
--ssd-cache <dir> SSD cold tier for the prompt cache: prefix KV
|
|
84
|
+
spills to disk on eviction and persists across
|
|
85
|
+
restarts (long-context agent TTFT drops from a
|
|
86
|
+
full re-prefill to a zero-copy mmap restore).
|
|
87
|
+
Off unless set.
|
|
88
|
+
--ssd-cache-max <GB> SSD tier byte cap [default: 32 GB]
|
|
89
|
+
--ssd-cache-verify Verify tensor hashes on every restore (reads all
|
|
90
|
+
bytes eagerly — integrity paranoia only)
|
|
68
91
|
--batch <n> Max concurrent requests batched through the
|
|
69
92
|
mlx-lm-parity engine [default: 1 = serial].
|
|
70
93
|
>1 opts the whole server into bf16 continuous
|
|
71
94
|
batching (= mlx-lm B=N); see --kv-quant.
|
|
95
|
+
--decode-concurrency is accepted as the
|
|
96
|
+
mlx_lm.server alias (semantics differ; see
|
|
97
|
+
docs/reference/server-config.md)
|
|
72
98
|
--no-open Don't open the chat UI in your browser on start.
|
|
73
99
|
By default an interactive terminal opens
|
|
74
100
|
http://<host>:<port>/#/chat once the server is up.
|
|
75
101
|
|
|
76
102
|
Model & quality:
|
|
103
|
+
--adapter <dir> Mount a LoRA adapter at startup and use it as the
|
|
104
|
+
default for requests that don't select one (a
|
|
105
|
+
request's \`adapter\` field, incl. "none", wins).
|
|
106
|
+
--adapter-path is accepted as the mlx_lm.server
|
|
107
|
+
alias. Hot-swap via POST /v1/adapters unchanged.
|
|
77
108
|
--kv-quant <mode> KV cache quantization: config (per-layer
|
|
78
109
|
kv_config.json when the model ships one), off
|
|
79
110
|
(bf16), or 4 / 8 (uniform bits)
|
|
@@ -86,8 +117,14 @@ Model & quality:
|
|
|
86
117
|
[default: the model's own, false for CPM]
|
|
87
118
|
--temperature <n> Server-wide sampling defaults; a per-request field
|
|
88
119
|
--top-p <n> still overrides them, and the browser chat (which
|
|
89
|
-
--top-k <n> sends none) inherits them
|
|
120
|
+
--top-k <n> sends none) inherits them. --temp is accepted as
|
|
121
|
+
an alias for --temperature (mlx_lm.server compat;
|
|
122
|
+
NOTE mlx_lm.server defaults temperature to 0.0 —
|
|
123
|
+
pass --temp 0 for its unset-request behavior)
|
|
90
124
|
[default: the model's generation_config.json]
|
|
125
|
+
--max-tokens <n> Completion cap when a request omits max_tokens
|
|
126
|
+
(mlx_lm.server flag; its default is 512 there)
|
|
127
|
+
[default: 65536 chat / 512 raw completion]
|
|
91
128
|
--hlg-sampling on|off Piecewise tone-curve sampling (HLG): rolls off the
|
|
92
129
|
top, boosts the mids, gentles the tail. Gain folds
|
|
93
130
|
from --temperature. [default: off]
|
|
@@ -97,10 +134,21 @@ Model & quality:
|
|
|
97
134
|
--hlg-toe <nats> HLG shadow rolloff scale [default: 6]
|
|
98
135
|
--hlg-pivot-offset <nats> HLG pivot: nats below the top token [default: 6]
|
|
99
136
|
|
|
137
|
+
Parity tier (sets the whole decode route; a per-fork flag below overrides):
|
|
138
|
+
--l1 Bit-for-bit IDENTICAL to mlx-lm (bf16 KV, unfused)
|
|
139
|
+
--l2 Bit-for-bit IDENTICAL to mlx-optiq (quantized KV +
|
|
140
|
+
fused prefill SDPA, stock unfused decode — the
|
|
141
|
+
optiq-golden composition; perf-kernel stays OFF)
|
|
142
|
+
--l3 Best performance, no bit-exact oracle (KL + test
|
|
143
|
+
gated; adds the envelope-gated perf kernel).
|
|
144
|
+
No tier given = the per-flag defaults below,
|
|
145
|
+
which equal --l3 today.
|
|
146
|
+
|
|
100
147
|
Performance levers (A/B levers; defaults are the measured winners):
|
|
101
148
|
--compiled-decode on|off Compiled decode graphs [default: on]
|
|
102
|
-
--perf-kernel on|off Fused quantized-KV decode-SDPA Metal kernel
|
|
103
|
-
|
|
149
|
+
--perf-kernel on|off Fused quantized-KV decode-SDPA Metal kernel —
|
|
150
|
+
NOT bit-exact, envelope-gated (an L3 lever; the
|
|
151
|
+
perf side of the compat A/B) [default: on]
|
|
104
152
|
--fused-decode on|off Fused-decode experiment lever [default: off]
|
|
105
153
|
--fused-sdpa on|off Fused SDPA path [default: on]
|
|
106
154
|
--force-wire Wire weights into memory at load
|
|
@@ -141,42 +189,73 @@ Already use pi? Connect your own pi to this local model instead:
|
|
|
141
189
|
Usage: mlx-bun serve [query] [options]
|
|
142
190
|
|
|
143
191
|
[query] Registry query (e.g. "12B", "e4b", a repo substring).
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
192
|
+
--query <q> is accepted for the positional. Omitted:
|
|
193
|
+
serves e4b (the default), else the largest downloaded
|
|
194
|
+
model that fits. Fresh install: downloads a small
|
|
195
|
+
starter model first, then e4b in the background
|
|
147
196
|
(default on next run).
|
|
148
197
|
|
|
149
198
|
${SERVER_FLAGS}
|
|
150
199
|
|
|
151
|
-
Endpoints: /v1/chat/completions, /v1/
|
|
152
|
-
/v1/adapters, /stats, /fit, /library, /downloads —
|
|
153
|
-
browser chat at /chat`,
|
|
200
|
+
Endpoints: /v1/chat/completions, /v1/completions, /v1/messages, /v1/responses,
|
|
201
|
+
/v1/models, /v1/adapters, /health, /stats, /fit, /library, /downloads —
|
|
202
|
+
status page at /, browser chat at /chat`,
|
|
154
203
|
|
|
155
204
|
get: `mlx-bun get — download a model from Hugging Face
|
|
156
205
|
|
|
157
|
-
Usage: mlx-bun get <org/repo> [options]
|
|
206
|
+
Usage: mlx-bun get <org/repo | substring> [options]
|
|
207
|
+
|
|
208
|
+
<org/repo> Full repo id (e.g. mlx-community/gemma-4-12B-it-OptiQ-4bit)
|
|
209
|
+
<substring> No "/" = a registry query over already-downloaded repos
|
|
210
|
+
(e.g. \`mlx-bun get 12B\` re-gets/refreshes the match)
|
|
158
211
|
|
|
159
212
|
Options:
|
|
160
213
|
--revision <rev> Git revision [default: main]
|
|
161
214
|
|
|
162
215
|
Resumable (Range requests against partial blobs) and verified (sha256
|
|
163
216
|
for LFS blobs). Re-running after an interruption continues where it
|
|
164
|
-
stopped. Uses HF_TOKEN / hf auth login credentials when present
|
|
217
|
+
stopped. Uses HF_TOKEN / hf auth login credentials when present.
|
|
218
|
+
When upstream pushed a new revision, the previous snapshot stays on
|
|
219
|
+
disk — \`mlx-bun gc\` reclaims it.`,
|
|
165
220
|
|
|
166
221
|
ls: `mlx-bun ls — list downloaded models
|
|
167
222
|
|
|
168
223
|
Usage: mlx-bun ls [query] [options]
|
|
169
224
|
|
|
225
|
+
One row per repo (the canonical revision: refs/main). The HF cache keeps
|
|
226
|
+
a snapshot dir per downloaded revision — superseded ones are hidden here
|
|
227
|
+
and reclaimed by \`mlx-bun gc\`.
|
|
228
|
+
|
|
229
|
+
Options:
|
|
230
|
+
--vision Only vision-capable models (SigLIP sidecar or the
|
|
231
|
+
unified encoder-free tower)
|
|
232
|
+
--max-size <size> Filter by weight size (e.g. 10GB, 800MB)
|
|
233
|
+
--all-revisions One row per cached snapshot (canonical marked *)`,
|
|
234
|
+
|
|
235
|
+
gc: `mlx-bun gc — reclaim superseded snapshots + dead blobs
|
|
236
|
+
|
|
237
|
+
Usage: mlx-bun gc [options]
|
|
238
|
+
|
|
239
|
+
Every \`get\` that follows an upstream push creates a NEW
|
|
240
|
+
snapshots/<commit> dir; the old one (and any blobs only it references)
|
|
241
|
+
stays on disk forever. gc keeps the snapshots refs/* point at, deletes
|
|
242
|
+
the rest, then deletes blobs no surviving snapshot links to.
|
|
243
|
+
|
|
244
|
+
Without --yes it only prints what would be deleted (per-repo reclaim).
|
|
245
|
+
|
|
170
246
|
Options:
|
|
171
|
-
--
|
|
172
|
-
--
|
|
247
|
+
--yes Actually delete (destructive; default prints only)
|
|
248
|
+
--dry-run Print the plan and never delete, even with --yes
|
|
249
|
+
--force Also prune superseded snapshots that contain files
|
|
250
|
+
the canonical revision lacks (normally skipped with
|
|
251
|
+
a warning — deleting them loses the only copy)`,
|
|
173
252
|
|
|
174
253
|
fit: `mlx-bun fit — will this model fit on this machine?
|
|
175
254
|
|
|
176
255
|
Usage: mlx-bun fit <query> [options]
|
|
177
256
|
|
|
178
257
|
Options:
|
|
179
|
-
--ctx <tokens> Context size to assess [default:
|
|
258
|
+
--ctx <tokens> Context size to assess [default: 8192]
|
|
180
259
|
--skus Also print the Apple silicon SKU matrix
|
|
181
260
|
|
|
182
261
|
Solves weights + KV growth + prefill transient against wired memory and
|
|
@@ -199,7 +278,7 @@ local model. (To just chat now with no setup, use \`mlx-bun pi\` — the
|
|
|
199
278
|
built-in agent.)
|
|
200
279
|
|
|
201
280
|
Options:
|
|
202
|
-
--base-url <url> Server base URL [default: http://
|
|
281
|
+
--base-url <url> Server base URL [default: http://127.0.0.1:8080/v1]
|
|
203
282
|
--remove Disconnect (delete the extension)
|
|
204
283
|
|
|
205
284
|
Installs a small discovery extension into ~/.pi/agent/extensions that
|
|
@@ -216,9 +295,13 @@ result to the eval DB. For quotable, cross-stack numbers use
|
|
|
216
295
|
./benchmark.sh from the repo — it preflight-gates on an idle machine
|
|
217
296
|
and runs the mlx-lm/optiq comparison legs.
|
|
218
297
|
|
|
219
|
-
[query] Model to benchmark
|
|
298
|
+
[query] Model to benchmark (--query <q> is accepted for the
|
|
299
|
+
positional) [default: auto-pick]
|
|
220
300
|
|
|
221
301
|
Options:
|
|
302
|
+
--model <path|query> Explicit model override: a directory containing
|
|
303
|
+
config.json loads from that path; anything else
|
|
304
|
+
resolves like the positional query. Wins over both.
|
|
222
305
|
--tokens <n> Tokens to decode per run [default: 256]
|
|
223
306
|
--runs <n> Runs (median reported) [default: 3]
|
|
224
307
|
--prompt-tokens <n> Pad the prompt to ~n tokens (long-context decode)
|
|
@@ -228,6 +311,61 @@ Options:
|
|
|
228
311
|
Performance levers (--compiled-decode, --perf-kernel, --fused-decode,
|
|
229
312
|
--fused-sdpa) apply to the run — A/B by running twice.`,
|
|
230
313
|
|
|
314
|
+
embed: `mlx-bun embed — text embeddings from a local embedding model
|
|
315
|
+
|
|
316
|
+
Usage: mlx-bun embed [query] --text "…" [options]
|
|
317
|
+
mlx-bun embed [query] "…" (text as a positional)
|
|
318
|
+
echo -e "line one\\nline two" | mlx-bun embed [query]
|
|
319
|
+
|
|
320
|
+
Embeds text with a local Qwen3-Embedding model (last-token pooled,
|
|
321
|
+
L2-normalized — bit-exact vs mlx-lm). One text per --text, or one per
|
|
322
|
+
line on stdin. Prints one JSON array (the vector) per input line.
|
|
323
|
+
|
|
324
|
+
[query] Embedding model (--query <q> is accepted for the
|
|
325
|
+
positional) [default: the first downloaded
|
|
326
|
+
embedding model; errors with an mlx-bun get
|
|
327
|
+
suggestion when none is downloaded]
|
|
328
|
+
|
|
329
|
+
Options:
|
|
330
|
+
--text "…" Text to embed (omit to read lines from stdin)
|
|
331
|
+
--instruct "…" Query instruction (Qwen3-Embedding query format);
|
|
332
|
+
steers WHICH similarity axis the geometry reflects.
|
|
333
|
+
Omit for plain document embeddings.
|
|
334
|
+
--json Emit one OpenAI-style {object:"list",data:[…]} object
|
|
335
|
+
instead of bare vector arrays
|
|
336
|
+
|
|
337
|
+
For a server endpoint use \`mlx-bun serve <embedding-model>\` then
|
|
338
|
+
POST /v1/embeddings.`,
|
|
339
|
+
|
|
340
|
+
generate: `mlx-bun generate — one-shot raw text generation (no server)
|
|
341
|
+
|
|
342
|
+
Usage: mlx-bun generate [query] --prompt "…" [options]
|
|
343
|
+
mlx-bun generate [query] "…" (prompt as a positional)
|
|
344
|
+
|
|
345
|
+
A direct one-shot entry point: load the model, generate, print, exit —
|
|
346
|
+
no server, no chat UI. By default the prompt is rendered through the
|
|
347
|
+
model's chat template; --raw feeds it verbatim.
|
|
348
|
+
|
|
349
|
+
[query] Model (registry query; --query is accepted for the
|
|
350
|
+
positional) [default: auto-pick]
|
|
351
|
+
|
|
352
|
+
Options:
|
|
353
|
+
--prompt "…" The prompt (or pass it as the positional)
|
|
354
|
+
--raw Skip the chat template (verbatim prompt)
|
|
355
|
+
--max-tokens <n> Completion cap [default: 256]
|
|
356
|
+
--temperature <n> Sampling temperature (--temp is accepted as an alias)
|
|
357
|
+
--top-p <n> Nucleus sampling
|
|
358
|
+
--top-k <n> Top-k sampling
|
|
359
|
+
--seed <n> Sampler seed (reproducible runs)
|
|
360
|
+
|
|
361
|
+
Decode-path levers mirror serve: --l1/--l2/--l3 and the per-fork flags
|
|
362
|
+
(--perf-kernel/--fused-sdpa/--compiled-decode/--fused-decode/--kv-quant).
|
|
363
|
+
The tier's KV scheme applies here too (--l2/--l3 = the model's kv_config,
|
|
364
|
+
same tokens as serve); with no tier/--kv-quant the KV cache stays bf16
|
|
365
|
+
(the bit-exact mlx-lm greedy path). mlx-lm compat = --l1.
|
|
366
|
+
|
|
367
|
+
Aliases: mlx-bun gen`,
|
|
368
|
+
|
|
231
369
|
evals: `mlx-bun evals — recent benchmark runs
|
|
232
370
|
|
|
233
371
|
Usage: mlx-bun evals [options]
|
|
@@ -261,7 +399,15 @@ Options:
|
|
|
261
399
|
--scale <f> LoRA scale [default: orpo 2.0 · else 1.0]
|
|
262
400
|
--seq <n> Max sequence length [default: gemma 8192 · else 4096]
|
|
263
401
|
--batch <n> Batch size [default: 1]
|
|
402
|
+
--grad-accum <n> Gradient accumulation steps (effective batch =
|
|
403
|
+
batch × grad-accum at batch-size-1 memory) [default: 1]
|
|
404
|
+
--grad-clip <f> Gradient-norm clip (0 = off) [default: 1.0]
|
|
405
|
+
--seed <n> Data-shuffle / init seed [default: 0]
|
|
406
|
+
--val-size <n> Max validation examples per eval [default: 256]
|
|
264
407
|
--lambda <f> ORPO odds-ratio weight [default: 0.1]
|
|
408
|
+
--sft-scope <s> ORPO chosen-NLL scope: full (paper/TRL-faithful,
|
|
409
|
+
prompt+response) | response (pre-2026-07 runs,
|
|
410
|
+
bit-exact) [default: full]
|
|
265
411
|
--seg <n> Layers per segment (segmented backward; orpo default 2)
|
|
266
412
|
--save-every <n> Crash-safe mountable checkpoint every n steps
|
|
267
413
|
--resume <dir> Warm-start LoRA weights from a checkpoint/adapter dir
|
|
@@ -274,7 +420,125 @@ ORPO defaults run the full stack: the [M,vocab]-free flash-CCE head +
|
|
|
274
420
|
prefix-sharing + segmented backward (each falls back + logs if a row's
|
|
275
421
|
preconditions aren't met). Gemma/e4b sets its required training env flags
|
|
276
422
|
automatically. Adapters are mountable directly (mlx-bun serve --adapter).
|
|
277
|
-
Run long jobs detached from your own shell: nohup mlx-bun train …
|
|
423
|
+
Run long jobs detached from your own shell: nohup mlx-bun train … &
|
|
424
|
+
Watch a run live from another tab: mlx-bun train-watch <adapter-dir>`,
|
|
425
|
+
|
|
426
|
+
"train-watch": `mlx-bun train-watch — live dashboard for a training run
|
|
427
|
+
|
|
428
|
+
Usage: mlx-bun train-watch [adapter-dir]
|
|
429
|
+
|
|
430
|
+
Tails <adapter-dir>/metrics.jsonl (written by mlx-bun train and the web
|
|
431
|
+
fine-tune tile) and renders a live loss/speed/memory dashboard. Point it
|
|
432
|
+
at the same --adapter directory the training run uses.
|
|
433
|
+
|
|
434
|
+
[adapter-dir] Adapter directory to watch; --adapter <dir> is
|
|
435
|
+
accepted for the positional
|
|
436
|
+
[default: ~/.cache/mlx-bun/mlx-bun-finetunes/orpo-cpm5]`,
|
|
437
|
+
fuse: `mlx-bun fuse — merge a LoRA adapter into the base weights
|
|
438
|
+
|
|
439
|
+
Usage: mlx-bun fuse <model-query-or-path> --adapter <dir> [options]
|
|
440
|
+
|
|
441
|
+
Folds the adapter's low-rank deltas into the base weights and writes a
|
|
442
|
+
standalone, loadable model snapshot (weights + config + tokenizer). A
|
|
443
|
+
quantized base keeps its exact quantization layout: fused modules are
|
|
444
|
+
dequantized, updated, and re-quantized with their own per-module spec.
|
|
445
|
+
|
|
446
|
+
<model> Base model (registry query or a snapshot path);
|
|
447
|
+
--model is accepted as the mlx_lm.fuse spelling
|
|
448
|
+
|
|
449
|
+
Options:
|
|
450
|
+
--adapter <dir> Adapter directory (adapters.safetensors +
|
|
451
|
+
adapter_config.json). --adapter-path is accepted
|
|
452
|
+
as the mlx_lm.fuse alias [default: adapters]
|
|
453
|
+
--save-path <dir> Output model directory [default: fused_model]
|
|
454
|
+
|
|
455
|
+
Not supported (mlx_lm.fuse flags we don't implement — the command exits
|
|
456
|
+
with an error rather than guessing): --de-quantize, --export-gguf,
|
|
457
|
+
--gguf-path, --upload-repo.
|
|
458
|
+
|
|
459
|
+
Serve the result: mlx-bun serve <save-path>`,
|
|
460
|
+
|
|
461
|
+
convert: `mlx-bun convert — quantize an HF model into a local MLX snapshot
|
|
462
|
+
|
|
463
|
+
Usage: mlx-bun convert --hf-path <repo-or-path> -q [options]
|
|
464
|
+
mlx-bun convert <repo-or-path> -q [options] (positional source)
|
|
465
|
+
|
|
466
|
+
Wraps mlx-bun's native quantize pipeline (the same engine as the web
|
|
467
|
+
/api/quantize). The source may be a local model directory, a downloaded
|
|
468
|
+
model (registry query), or a Hugging Face repo id — an un-downloaded
|
|
469
|
+
repo is fetched first (resumable, verified; can be many GB).
|
|
470
|
+
|
|
471
|
+
Options:
|
|
472
|
+
--hf-path <src> Source model: local path, downloaded model, or HF
|
|
473
|
+
repo id (--model and the bare positional are
|
|
474
|
+
accepted as aliases)
|
|
475
|
+
--mlx-path <dir> Output directory [default: mlx_model; must not
|
|
476
|
+
already exist, matching mlx_lm.convert]
|
|
477
|
+
-q, --quantize Quantize the model (uniform affine)
|
|
478
|
+
--q-bits <n> Bits per weight: 4 or 8 [default: 4]
|
|
479
|
+
--q-group-size <n> Quantization group size: 32 or 64 [default: 64]
|
|
480
|
+
--upload-repo <id> Push the converted model to this Hugging Face repo
|
|
481
|
+
after converting (mlx_lm.convert parity; same engine
|
|
482
|
+
as \`mlx-bun upload\`, needs a write token)
|
|
483
|
+
|
|
484
|
+
Mixed precision (the mlx-bun differentiator — OptiQ sensitivity sweep +
|
|
485
|
+
knapsack per-layer bit allocation; implies quantization, no -q needed):
|
|
486
|
+
--target-bpw <f> Target bits-per-weight (e.g. 4.5). Runs
|
|
487
|
+
calibration → per-layer KL sensitivity → greedy
|
|
488
|
+
knapsack; writes per-module bits into config.json
|
|
489
|
+
--candidate-bits <l> Comma list the knapsack may pick from [default: 4,8]
|
|
490
|
+
--calibration-mix <m> "optiq" or a JSONL path [default: optiq]
|
|
491
|
+
--n-calibration <n> Calibration samples [default: 2]
|
|
492
|
+
|
|
493
|
+
Not supported (mlx_lm.convert flags we don't implement — the command
|
|
494
|
+
exits with an error rather than guessing): --dtype, -d/--dequantize,
|
|
495
|
+
--q-mode other than affine, and the mlx-lm --quant-predicate recipes
|
|
496
|
+
(use --target-bpw instead). Plain non-quantizing conversion is also not
|
|
497
|
+
supported: pass -q or --target-bpw.`,
|
|
498
|
+
|
|
499
|
+
upload: `mlx-bun upload — push a local model directory to the Hugging Face Hub
|
|
500
|
+
|
|
501
|
+
Usage: mlx-bun upload --path <model-dir> --upload-repo <org/repo> [options]
|
|
502
|
+
|
|
503
|
+
Native push-to-hub, no Python (src/hf-push.ts — the same engine as the
|
|
504
|
+
web UI's push buttons): creates the repo if needed (idempotent), uploads
|
|
505
|
+
weights via the git-LFS batch protocol, and commits. Flag names match
|
|
506
|
+
mlx_lm.upload.
|
|
507
|
+
|
|
508
|
+
Options:
|
|
509
|
+
--path <dir> Local model directory to upload [default: mlx_model]
|
|
510
|
+
--upload-repo <id> Hub repo id, org/name or bare name (required)
|
|
511
|
+
--private Create the repo as private (mlx-bun extension)
|
|
512
|
+
|
|
513
|
+
Auth: needs a Hugging Face WRITE token, resolved from ~/.mlx-bun/hf.json
|
|
514
|
+
(web UI Settings → Hugging Face), $HF_TOKEN, or ~/.cache/huggingface/token
|
|
515
|
+
(hf auth login). Errors before uploading anything if none is found.
|
|
516
|
+
|
|
517
|
+
Convert + push in one step: mlx-bun convert … --upload-repo <org/repo>`,
|
|
518
|
+
|
|
519
|
+
perplexity: `mlx-bun perplexity — perplexity over a local text/JSONL dataset
|
|
520
|
+
|
|
521
|
+
Usage: mlx-bun perplexity <model-query-or-path> --data-path <file> [options]
|
|
522
|
+
|
|
523
|
+
mlx_lm.perplexity methodology, exactly: samples are visited in a seeded
|
|
524
|
+
random order, tokenized, concatenated, and cut into NON-OVERLAPPING rows
|
|
525
|
+
of --sequence-length tokens; per batch the model scores rows[:, :-1]
|
|
526
|
+
against rows[:, 1:] in f32 (every position counts); reported as
|
|
527
|
+
ppl = exp(mean CE) ± the delta-method standard error. The one deliberate
|
|
528
|
+
difference: the data source is a LOCAL file (never a Hugging Face
|
|
529
|
+
dataset download).
|
|
530
|
+
|
|
531
|
+
<model> Model (registry query or snapshot path);
|
|
532
|
+
--model (mlx_lm spelling) and --query are accepted
|
|
533
|
+
for the positional
|
|
534
|
+
|
|
535
|
+
Options:
|
|
536
|
+
--data-path <file> .jsonl ({"text": …} rows) or plain .txt (required)
|
|
537
|
+
--sequence-length <n> Tokens per row [default: 512]
|
|
538
|
+
--num-samples <n> Rows to score (-1 = all available) [default: 256]
|
|
539
|
+
--batch-size <n> Rows per forward [default: 8]
|
|
540
|
+
--seed <n> Sample-shuffle seed [default: 123]`,
|
|
541
|
+
|
|
278
542
|
memory: `mlx-bun memory — your local AI's personal wiki
|
|
279
543
|
|
|
280
544
|
Usage: mlx-bun memory <subcommand>
|
|
@@ -301,8 +565,16 @@ Subcommands:
|
|
|
301
565
|
Print one article section
|
|
302
566
|
links <article> Show resolved outbound + inbound wikilinks
|
|
303
567
|
read <article> Print an article (stem, e.g. Archie_Project)
|
|
304
|
-
synthesize Run the synthesis
|
|
305
|
-
--dry-run)
|
|
568
|
+
synthesize Run the FULL synthesis DAG now (--since, --model,
|
|
569
|
+
--dry-run); also: pipeline, all
|
|
570
|
+
segment | extract | route | synthesize-stage
|
|
571
|
+
Run ONE decomposed stage worker (--limit N —
|
|
572
|
+
segment/extract/synthesize-stage only; --convs a,b).
|
|
573
|
+
Each pulls its eligible work from the DB by state, walks
|
|
574
|
+
oldest-conversation-first, persists, and exits — resumable,
|
|
575
|
+
and runnable as separate concurrent processes on slices.
|
|
576
|
+
link Deterministic cross-linking stage: inline-link first
|
|
577
|
+
mentions + rebuild ## See also (--limit N; no model)
|
|
306
578
|
schedule Install the nightly launchd job (--at HH:MM [03:00])
|
|
307
579
|
unschedule Remove the nightly launchd job
|
|
308
580
|
|
|
@@ -310,12 +582,13 @@ The read path is live: the assistant reads a wiki you set up by hand or
|
|
|
310
582
|
import during \`memory init\`. Ask the assistant to "open my memory" or run
|
|
311
583
|
\`mlx-bun memory open\` to browse it in Obsidian/Finder, or
|
|
312
584
|
\`mlx-bun memory open <article>\` to jump to a specific page. Synthesis
|
|
313
|
-
(conversations → articles)
|
|
314
|
-
|
|
315
|
-
|
|
585
|
+
(conversations → articles) runs the full local pipeline via
|
|
586
|
+
\`mlx-bun memory synthesize\` (or per-stage: segment/extract/route/
|
|
587
|
+
synthesize-stage/link); the nightly job runs it on a schedule.`,
|
|
316
588
|
};
|
|
317
589
|
|
|
318
590
|
HELP.bench = HELP.benchmark!;
|
|
591
|
+
HELP.gen = HELP.generate!;
|
|
319
592
|
HELP.setup = HELP.memory!;
|
|
320
593
|
|
|
321
594
|
function printHelp(topic?: string): never {
|
|
@@ -454,11 +727,16 @@ function openChatUi(url: string, hostPort: string): void {
|
|
|
454
727
|
* Each tier is a GUARANTEE about which reference you reproduce bit-for-bit:
|
|
455
728
|
* --l1 bit-for-bit IDENTICAL to mlx-lm — drop-in replacement for mlx-lm
|
|
456
729
|
* --l2 bit-for-bit IDENTICAL to mlx-optiq — drop-in replacement for mlx-optiq.
|
|
457
|
-
* optiq
|
|
458
|
-
*
|
|
730
|
+
* The optiq-golden decode composition (scripts/regen-kvq-goldens.ts) is
|
|
731
|
+
* fused N-tiled SDPA for L>1 + STOCK unfused L=1 decode, so L2 = fused-sdpa
|
|
732
|
+
* ON, perf-kernel OFF. The perf kernel is an mlx-bun ORIGINAL Metal kernel,
|
|
733
|
+
* envelope-gated (≥56/64 teacher-forced argmax vs OUR OWN frozen compat
|
|
734
|
+
* trajectory, tests/perf-kernel-oracle.test.ts) — an L3 node, never the
|
|
735
|
+
* bare-L2 default. (Commit f1bf5cc put it in L2 claiming the goldens track
|
|
736
|
+
* it; they don't — reverted 2026-07-01 with the evidence above.)
|
|
459
737
|
* --l3 best performance, NO bit-exact guarantee — correct, but gated by KL +
|
|
460
738
|
* tests, because mlx-optiq has no analogy for what L3 does.
|
|
461
|
-
* L2 ⊂ L3 (L3 = L2 +
|
|
739
|
+
* L2 ⊂ L3 (L3 = L2 + perf kernel). L1 is a separate target (bf16, unfused). */
|
|
462
740
|
function applyDecodeRoute(): { kvQuant?: "off" | "config" | number } {
|
|
463
741
|
const onOff = (name: string): boolean | null => {
|
|
464
742
|
const v = opt(name); if (v == null) return null;
|
|
@@ -471,15 +749,18 @@ function applyDecodeRoute(): { kvQuant?: "off" | "config" | number } {
|
|
|
471
749
|
// A tier is the GUARANTEE; within it we default to the FAST kernel that still
|
|
472
750
|
// holds it, and expose the slow one as an opt-in. compiled-decode is BIT-EXACT
|
|
473
751
|
// with uncompiled (compiled-decode.test: on==off) → free speed, so it's ON in
|
|
474
|
-
// EVERY tier (--compiled-decode off is the slow, same-tier opt-in).
|
|
475
|
-
//
|
|
476
|
-
// quantized-KV only →
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
//
|
|
752
|
+
// EVERY tier (--compiled-decode off is the slow, same-tier opt-in). fused-sdpa
|
|
753
|
+
// matches OPTIQ bit-for-bit (tier-a goldens, fused-sdpa.test) and is
|
|
754
|
+
// quantized-KV only → it's the L2 bridge, a no-op on L1's bf16. perf-kernel is
|
|
755
|
+
// NOT: it's an original flash-decoding kernel, envelope-gated against our own
|
|
756
|
+
// frozen trajectory (perf-kernel-oracle.test) — bare --l2 must ship the
|
|
757
|
+
// optiq-golden decode composition (stock unfused L=1), so perf-kernel is
|
|
758
|
+
// L3/explicit-only (--perf-kernel on opts a tier into it by choice). This is
|
|
759
|
+
// the decode-axis difference between L2 and L3; L3 also owns the no-oracle
|
|
760
|
+
// FEATURES (HLG sampler, expert offload, batched mixed-precision).
|
|
480
761
|
l1: { kv: "off", perf: false, fusedSdpa: false, compiled: true, fusedDecode: false }, // = mlx-lm bit-for-bit (bf16)
|
|
481
|
-
l2: { kv: "config", perf:
|
|
482
|
-
l3: { kv: "config", perf: true, fusedSdpa: true, compiled: true, fusedDecode: false }, // best perf (decode
|
|
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)
|
|
483
764
|
};
|
|
484
765
|
const tier = flag("l1") ? "l1" : flag("l2") ? "l2" : flag("l3") ? "l3" : null;
|
|
485
766
|
const p = tier ? TIERS[tier]! : null;
|
|
@@ -521,9 +802,20 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
|
|
|
521
802
|
if (budgetGB > 0) serverOptions.memoryBudgetBytes = budgetGB * 1e9;
|
|
522
803
|
const pcRaw = opt("prompt-cache"); // null if absent; an explicit value (incl. 0) wins → `--prompt-cache 0` DISABLES the cache
|
|
523
804
|
if (pcRaw !== null) serverOptions.promptCacheBytes = Math.max(0, Number(pcRaw)) * 2 ** 30;
|
|
805
|
+
// SSD cold tier (docs/design/ssd-kv-cold-tier.md) — off unless a dir is given.
|
|
806
|
+
const ssdDir = opt("ssd-cache");
|
|
807
|
+
if (ssdDir !== null) {
|
|
808
|
+
serverOptions.ssdCacheDir = ssdDir;
|
|
809
|
+
const maxRaw = opt("ssd-cache-max");
|
|
810
|
+
if (maxRaw !== null) serverOptions.ssdCacheMaxBytes = Math.max(1, Number(maxRaw)) * 2 ** 30;
|
|
811
|
+
if (flag("ssd-cache-verify")) serverOptions.ssdCacheVerify = true;
|
|
812
|
+
}
|
|
524
813
|
// --batch N: max concurrent requests batched through the mlx-lm-parity
|
|
525
|
-
// engine (N=1 = today's serial path). --decode-concurrency is accepted
|
|
526
|
-
//
|
|
814
|
+
// engine (N=1 = today's serial path). --decode-concurrency is accepted for
|
|
815
|
+
// drop-in compatibility with mlx_lm.server, but the semantics differ: there
|
|
816
|
+
// it caps per-BatchGenerator decode parallelism (default 32); in mlx-bun it
|
|
817
|
+
// enables continuous batching with this cap (the default is the optimized
|
|
818
|
+
// serial path).
|
|
527
819
|
const batchRaw = opt("batch") ?? opt("decode-concurrency");
|
|
528
820
|
if (batchRaw !== null) {
|
|
529
821
|
const n = Number(batchRaw);
|
|
@@ -534,8 +826,11 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
|
|
|
534
826
|
serverOptions.batch = n;
|
|
535
827
|
}
|
|
536
828
|
if (route.kvQuant !== undefined) serverOptions.kvQuant = route.kvQuant;
|
|
537
|
-
|
|
538
|
-
|
|
829
|
+
// Bind loopback unless asked otherwise (mlx_lm.server parity); --host
|
|
830
|
+
// 0.0.0.0 is the explicit opt-in for LAN exposure. The chat-UI open and
|
|
831
|
+
// `mlx-bun pi` attach both go through localhost, so loopback-only is
|
|
832
|
+
// transparent to them.
|
|
833
|
+
serverOptions.hostname = opt("host", "127.0.0.1")!;
|
|
539
834
|
// Server-wide default for the chat template's enable_thinking variable
|
|
540
835
|
// (CPM/MiniCPM5 and other hybrid-reasoning models). Unset ⇒ the model's
|
|
541
836
|
// own default (false for MiniCPM5). onOff accepts true|false|on|off|1|0.
|
|
@@ -554,12 +849,18 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
|
|
|
554
849
|
}
|
|
555
850
|
return n;
|
|
556
851
|
};
|
|
557
|
-
|
|
852
|
+
// --temp is the mlx_lm.server spelling; explicit --temperature wins.
|
|
853
|
+
const temp = numFlag("temperature", 0, 5) ?? numFlag("temp", 0, 5);
|
|
558
854
|
if (temp !== null) serverOptions.defaultTemperature = temp;
|
|
559
855
|
const topP = numFlag("top-p", 0, 1);
|
|
560
856
|
if (topP !== null) serverOptions.defaultTopP = topP;
|
|
561
857
|
const topK = numFlag("top-k", 0, 1_000_000);
|
|
562
858
|
if (topK !== null) serverOptions.defaultTopK = topK;
|
|
859
|
+
// mlx_lm.server's --max-tokens: the completion cap when a request omits
|
|
860
|
+
// max_tokens. Our built-in default stays 65,536 (chat) / 512 (raw text
|
|
861
|
+
// completion); `--max-tokens 512` reproduces mlx-lm's behavior exactly.
|
|
862
|
+
const maxTok = numFlag("max-tokens", 1, 10_000_000);
|
|
863
|
+
if (maxTok !== null) serverOptions.defaultMaxTokens = Math.floor(maxTok);
|
|
563
864
|
// HLG sampling — piecewise tone curve on the logprobs (default off). Knobs in
|
|
564
865
|
// nats; the mid gain folds from --temperature. docs/design/hlg-sampling.md.
|
|
565
866
|
if (onOff("hlg-sampling") === true) {
|
|
@@ -572,7 +873,33 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
|
|
|
572
873
|
pivot: "top",
|
|
573
874
|
};
|
|
574
875
|
}
|
|
575
|
-
return { port: Number(opt("port", "
|
|
876
|
+
return { port: Number(opt("port", "8080")), serverOptions };
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/** `--adapter <dir>` (alias `--adapter-path`, mlx_lm.server's spelling):
|
|
880
|
+
* mount a LoRA adapter at startup — the same machinery as POST /v1/adapters —
|
|
881
|
+
* and make it the default for requests that don't select one (a request's
|
|
882
|
+
* explicit `adapter`, including "none", still wins; hot-swap unchanged).
|
|
883
|
+
* This is the flag `mlx-bun train`'s completion message points at. Shared by
|
|
884
|
+
* `serve` and the `pi` session server. Exits on a bad adapter — better a
|
|
885
|
+
* loud startup failure than silently serving the base model. */
|
|
886
|
+
async function mountStartupAdapter(
|
|
887
|
+
ctx: import("./server").ServerContext,
|
|
888
|
+
serverOptions: import("./server").ServerOptions,
|
|
889
|
+
): Promise<void> {
|
|
890
|
+
const adapterDir = opt("adapter") ?? opt("adapter-path");
|
|
891
|
+
if (!adapterDir) return;
|
|
892
|
+
const { step, style } = await import("./tui");
|
|
893
|
+
const sAdp = step("mounting adapter");
|
|
894
|
+
try {
|
|
895
|
+
const adapterId = adapterDir.replace(/\/+$/, "").split("/").pop()!;
|
|
896
|
+
const info = await ctx.adapters.mount(adapterId, adapterDir);
|
|
897
|
+
serverOptions.defaultAdapter = info.id;
|
|
898
|
+
sAdp.done(`adapter ${style.bold(info.id)} ${style.dim(`· ${info.mountedLayers} layers · default for requests (select others via \`adapter\`)`)}`);
|
|
899
|
+
} catch (e) {
|
|
900
|
+
sAdp.fail(`adapter mount failed: ${(e as Error).message}`);
|
|
901
|
+
process.exit(1);
|
|
902
|
+
}
|
|
576
903
|
}
|
|
577
904
|
|
|
578
905
|
/** One-line summary of the active runtime levers for the ready card. */
|
|
@@ -591,8 +918,21 @@ function runtimeSummary(o: import("./server").ServerOptions): string {
|
|
|
591
918
|
|
|
592
919
|
/** Shared model resolution: explicit query wins; otherwise e4b (the default
|
|
593
920
|
* model everywhere) if it's downloaded, else the largest supported model that
|
|
594
|
-
* fits this machine. Downloads the recommended model first on a fresh install.
|
|
921
|
+
* fits this machine. Downloads the recommended model first on a fresh install.
|
|
922
|
+
* A query that is a DIRECTORY containing config.json loads directly via
|
|
923
|
+
* scanSnapshot (no registry dependency) — the `--model <path>` bench/CI shape. */
|
|
595
924
|
async function resolveModelAuto(query: string | null): Promise<{ m: import("./registry").ModelRecord; picked: boolean }> {
|
|
925
|
+
if (query && existsSync(join(query, "config.json"))) {
|
|
926
|
+
const { scanSnapshot } = await import("./registry");
|
|
927
|
+
const dir = resolve(query);
|
|
928
|
+
// Recover the repo id from an HF-cache path (…/models--org--name/snapshots/<rev>);
|
|
929
|
+
// otherwise the directory basename is the display id.
|
|
930
|
+
const hf = /models--([^/]+)--([^/]+)\/snapshots\//.exec(dir);
|
|
931
|
+
const repoId = hf ? `${hf[1]}/${hf[2]}` : basename(dir);
|
|
932
|
+
const m = await scanSnapshot(dir, repoId);
|
|
933
|
+
if (!m) throw new Error(`${dir} has config.json but no weights (*.safetensors)`);
|
|
934
|
+
return { m, picked: false };
|
|
935
|
+
}
|
|
596
936
|
const reg = new Registry();
|
|
597
937
|
if (reg.list().length === 0) await reg.scan();
|
|
598
938
|
if (query) return { m: reg.resolve(query), picked: false };
|
|
@@ -664,13 +1004,33 @@ function parseSize(s: string): number {
|
|
|
664
1004
|
|
|
665
1005
|
switch (cmd) {
|
|
666
1006
|
case "get": {
|
|
667
|
-
|
|
668
|
-
if (!repoId
|
|
669
|
-
console.error("usage: mlx-bun get <org/repo> [--revision main]");
|
|
1007
|
+
let repoId = positional(0);
|
|
1008
|
+
if (!repoId) {
|
|
1009
|
+
console.error("usage: mlx-bun get <org/repo | substring> [--revision main]");
|
|
670
1010
|
process.exit(1);
|
|
671
1011
|
}
|
|
1012
|
+
if (!repoId.includes("/")) {
|
|
1013
|
+
// No org/name — treat it as a registry query over already-downloaded
|
|
1014
|
+
// repos (re-get/refresh, e.g. `mlx-bun get 12B` after an upstream push).
|
|
1015
|
+
try {
|
|
1016
|
+
const regQ = new Registry();
|
|
1017
|
+
if (regQ.list().length === 0) await regQ.scan();
|
|
1018
|
+
repoId = regQ.resolve(repoId).repoId;
|
|
1019
|
+
} catch (e) {
|
|
1020
|
+
console.error((e as Error).message);
|
|
1021
|
+
console.error(`usage: mlx-bun get <org/repo> [--revision main] — or a substring of a downloaded repo (try \`mlx-bun ls ${repoId}\`)`);
|
|
1022
|
+
process.exit(1);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
672
1025
|
const { downloadModel } = await import("./download");
|
|
673
1026
|
const { step, style } = await import("./tui");
|
|
1027
|
+
const { join: joinPath, basename } = await import("node:path");
|
|
1028
|
+
const { readdirSync, existsSync } = await import("node:fs");
|
|
1029
|
+
const { DEFAULT_HUB, planRepoGc } = await import("./registry");
|
|
1030
|
+
const repoDir = joinPath(DEFAULT_HUB, `models--${repoId.replaceAll("/", "--")}`);
|
|
1031
|
+
const priorSnapshots = existsSync(joinPath(repoDir, "snapshots"))
|
|
1032
|
+
? readdirSync(joinPath(repoDir, "snapshots"))
|
|
1033
|
+
: [];
|
|
674
1034
|
const s = step(`downloading ${repoId}`);
|
|
675
1035
|
const snap = await downloadModel(repoId, {
|
|
676
1036
|
revision: opt("revision", "main")!,
|
|
@@ -684,6 +1044,18 @@ switch (cmd) {
|
|
|
684
1044
|
const reg = new Registry();
|
|
685
1045
|
await reg.scan();
|
|
686
1046
|
sScan.done(`registry updated ${style.dim(`· ${snap}`)}`);
|
|
1047
|
+
// Upstream pushed a new revision → this get created a NEW snapshots/<sha>
|
|
1048
|
+
// dir; the old one stays on disk (that's how ~25 GB of dead blobs pile
|
|
1049
|
+
// up). Say so, with what `gc` would get back.
|
|
1050
|
+
if (priorSnapshots.length > 0 && !priorSnapshots.includes(basename(snap))) {
|
|
1051
|
+
const plan = planRepoGc(repoDir);
|
|
1052
|
+
const note = plan.skippedSnapshots.length > 0
|
|
1053
|
+
? " (a previous snapshot has files this revision lacks — gc will warn; --force to prune it)"
|
|
1054
|
+
: "";
|
|
1055
|
+
console.log(style.dim(
|
|
1056
|
+
` previous revision kept on disk — \`mlx-bun gc\` reclaims ~${gb(plan.reclaimBytes)}${note}`,
|
|
1057
|
+
));
|
|
1058
|
+
}
|
|
687
1059
|
break;
|
|
688
1060
|
}
|
|
689
1061
|
|
|
@@ -700,43 +1072,125 @@ switch (cmd) {
|
|
|
700
1072
|
const reg = new Registry();
|
|
701
1073
|
if ((reg.list().length ?? 0) === 0) await reg.scan();
|
|
702
1074
|
const maxSize = opt("max-size");
|
|
703
|
-
const
|
|
1075
|
+
const allRevisions = flag("all-revisions");
|
|
1076
|
+
const lsFilter = {
|
|
704
1077
|
vision: flag("vision") ? true : undefined,
|
|
705
1078
|
maxBytes: maxSize ? parseSize(maxSize) : undefined,
|
|
706
1079
|
query: positional(0),
|
|
707
|
-
}
|
|
1080
|
+
};
|
|
1081
|
+
// One row per repo by default: the HF cache keeps a snapshots/<commit>
|
|
1082
|
+
// dir per downloaded revision, and re-getting after an upstream push
|
|
1083
|
+
// strands the old one — those are duplicates, not separate models.
|
|
1084
|
+
// --all-revisions shows the per-snapshot truth (canonical marked *).
|
|
1085
|
+
const models = allRevisions ? reg.list(lsFilter) : reg.listCanonical(lsFilter);
|
|
708
1086
|
if (models.length === 0) {
|
|
709
1087
|
console.log("no models match (try `mlx-bun scan`)");
|
|
710
1088
|
break;
|
|
711
1089
|
}
|
|
1090
|
+
const { visionCapable } = await import("./registry");
|
|
1091
|
+
const { supportTier } = await import("./model/support");
|
|
1092
|
+
const canonicalPaths = new Set(reg.listCanonical(lsFilter).map((m) => m.path));
|
|
1093
|
+
const capabilities = (m: (typeof models)[number]) => {
|
|
1094
|
+
const tier = supportTier(m.modelType, m.repoId);
|
|
1095
|
+
return [
|
|
1096
|
+
tier ? `supported (${tier})` : `unsupported (${m.modelType})`,
|
|
1097
|
+
visionCapable(m) ? "vision" : null,
|
|
1098
|
+
m.hasToolTemplate ? "tools" : null,
|
|
1099
|
+
m.hasKvConfig ? "kv-quant" : null,
|
|
1100
|
+
].filter(Boolean).join(" · ");
|
|
1101
|
+
};
|
|
712
1102
|
const { table, style, h1 } = await import("./tui");
|
|
713
1103
|
h1("library");
|
|
714
1104
|
console.log();
|
|
715
1105
|
table(
|
|
716
1106
|
[
|
|
717
1107
|
{ header: "model", paint: (c) => style.bold(c) },
|
|
718
|
-
{ header: "
|
|
719
|
-
{ header: "
|
|
1108
|
+
...(allRevisions ? [{ header: "revision" }] : []),
|
|
1109
|
+
{ header: "size", align: "right" as const },
|
|
1110
|
+
{ header: "params", align: "right" as const },
|
|
720
1111
|
{ header: "quant" },
|
|
721
|
-
{ header: "license", paint: (c) => style.dim(c) },
|
|
722
|
-
{ header: "capabilities", paint: (c) => style.dim(c) },
|
|
1112
|
+
{ header: "license", paint: (c: string) => style.dim(c) },
|
|
1113
|
+
{ header: "capabilities", paint: (c: string) => style.dim(c) },
|
|
723
1114
|
],
|
|
724
1115
|
models.map((m) => [
|
|
725
1116
|
m.repoId,
|
|
1117
|
+
...(allRevisions
|
|
1118
|
+
? [`${(m.path.split("/snapshots/")[1] ?? "").slice(0, 12)}${canonicalPaths.has(m.path) ? " *" : ""}`]
|
|
1119
|
+
: []),
|
|
726
1120
|
gb(m.sizeBytes),
|
|
727
1121
|
m.paramCount ? `${(m.paramCount / 1e9).toFixed(1)}B` : "?",
|
|
728
1122
|
m.quantBits ? `${m.quantBits}-bit g${m.quantGroupSize}` : "full",
|
|
729
1123
|
m.license ?? "?",
|
|
730
|
-
|
|
731
|
-
isSupportedModelRecord(m.modelType, m.repoId) ? "supported" : `unsupported (${m.modelType})`,
|
|
732
|
-
m.hasVisionSidecar ? "vision" : null,
|
|
733
|
-
m.hasToolTemplate ? "tools" : null,
|
|
734
|
-
m.hasKvConfig ? "kv-quant" : null,
|
|
735
|
-
].filter(Boolean).join(" · "),
|
|
1124
|
+
capabilities(m),
|
|
736
1125
|
]),
|
|
737
1126
|
);
|
|
738
1127
|
console.log();
|
|
739
|
-
|
|
1128
|
+
if (allRevisions) {
|
|
1129
|
+
console.log(style.dim(
|
|
1130
|
+
` ${models.length} snapshot(s) · * = canonical (refs/main) · superseded snapshots: \`mlx-bun gc\``,
|
|
1131
|
+
));
|
|
1132
|
+
} else {
|
|
1133
|
+
console.log(style.dim(` ${models.length} model(s) · mlx-bun fit <query> for a memory assessment`));
|
|
1134
|
+
}
|
|
1135
|
+
break;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
case "gc": {
|
|
1139
|
+
const { planGc, executeGc, DEFAULT_HUB } = await import("./registry");
|
|
1140
|
+
const { table, style, h1 } = await import("./tui");
|
|
1141
|
+
const force = flag("force");
|
|
1142
|
+
const plans = planGc(DEFAULT_HUB, { force }).filter(
|
|
1143
|
+
(p) => p.pruneSnapshots.length || p.skippedSnapshots.length || p.deadBlobs.length,
|
|
1144
|
+
);
|
|
1145
|
+
h1("gc — superseded snapshots + dead blobs");
|
|
1146
|
+
console.log();
|
|
1147
|
+
if (plans.length === 0) {
|
|
1148
|
+
console.log(" nothing to reclaim — every snapshot is referenced by refs/*");
|
|
1149
|
+
break;
|
|
1150
|
+
}
|
|
1151
|
+
table(
|
|
1152
|
+
[
|
|
1153
|
+
{ header: "repo", paint: (c) => style.bold(c) },
|
|
1154
|
+
{ header: "keep", align: "right" },
|
|
1155
|
+
{ header: "prune", align: "right" },
|
|
1156
|
+
{ header: "skip", align: "right" },
|
|
1157
|
+
{ header: "reclaims", align: "right" },
|
|
1158
|
+
],
|
|
1159
|
+
plans.map((p) => [
|
|
1160
|
+
p.repoId,
|
|
1161
|
+
String(p.keepSnapshots.length),
|
|
1162
|
+
String(p.pruneSnapshots.length),
|
|
1163
|
+
String(p.skippedSnapshots.length),
|
|
1164
|
+
gb(p.reclaimBytes),
|
|
1165
|
+
]),
|
|
1166
|
+
);
|
|
1167
|
+
console.log();
|
|
1168
|
+
for (const p of plans) {
|
|
1169
|
+
for (const s of p.skippedSnapshots) {
|
|
1170
|
+
const rev = (s.path.split("/snapshots/")[1] ?? s.path).slice(0, 12);
|
|
1171
|
+
console.log(style.accent(
|
|
1172
|
+
` WARNING ${p.repoId}@${rev}: superseded snapshot has files the canonical revision lacks — skipped.`,
|
|
1173
|
+
));
|
|
1174
|
+
console.log(style.dim(` only copy of: ${s.extraFiles.join(", ")}`));
|
|
1175
|
+
console.log(style.dim(" deleting it loses those files — rerun with --force if that's intended."));
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
const totalReclaim = plans.reduce((a, p) => a + p.reclaimBytes, 0);
|
|
1179
|
+
const totalSnaps = plans.reduce((a, p) => a + p.pruneSnapshots.length, 0);
|
|
1180
|
+
const totalBlobs = plans.reduce((a, p) => a + p.deadBlobs.length, 0);
|
|
1181
|
+
console.log(
|
|
1182
|
+
` total: ${totalSnaps} snapshot(s), ${totalBlobs} blob(s), ${style.bold(gb(totalReclaim))}`,
|
|
1183
|
+
);
|
|
1184
|
+
if (flag("dry-run") || !flag("yes")) {
|
|
1185
|
+
console.log(style.dim(" nothing deleted — rerun with --yes to delete (destructive)."));
|
|
1186
|
+
break;
|
|
1187
|
+
}
|
|
1188
|
+
const res = executeGc(plans);
|
|
1189
|
+
console.log(
|
|
1190
|
+
` deleted ${res.snapshots} snapshot(s) + ${res.blobs} blob(s) — reclaimed ${style.bold(gb(res.reclaimedBytes))}`,
|
|
1191
|
+
);
|
|
1192
|
+
const reg = new Registry();
|
|
1193
|
+
await reg.scan(); // reap deleted snapshots from the registry
|
|
740
1194
|
break;
|
|
741
1195
|
}
|
|
742
1196
|
|
|
@@ -804,7 +1258,12 @@ switch (cmd) {
|
|
|
804
1258
|
process.exit(1);
|
|
805
1259
|
}
|
|
806
1260
|
}
|
|
807
|
-
|
|
1261
|
+
// --model <path-or-query> is the explicit override (mlx_lm.server's
|
|
1262
|
+
// spelling — previously accepted-but-IGNORED here, which silently
|
|
1263
|
+
// auto-picked the wrong model). A directory with config.json loads
|
|
1264
|
+
// straight from that path; anything else resolves like the positional
|
|
1265
|
+
// fuzzy query. Precedence: --model > positional > --query > auto-pick.
|
|
1266
|
+
const { m, picked } = await resolveModelAuto(opt("model") ?? positional(0) ?? opt("query"));
|
|
808
1267
|
const sFit = step("assessing fit");
|
|
809
1268
|
const config = await loadModelConfig(m.path);
|
|
810
1269
|
const report = fit(config, m.sizeBytes, 8192, undefined, undefined, m.expertsBytes);
|
|
@@ -829,8 +1288,9 @@ switch (cmd) {
|
|
|
829
1288
|
const sLoad = step("loading weights");
|
|
830
1289
|
const t0 = performance.now();
|
|
831
1290
|
const ctx = await loadContext(m.path, m.repoId, { memoryBudgetBytes: rt.serverOptions.memoryBudgetBytes });
|
|
832
|
-
const server = createServer(ctx, rt.port, { ...rt.serverOptions, owner: "serve" });
|
|
833
1291
|
sLoad.done(`weights loaded ${style.dim(`in ${(performance.now() - t0).toFixed(0)} ms`)}`);
|
|
1292
|
+
await mountStartupAdapter(ctx, rt.serverOptions);
|
|
1293
|
+
const server = createServer(ctx, rt.port, { ...rt.serverOptions, owner: "serve" });
|
|
834
1294
|
const shownHost = rt.serverOptions.hostname ?? "localhost";
|
|
835
1295
|
console.log();
|
|
836
1296
|
box([
|
|
@@ -860,35 +1320,115 @@ switch (cmd) {
|
|
|
860
1320
|
// RAW generate — a direct one-shot entry point (no server, no chat UI). The
|
|
861
1321
|
// three entry points (raw / OpenAI API / chat UI) differ only in how params
|
|
862
1322
|
// are POPULATED; here every param is explicit. Decode-path levers mirror
|
|
863
|
-
// serve so you can pin the route (mlx-lm compat = --
|
|
864
|
-
// --
|
|
865
|
-
|
|
1323
|
+
// serve so you can pin the route (mlx-lm compat = --l1).
|
|
1324
|
+
// --l1/--l2/--l3 + per-fork overrides set the decode env levers; the
|
|
1325
|
+
// returned KV scheme is applied below so `generate --l2` runs the same
|
|
1326
|
+
// quantized-KV route (and produces the same tokens) as `serve --l2`.
|
|
1327
|
+
const route = applyDecodeRoute();
|
|
866
1328
|
|
|
867
1329
|
const prompt = opt("prompt") ?? positional(1);
|
|
868
1330
|
if (!prompt) {
|
|
869
1331
|
console.error('usage: mlx-bun generate [query] --prompt "…" [--raw] [--max-tokens N]');
|
|
870
|
-
console.error(' sampling: --temperature N --top-p N --top-k N --seed N');
|
|
871
|
-
console.error(' decode path: --
|
|
1332
|
+
console.error(' sampling: --temperature N (alias --temp) --top-p N --top-k N --seed N');
|
|
1333
|
+
console.error(' decode path: --l1 (= mlx-lm compat) / --l2 / --l3, or per-fork flags');
|
|
872
1334
|
process.exit(1);
|
|
873
1335
|
}
|
|
874
1336
|
const { loadTaskModel, generateText } = await import("./eval/runner");
|
|
875
1337
|
const tm = await loadTaskModel(positional(0) ?? opt("query") ?? "");
|
|
1338
|
+
// Resolve the tier/--kv-quant KV scheme exactly the way serve does
|
|
1339
|
+
// (server.ts kvScheme): "config" = the model's kv_config.json (absent →
|
|
1340
|
+
// bf16, same silent fallback as serve), N = uniform bits from decode
|
|
1341
|
+
// start. Unset (no tier, no --kv-quant) keeps generate's historical
|
|
1342
|
+
// default: bf16 bit-exact greedy (generateText's parity path).
|
|
1343
|
+
const kvScheme =
|
|
1344
|
+
route.kvQuant === "off" ? {}
|
|
1345
|
+
: route.kvQuant === "config"
|
|
1346
|
+
? (tm.config.kvQuant?.length ? { kvConfig: tm.config.kvQuant } : {})
|
|
1347
|
+
: typeof route.kvQuant === "number"
|
|
1348
|
+
? { kvBits: route.kvQuant, quantizedKvStart: 0 }
|
|
1349
|
+
: undefined;
|
|
876
1350
|
const num = (n: string): number | undefined => { const v = opt(n); return v == null ? undefined : Number(v); };
|
|
877
1351
|
const text = await generateText(tm, prompt, {
|
|
878
1352
|
maxTokens: Number(opt("max-tokens", "256")),
|
|
879
1353
|
useChat: !flag("raw"),
|
|
880
|
-
|
|
1354
|
+
...(kvScheme !== undefined ? { kvScheme } : {}),
|
|
1355
|
+
sampler: { temperature: num("temperature") ?? num("temp"), topP: num("top-p"), topK: num("top-k"), seed: num("seed") },
|
|
881
1356
|
});
|
|
882
1357
|
process.stdout.write(text.endsWith("\n") ? text : text + "\n");
|
|
883
1358
|
break;
|
|
884
1359
|
}
|
|
885
1360
|
|
|
1361
|
+
case "embed": {
|
|
1362
|
+
// Raw text embeddings — one-shot, no server. Text from --text, or one per
|
|
1363
|
+
// line on stdin. Uses the same embedPooled path the parity test verifies.
|
|
1364
|
+
const { Weights } = await import("./weights");
|
|
1365
|
+
const { createModel } = await import("./model/factory");
|
|
1366
|
+
const { loadTokenizer } = await import("./tokenizer");
|
|
1367
|
+
const { embedMany, isEmbeddingModel } = await import("./embed");
|
|
1368
|
+
|
|
1369
|
+
let texts: string[];
|
|
1370
|
+
const oneText = opt("text") ?? positional(1);
|
|
1371
|
+
if (oneText != null) {
|
|
1372
|
+
texts = [oneText];
|
|
1373
|
+
} else if (!process.stdin.isTTY) {
|
|
1374
|
+
texts = (await Bun.stdin.text()).split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
1375
|
+
} else {
|
|
1376
|
+
texts = [];
|
|
1377
|
+
}
|
|
1378
|
+
if (texts.length === 0) {
|
|
1379
|
+
console.error('usage: mlx-bun embed [query] --text "…" (or pipe text, one per line)');
|
|
1380
|
+
console.error(' --instruct "…" query instruction · --json OpenAI-style output');
|
|
1381
|
+
process.exit(1);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// Model resolution: an explicit query resolves normally; the no-query
|
|
1385
|
+
// path auto-picks the first DOWNLOADED embedding model (plain Qwen3 —
|
|
1386
|
+
// the record-level equivalent of isEmbeddingModel, which the factory
|
|
1387
|
+
// maps to Qwen3Model). Never the chat default, and never the fresh-
|
|
1388
|
+
// install starter download — a chat model can't embed, so downloading
|
|
1389
|
+
// one here only to fail the isEmbeddingModel check would be a trap.
|
|
1390
|
+
const embedQuery = positional(0) ?? opt("query");
|
|
1391
|
+
let m: import("./registry").ModelRecord;
|
|
1392
|
+
if (embedQuery) {
|
|
1393
|
+
({ m } = await resolveModelAuto(embedQuery));
|
|
1394
|
+
} else {
|
|
1395
|
+
const reg = new Registry();
|
|
1396
|
+
if (reg.list().length === 0) await reg.scan();
|
|
1397
|
+
const embedder = reg.list().find((r) => r.modelType === "qwen3");
|
|
1398
|
+
if (!embedder) {
|
|
1399
|
+
console.error("no embedding model downloaded — try: mlx-bun get mlx-community/Qwen3-Embedding-4B-4bit-DWQ");
|
|
1400
|
+
process.exit(1);
|
|
1401
|
+
}
|
|
1402
|
+
m = embedder;
|
|
1403
|
+
}
|
|
1404
|
+
const config = await loadModelConfig(m.path);
|
|
1405
|
+
const model = createModel(await Weights.open(m.path), config);
|
|
1406
|
+
if (!isEmbeddingModel(model)) {
|
|
1407
|
+
console.error(`"${m.repoId}" is not an embedding model (need plain Qwen3, e.g. Qwen3-Embedding).`);
|
|
1408
|
+
process.exit(1);
|
|
1409
|
+
}
|
|
1410
|
+
const tok = await loadTokenizer(m.path);
|
|
1411
|
+
const results = embedMany(model, tok, texts, opt("instruct") ?? undefined);
|
|
1412
|
+
|
|
1413
|
+
if (flag("json")) {
|
|
1414
|
+
let total = 0;
|
|
1415
|
+
const data = results.map((r, index) => { total += r.tokens; return { object: "embedding", index, embedding: Array.from(r.vector) }; });
|
|
1416
|
+
process.stdout.write(JSON.stringify({
|
|
1417
|
+
object: "list", data, model: m.repoId,
|
|
1418
|
+
usage: { prompt_tokens: total, total_tokens: total },
|
|
1419
|
+
}) + "\n");
|
|
1420
|
+
} else {
|
|
1421
|
+
for (const r of results) process.stdout.write(JSON.stringify(Array.from(r.vector)) + "\n");
|
|
1422
|
+
}
|
|
1423
|
+
break;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
886
1426
|
case "bench":
|
|
887
1427
|
case "benchmark": {
|
|
888
1428
|
const { banner, step, box, style } = await import("./tui");
|
|
889
1429
|
banner(pkg.version);
|
|
890
1430
|
serverRuntimeFlags(); // env levers (--compiled-decode, --perf-kernel, ...) apply to the run
|
|
891
|
-
const { m, picked } = await resolveModelAuto(positional(0) ?? opt("query"));
|
|
1431
|
+
const { m, picked } = await resolveModelAuto(opt("model") ?? positional(0) ?? opt("query"));
|
|
892
1432
|
const tokens = Number(opt("tokens", "256"));
|
|
893
1433
|
const runs = Number(opt("runs", "3"));
|
|
894
1434
|
const promptTokens = Number(opt("prompt-tokens", "0"));
|
|
@@ -1076,11 +1616,14 @@ switch (cmd) {
|
|
|
1076
1616
|
// (src/pi-terminal.ts) — pi is bundled, nothing to install. Users who
|
|
1077
1617
|
// already run their own pi connect it with `mlx-bun harness pi`.
|
|
1078
1618
|
const OURS_VAL = new Set([
|
|
1079
|
-
"--query", "-q", "--port", "--host", "--memory-budget", "--prompt-cache",
|
|
1619
|
+
"--query", "-q", "--model", "--port", "--host", "--memory-budget", "--prompt-cache",
|
|
1620
|
+
"--ssd-cache", "--ssd-cache-max", "--kv-quant",
|
|
1621
|
+
"--batch", "--decode-concurrency", "--adapter", "--adapter-path",
|
|
1080
1622
|
"--compiled-decode", "--perf-kernel", "--fused-decode", "--fused-sdpa", "--thinking",
|
|
1081
|
-
"--temperature", "--top-p", "--top-k",
|
|
1623
|
+
"--temperature", "--temp", "--top-p", "--top-k", "--max-tokens",
|
|
1624
|
+
"--hlg-sampling", "--hlg-width", "--hlg-shoulder", "--hlg-toe", "--hlg-pivot-offset",
|
|
1082
1625
|
]);
|
|
1083
|
-
const OURS_BOOL = new Set(["--force-wire", "--expert-offload"]);
|
|
1626
|
+
const OURS_BOOL = new Set(["--force-wire", "--expert-offload", "--no-open", "--ssd-cache-verify", "--l1", "--l2", "--l3"]);
|
|
1084
1627
|
const passthrough: string[] = [];
|
|
1085
1628
|
for (let i = 1; i < argv.length; i++) {
|
|
1086
1629
|
const a = argv[i]!;
|
|
@@ -1108,7 +1651,7 @@ switch (cmd) {
|
|
|
1108
1651
|
|
|
1109
1652
|
const rt = serverRuntimeFlags();
|
|
1110
1653
|
const port = rt.port;
|
|
1111
|
-
const baseUrl = `http://
|
|
1654
|
+
const baseUrl = `http://127.0.0.1:${port}/v1`;
|
|
1112
1655
|
const { probeServer } = await import("./harness-pi");
|
|
1113
1656
|
let models = await probeServer(baseUrl);
|
|
1114
1657
|
let startedServer = false;
|
|
@@ -1123,7 +1666,7 @@ switch (cmd) {
|
|
|
1123
1666
|
console.log(style.dim(" (stop it or use --port <other> to start a fresh server with these flags)"));
|
|
1124
1667
|
}
|
|
1125
1668
|
try {
|
|
1126
|
-
const stats = await (await fetch(`http://
|
|
1669
|
+
const stats = await (await fetch(`http://127.0.0.1:${port}/stats`)).json() as
|
|
1127
1670
|
{ server?: { owner?: string } };
|
|
1128
1671
|
if (stats.server?.owner === "pi-session") {
|
|
1129
1672
|
console.log("note: that server belongs to another `mlx-bun pi` session and ends when it does —");
|
|
@@ -1143,6 +1686,7 @@ switch (cmd) {
|
|
|
1143
1686
|
const sLoad = step(`loading ${m.repoId}`);
|
|
1144
1687
|
const t0 = performance.now();
|
|
1145
1688
|
const ctx = await loadContext(m.path, m.repoId, { memoryBudgetBytes: rt.serverOptions.memoryBudgetBytes });
|
|
1689
|
+
await mountStartupAdapter(ctx, rt.serverOptions);
|
|
1146
1690
|
createServer(ctx, port, { ...rt.serverOptions, owner: "pi-session" });
|
|
1147
1691
|
startedServer = true;
|
|
1148
1692
|
sLoad.done(`serving ${style.bold(m.repoId)} ${style.dim(`at ${baseUrl} · ready in ${(performance.now() - t0).toFixed(0)} ms`)}`);
|
|
@@ -1167,11 +1711,17 @@ switch (cmd) {
|
|
|
1167
1711
|
if (parsed.mode === "interactive")
|
|
1168
1712
|
console.log(`launching mlx-bun pi (model ${models[0]!.id}) — /help for commands, double-Ctrl+C to exit`);
|
|
1169
1713
|
restoreStdout();
|
|
1714
|
+
if (parsed.ignored.length) {
|
|
1715
|
+
const { style } = await import("./tui");
|
|
1716
|
+
console.log(style.dim(` ignoring pi flags: ${parsed.ignored.join(", ")}`));
|
|
1717
|
+
}
|
|
1170
1718
|
const code = await runEmbeddedPi({
|
|
1171
1719
|
baseUrl,
|
|
1172
1720
|
modelLabel: models[0]!.id,
|
|
1173
1721
|
contextWindow: models[0]!.contextWindow,
|
|
1174
1722
|
reasoning: models[0]!.reasoning,
|
|
1723
|
+
vision: models[0]!.vision,
|
|
1724
|
+
verbose: parsed.verbose,
|
|
1175
1725
|
mode: parsed.mode,
|
|
1176
1726
|
printFormat: parsed.printFormat,
|
|
1177
1727
|
initialMessage: message,
|
|
@@ -1238,6 +1788,15 @@ switch (cmd) {
|
|
|
1238
1788
|
process.exit(1);
|
|
1239
1789
|
}
|
|
1240
1790
|
const isOrpo = method === "orpo";
|
|
1791
|
+
// ORPO chosen-NLL scope (trainer sftScope; job.ts sft_scope). "full" =
|
|
1792
|
+
// paper/TRL-faithful prompt+response CE (the default); "response" =
|
|
1793
|
+
// response-only, bit-exact to pre-2026-07 runs. Validated here so junk
|
|
1794
|
+
// fails fast, before any model resolution or weight loading.
|
|
1795
|
+
const sftScope = opt("sft-scope");
|
|
1796
|
+
if (sftScope !== null && sftScope !== "full" && sftScope !== "response") {
|
|
1797
|
+
console.error(`--sft-scope must be full | response (got "${sftScope}")`);
|
|
1798
|
+
process.exit(1);
|
|
1799
|
+
}
|
|
1241
1800
|
// Validated numeric flag: default if absent, hard-exit on a non-number.
|
|
1242
1801
|
const numFlag = (name: string, dflt: number): number => {
|
|
1243
1802
|
const v = opt(name);
|
|
@@ -1297,6 +1856,7 @@ switch (cmd) {
|
|
|
1297
1856
|
grad_clip_norm: numFlag("grad-clip", 1.0),
|
|
1298
1857
|
val_max_examples: numFlag("val-size", 256),
|
|
1299
1858
|
warm_start_adapter: resume,
|
|
1859
|
+
...(sftScope ? { sft_scope: sftScope } : {}),
|
|
1300
1860
|
...(isOrpo ? {
|
|
1301
1861
|
orpo_lambda: numFlag("lambda", 0.1),
|
|
1302
1862
|
orpo_lr_schedule: "cosine",
|
|
@@ -1387,6 +1947,368 @@ switch (cmd) {
|
|
|
1387
1947
|
break;
|
|
1388
1948
|
}
|
|
1389
1949
|
|
|
1950
|
+
case "fuse": {
|
|
1951
|
+
// mlx_lm.fuse counterpart over our native machinery (src/train/fuse.ts):
|
|
1952
|
+
// fold a LoRA adapter into the base weights → standalone snapshot.
|
|
1953
|
+
const unsupported = ["--de-quantize", "--dequantize", "--export-gguf", "--gguf-path", "--upload-repo"]
|
|
1954
|
+
.filter((f) => argv.includes(f));
|
|
1955
|
+
if (unsupported.length > 0) {
|
|
1956
|
+
console.error(`${unsupported.join(", ")}: not supported (see: mlx-bun help fuse)`);
|
|
1957
|
+
process.exit(1);
|
|
1958
|
+
}
|
|
1959
|
+
const modelArg = positional(0) ?? opt("model");
|
|
1960
|
+
if (!modelArg) {
|
|
1961
|
+
console.error("usage: mlx-bun fuse <model-query-or-path> --adapter <dir> [--save-path <dir>]");
|
|
1962
|
+
process.exit(1);
|
|
1963
|
+
}
|
|
1964
|
+
const adapterDir = opt("adapter") ?? opt("adapter-path") ?? "adapters";
|
|
1965
|
+
const savePath = opt("save-path") ?? "fused_model";
|
|
1966
|
+
const { existsSync } = await import("node:fs");
|
|
1967
|
+
if (!existsSync(adapterDir)) {
|
|
1968
|
+
console.error(`adapter dir not found: ${adapterDir}`);
|
|
1969
|
+
process.exit(1);
|
|
1970
|
+
}
|
|
1971
|
+
let modelDir = modelArg;
|
|
1972
|
+
if (!existsSync(`${modelArg}/config.json`)) {
|
|
1973
|
+
const reg = new Registry();
|
|
1974
|
+
if (reg.list().length === 0) await reg.scan();
|
|
1975
|
+
modelDir = reg.resolve(modelArg).path;
|
|
1976
|
+
}
|
|
1977
|
+
const { banner, step, box, style } = await import("./tui");
|
|
1978
|
+
banner(pkg.version);
|
|
1979
|
+
const sNative = step("native runtime");
|
|
1980
|
+
await ensureNative(sNative);
|
|
1981
|
+
sNative.done("native runtime ready");
|
|
1982
|
+
const s = step(`fusing ${adapterDir} into ${modelDir}`);
|
|
1983
|
+
const { fuseAdapter } = await import("./train");
|
|
1984
|
+
try {
|
|
1985
|
+
const stats = await fuseAdapter(modelDir, adapterDir, savePath, (e) => s.update(e.message));
|
|
1986
|
+
s.done(`fused ${stats.fusedModules} module(s) ${style.dim(`· ${stats.totalTensors} tensors written`)}`);
|
|
1987
|
+
console.log();
|
|
1988
|
+
box([
|
|
1989
|
+
`${style.green("●")} ${style.bold("fuse complete")}`,
|
|
1990
|
+
"",
|
|
1991
|
+
`base ${style.dim(modelDir)}`,
|
|
1992
|
+
`adapter ${style.dim(adapterDir)}`,
|
|
1993
|
+
`model ${style.bold(stats.outDir)}`,
|
|
1994
|
+
...(stats.skippedAdapterTensors > 0
|
|
1995
|
+
? [`skipped ${style.dim(`${stats.skippedAdapterTensors} adapter tensor(s) with no matching base weight`)}`] : []),
|
|
1996
|
+
"",
|
|
1997
|
+
`serve it ${style.accent(`mlx-bun serve ${stats.outDir}`)}`,
|
|
1998
|
+
]);
|
|
1999
|
+
} catch (e) {
|
|
2000
|
+
s.fail(`fuse failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
2001
|
+
process.exit(1);
|
|
2002
|
+
}
|
|
2003
|
+
break;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
case "convert": {
|
|
2007
|
+
// mlx_lm.convert counterpart over our native quantize pipeline (the same
|
|
2008
|
+
// engine as the web /api/quantize): uniform affine 4/8-bit, or the OptiQ
|
|
2009
|
+
// mixed-precision path via --target-bpw.
|
|
2010
|
+
// --upload-repo (mlx_lm.convert parity): push the converted model to the
|
|
2011
|
+
// Hub afterwards, via the same engine as `mlx-bun upload`. The token is
|
|
2012
|
+
// resolved BEFORE any conversion work — failing after minutes of
|
|
2013
|
+
// quantizing because auth is missing would be cruel.
|
|
2014
|
+
const uploadRepo = ((): string | null => {
|
|
2015
|
+
if (!argv.includes("--upload-repo")) return null;
|
|
2016
|
+
const v = opt("upload-repo");
|
|
2017
|
+
if (!v || v.startsWith("-")) {
|
|
2018
|
+
console.error("--upload-repo expects a repo id (org/name)");
|
|
2019
|
+
process.exit(1);
|
|
2020
|
+
}
|
|
2021
|
+
return v;
|
|
2022
|
+
})();
|
|
2023
|
+
let uploadToken: string | null = null;
|
|
2024
|
+
if (uploadRepo) {
|
|
2025
|
+
const { getHfToken } = await import("./hf-push");
|
|
2026
|
+
uploadToken = getHfToken();
|
|
2027
|
+
if (!uploadToken) {
|
|
2028
|
+
console.error("--upload-repo needs a Hugging Face WRITE token and none was found —");
|
|
2029
|
+
console.error("run `hf auth login`, export HF_TOKEN, or save one in the web UI (Settings → Hugging Face).");
|
|
2030
|
+
process.exit(1);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
const unsupported = ["--dtype", "-d", "--dequantize", "--quant-predicate"]
|
|
2034
|
+
.filter((f) => argv.includes(f));
|
|
2035
|
+
if (unsupported.length > 0) {
|
|
2036
|
+
console.error(`${unsupported.join(", ")}: not supported (mixed precision: --target-bpw; see: mlx-bun help convert)`);
|
|
2037
|
+
process.exit(1);
|
|
2038
|
+
}
|
|
2039
|
+
const qMode = opt("q-mode", "affine")!;
|
|
2040
|
+
if (qMode !== "affine") {
|
|
2041
|
+
console.error(`--q-mode ${qMode}: only "affine" is supported`);
|
|
2042
|
+
process.exit(1);
|
|
2043
|
+
}
|
|
2044
|
+
const hfPath = opt("hf-path") ?? opt("model") ?? positional(0);
|
|
2045
|
+
if (!hfPath) {
|
|
2046
|
+
console.error("usage: mlx-bun convert --hf-path <repo-or-path> -q [--q-bits N] [--q-group-size N] [--mlx-path <dir>] [--target-bpw F]");
|
|
2047
|
+
process.exit(1);
|
|
2048
|
+
}
|
|
2049
|
+
const targetBpwRaw = opt("target-bpw");
|
|
2050
|
+
if (!argv.includes("-q") && !argv.includes("--quantize") && targetBpwRaw === null) {
|
|
2051
|
+
console.error("plain (non-quantizing) conversion is not supported yet — pass -q or --target-bpw");
|
|
2052
|
+
process.exit(1);
|
|
2053
|
+
}
|
|
2054
|
+
const targetBpw = targetBpwRaw !== null ? Number(targetBpwRaw) : undefined;
|
|
2055
|
+
if (targetBpwRaw !== null && (!Number.isFinite(targetBpw!) || targetBpw! <= 0)) {
|
|
2056
|
+
console.error(`--target-bpw expects a positive number (got "${targetBpwRaw}")`);
|
|
2057
|
+
process.exit(1);
|
|
2058
|
+
}
|
|
2059
|
+
const qBits = Number(opt("q-bits", "4"));
|
|
2060
|
+
if (qBits !== 4 && qBits !== 8) {
|
|
2061
|
+
console.error(`--q-bits must be 4 or 8 (got "${opt("q-bits")}")`);
|
|
2062
|
+
process.exit(1);
|
|
2063
|
+
}
|
|
2064
|
+
const qGroup = Number(opt("q-group-size", "64"));
|
|
2065
|
+
if (qGroup !== 32 && qGroup !== 64) {
|
|
2066
|
+
console.error(`--q-group-size must be 32 or 64 (got "${opt("q-group-size")}")`);
|
|
2067
|
+
process.exit(1);
|
|
2068
|
+
}
|
|
2069
|
+
const candidateBits = opt("candidate-bits")?.split(",").map((s) => Number(s.trim()));
|
|
2070
|
+
if (candidateBits && candidateBits.some((b) => !Number.isInteger(b) || b < 2 || b > 8)) {
|
|
2071
|
+
console.error(`--candidate-bits expects a comma list of integers in [2, 8] (got "${opt("candidate-bits")}")`);
|
|
2072
|
+
process.exit(1);
|
|
2073
|
+
}
|
|
2074
|
+
const mlxPath = opt("mlx-path", "mlx_model")!;
|
|
2075
|
+
const { existsSync } = await import("node:fs");
|
|
2076
|
+
if (existsSync(mlxPath)) {
|
|
2077
|
+
console.error(`Cannot save to the path ${mlxPath} as it already exists — delete it or pass a fresh --mlx-path.`);
|
|
2078
|
+
process.exit(1);
|
|
2079
|
+
}
|
|
2080
|
+
const { banner, step, box, style } = await import("./tui");
|
|
2081
|
+
banner(pkg.version);
|
|
2082
|
+
const sNative = step("native runtime");
|
|
2083
|
+
await ensureNative(sNative);
|
|
2084
|
+
sNative.done("native runtime ready");
|
|
2085
|
+
// Resolve the source: local dir → itself; downloaded model → registry;
|
|
2086
|
+
// otherwise an HF repo id, downloaded first (the point of the verb).
|
|
2087
|
+
let srcDir = hfPath;
|
|
2088
|
+
if (!existsSync(`${hfPath}/config.json`)) {
|
|
2089
|
+
const reg = new Registry();
|
|
2090
|
+
if (reg.list().length === 0) await reg.scan();
|
|
2091
|
+
try {
|
|
2092
|
+
srcDir = reg.resolve(hfPath).path;
|
|
2093
|
+
} catch (e) {
|
|
2094
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(hfPath)) {
|
|
2095
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
2096
|
+
process.exit(1);
|
|
2097
|
+
}
|
|
2098
|
+
const { downloadModel } = await import("./download");
|
|
2099
|
+
const sDl = step(`downloading ${hfPath}`);
|
|
2100
|
+
srcDir = await downloadModel(hfPath, {
|
|
2101
|
+
onProgress: (file, received, total) => {
|
|
2102
|
+
const pct = total ? Math.floor((received / total) * 100) : 0;
|
|
2103
|
+
sDl.update(`${style.bold(hfPath)} ${style.dim(`· ${file} · ${gb(received)} / ${gb(total)} (${pct}%)`)}`);
|
|
2104
|
+
},
|
|
2105
|
+
});
|
|
2106
|
+
sDl.done(`${style.bold(hfPath)} ${style.dim("downloaded · verified")}`);
|
|
2107
|
+
await reg.scan();
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
const sQ = step(
|
|
2111
|
+
targetBpw !== undefined
|
|
2112
|
+
? `quantizing (mixed, target ${targetBpw} bpw — sensitivity sweep, ~minutes)`
|
|
2113
|
+
: `quantizing (${qBits}-bit, group ${qGroup})`,
|
|
2114
|
+
);
|
|
2115
|
+
const { quantizeModelDir } = await import("./quantize");
|
|
2116
|
+
try {
|
|
2117
|
+
const r = await quantizeModelDir(
|
|
2118
|
+
srcDir,
|
|
2119
|
+
mlxPath,
|
|
2120
|
+
{
|
|
2121
|
+
bits: qBits as 4 | 8,
|
|
2122
|
+
groupSize: qGroup as 32 | 64,
|
|
2123
|
+
mode: "affine",
|
|
2124
|
+
...(targetBpw !== undefined ? { targetBpw } : {}),
|
|
2125
|
+
...(candidateBits ? { candidateBits } : {}),
|
|
2126
|
+
...(opt("calibration-mix") ? { calibrationMix: opt("calibration-mix")! } : {}),
|
|
2127
|
+
...(opt("n-calibration") ? { nCalibration: Number(opt("n-calibration")) } : {}),
|
|
2128
|
+
},
|
|
2129
|
+
(e) => sQ.update(e.message),
|
|
2130
|
+
);
|
|
2131
|
+
sQ.done(`quantized ${r.nQuantized} module(s) ${style.dim(`· ${r.achievedBpw.toFixed(2)} bpw achieved`)}`);
|
|
2132
|
+
console.log();
|
|
2133
|
+
box([
|
|
2134
|
+
`${style.green("●")} ${style.bold("convert complete")}`,
|
|
2135
|
+
"",
|
|
2136
|
+
`source ${style.dim(srcDir)}`,
|
|
2137
|
+
`model ${style.bold(r.outDir)} ${style.dim(`· ${gb(r.write.totalSize)}`)}`,
|
|
2138
|
+
`quant ${style.dim(targetBpw !== undefined ? `mixed ${r.achievedBpw.toFixed(2)} bpw (target ${targetBpw})` : `${qBits}-bit g${qGroup} affine`)}`,
|
|
2139
|
+
"",
|
|
2140
|
+
`serve it ${style.accent(`mlx-bun serve ${r.outDir}`)}`,
|
|
2141
|
+
]);
|
|
2142
|
+
if (uploadRepo) {
|
|
2143
|
+
const sUp = step(`uploading ${r.outDir} → ${uploadRepo}`);
|
|
2144
|
+
try {
|
|
2145
|
+
const { uploadFolder } = await import("./hf-push");
|
|
2146
|
+
const up = await uploadFolder(r.outDir, uploadRepo, {
|
|
2147
|
+
repoType: "model",
|
|
2148
|
+
token: uploadToken,
|
|
2149
|
+
commitMessage: "Upload with mlx-bun convert",
|
|
2150
|
+
onProgress: (file, sent, total) =>
|
|
2151
|
+
sUp.update(`${style.bold(uploadRepo)} ${style.dim(`· ${file} · ${gb(sent)} / ${gb(total)}`)}`),
|
|
2152
|
+
});
|
|
2153
|
+
sUp.done(`uploaded ${style.bold(up.url)}`);
|
|
2154
|
+
} catch (e) {
|
|
2155
|
+
sUp.fail(`upload failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
2156
|
+
console.error(`the converted model is intact at ${r.outDir} — retry with: mlx-bun upload --path ${r.outDir} --upload-repo ${uploadRepo}`);
|
|
2157
|
+
process.exit(1);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
} catch (e) {
|
|
2161
|
+
sQ.fail(`convert failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
2162
|
+
process.exit(1);
|
|
2163
|
+
}
|
|
2164
|
+
break;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
case "upload": {
|
|
2168
|
+
// mlx_lm.upload counterpart (same flag names: --path, --upload-repo) over
|
|
2169
|
+
// our native push-to-hub (src/hf-push.ts — the exact engine the web
|
|
2170
|
+
// /api/{quantize,finetune,dataset}/push routes call). Model-free until
|
|
2171
|
+
// the network: everything before uploadFolder is arg + token validation.
|
|
2172
|
+
const dir = opt("path", "mlx_model")!; // mlx_lm.upload's --path default
|
|
2173
|
+
const repo = ((): string | null => {
|
|
2174
|
+
const v = opt("upload-repo");
|
|
2175
|
+
return v && !v.startsWith("-") ? v : null;
|
|
2176
|
+
})();
|
|
2177
|
+
if (!repo) {
|
|
2178
|
+
console.error("usage: mlx-bun upload --path <model-dir> --upload-repo <org/repo> [--private]");
|
|
2179
|
+
process.exit(1);
|
|
2180
|
+
}
|
|
2181
|
+
const { existsSync, statSync } = await import("node:fs");
|
|
2182
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
|
|
2183
|
+
console.error(`not a directory: ${dir} (pass the model directory via --path)`);
|
|
2184
|
+
process.exit(1);
|
|
2185
|
+
}
|
|
2186
|
+
const { getHfToken, uploadFolder } = await import("./hf-push");
|
|
2187
|
+
const token = getHfToken();
|
|
2188
|
+
if (!token) {
|
|
2189
|
+
console.error("no Hugging Face token found — uploading needs a WRITE token from one of:");
|
|
2190
|
+
console.error(" hf auth login (~/.cache/huggingface/token)");
|
|
2191
|
+
console.error(" export HF_TOKEN=hf_…");
|
|
2192
|
+
console.error(" web UI Settings → Hugging Face (~/.mlx-bun/hf.json)");
|
|
2193
|
+
process.exit(1);
|
|
2194
|
+
}
|
|
2195
|
+
const { banner, step, box, style } = await import("./tui");
|
|
2196
|
+
banner(pkg.version);
|
|
2197
|
+
const s = step(`uploading ${dir} → ${repo}`);
|
|
2198
|
+
try {
|
|
2199
|
+
const r = await uploadFolder(dir, repo, {
|
|
2200
|
+
repoType: "model",
|
|
2201
|
+
private: flag("private"),
|
|
2202
|
+
token,
|
|
2203
|
+
commitMessage: "Upload with mlx-bun",
|
|
2204
|
+
onProgress: (file, sent, total) =>
|
|
2205
|
+
s.update(`${style.bold(repo)} ${style.dim(`· ${file} · ${gb(sent)} / ${gb(total)}`)}`),
|
|
2206
|
+
});
|
|
2207
|
+
s.done(`uploaded ${style.bold(repo)}`);
|
|
2208
|
+
console.log();
|
|
2209
|
+
box([
|
|
2210
|
+
`${style.green("●")} ${style.bold("upload complete")}`,
|
|
2211
|
+
"",
|
|
2212
|
+
`source ${style.dim(dir)}`,
|
|
2213
|
+
`repo ${style.url(r.url)}${flag("private") ? style.dim(" · private") : ""}`,
|
|
2214
|
+
"",
|
|
2215
|
+
`get it back anywhere: ${style.accent(`mlx-bun get ${repo}`)}`,
|
|
2216
|
+
]);
|
|
2217
|
+
} catch (e) {
|
|
2218
|
+
s.fail(`upload failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
2219
|
+
process.exit(1);
|
|
2220
|
+
}
|
|
2221
|
+
break;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
case "perplexity": {
|
|
2225
|
+
// mlx_lm.perplexity counterpart (methodology reproduced exactly — see
|
|
2226
|
+
// src/eval/perplexity.ts) over a LOCAL dataset file; never downloads data.
|
|
2227
|
+
const modelArg = positional(0) ?? opt("model") ?? opt("query");
|
|
2228
|
+
const dataPath = opt("data-path");
|
|
2229
|
+
if (!modelArg || !dataPath) {
|
|
2230
|
+
console.error("usage: mlx-bun perplexity <model-query-or-path> --data-path <file.txt|file.jsonl> [--sequence-length 512] [--num-samples 256] [--batch-size 8] [--seed 123]");
|
|
2231
|
+
process.exit(1);
|
|
2232
|
+
}
|
|
2233
|
+
const { existsSync } = await import("node:fs");
|
|
2234
|
+
if (!existsSync(dataPath)) {
|
|
2235
|
+
console.error(`data file not found: ${dataPath} (a local .txt or .jsonl — HF datasets are not downloaded)`);
|
|
2236
|
+
process.exit(1);
|
|
2237
|
+
}
|
|
2238
|
+
const intFlag = (name: string, dflt: number, lo: number): number => {
|
|
2239
|
+
const v = Number(opt(name, String(dflt)));
|
|
2240
|
+
if (!Number.isInteger(v) || (v < lo && v !== -1)) {
|
|
2241
|
+
console.error(`--${name} expects an integer >= ${lo} (got "${opt(name)}")`);
|
|
2242
|
+
process.exit(1);
|
|
2243
|
+
}
|
|
2244
|
+
return v;
|
|
2245
|
+
};
|
|
2246
|
+
const seqLen = intFlag("sequence-length", 512, 2);
|
|
2247
|
+
const numSamples = intFlag("num-samples", 256, 1); // -1 = all
|
|
2248
|
+
const batchSize = intFlag("batch-size", 8, 1);
|
|
2249
|
+
const seed = intFlag("seed", 123, 0);
|
|
2250
|
+
const { banner, step, box, style } = await import("./tui");
|
|
2251
|
+
banner(pkg.version);
|
|
2252
|
+
const sNative = step("native runtime");
|
|
2253
|
+
await ensureNative(sNative);
|
|
2254
|
+
sNative.done("native runtime ready");
|
|
2255
|
+
let modelDir = modelArg;
|
|
2256
|
+
if (!existsSync(`${modelArg}/config.json`)) {
|
|
2257
|
+
const reg = new Registry();
|
|
2258
|
+
if (reg.list().length === 0) await reg.scan();
|
|
2259
|
+
modelDir = reg.resolve(modelArg).path;
|
|
2260
|
+
}
|
|
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
|
+
const sLoad = step(`loading ${modelDir}`);
|
|
2267
|
+
const { Weights } = await import("./weights");
|
|
2268
|
+
const { createModel } = await import("./model/factory");
|
|
2269
|
+
const { loadTokenizer } = await import("./tokenizer");
|
|
2270
|
+
const { peakMemory, resetPeakMemory } = await import("./mlx/ffi");
|
|
2271
|
+
const config = await loadModelConfig(modelDir);
|
|
2272
|
+
const model = createModel(await Weights.open(modelDir), config);
|
|
2273
|
+
const tok = await loadTokenizer(modelDir);
|
|
2274
|
+
sLoad.done(`model loaded ${style.dim(`· ${modelDir}`)}`);
|
|
2275
|
+
|
|
2276
|
+
const { parseSamples, packRows, evalPpl } = await import("./eval/perplexity");
|
|
2277
|
+
const sData = step(`tokenizing ${dataPath}`);
|
|
2278
|
+
const samples = parseSamples(await Bun.file(dataPath).text(), dataPath);
|
|
2279
|
+
const rows = packRows(
|
|
2280
|
+
samples.map((t) => tok.encode(t)),
|
|
2281
|
+
{ sequenceLength: seqLen, numSamples, seed },
|
|
2282
|
+
);
|
|
2283
|
+
if (rows.length === 0) {
|
|
2284
|
+
sData.fail(`dataset too small: fewer than ${seqLen} tokens (need at least one full row)`);
|
|
2285
|
+
process.exit(1);
|
|
2286
|
+
}
|
|
2287
|
+
sData.done(`${rows.length} row(s) × ${seqLen} tokens ${style.dim(`· ${samples.length} sample(s), seed ${seed}`)}`);
|
|
2288
|
+
|
|
2289
|
+
const sEval = step(`evaluating (batch ${batchSize})`);
|
|
2290
|
+
resetPeakMemory();
|
|
2291
|
+
const t0 = performance.now();
|
|
2292
|
+
const r = evalPpl(model, rows, batchSize, (done, total) =>
|
|
2293
|
+
sEval.update(`batch ${done}/${total}`));
|
|
2294
|
+
const seconds = (performance.now() - t0) / 1000;
|
|
2295
|
+
sEval.done(`evaluated ${r.tokens.toLocaleString()} tokens ${style.dim(`in ${seconds.toFixed(1)} s`)}`);
|
|
2296
|
+
console.log();
|
|
2297
|
+
box([
|
|
2298
|
+
`${style.green("●")} ${style.bold("perplexity")} ${style.dim(`· ${modelDir.split("/snapshots/")[0]?.split("/").at(-1) ?? modelDir}`)}`,
|
|
2299
|
+
"",
|
|
2300
|
+
`ppl ${style.green(style.bold(r.ppl.toFixed(3)))} ${style.dim(`± ${r.standardError.toFixed(3)}`)}`,
|
|
2301
|
+
`mean CE ${style.bold(r.meanLoss.toFixed(4))} ${style.dim("nats/token")}`,
|
|
2302
|
+
`tokens ${style.bold(r.tokens.toLocaleString())} ${style.dim(`· ${r.rows} row(s) × ${seqLen}`)}`,
|
|
2303
|
+
`speed ${style.dim(`${(r.tokens / seconds).toFixed(0)} tok/s · peak ${gb(peakMemory())}`)}`,
|
|
2304
|
+
]);
|
|
2305
|
+
break;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
// `setup` is a true alias for `memory` — same handler, same subcommands
|
|
2309
|
+
// (`mlx-bun setup init` == `mlx-bun memory init`; the handler reads the
|
|
2310
|
+
// subcommand from argv[1] onward, identical for both spellings).
|
|
2311
|
+
case "setup":
|
|
1390
2312
|
case "memory": {
|
|
1391
2313
|
const { style, box, step, banner } = await import("./tui");
|
|
1392
2314
|
const {
|
|
@@ -1475,13 +2397,14 @@ switch (cmd) {
|
|
|
1475
2397
|
}
|
|
1476
2398
|
|
|
1477
2399
|
// Offer to install the nightly synthesis job (a launchd agent). The job
|
|
1478
|
-
//
|
|
1479
|
-
//
|
|
1480
|
-
//
|
|
2400
|
+
// runs the real synthesis DAG (`mlx-bun memory synthesize`) — create new
|
|
2401
|
+
// entity articles, then the editorial wikify sweep — so installing the
|
|
2402
|
+
// schedule keeps your wiki current automatically. Persistent action →
|
|
2403
|
+
// TTY-only, defaults to no.
|
|
1481
2404
|
let scheduled: string | null = null;
|
|
1482
2405
|
console.log(style.dim(
|
|
1483
|
-
"\n Synthesis (turning your conversations into articles)
|
|
1484
|
-
"
|
|
2406
|
+
"\n Synthesis (turning your conversations into articles) runs as a nightly job;\n" +
|
|
2407
|
+
" install it now so your wiki stays current automatically.",
|
|
1485
2408
|
));
|
|
1486
2409
|
if (await confirmYN(" Install the nightly synthesis job (runs in the background)?", false)) {
|
|
1487
2410
|
const atRaw = await ask(` At what time? 24h HH:MM ${style.dim("[03:00]")}: `, "03:00");
|
|
@@ -1507,8 +2430,8 @@ switch (cmd) {
|
|
|
1507
2430
|
? `nightly ${style.green("scheduled")} ${style.dim(`· ${scheduled} daily · mlx-bun memory unschedule to undo`)}`
|
|
1508
2431
|
: `nightly ${style.dim("not scheduled · mlx-bun memory schedule to set it up")}`,
|
|
1509
2432
|
"",
|
|
1510
|
-
style.dim("Synthesis (conversations → articles)
|
|
1511
|
-
style.dim("
|
|
2433
|
+
style.dim("Synthesis (conversations → articles) runs the full DAG: create new entity"),
|
|
2434
|
+
style.dim("articles, then an editorial wikify sweep. Run it now: mlx-bun memory synthesize."),
|
|
1512
2435
|
]);
|
|
1513
2436
|
break;
|
|
1514
2437
|
}
|
|
@@ -1528,8 +2451,7 @@ switch (cmd) {
|
|
|
1528
2451
|
`articles ${style.bold(String(st.articleCount))}`,
|
|
1529
2452
|
`reference ${style.bold(String(st.referenceCount))} ${style.dim("read-only docs")}`,
|
|
1530
2453
|
`git ${st.isGitRepo ? style.green("tracked") : style.dim("not a git repo")}`,
|
|
1531
|
-
`synthesis ${style.
|
|
1532
|
-
`last run ${style.dim("not available yet")}`,
|
|
2454
|
+
`synthesis ${style.green("available")} ${style.dim("· mlx-bun memory synthesize")}`,
|
|
1533
2455
|
`nightly ${
|
|
1534
2456
|
sched.installed
|
|
1535
2457
|
? `${sched.loaded ? style.green("scheduled") : style.accent("installed (not loaded)")} ${style.dim(`· ${sched.plistPath}`)}`
|
|
@@ -1570,7 +2492,70 @@ switch (cmd) {
|
|
|
1570
2492
|
break;
|
|
1571
2493
|
}
|
|
1572
2494
|
|
|
1573
|
-
|
|
2495
|
+
// Independent, chronological, resumable STAGE WORKERS. Each pulls its own
|
|
2496
|
+
// eligible work from the DB by state, processes a bounded batch, persists,
|
|
2497
|
+
// and exits — so a user can run the four as four separate processes on
|
|
2498
|
+
// different slices concurrently (GPU/memory allowing). `synthesize` (below)
|
|
2499
|
+
// remains the FULL DAG; these are its decomposed pieces.
|
|
2500
|
+
if (sub === "segment" || sub === "extract" || sub === "route" || sub === "synthesize-stage" || sub === "stage-synthesize") {
|
|
2501
|
+
const stages = await import("./memory/stages");
|
|
2502
|
+
const { MemoryStore } = await import("./memory/db");
|
|
2503
|
+
const limit = opt("limit") ? parseInt(opt("limit")!, 10) : undefined;
|
|
2504
|
+
const convsRaw = opt("convs");
|
|
2505
|
+
const convIds = convsRaw ? convsRaw.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
|
|
2506
|
+
const onEvent = (e: import("./memory/pipeline").SynthesisEvent) => {
|
|
2507
|
+
if (e.type === "stage") console.log(` ${style.dim("·")} ${e.message}`);
|
|
2508
|
+
else console.log(` ${e.message}`);
|
|
2509
|
+
};
|
|
2510
|
+
const store = new MemoryStore();
|
|
2511
|
+
console.log();
|
|
2512
|
+
try {
|
|
2513
|
+
if (sub === "segment") {
|
|
2514
|
+
const r = await stages.runSegmentStage(store, { convIds, limit, onEvent });
|
|
2515
|
+
console.log(style.dim(`\n segment: ${r.valid} segmented, ${r.chunks} chunks, ${r.skipped} skipped, ${r.errored} errored`));
|
|
2516
|
+
} else if (sub === "extract") {
|
|
2517
|
+
const r = await stages.runExtractStage(store, { convIds, limit, onEvent });
|
|
2518
|
+
console.log(style.dim(`\n extract: ${r.extracted} chunk(s) extracted, ${r.remaining} still pending`));
|
|
2519
|
+
} else if (sub === "route") {
|
|
2520
|
+
const r = await stages.runRouteStage(store, { convIds, onEvent });
|
|
2521
|
+
console.log(style.dim(`\n route: ${r.decisions.length} entities — ${r.createEligible} create-eligible, ${r.captured.length} captured`));
|
|
2522
|
+
} else {
|
|
2523
|
+
const r = await stages.runSynthesizeStage(store, { root, convIds, limit, onEvent });
|
|
2524
|
+
console.log(style.dim(`\n synthesize: ${r.created.length} created, ${r.patched.length} patched, ${r.skippedByGate.length} gated`));
|
|
2525
|
+
}
|
|
2526
|
+
} finally {
|
|
2527
|
+
store.close();
|
|
2528
|
+
}
|
|
2529
|
+
break;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// CROSS-LINK — the dedicated edge-building stage. Deterministic + idempotent:
|
|
2533
|
+
// inline-link first mentions of other articles + rebuild each ## See also from
|
|
2534
|
+
// mentions + co-occurrence. No model required.
|
|
2535
|
+
if (sub === "link") {
|
|
2536
|
+
const { runLinkStage } = await import("./memory/crosslink");
|
|
2537
|
+
const { MemoryStore } = await import("./memory/db");
|
|
2538
|
+
const limit = opt("limit") ? parseInt(opt("limit")!, 10) : undefined;
|
|
2539
|
+
const store = new MemoryStore();
|
|
2540
|
+
console.log();
|
|
2541
|
+
try {
|
|
2542
|
+
const r = await runLinkStage(store, {
|
|
2543
|
+
root,
|
|
2544
|
+
limit,
|
|
2545
|
+
onEvent: (e) => console.log(e.type === "stage" ? ` ${style.dim("·")} ${e.message}` : ` ${e.message}`),
|
|
2546
|
+
});
|
|
2547
|
+
console.log(
|
|
2548
|
+
style.dim(
|
|
2549
|
+
`\n link: ${r.linked.length} article(s) linked · ${r.mentionEdges} mention edge(s) · ${r.skippedByGate.length} gated`,
|
|
2550
|
+
),
|
|
2551
|
+
);
|
|
2552
|
+
} finally {
|
|
2553
|
+
store.close();
|
|
2554
|
+
}
|
|
2555
|
+
break;
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
if (sub === "synthesize" || sub === "pipeline" || sub === "all") {
|
|
1574
2559
|
const { runSynthesis } = await import("./memory/pipeline");
|
|
1575
2560
|
const dryRun = flag("dry-run");
|
|
1576
2561
|
console.log();
|
|
@@ -1595,7 +2580,7 @@ switch (cmd) {
|
|
|
1595
2580
|
: `${style.accent("●")} ${style.bold("plist written, but launchctl load failed")}`,
|
|
1596
2581
|
"",
|
|
1597
2582
|
`plist ${style.dim(r.plistPath)}`,
|
|
1598
|
-
`runs ${style.dim("mlx-bun memory synthesize (
|
|
2583
|
+
`runs ${style.dim("mlx-bun memory synthesize (full DAG: create + wikify sweep)")}`,
|
|
1599
2584
|
`undo ${style.accent("mlx-bun memory unschedule")}`,
|
|
1600
2585
|
]);
|
|
1601
2586
|
break;
|
|
@@ -1698,9 +2683,11 @@ switch (cmd) {
|
|
|
1698
2683
|
}
|
|
1699
2684
|
|
|
1700
2685
|
|
|
2686
|
+
// Unknown subcommand: help on stdout, but a FAILURE exit — scripts and
|
|
2687
|
+
// launchd jobs must be able to detect typos (printHelp would exit 0).
|
|
1701
2688
|
console.error(`unknown: mlx-bun memory ${sub}`);
|
|
1702
|
-
|
|
1703
|
-
|
|
2689
|
+
console.log(renderHelp(HELP.memory!));
|
|
2690
|
+
process.exit(1);
|
|
1704
2691
|
}
|
|
1705
2692
|
|
|
1706
2693
|
default:
|