mlx-bun 0.0.4
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/LICENSE +21 -0
- package/README.md +362 -0
- package/THIRD_PARTY_LICENSES.md +41 -0
- package/bin/mlx-bun.mjs +32 -0
- package/docs/curve-designer.html +303 -0
- package/docs/design/adapters-end-to-end.md +201 -0
- package/docs/design/compat-cli-surface-design.md +227 -0
- package/docs/design/docs-reorg-plan.md +152 -0
- package/docs/design/hlg-sampling.md +340 -0
- package/docs/design/memory-system.md +339 -0
- package/docs/design/optimization_plan.md +344 -0
- package/docs/design/parallel-slots.md +515 -0
- package/docs/design/runtime-isolation.md +247 -0
- package/docs/design/segmented-backward-training.md +508 -0
- package/docs/investigations/chunk-finetune-experiment.md +84 -0
- package/docs/investigations/curve-bisector-routing.md +123 -0
- package/docs/investigations/curve-runs/bisector-route.json +98 -0
- package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
- package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
- package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
- package/docs/investigations/curve-terrain.html +105 -0
- package/docs/investigations/expert-offload-single-user-moe.md +292 -0
- package/docs/investigations/hlg-ab.html +34 -0
- package/docs/investigations/hlg-explorer.html +238 -0
- package/docs/investigations/hlg-figs/cliff.svg +1 -0
- package/docs/investigations/hlg-figs/cross.svg +1 -0
- package/docs/investigations/hlg-figs/edges.svg +1 -0
- package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
- package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
- package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
- package/docs/investigations/hlg-figs/terrain.svg +1 -0
- package/docs/investigations/hlg-report.html +262 -0
- package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
- package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
- package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
- package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
- package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
- package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
- package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
- package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
- package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
- package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
- package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
- package/docs/investigations/hlg-runs/hlg-map.json +383 -0
- package/docs/investigations/hlg-runs/run0-original.log +159 -0
- package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
- package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
- package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
- package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
- package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
- package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
- package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
- package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
- package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
- package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
- package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
- package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
- package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
- package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
- package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
- package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
- package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
- package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
- package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
- package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
- package/docs/investigations/hlg-runs/runN-verify.log +116 -0
- package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
- package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
- package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
- package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
- package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
- package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
- package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
- package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
- package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
- package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
- package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
- package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
- package/docs/investigations/hlg-sampling-investigation.md +970 -0
- package/docs/investigations/lab-build-journal.md +157 -0
- package/docs/investigations/pi-builtin-investigation.md +308 -0
- package/docs/investigations/starter-model-port-handoff.md +323 -0
- package/docs/planning/IDEAS.md +26 -0
- package/docs/planning/PRODUCT_ROADMAP.md +426 -0
- package/docs/planning/ResearchTopics.md +119 -0
- package/docs/planning/chunk-task-roadmap.md +77 -0
- package/docs/planning/journal.md +155 -0
- package/docs/reference/distribution.md +121 -0
- package/docs/reference/embedding.md +168 -0
- package/docs/reference/library-api.md +153 -0
- package/docs/reference/server-api.md +224 -0
- package/docs/reference/server-config.md +243 -0
- package/docs/segmented-backward-handoff.md +148 -0
- package/package.json +68 -0
- package/src/anthropic.ts +491 -0
- package/src/chat-template.ts +186 -0
- package/src/cli.ts +1075 -0
- package/src/config.ts +229 -0
- package/src/curve-sampler.ts +122 -0
- package/src/dataset/generators.ts +1087 -0
- package/src/dataset/index.ts +12 -0
- package/src/dataset/job.ts +81 -0
- package/src/dataset/llm.ts +71 -0
- package/src/dataset/registry.ts +528 -0
- package/src/download.ts +283 -0
- package/src/eval/README.md +86 -0
- package/src/eval/kl-prompts.ts +29 -0
- package/src/eval/kl.ts +294 -0
- package/src/eval/quality-db.ts +92 -0
- package/src/eval/runner.ts +102 -0
- package/src/eval/score.ts +49 -0
- package/src/eval/tasks/bfcl.ts +500 -0
- package/src/eval/tasks/gsm8k.ts +85 -0
- package/src/eval/tasks/hashhop.ts +109 -0
- package/src/eval/tasks/humaneval.ts +252 -0
- package/src/eval/tasks/ifeval.ts +354 -0
- package/src/eval/tasks/mmlu.ts +145 -0
- package/src/evaldb.ts +199 -0
- package/src/expert-offload-build.ts +77 -0
- package/src/expert-offload.ts +61 -0
- package/src/expert-trace.ts +67 -0
- package/src/fit.ts +254 -0
- package/src/generate.ts +451 -0
- package/src/harness-pi.ts +177 -0
- package/src/hf-push.ts +628 -0
- package/src/index.ts +22 -0
- package/src/jobs/db.ts +139 -0
- package/src/jobs/index.ts +29 -0
- package/src/jobs/job-entry.ts +106 -0
- package/src/jobs/runner.ts +304 -0
- package/src/jobs/sse.ts +114 -0
- package/src/jobs/types.ts +58 -0
- package/src/kv-store.ts +185 -0
- package/src/lora.ts +314 -0
- package/src/md-modules.d.ts +7 -0
- package/src/mlx/array.ts +237 -0
- package/src/mlx/autograd.ts +267 -0
- package/src/mlx/checkpoint.ts +116 -0
- package/src/mlx/compile.ts +158 -0
- package/src/mlx/custom-vjp.ts +140 -0
- package/src/mlx/ffi.ts +433 -0
- package/src/mlx/metal-kernel.ts +130 -0
- package/src/mlx/ops.ts +707 -0
- package/src/mmap.ts +90 -0
- package/src/model/batched-mask.ts +204 -0
- package/src/model/batched-rotating.ts +300 -0
- package/src/model/compiled-decode.ts +681 -0
- package/src/model/factory.ts +27 -0
- package/src/model/fingerprint.ts +33 -0
- package/src/model/flash-attention.ts +674 -0
- package/src/model/fused-decode-kernel.ts +305 -0
- package/src/model/fused-geglu-kernel.ts +87 -0
- package/src/model/gemma4-base.ts +1616 -0
- package/src/model/gemma4.ts +1012 -0
- package/src/model/generated/gemma4-12b.ts +526 -0
- package/src/model/generated/gemma4-26b.ts +500 -0
- package/src/model/generated/gemma4-e4b.ts +757 -0
- package/src/model/generated/index.ts +18 -0
- package/src/model/minicpm5.ts +278 -0
- package/src/model/qwen3-delta.ts +224 -0
- package/src/model/qwen3_5.ts +429 -0
- package/src/model/support.ts +46 -0
- package/src/native-pack.ts +152 -0
- package/src/pi-provider.ts +95 -0
- package/src/pi-terminal.ts +264 -0
- package/src/pi-web.ts +924 -0
- package/src/preflight.ts +123 -0
- package/src/prompt-cache.ts +113 -0
- package/src/quantize/allocator.ts +627 -0
- package/src/quantize/calibration.ts +180 -0
- package/src/quantize/config-writer.ts +132 -0
- package/src/quantize/index.ts +57 -0
- package/src/quantize/job.ts +136 -0
- package/src/quantize/quantizer.ts +462 -0
- package/src/quantize/safetensors-writer.ts +171 -0
- package/src/quantize/sensitivity.ts +234 -0
- package/src/registry.ts +336 -0
- package/src/responses.ts +774 -0
- package/src/safetensors.ts +158 -0
- package/src/sampler.ts +517 -0
- package/src/serve/batch-scheduler.ts +362 -0
- package/src/serve/generation-gateway.ts +160 -0
- package/src/server.ts +1891 -0
- package/src/spec/drafter.ts +261 -0
- package/src/spec/generate.ts +221 -0
- package/src/status-page.html +497 -0
- package/src/tokenizer.ts +38 -0
- package/src/tool-call.ts +242 -0
- package/src/train/dataset.ts +461 -0
- package/src/train/export.ts +32 -0
- package/src/train/forward.ts +229 -0
- package/src/train/index.ts +43 -0
- package/src/train/job.ts +148 -0
- package/src/train/lora-params.ts +194 -0
- package/src/train/loss.ts +342 -0
- package/src/train/merge.ts +195 -0
- package/src/train/optimizer.ts +176 -0
- package/src/train/rank.ts +101 -0
- package/src/train/segmented.ts +520 -0
- package/src/train/trainer.ts +600 -0
- package/src/tui.ts +161 -0
- package/src/vision/embedder.ts +168 -0
- package/src/vision/preprocess.ts +247 -0
- package/src/vision/prompt.ts +158 -0
- package/src/vision/siglip.ts +599 -0
- package/src/web/app.html +2471 -0
- package/src/web/skills/web-research/SKILL.md +44 -0
- package/src/web/skills.ts +59 -0
- package/src/web-tools.ts +426 -0
- package/src/weights.ts +91 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,1075 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// mlx-bun CLI: the appliance interface.
|
|
3
|
+
//
|
|
4
|
+
// mlx-bun get <org/repo> [--revision main] resumable verified download
|
|
5
|
+
// mlx-bun scan index the HF cache
|
|
6
|
+
// mlx-bun ls [--vision] [--max-size 10GB] [query]
|
|
7
|
+
// mlx-bun fit <query> [--ctx 32768] [--skus]
|
|
8
|
+
// mlx-bun serve [query] [--port 8090] [--memory-budget GB]
|
|
9
|
+
// mlx-bun evals recent benchmark runs
|
|
10
|
+
// mlx-bun harness pi [--base-url <url>] [--remove] connect your own pi to the local server
|
|
11
|
+
|
|
12
|
+
import { Registry } from "./registry";
|
|
13
|
+
import { loadModelConfig } from "./config";
|
|
14
|
+
import { fit, skuMatrix, thisMachine } from "./fit";
|
|
15
|
+
import { EvalDB } from "./evaldb";
|
|
16
|
+
import pkg from "../package.json" with { type: "json" };
|
|
17
|
+
import { renderHelp } from "./tui";
|
|
18
|
+
import { isSupportedModelRecord } from "./model/support";
|
|
19
|
+
import { perfKernelEnabled } from "./model/fused-decode-kernel";
|
|
20
|
+
|
|
21
|
+
const argv = process.argv.slice(2);
|
|
22
|
+
// The appliance path: naked `mlx-bun` (or only options, e.g.
|
|
23
|
+
// `mlx-bun --port 9000`) runs `serve` — first run downloads a model and
|
|
24
|
+
// opens the chat UI. `--help`/`--version` and explicit subcommands still win.
|
|
25
|
+
const HELP_VERSION_FLAGS = ["--help", "-h", "--version", "-v"];
|
|
26
|
+
const noSubcommand =
|
|
27
|
+
!argv[0] || (argv[0].startsWith("-") && !HELP_VERSION_FLAGS.includes(argv[0]));
|
|
28
|
+
const cmd = noSubcommand ? "serve" : argv[0];
|
|
29
|
+
|
|
30
|
+
const OVERVIEW = `mlx-bun ${pkg.version} — local AI on Apple silicon. One binary, no Python.
|
|
31
|
+
|
|
32
|
+
Usage: mlx-bun [command] [options] (no command = serve)
|
|
33
|
+
|
|
34
|
+
Commands:
|
|
35
|
+
pi Launch a pi coding-agent session on a local model (the appliance path)
|
|
36
|
+
serve Start the OpenAI/Anthropic-compatible server + status page
|
|
37
|
+
get Download a model from Hugging Face (resumable, verified)
|
|
38
|
+
ls List downloaded models
|
|
39
|
+
fit Will a model fit this machine? Memory + speed assessment
|
|
40
|
+
scan Re-index the Hugging Face cache
|
|
41
|
+
harness Connect your own pi install to the local mlx-bun server
|
|
42
|
+
benchmark Measure decode/prefill speed of OUR stack on this machine
|
|
43
|
+
evals Show recent benchmark runs (all stacks)
|
|
44
|
+
help Show help for a command (also: mlx-bun <command> --help)
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
-h, --help Show help
|
|
48
|
+
-v, --version Show version
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
mlx-bun # download (first run) + serve + open the chat UI
|
|
52
|
+
mlx-bun pi # start (download if needed) and chat with an agent
|
|
53
|
+
mlx-bun serve 12B # serve the 12B; status page at http://localhost:8090/
|
|
54
|
+
mlx-bun get mlx-community/gemma-4-12B-it-OptiQ-4bit`;
|
|
55
|
+
|
|
56
|
+
const SERVER_FLAGS = `Server options:
|
|
57
|
+
--host <addr> Interface to bind [default: all interfaces;
|
|
58
|
+
use 127.0.0.1 for loopback-only]
|
|
59
|
+
--port <n> Listen port [default: 8090]
|
|
60
|
+
--memory-budget <GB> Admission-control memory budget; requests that
|
|
61
|
+
cannot fit are rejected instead of crashing the
|
|
62
|
+
GPU [default: machine RAM × 0.75, check-only]
|
|
63
|
+
--prompt-cache <GB> Prompt (KV) cache byte cap [default: 2 GB]
|
|
64
|
+
--batch <n> Max concurrent requests batched through the
|
|
65
|
+
mlx-lm-parity engine [default: 1 = serial].
|
|
66
|
+
>1 opts the whole server into bf16 continuous
|
|
67
|
+
batching (= mlx-lm B=N); see --kv-quant.
|
|
68
|
+
--no-open Don't open the chat UI in your browser on start.
|
|
69
|
+
By default an interactive terminal opens
|
|
70
|
+
http://<host>:<port>/#/chat once the server is up.
|
|
71
|
+
|
|
72
|
+
Model & quality:
|
|
73
|
+
--kv-quant <mode> KV cache quantization: config (per-layer
|
|
74
|
+
kv_config.json when the model ships one), off
|
|
75
|
+
(bf16), or 4 / 8 (uniform bits)
|
|
76
|
+
[default: config; bf16 under --batch N — the
|
|
77
|
+
batched engine is bf16-only, so an explicit
|
|
78
|
+
--kv-quant routes those requests to the serial path]
|
|
79
|
+
--thinking <true|false> Default for the chat template's enable_thinking
|
|
80
|
+
variable (CPM and other hybrid-reasoning models);
|
|
81
|
+
a request's chat_template_kwargs overrides it
|
|
82
|
+
[default: the model's own, false for CPM]
|
|
83
|
+
--temperature <n> Server-wide sampling defaults; a per-request field
|
|
84
|
+
--top-p <n> still overrides them, and the browser chat (which
|
|
85
|
+
--top-k <n> sends none) inherits them
|
|
86
|
+
[default: the model's generation_config.json]
|
|
87
|
+
--hlg-sampling on|off Piecewise tone-curve sampling (HLG): rolls off the
|
|
88
|
+
top, boosts the mids, gentles the tail. Gain folds
|
|
89
|
+
from --temperature. [default: off]
|
|
90
|
+
See docs/design/hlg-sampling.md
|
|
91
|
+
--hlg-width <nats> HLG mid-region half-width [default: 4]
|
|
92
|
+
--hlg-shoulder <nats> HLG highlight rolloff scale [default: 4]
|
|
93
|
+
--hlg-toe <nats> HLG shadow rolloff scale [default: 6]
|
|
94
|
+
--hlg-pivot-offset <nats> HLG pivot: nats below the top token [default: 6]
|
|
95
|
+
|
|
96
|
+
Performance levers (A/B levers; defaults are the measured winners):
|
|
97
|
+
--compiled-decode on|off Compiled decode graphs [default: on]
|
|
98
|
+
--perf-kernel on|off Fused quantized-KV decode-SDPA Metal kernel
|
|
99
|
+
(perf side of the compat A/B) [default: on]
|
|
100
|
+
--fused-decode on|off Fused-decode experiment lever [default: off]
|
|
101
|
+
--fused-sdpa on|off Fused SDPA path [default: on]
|
|
102
|
+
--force-wire Wire weights into memory at load
|
|
103
|
+
--expert-offload MoE only: serve experts from a page-aligned file
|
|
104
|
+
mmap (built on first use) — keeps the model out of
|
|
105
|
+
memory pressure (phys_footprint ≈ active params),
|
|
106
|
+
bit-exact with the resident path`;
|
|
107
|
+
|
|
108
|
+
const HELP: Record<string, string> = {
|
|
109
|
+
pi: `mlx-bun pi — drop into a coding-agent session on a local model
|
|
110
|
+
|
|
111
|
+
Usage: mlx-bun pi [options] [message...]
|
|
112
|
+
|
|
113
|
+
Runs the pi coding agent's own terminal UI in-process against the local
|
|
114
|
+
model — nothing to install, pi is built in. Reuses a healthy local
|
|
115
|
+
server when one is running; otherwise serves e4b by default (override
|
|
116
|
+
with --query) and starts a server for the session (the server ends with
|
|
117
|
+
the session). Fresh install: downloads a small starter model first so
|
|
118
|
+
you're chatting in minutes, then streams e4b in the background — it
|
|
119
|
+
becomes the default on the next run.
|
|
120
|
+
|
|
121
|
+
Model selection:
|
|
122
|
+
-q, --query <q> Model to serve when starting a server (registry query)
|
|
123
|
+
|
|
124
|
+
${SERVER_FLAGS}
|
|
125
|
+
|
|
126
|
+
Run modes:
|
|
127
|
+
message... Pre-fill the first turn (interactive)
|
|
128
|
+
-p, --print [msg] One-shot: print the reply and exit (also reads piped
|
|
129
|
+
stdin); add --json for the full event stream
|
|
130
|
+
--mode json|rpc Structured print (json) or JSONL RPC over stdio
|
|
131
|
+
|
|
132
|
+
Already use pi? Connect your own pi to this local model instead:
|
|
133
|
+
mlx-bun harness pi`,
|
|
134
|
+
|
|
135
|
+
serve: `mlx-bun serve — OpenAI/Anthropic-compatible server for a local model
|
|
136
|
+
|
|
137
|
+
Usage: mlx-bun serve [query] [options]
|
|
138
|
+
|
|
139
|
+
[query] Registry query (e.g. "12B", "e4b", a repo substring).
|
|
140
|
+
Omitted: serves e4b (the default), else the largest
|
|
141
|
+
downloaded model that fits. Fresh install: downloads a
|
|
142
|
+
small starter model first, then e4b in the background
|
|
143
|
+
(default on next run).
|
|
144
|
+
|
|
145
|
+
${SERVER_FLAGS}
|
|
146
|
+
|
|
147
|
+
Endpoints: /v1/chat/completions, /v1/messages, /v1/responses, /v1/models,
|
|
148
|
+
/v1/adapters, /stats, /fit, /library, /downloads — status page at /,
|
|
149
|
+
browser chat at /chat`,
|
|
150
|
+
|
|
151
|
+
get: `mlx-bun get — download a model from Hugging Face
|
|
152
|
+
|
|
153
|
+
Usage: mlx-bun get <org/repo> [options]
|
|
154
|
+
|
|
155
|
+
Options:
|
|
156
|
+
--revision <rev> Git revision [default: main]
|
|
157
|
+
|
|
158
|
+
Resumable (Range requests against partial blobs) and verified (sha256
|
|
159
|
+
for LFS blobs). Re-running after an interruption continues where it
|
|
160
|
+
stopped. Uses HF_TOKEN / hf auth login credentials when present.`,
|
|
161
|
+
|
|
162
|
+
ls: `mlx-bun ls — list downloaded models
|
|
163
|
+
|
|
164
|
+
Usage: mlx-bun ls [query] [options]
|
|
165
|
+
|
|
166
|
+
Options:
|
|
167
|
+
--vision Only models with a vision sidecar
|
|
168
|
+
--max-size <size> Filter by weight size (e.g. 10GB, 800MB)`,
|
|
169
|
+
|
|
170
|
+
fit: `mlx-bun fit — will this model fit on this machine?
|
|
171
|
+
|
|
172
|
+
Usage: mlx-bun fit <query> [options]
|
|
173
|
+
|
|
174
|
+
Options:
|
|
175
|
+
--ctx <tokens> Context size to assess [default: 32768]
|
|
176
|
+
--skus Also print the Apple silicon SKU matrix
|
|
177
|
+
|
|
178
|
+
Solves weights + KV growth + prefill transient against wired memory and
|
|
179
|
+
predicts decode speed from memory bandwidth.`,
|
|
180
|
+
|
|
181
|
+
scan: `mlx-bun scan — re-index the Hugging Face cache
|
|
182
|
+
|
|
183
|
+
Usage: mlx-bun scan
|
|
184
|
+
|
|
185
|
+
Reads config.json + safetensors headers (never tensor bytes) for every
|
|
186
|
+
snapshot in ~/.cache/huggingface/hub and refreshes the model registry.`,
|
|
187
|
+
|
|
188
|
+
harness: `mlx-bun harness pi — connect your own pi to the local model
|
|
189
|
+
|
|
190
|
+
Usage: mlx-bun harness pi [options]
|
|
191
|
+
|
|
192
|
+
For people who already use pi: this points your existing pi install at
|
|
193
|
+
the local mlx-bun server so you can run pi the way you already do, on a
|
|
194
|
+
local model. (To just chat now with no setup, use \`mlx-bun pi\` — the
|
|
195
|
+
built-in agent.)
|
|
196
|
+
|
|
197
|
+
Options:
|
|
198
|
+
--base-url <url> Server base URL [default: http://localhost:8090/v1]
|
|
199
|
+
--remove Disconnect (delete the extension)
|
|
200
|
+
|
|
201
|
+
Installs a small discovery extension into ~/.pi/agent/extensions that
|
|
202
|
+
registers mlx-bun as a pi provider (models discovered live from
|
|
203
|
+
/v1/models at pi startup). One file, reversible with --remove; never
|
|
204
|
+
touches your existing pi config. Then: pi --provider mlx-bun`,
|
|
205
|
+
|
|
206
|
+
benchmark: `mlx-bun benchmark — measure OUR stack on this machine
|
|
207
|
+
|
|
208
|
+
Usage: mlx-bun benchmark [query] [options]
|
|
209
|
+
|
|
210
|
+
Runs mlx-bun by itself (no other systems required) and records the
|
|
211
|
+
result to the eval DB. For quotable, cross-stack numbers use
|
|
212
|
+
./benchmark.sh from the repo — it preflight-gates on an idle machine
|
|
213
|
+
and runs the mlx-lm/optiq comparison legs.
|
|
214
|
+
|
|
215
|
+
[query] Model to benchmark [default: auto-pick]
|
|
216
|
+
|
|
217
|
+
Options:
|
|
218
|
+
--tokens <n> Tokens to decode per run [default: 256]
|
|
219
|
+
--runs <n> Runs (median reported) [default: 3]
|
|
220
|
+
--prompt-tokens <n> Pad the prompt to ~n tokens (long-context decode)
|
|
221
|
+
--kv-quant <mode> config | off | 4 | 8 [default: off, the
|
|
222
|
+
historical baseline]
|
|
223
|
+
|
|
224
|
+
Performance levers (--compiled-decode, --perf-kernel, --fused-decode,
|
|
225
|
+
--fused-sdpa) apply to the run — A/B by running twice.`,
|
|
226
|
+
|
|
227
|
+
evals: `mlx-bun evals — recent benchmark runs
|
|
228
|
+
|
|
229
|
+
Usage: mlx-bun evals [options]
|
|
230
|
+
|
|
231
|
+
Options:
|
|
232
|
+
--limit <n> Rows to show [default: 20]
|
|
233
|
+
--raw Full records as JSON lines (for scripts/jq)
|
|
234
|
+
--clear Archive the eval DB (timestamped backup) and start fresh
|
|
235
|
+
|
|
236
|
+
Table view: when, model, bench kind, KV mode, decode tok/s, TTFT,
|
|
237
|
+
peak memory, commit. Runs are written by ./benchmark.sh.`,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
HELP.bench = HELP.benchmark!;
|
|
241
|
+
|
|
242
|
+
function printHelp(topic?: string): never {
|
|
243
|
+
if (topic && HELP[topic]) console.log(renderHelp(HELP[topic]));
|
|
244
|
+
else if (topic) {
|
|
245
|
+
console.error(`unknown command: ${topic}\n`);
|
|
246
|
+
console.log(renderHelp(OVERVIEW));
|
|
247
|
+
process.exit(1);
|
|
248
|
+
} else console.log(renderHelp(OVERVIEW));
|
|
249
|
+
process.exit(0);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (cmd === "--help" || cmd === "-h") printHelp();
|
|
253
|
+
if (cmd === "--version" || cmd === "-v" || cmd === "version") {
|
|
254
|
+
console.log(`mlx-bun ${pkg.version}`);
|
|
255
|
+
process.exit(0);
|
|
256
|
+
}
|
|
257
|
+
if (cmd === "help") printHelp(argv[1]);
|
|
258
|
+
// Per-command --help/-h — except `pi`, whose extra args pass through
|
|
259
|
+
// (use `mlx-bun help pi` for our help, `pi --help` for pi's).
|
|
260
|
+
if (cmd !== "pi" && (argv.includes("--help") || argv.includes("-h"))) printHelp(cmd);
|
|
261
|
+
if (cmd === "pi" && (argv[1] === "--help" || argv[1] === "-h") && argv.length === 2) printHelp("pi");
|
|
262
|
+
|
|
263
|
+
const flag = (name: string): boolean => argv.includes(`--${name}`);
|
|
264
|
+
const opt = (name: string, dflt: string | null = null): string | null => {
|
|
265
|
+
const i = argv.indexOf(`--${name}`);
|
|
266
|
+
return i > -1 && argv[i + 1] ? argv[i + 1]! : dflt;
|
|
267
|
+
};
|
|
268
|
+
/** Single-dash value flag (e.g. -q), the short-alias counterpart to opt(). */
|
|
269
|
+
const optShort = (short: string, dflt: string | null = null): string | null => {
|
|
270
|
+
const i = argv.indexOf(`-${short}`);
|
|
271
|
+
return i > -1 && argv[i + 1] ? argv[i + 1]! : dflt;
|
|
272
|
+
};
|
|
273
|
+
const positional = (n: number): string | undefined =>
|
|
274
|
+
argv.filter((a, i) => !a.startsWith("--") && (i === 0 || !argv[i - 1]!.startsWith("--"))).slice(1)[n];
|
|
275
|
+
|
|
276
|
+
const gb = (bytes: number) => `${(bytes / 2 ** 30).toFixed(2)} GB`;
|
|
277
|
+
|
|
278
|
+
/** First-run: fetch the MLX native runtime pack if no copy is resolvable
|
|
279
|
+
* (env / beside the binary / cache / homebrew). ~52 MB, resumable,
|
|
280
|
+
* sha256-verified. Must run BEFORE importing anything that dlopens
|
|
281
|
+
* (./server, ./generate — they load libmlxc at module scope). */
|
|
282
|
+
async function ensureNative(s?: import("./tui").Step): Promise<void> {
|
|
283
|
+
const { nativeRuntimeDir, ensureNativeRuntime, NATIVE_PACK_VERSION } = await import("./native-pack");
|
|
284
|
+
if (nativeRuntimeDir()) return;
|
|
285
|
+
s?.update(`downloading the MLX native runtime (v${NATIVE_PACK_VERSION}, ~52 MB)`);
|
|
286
|
+
await ensureNativeRuntime({
|
|
287
|
+
onProgress: (received, total) => {
|
|
288
|
+
const pct = total ? Math.floor((received / total) * 100) : 0;
|
|
289
|
+
const line = `MLX native runtime ${gb(received)} / ${gb(total)} (${pct}%)`;
|
|
290
|
+
if (s) s.update(line); else process.stdout.write(`\r ${line} `);
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
if (!s) process.stdout.write("\n");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** AppleScript that focuses the first tab whose URL contains `match` in a
|
|
297
|
+
* literal-named browser, returning "ok"/"miss". The app name MUST be a literal
|
|
298
|
+
* (not a variable): AppleScript loads an app's scripting dictionary by literal
|
|
299
|
+
* name at COMPILE time, so `tell application someVar` can't resolve app-specific
|
|
300
|
+
* terms like `active tab index` (fails with -2740). Referencing an uninstalled
|
|
301
|
+
* app by literal name is also a compile error a `try` can't catch — hence we
|
|
302
|
+
* only ever run a browser's script when pgrep says it's running (= installed). */
|
|
303
|
+
function focusTabScript(app: string, kind: "chromium" | "safari", match: string): string {
|
|
304
|
+
const m = JSON.stringify(match); // safe AppleScript string literal
|
|
305
|
+
if (kind === "safari") {
|
|
306
|
+
return `tell application "${app}"
|
|
307
|
+
repeat with w in windows
|
|
308
|
+
repeat with t in tabs of w
|
|
309
|
+
if URL of t contains ${m} then
|
|
310
|
+
set current tab of w to t
|
|
311
|
+
activate
|
|
312
|
+
return "ok"
|
|
313
|
+
end if
|
|
314
|
+
end repeat
|
|
315
|
+
end repeat
|
|
316
|
+
end tell
|
|
317
|
+
return "miss"`;
|
|
318
|
+
}
|
|
319
|
+
return `tell application "${app}"
|
|
320
|
+
repeat with w in windows
|
|
321
|
+
set k to 0
|
|
322
|
+
repeat with t in tabs of w
|
|
323
|
+
set k to k + 1
|
|
324
|
+
if URL of t contains ${m} then
|
|
325
|
+
set active tab index of w to k
|
|
326
|
+
set index of w to 1
|
|
327
|
+
activate
|
|
328
|
+
return "ok"
|
|
329
|
+
end if
|
|
330
|
+
end repeat
|
|
331
|
+
end repeat
|
|
332
|
+
end tell
|
|
333
|
+
return "miss"`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Open the chat UI, reusing an already-open tab on this host:port instead of
|
|
337
|
+
* spawning a duplicate. Each browser is scripted via its own literal-named
|
|
338
|
+
* AppleScript, run ONLY when pgrep confirms it's running (so the literal `tell`
|
|
339
|
+
* always compiles). Falls back to a plain `open` (new tab) when no running
|
|
340
|
+
* browser has the tab, for browsers we don't script (e.g. Firefox), or if
|
|
341
|
+
* AppleScript is blocked. Best-effort and fire-and-forget: any failure just
|
|
342
|
+
* means a fresh tab, so it never regresses the plain-open path. The first focus
|
|
343
|
+
* may trigger a one-time macOS "control your browser" permission prompt;
|
|
344
|
+
* declining it just falls back to opening a new tab. */
|
|
345
|
+
function openChatUi(url: string, hostPort: string): void {
|
|
346
|
+
const browsers: Array<{ proc: string; kind: "chromium" | "safari" }> = [
|
|
347
|
+
{ proc: "Google Chrome", kind: "chromium" },
|
|
348
|
+
{ proc: "Arc", kind: "chromium" },
|
|
349
|
+
{ proc: "Brave Browser", kind: "chromium" },
|
|
350
|
+
{ proc: "Microsoft Edge", kind: "chromium" },
|
|
351
|
+
{ proc: "Safari", kind: "safari" },
|
|
352
|
+
];
|
|
353
|
+
void (async () => {
|
|
354
|
+
for (const b of browsers) {
|
|
355
|
+
try {
|
|
356
|
+
const running = Bun.spawn(["pgrep", "-x", b.proc], { stdout: "ignore", stderr: "ignore" });
|
|
357
|
+
if ((await running.exited) !== 0) continue; // not running → skip (and don't compile its tell)
|
|
358
|
+
const script = focusTabScript(b.proc, b.kind, hostPort);
|
|
359
|
+
const p = Bun.spawn(["osascript", "-e", script], { stdout: "pipe", stderr: "ignore", timeout: 8000 });
|
|
360
|
+
const out = await new Response(p.stdout).text();
|
|
361
|
+
if ((await p.exited) === 0 && out.trim() === "ok") return; // focused an existing tab
|
|
362
|
+
} catch { /* try the next browser */ }
|
|
363
|
+
}
|
|
364
|
+
try { Bun.spawn(["open", url], { stdout: "ignore", stderr: "ignore" }); } catch { /* best-effort */ }
|
|
365
|
+
})();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Server/runtime flags shared by every mode that loads a model
|
|
369
|
+
* (serve, pi). Env levers are set here so they're in place before the
|
|
370
|
+
* generate/compiled-decode modules read them. */
|
|
371
|
+
function serverRuntimeFlags(): { port: number; serverOptions: import("./server").ServerOptions } {
|
|
372
|
+
const onOff = (name: string): boolean | null => {
|
|
373
|
+
const v = opt(name);
|
|
374
|
+
if (v === null) return null;
|
|
375
|
+
if (v === "on" || v === "1" || v === "true") return true;
|
|
376
|
+
if (v === "off" || v === "0" || v === "false") return false;
|
|
377
|
+
console.error(`--${name} expects on|off (got "${v}")`);
|
|
378
|
+
process.exit(1);
|
|
379
|
+
};
|
|
380
|
+
const cd = onOff("compiled-decode");
|
|
381
|
+
if (cd !== null) process.env.MLX_BUN_COMPILED_DECODE = cd ? "1" : "0";
|
|
382
|
+
const pk = onOff("perf-kernel");
|
|
383
|
+
if (pk !== null) process.env.MLX_BUN_PERF_KERNEL = pk ? "1" : "0";
|
|
384
|
+
const fd = onOff("fused-decode");
|
|
385
|
+
if (fd !== null) process.env.MLX_BUN_FUSED_DECODE = fd ? "1" : "0";
|
|
386
|
+
const fs = onOff("fused-sdpa");
|
|
387
|
+
if (fs !== null) process.env.MLX_BUN_NO_FUSED_SDPA = fs ? "0" : "1"; // inverted env
|
|
388
|
+
if (flag("force-wire")) process.env.MLX_BUN_FORCE_WIRE = "1";
|
|
389
|
+
|
|
390
|
+
const serverOptions: import("./server").ServerOptions = {};
|
|
391
|
+
const budgetGB = Number(opt("memory-budget", "0"));
|
|
392
|
+
if (budgetGB > 0) serverOptions.memoryBudgetBytes = budgetGB * 1e9;
|
|
393
|
+
const pcGB = Number(opt("prompt-cache", "0"));
|
|
394
|
+
if (pcGB > 0) serverOptions.promptCacheBytes = pcGB * 2 ** 30;
|
|
395
|
+
// --batch N: max concurrent requests batched through the mlx-lm-parity
|
|
396
|
+
// engine (N=1 = today's serial path). --decode-concurrency is accepted as
|
|
397
|
+
// an mlx_lm.server-compatible alias (drop-in).
|
|
398
|
+
const batchRaw = opt("batch") ?? opt("decode-concurrency");
|
|
399
|
+
if (batchRaw !== null) {
|
|
400
|
+
const n = Number(batchRaw);
|
|
401
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
402
|
+
console.error(`--batch expects an integer >= 1 (got "${batchRaw}")`);
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
serverOptions.batch = n;
|
|
406
|
+
}
|
|
407
|
+
const kv = opt("kv-quant");
|
|
408
|
+
if (kv === "off") serverOptions.kvQuant = "off";
|
|
409
|
+
else if (kv === "config") serverOptions.kvQuant = "config";
|
|
410
|
+
else if (kv) {
|
|
411
|
+
const bits = Number(kv);
|
|
412
|
+
if (![4, 8].includes(bits)) { console.error(`--kv-quant expects config|off|4|8 (got "${kv}")`); process.exit(1); }
|
|
413
|
+
serverOptions.kvQuant = bits;
|
|
414
|
+
}
|
|
415
|
+
const host = opt("host");
|
|
416
|
+
if (host) serverOptions.hostname = host;
|
|
417
|
+
// Server-wide default for the chat template's enable_thinking variable
|
|
418
|
+
// (CPM/MiniCPM5 and other hybrid-reasoning models). Unset ⇒ the model's
|
|
419
|
+
// own default (false for MiniCPM5). onOff accepts true|false|on|off|1|0.
|
|
420
|
+
const thinking = onOff("thinking");
|
|
421
|
+
if (thinking !== null) serverOptions.defaultThinking = thinking;
|
|
422
|
+
// Server-wide sampling defaults. Unset ⇒ the model's generation_config.json
|
|
423
|
+
// (MiniCPM5 ships 0.9 / 0.95 — its think recipe). A per-request field still
|
|
424
|
+
// overrides these; the browser chat sends none, so this is its only lever.
|
|
425
|
+
const numFlag = (name: string, lo: number, hi: number): number | null => {
|
|
426
|
+
const v = opt(name);
|
|
427
|
+
if (v === null) return null;
|
|
428
|
+
const n = Number(v);
|
|
429
|
+
if (!Number.isFinite(n) || n < lo || n > hi) {
|
|
430
|
+
console.error(`--${name} expects a number in [${lo}, ${hi}] (got "${v}")`);
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
return n;
|
|
434
|
+
};
|
|
435
|
+
const temp = numFlag("temperature", 0, 5);
|
|
436
|
+
if (temp !== null) serverOptions.defaultTemperature = temp;
|
|
437
|
+
const topP = numFlag("top-p", 0, 1);
|
|
438
|
+
if (topP !== null) serverOptions.defaultTopP = topP;
|
|
439
|
+
const topK = numFlag("top-k", 0, 1_000_000);
|
|
440
|
+
if (topK !== null) serverOptions.defaultTopK = topK;
|
|
441
|
+
// HLG sampling — piecewise tone curve on the logprobs (default off). Knobs in
|
|
442
|
+
// nats; the mid gain folds from --temperature. docs/design/hlg-sampling.md.
|
|
443
|
+
if (onOff("hlg-sampling") === true) {
|
|
444
|
+
serverOptions.hlg = {
|
|
445
|
+
enabled: true,
|
|
446
|
+
width: numFlag("hlg-width", 0, 100) ?? 4,
|
|
447
|
+
shoulder: numFlag("hlg-shoulder", 0, 100) ?? 4,
|
|
448
|
+
toe: numFlag("hlg-toe", 0, 100) ?? 6,
|
|
449
|
+
pivotOffset: numFlag("hlg-pivot-offset", 0, 100) ?? 6,
|
|
450
|
+
pivot: "top",
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
return { port: Number(opt("port", "8090")), serverOptions };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** One-line summary of the active runtime levers for the ready card. */
|
|
457
|
+
function runtimeSummary(o: import("./server").ServerOptions): string {
|
|
458
|
+
const kv = o.kvQuant === "off" ? "off" : typeof o.kvQuant === "number" ? `kv${o.kvQuant}` : "config";
|
|
459
|
+
const lever = (env: string, dflt: string) => process.env[env] ?? dflt;
|
|
460
|
+
return `kv-quant ${kv} · compiled-decode ${lever("MLX_BUN_COMPILED_DECODE", "1") === "1" ? "on" : "off"}` +
|
|
461
|
+
` · perf-kernel ${perfKernelEnabled() ? "on" : "off"}` +
|
|
462
|
+
(lever("MLX_BUN_FUSED_DECODE", "0") === "1" ? " · fused-decode on" : "") +
|
|
463
|
+
(o.batch && o.batch > 1 ? ` · batch ${o.batch}` : "") +
|
|
464
|
+
(o.defaultThinking !== undefined ? ` · thinking ${o.defaultThinking ? "on" : "off"}` : "") +
|
|
465
|
+
(o.defaultTemperature !== undefined ? ` · temp ${o.defaultTemperature}` : "") +
|
|
466
|
+
(o.defaultTopP !== undefined ? ` · top-p ${o.defaultTopP}` : "") +
|
|
467
|
+
(o.defaultTopK !== undefined ? ` · top-k ${o.defaultTopK}` : "");
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Shared model resolution: explicit query wins; otherwise e4b (the default
|
|
471
|
+
* model everywhere) if it's downloaded, else the largest supported model that
|
|
472
|
+
* fits this machine. Downloads the recommended model first on a fresh install. */
|
|
473
|
+
async function resolveModelAuto(query: string | null): Promise<{ m: import("./registry").ModelRecord; picked: boolean }> {
|
|
474
|
+
const reg = new Registry();
|
|
475
|
+
if (reg.list().length === 0) await reg.scan();
|
|
476
|
+
if (query) return { m: reg.resolve(query), picked: false };
|
|
477
|
+
const { recommendedRepoId } = await import("./fit");
|
|
478
|
+
let candidates = reg.list().filter((r) => isSupportedModelRecord(r.modelType, r.repoId));
|
|
479
|
+
if (candidates.length === 0) {
|
|
480
|
+
// First run: starter-model flow (PRODUCT_ROADMAP / PLAN Phase 16).
|
|
481
|
+
// Download the sub-GB starter first so the user is chatting quickly,
|
|
482
|
+
// then stream the recommended Gemma for this Mac in the background —
|
|
483
|
+
// it becomes the auto-pick next run.
|
|
484
|
+
const starterRepo = "mlx-community/MiniCPM5-1B-OptiQ-4bit";
|
|
485
|
+
const recRepo = recommendedRepoId();
|
|
486
|
+
const { step, style } = await import("./tui");
|
|
487
|
+
const { downloadModel } = await import("./download");
|
|
488
|
+
const s = step(`downloading starter model ${starterRepo}`);
|
|
489
|
+
await downloadModel(starterRepo, {
|
|
490
|
+
onProgress: (file, received, total) => {
|
|
491
|
+
const pct = total ? Math.floor((received / total) * 100) : 0;
|
|
492
|
+
s.update(`starter model ${starterRepo} — ${file} ${gb(received)} / ${gb(total)} (${pct}%)`);
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
s.done(`starter model ready ${style.dim(`· ${starterRepo}`)}`);
|
|
496
|
+
await reg.scan();
|
|
497
|
+
if (recRepo !== starterRepo) {
|
|
498
|
+
console.log(` ${style.dim(`downloading ${recRepo} (recommended for this Mac) in the background —`)}`);
|
|
499
|
+
console.log(` ${style.dim("it becomes the default next run · progress on the status page (/downloads) · resumable")}`);
|
|
500
|
+
downloadModel(recRepo)
|
|
501
|
+
.then(async () => {
|
|
502
|
+
await new Registry().scan();
|
|
503
|
+
console.log(` ${style.dim(`background download complete: ${recRepo} (used on next run)`)}`);
|
|
504
|
+
})
|
|
505
|
+
.catch(() => { /* resumable — the next run continues it */ });
|
|
506
|
+
}
|
|
507
|
+
candidates = reg.list().filter((r) => isSupportedModelRecord(r.modelType, r.repoId));
|
|
508
|
+
}
|
|
509
|
+
// No query: e4b by default; otherwise the largest model that still leaves
|
|
510
|
+
// the machine usable for other apps. Never auto-grab the 26B "dedicate the
|
|
511
|
+
// machine" model on a small-RAM Mac — that's an explicit --query choice —
|
|
512
|
+
// but allow it as a last resort if it's all the user has downloaded.
|
|
513
|
+
const { DEFAULT_REPO_ID, COEXIST_FRACTION, chooseAutoModel, thisMachine } = await import("./fit");
|
|
514
|
+
const machine = thisMachine();
|
|
515
|
+
const fitsFull = new Map<string, boolean>();
|
|
516
|
+
const fitsCoexist = new Map<string, boolean>();
|
|
517
|
+
for (const r of candidates) {
|
|
518
|
+
const config = await loadModelConfig(r.path);
|
|
519
|
+
fitsFull.set(r.repoId, fit(config, r.sizeBytes, 8192, machine, undefined, r.expertsBytes).fits);
|
|
520
|
+
fitsCoexist.set(
|
|
521
|
+
r.repoId,
|
|
522
|
+
fit(config, r.sizeBytes, 8192, machine, undefined, r.expertsBytes, machine.ramBytes * COEXIST_FRACTION).fits,
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
const chosen = chooseAutoModel(
|
|
526
|
+
candidates,
|
|
527
|
+
DEFAULT_REPO_ID,
|
|
528
|
+
(c) => fitsFull.get(c.repoId) ?? false,
|
|
529
|
+
(c) => fitsCoexist.get(c.repoId) ?? false,
|
|
530
|
+
);
|
|
531
|
+
if (chosen) return { m: chosen, picked: true };
|
|
532
|
+
console.error("no downloaded supported model fits this machine — pick one explicitly (mlx-bun ls)");
|
|
533
|
+
process.exit(1);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function parseSize(s: string): number {
|
|
537
|
+
const m = /^([\d.]+)\s*(GB|MB|G|M)?$/i.exec(s.trim());
|
|
538
|
+
if (!m) throw new Error(`bad size: ${s}`);
|
|
539
|
+
const mult = /^m/i.test(m[2] ?? "G") ? 2 ** 20 : 2 ** 30;
|
|
540
|
+
return Number(m[1]) * mult;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
switch (cmd) {
|
|
544
|
+
case "get": {
|
|
545
|
+
const repoId = positional(0);
|
|
546
|
+
if (!repoId || !repoId.includes("/")) {
|
|
547
|
+
console.error("usage: mlx-bun get <org/repo> [--revision main]");
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
const { downloadModel } = await import("./download");
|
|
551
|
+
const { step, style } = await import("./tui");
|
|
552
|
+
const s = step(`downloading ${repoId}`);
|
|
553
|
+
const snap = await downloadModel(repoId, {
|
|
554
|
+
revision: opt("revision", "main")!,
|
|
555
|
+
onProgress: (file, received, total) => {
|
|
556
|
+
const pct = total ? Math.floor((received / total) * 100) : 0;
|
|
557
|
+
s.update(`${style.bold(repoId)} ${style.dim(`· ${file} · ${gb(received)} / ${gb(total)} (${pct}%)`)}`);
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
s.done(`${style.bold(repoId)} ${style.dim("downloaded · verified")}`);
|
|
561
|
+
const sScan = step("updating registry");
|
|
562
|
+
const reg = new Registry();
|
|
563
|
+
await reg.scan();
|
|
564
|
+
sScan.done(`registry updated ${style.dim(`· ${snap}`)}`);
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
case "scan": {
|
|
569
|
+
const { step } = await import("./tui");
|
|
570
|
+
const s = step("scanning the Hugging Face cache");
|
|
571
|
+
const reg = new Registry();
|
|
572
|
+
const n = await reg.scan();
|
|
573
|
+
s.done(`indexed ${n} model snapshot(s)`);
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
case "ls": {
|
|
578
|
+
const reg = new Registry();
|
|
579
|
+
if ((reg.list().length ?? 0) === 0) await reg.scan();
|
|
580
|
+
const maxSize = opt("max-size");
|
|
581
|
+
const models = reg.list({
|
|
582
|
+
vision: flag("vision") ? true : undefined,
|
|
583
|
+
maxBytes: maxSize ? parseSize(maxSize) : undefined,
|
|
584
|
+
query: positional(0),
|
|
585
|
+
});
|
|
586
|
+
if (models.length === 0) {
|
|
587
|
+
console.log("no models match (try `mlx-bun scan`)");
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
const { table, style, h1 } = await import("./tui");
|
|
591
|
+
h1("library");
|
|
592
|
+
console.log();
|
|
593
|
+
table(
|
|
594
|
+
[
|
|
595
|
+
{ header: "model", paint: (c) => style.bold(c) },
|
|
596
|
+
{ header: "size", align: "right" },
|
|
597
|
+
{ header: "params", align: "right" },
|
|
598
|
+
{ header: "quant" },
|
|
599
|
+
{ header: "license", paint: (c) => style.dim(c) },
|
|
600
|
+
{ header: "capabilities", paint: (c) => style.dim(c) },
|
|
601
|
+
],
|
|
602
|
+
models.map((m) => [
|
|
603
|
+
m.repoId,
|
|
604
|
+
gb(m.sizeBytes),
|
|
605
|
+
m.paramCount ? `${(m.paramCount / 1e9).toFixed(1)}B` : "?",
|
|
606
|
+
m.quantBits ? `${m.quantBits}-bit g${m.quantGroupSize}` : "full",
|
|
607
|
+
m.license ?? "?",
|
|
608
|
+
[
|
|
609
|
+
isSupportedModelRecord(m.modelType, m.repoId) ? "supported" : `unsupported (${m.modelType})`,
|
|
610
|
+
m.hasVisionSidecar ? "vision" : null,
|
|
611
|
+
m.hasToolTemplate ? "tools" : null,
|
|
612
|
+
m.hasKvConfig ? "kv-quant" : null,
|
|
613
|
+
].filter(Boolean).join(" · "),
|
|
614
|
+
]),
|
|
615
|
+
);
|
|
616
|
+
console.log();
|
|
617
|
+
console.log(style.dim(` ${models.length} model(s) · mlx-bun fit <query> for a memory assessment`));
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
case "fit": {
|
|
622
|
+
const query = positional(0);
|
|
623
|
+
if (!query) { console.error("usage: mlx-bun fit <query> [--ctx N] [--skus]"); process.exit(1); }
|
|
624
|
+
const reg = new Registry();
|
|
625
|
+
if (reg.list().length === 0) await reg.scan();
|
|
626
|
+
const m = reg.resolve(query);
|
|
627
|
+
const config = await loadModelConfig(m.path);
|
|
628
|
+
const ctx = Number(opt("ctx", "8192"));
|
|
629
|
+
const r = fit(config, m.sizeBytes, ctx, thisMachine(), undefined, m.expertsBytes);
|
|
630
|
+
const { box, table, style, h1, gradient } = await import("./tui");
|
|
631
|
+
h1("will it fit?");
|
|
632
|
+
console.log(` ${style.bold(m.repoId)} ${style.dim(`@ ${ctx.toLocaleString()} context · this machine`)}`);
|
|
633
|
+
console.log();
|
|
634
|
+
const expertsNote = config.text.enableMoeBlock && m.expertsBytes > 0
|
|
635
|
+
? style.dim(` (experts ${gb(m.expertsBytes)}; top ${config.text.topKExperts}/${config.text.numExperts} read per token)`)
|
|
636
|
+
: "";
|
|
637
|
+
box([
|
|
638
|
+
`weights ${gb(r.weightsBytes).padStart(9)}${expertsNote}`,
|
|
639
|
+
...(m.sidecarBytes > 0
|
|
640
|
+
? [style.dim(` + vision sidecar ${gb(m.sidecarBytes)} (bf16, loads only for vision)`)] : []),
|
|
641
|
+
`kv cache ${gb(r.kvBytes).padStart(9)}`,
|
|
642
|
+
`transient ${gb(r.transientBytes).padStart(9)}`,
|
|
643
|
+
`total ${gb(r.totalBytes).padStart(9)} ${style.dim(`of ${gb(r.usableBytes)} usable`)} ${r.fits ? style.green(style.bold("FITS")) : style.bold("DOES NOT FIT")}`,
|
|
644
|
+
"",
|
|
645
|
+
`max safe context ${style.bold(r.maxSafeContext.toLocaleString())} tokens`,
|
|
646
|
+
`predicted decode ${gradient(`${r.predictedDecodeTps.toFixed(1)} tok/s`)}`,
|
|
647
|
+
]);
|
|
648
|
+
if (flag("skus")) {
|
|
649
|
+
h1("apple silicon matrix");
|
|
650
|
+
console.log();
|
|
651
|
+
table(
|
|
652
|
+
[
|
|
653
|
+
{ header: "chip" }, { header: "ram", align: "right" },
|
|
654
|
+
{ header: "fits", paint: (c) => (c.includes("fits") ? style.green(c) : style.dim(c)) },
|
|
655
|
+
{ header: "max context", align: "right" }, { header: "decode", align: "right" },
|
|
656
|
+
],
|
|
657
|
+
skuMatrix(config, m.sizeBytes, ctx, m.expertsBytes).map((row) => [
|
|
658
|
+
row.sku, `${row.ramGB} GB`, row.fits ? "fits" : "—",
|
|
659
|
+
row.fits ? row.maxContext.toLocaleString() : "—",
|
|
660
|
+
row.fits ? `~${row.decodeTps.toFixed(0)} tok/s` : "—",
|
|
661
|
+
]),
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
console.log();
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
case "serve": {
|
|
669
|
+
const { banner, step, box, style } = await import("./tui");
|
|
670
|
+
banner(pkg.version);
|
|
671
|
+
if (noSubcommand)
|
|
672
|
+
console.log(style.dim(" no command given — starting the server · mlx-bun --help for all commands"));
|
|
673
|
+
const rt = serverRuntimeFlags();
|
|
674
|
+
// Friendly collision check before loading gigabytes of weights.
|
|
675
|
+
{
|
|
676
|
+
const { probeServer } = await import("./harness-pi");
|
|
677
|
+
const running = await probeServer(`http://localhost:${rt.port}/v1`);
|
|
678
|
+
if (running) {
|
|
679
|
+
console.error(`port ${rt.port} is already serving ${running.map((m) => m.id).join(", ")}.`);
|
|
680
|
+
console.error("reuse it (mlx-bun pi attaches automatically), stop it, or pick --port <other>.");
|
|
681
|
+
console.error("NOTE: a second server is a second model in memory — check `mlx-bun fit` first.");
|
|
682
|
+
process.exit(1);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const { m, picked } = await resolveModelAuto(positional(0) ?? opt("query"));
|
|
686
|
+
const sFit = step("assessing fit");
|
|
687
|
+
const config = await loadModelConfig(m.path);
|
|
688
|
+
const report = fit(config, m.sizeBytes, 8192, undefined, undefined, m.expertsBytes);
|
|
689
|
+
sFit.done(`${style.bold(m.repoId)} ${style.dim(`· ${gb(m.sizeBytes)} · ~${report.predictedDecodeTps.toFixed(0)} tok/s predicted`)}${picked ? style.dim(" · auto-picked (override: mlx-bun serve <query>)") : ""}`);
|
|
690
|
+
const sNative = step("native runtime");
|
|
691
|
+
await ensureNative(sNative);
|
|
692
|
+
sNative.done("native runtime ready");
|
|
693
|
+
const { createServer, loadContext } = await import("./server");
|
|
694
|
+
if (flag("expert-offload")) {
|
|
695
|
+
if (m.expertsBytes === 0) {
|
|
696
|
+
console.error(style.dim("--expert-offload ignored: this model has no experts (dense)"));
|
|
697
|
+
} else {
|
|
698
|
+
const sOff = step("expert offload");
|
|
699
|
+
const { ensureOffloadFile } = await import("./expert-offload-build");
|
|
700
|
+
const { activateExpertOffload } = await import("./expert-offload");
|
|
701
|
+
// builds <model>/.mlx-bun-offload on first use (~one-time), then reuses
|
|
702
|
+
const dir = await ensureOffloadFile(m.path, (msg) => sOff.update(msg));
|
|
703
|
+
activateExpertOffload(dir);
|
|
704
|
+
sOff.done(`experts mmap'd ${style.dim(dir)} ${style.dim("· phys_footprint ≈ core")}`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
const sLoad = step("loading weights");
|
|
708
|
+
const t0 = performance.now();
|
|
709
|
+
const ctx = await loadContext(m.path, m.repoId, { memoryBudgetBytes: rt.serverOptions.memoryBudgetBytes });
|
|
710
|
+
const server = createServer(ctx, rt.port, { ...rt.serverOptions, owner: "serve" });
|
|
711
|
+
sLoad.done(`weights loaded ${style.dim(`in ${(performance.now() - t0).toFixed(0)} ms`)}`);
|
|
712
|
+
const shownHost = rt.serverOptions.hostname ?? "localhost";
|
|
713
|
+
console.log();
|
|
714
|
+
box([
|
|
715
|
+
`${style.green("●")} ${style.bold("serving")} ${m.repoId}`,
|
|
716
|
+
"",
|
|
717
|
+
`API ${style.url(`http://${shownHost}:${server.port}/v1`)} ${style.dim("(OpenAI · Anthropic · Responses)")}`,
|
|
718
|
+
`App ${style.url(`http://${shownHost}:${server.port}/`)} ${style.dim("(chat · quantize · finetune · dataset · status)")}`,
|
|
719
|
+
...(rt.serverOptions.memoryBudgetBytes
|
|
720
|
+
? [`Mem ${gb(rt.serverOptions.memoryBudgetBytes)} budget ${style.dim("(admission control on)")}`] : []),
|
|
721
|
+
`Perf ${style.dim(runtimeSummary(rt.serverOptions))}`,
|
|
722
|
+
"",
|
|
723
|
+
style.dim("agent session: mlx-bun pi stop: Ctrl+C"),
|
|
724
|
+
]);
|
|
725
|
+
// Open the chat UI for interactive runs (--no-open, or a non-TTY such as
|
|
726
|
+
// a piped/headless/benchmark run, skips it). 0.0.0.0/:: bind → localhost
|
|
727
|
+
// for the browser. Reuses an existing tab on this host:port when possible
|
|
728
|
+
// (see openChatUi); the URL is printed above either way.
|
|
729
|
+
if (!flag("no-open") && process.stdout.isTTY) {
|
|
730
|
+
const browserHost = shownHost === "0.0.0.0" || shownHost === "::" ? "localhost" : shownHost;
|
|
731
|
+
openChatUi(`http://${browserHost}:${server.port}/#/chat`, `${browserHost}:${server.port}`);
|
|
732
|
+
}
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
case "bench":
|
|
737
|
+
case "benchmark": {
|
|
738
|
+
const { banner, step, box, style } = await import("./tui");
|
|
739
|
+
banner(pkg.version);
|
|
740
|
+
serverRuntimeFlags(); // env levers (--compiled-decode, --perf-kernel, ...) apply to the run
|
|
741
|
+
const { m, picked } = await resolveModelAuto(positional(0) ?? opt("query"));
|
|
742
|
+
const tokens = Number(opt("tokens", "256"));
|
|
743
|
+
const runs = Number(opt("runs", "3"));
|
|
744
|
+
const promptTokens = Number(opt("prompt-tokens", "0"));
|
|
745
|
+
const kvMode = opt("kv-quant", "off")!;
|
|
746
|
+
console.log(` ${style.dim("measures THIS machine, OUR stack only — no other systems needed.")}`);
|
|
747
|
+
console.log(` ${style.dim("numbers on a loaded machine are not quotable; ./benchmark.sh is the preflight-gated harness.")}`);
|
|
748
|
+
const sNative = step("native runtime");
|
|
749
|
+
await ensureNative(sNative);
|
|
750
|
+
sNative.done("native runtime ready");
|
|
751
|
+
const sLoad = step(`loading ${m.repoId}${picked ? " (auto-picked)" : ""}`);
|
|
752
|
+
const { Weights } = await import("./weights");
|
|
753
|
+
const { createModel } = await import("./model/factory");
|
|
754
|
+
const { generate } = await import("./generate");
|
|
755
|
+
const { ChatTemplate } = await import("./chat-template");
|
|
756
|
+
const { loadTokenizer } = await import("./tokenizer");
|
|
757
|
+
const { peakMemory, resetPeakMemory } = await import("./mlx/ffi");
|
|
758
|
+
const config = await loadModelConfig(m.path);
|
|
759
|
+
const weights = await Weights.open(m.path);
|
|
760
|
+
const model = createModel(weights, config);
|
|
761
|
+
const tok = await loadTokenizer(m.path);
|
|
762
|
+
const template = await ChatTemplate.load(m.path);
|
|
763
|
+
sLoad.done(`${style.bold(m.repoId)} ${style.dim(`· ${gb(m.sizeBytes)}`)}`);
|
|
764
|
+
|
|
765
|
+
let userMsg = "Write a detailed essay about the history of computing, starting with mechanical calculators.";
|
|
766
|
+
if (promptTokens > 0) {
|
|
767
|
+
const filler = "Background context: the history of computation spans mechanical " +
|
|
768
|
+
"calculators, electromechanical relays, vacuum tubes, transistors, " +
|
|
769
|
+
"integrated circuits, and modern accelerators. ";
|
|
770
|
+
while (tok.encode(userMsg).length < promptTokens - 24) userMsg = filler + userMsg;
|
|
771
|
+
}
|
|
772
|
+
const rendered = template.render([{ role: "user", content: userMsg }]);
|
|
773
|
+
const ids = tok.encode(rendered);
|
|
774
|
+
const promptIds = ids[0] === ids[1] && ids[0] === tok.bosTokenId ? ids.slice(1) : ids;
|
|
775
|
+
const kvOptions = kvMode === "config"
|
|
776
|
+
? (() => {
|
|
777
|
+
if (!config.kvQuant?.length) { console.error("model has no kv_config.json (--kv-quant config)"); process.exit(1); }
|
|
778
|
+
return { kvConfig: config.kvQuant };
|
|
779
|
+
})()
|
|
780
|
+
: kvMode !== "off" ? { kvBits: Number(kvMode), quantizedKvStart: 0 } : {};
|
|
781
|
+
|
|
782
|
+
// Warmup materializes weights so prefill timing measures prefill,
|
|
783
|
+
// not lazy page-in (same protocol as scripts/bench.ts).
|
|
784
|
+
const sWarm = step("warmup");
|
|
785
|
+
{
|
|
786
|
+
const wCache = model.makeCache();
|
|
787
|
+
const wGen = generate(model, promptIds.slice(0, Math.min(8, promptIds.length - 1)), {
|
|
788
|
+
maxTokens: 1, temperature: 0, cache: wCache,
|
|
789
|
+
});
|
|
790
|
+
for await (const _ of wGen) { /* discard */ }
|
|
791
|
+
for (const c of wCache) c.dispose();
|
|
792
|
+
}
|
|
793
|
+
resetPeakMemory();
|
|
794
|
+
sWarm.done("warmup complete");
|
|
795
|
+
|
|
796
|
+
const decodes: number[] = [];
|
|
797
|
+
const prefills: number[] = [];
|
|
798
|
+
let lastStats: { promptTokens: number; generatedTokens: number } = { promptTokens: 0, generatedTokens: 0 };
|
|
799
|
+
for (let i = 0; i < runs; i++) {
|
|
800
|
+
const sRun = step(`run ${i + 1}/${runs} · ${tokens} tokens`);
|
|
801
|
+
const gen = generate(model, promptIds, { maxTokens: tokens, temperature: 0, ...kvOptions });
|
|
802
|
+
for await (const _ of gen) { /* timing only */ }
|
|
803
|
+
const s = gen.stats!;
|
|
804
|
+
decodes.push(s.decodeTps);
|
|
805
|
+
prefills.push(s.prefillTps);
|
|
806
|
+
lastStats = s;
|
|
807
|
+
sRun.done(`run ${i + 1}/${runs} ${style.dim("·")} ${style.green(style.bold(`${s.decodeTps.toFixed(1)} tok/s`))} ${style.dim(`decode · prefill ${s.prefillTps.toFixed(0)} tok/s`)}`);
|
|
808
|
+
}
|
|
809
|
+
const median = (a: number[]) => [...a].sort((x, y) => x - y)[Math.floor(a.length / 2)]!;
|
|
810
|
+
const peak = peakMemory();
|
|
811
|
+
|
|
812
|
+
const { EvalDB: DB, gitCommit } = await import("./evaldb");
|
|
813
|
+
new DB().record({
|
|
814
|
+
modelPath: m.path,
|
|
815
|
+
commitSha: gitCommit() ?? undefined,
|
|
816
|
+
promptTokens: lastStats.promptTokens, cachedTokens: 0,
|
|
817
|
+
generatedTokens: lastStats.generatedTokens,
|
|
818
|
+
prefillTps: median(prefills), decodeTps: median(decodes),
|
|
819
|
+
peakBytes: peak,
|
|
820
|
+
notes: `cli-bench median-of-${runs} ${tokens}tok kv=${kvMode}${promptTokens ? ` ctx=${lastStats.promptTokens}` : ""} decode[${decodes.map((d) => d.toFixed(1)).join(",")}]`,
|
|
821
|
+
});
|
|
822
|
+
console.log();
|
|
823
|
+
box([
|
|
824
|
+
`${style.green("●")} ${style.bold("benchmark complete")} ${style.dim(`· ${m.repoId}`)}`,
|
|
825
|
+
"",
|
|
826
|
+
`decode ${style.green(style.bold(`${median(decodes).toFixed(1)} tok/s`))} ${style.dim(`median of ${runs} · [${decodes.map((d) => d.toFixed(1)).join(", ")}]`)}`,
|
|
827
|
+
`prefill ${style.bold(`${median(prefills).toFixed(0)} tok/s`)} ${style.dim(`· ${lastStats.promptTokens} prompt tokens`)}`,
|
|
828
|
+
`peak mem ${style.bold(gb(peak))} ${style.dim("(generation only)")}`,
|
|
829
|
+
`levers ${style.dim(runtimeSummary({ kvQuant: kvMode === "off" ? "off" : kvMode === "config" ? undefined : Number(kvMode) }))}`,
|
|
830
|
+
"",
|
|
831
|
+
style.dim("recorded to the eval DB — see mlx-bun evals"),
|
|
832
|
+
]);
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
case "evals": {
|
|
837
|
+
if (flag("clear")) {
|
|
838
|
+
// Eval rows are real data — archive, never destroy.
|
|
839
|
+
const { DEFAULT_EVAL_DB } = await import("./evaldb");
|
|
840
|
+
const { existsSync, renameSync } = await import("node:fs");
|
|
841
|
+
if (!existsSync(DEFAULT_EVAL_DB)) { console.log("no eval DB to clear"); break; }
|
|
842
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
843
|
+
const backup = DEFAULT_EVAL_DB.replace(/\.sqlite$/, `-${stamp}.sqlite`);
|
|
844
|
+
renameSync(DEFAULT_EVAL_DB, backup);
|
|
845
|
+
console.log(`eval DB archived to ${backup}`);
|
|
846
|
+
console.log("fresh DB starts with the next mlx-bun benchmark run");
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
const db = new EvalDB();
|
|
850
|
+
const limit = Number(opt("limit", "20"));
|
|
851
|
+
const rows = db.recent(limit);
|
|
852
|
+
if (flag("raw")) {
|
|
853
|
+
for (const r of rows) console.log(JSON.stringify(r));
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
const { style } = await import("./tui");
|
|
857
|
+
if (rows.length === 0) {
|
|
858
|
+
console.log("no eval runs recorded yet — ./benchmark.sh writes them");
|
|
859
|
+
break;
|
|
860
|
+
}
|
|
861
|
+
const ago = (ts: number) => {
|
|
862
|
+
const s = (Date.now() - ts) / 1000;
|
|
863
|
+
if (s < 90) return `${Math.round(s)}s ago`;
|
|
864
|
+
if (s < 5400) return `${Math.round(s / 60)}m ago`;
|
|
865
|
+
if (s < 129600) return `${Math.round(s / 3600)}h ago`;
|
|
866
|
+
return `${Math.round(s / 86400)}d ago`;
|
|
867
|
+
};
|
|
868
|
+
const modelName = (p: string) =>
|
|
869
|
+
/models--[^/]+--([^/]+)/.exec(p)?.[1] ?? p.split("/").at(-1) ?? p;
|
|
870
|
+
const note = (r: Record<string, unknown>, key: string) =>
|
|
871
|
+
new RegExp(`${key}=([\\w.]+)`).exec((r.notes as string) ?? "")?.[1] ?? "";
|
|
872
|
+
const BENCH_LABELS: Record<string, string> = {
|
|
873
|
+
"h2h-server": "server e2e",
|
|
874
|
+
"h2h-direct": "direct decode",
|
|
875
|
+
"bench.ts": "decode script",
|
|
876
|
+
"cli-bench": "cli benchmark",
|
|
877
|
+
};
|
|
878
|
+
const table = rows.map((r) => ({
|
|
879
|
+
when: ago(r.ts as number),
|
|
880
|
+
model: modelName(r.model_path as string).replace(/-OptiQ-4bit$/, "").replace(/^gemma-4-/, "g4-"),
|
|
881
|
+
bench: BENCH_LABELS[((r.notes as string) ?? "").split(" ")[0] ?? ""] ?? ((r.notes as string) ?? "").split(" ")[0] ?? "",
|
|
882
|
+
kv: note(r, "kv"),
|
|
883
|
+
stack: (r.stack as string) ?? "mlx-bun",
|
|
884
|
+
prefill: (r.prefill_tps as number) > 0 ? `${(r.prefill_tps as number).toFixed(0)}` : "",
|
|
885
|
+
decode: (r.decode_tps as number).toFixed(1),
|
|
886
|
+
ttft: note(r, "ttft_ms") ? `${note(r, "ttft_ms")}ms` : "",
|
|
887
|
+
peak: gb(r.peak_bytes as number),
|
|
888
|
+
commit: (r.commit_sha as string) ?? "",
|
|
889
|
+
}));
|
|
890
|
+
type Row = (typeof table)[number];
|
|
891
|
+
const cols: Array<[string, keyof Row, "left" | "right"]> = [
|
|
892
|
+
["WHEN", "when", "right"], ["STACK", "stack", "left"], ["MODEL", "model", "left"],
|
|
893
|
+
["BENCH", "bench", "left"], ["KV", "kv", "left"],
|
|
894
|
+
["PREFILL", "prefill", "right"], ["DECODE", "decode", "right"], ["TTFT", "ttft", "right"],
|
|
895
|
+
["PEAK", "peak", "right"], ["COMMIT", "commit", "left"],
|
|
896
|
+
];
|
|
897
|
+
const widths = cols.map(([h, k]) => Math.max(h.length, ...table.map((t) => t[k].length)));
|
|
898
|
+
console.log();
|
|
899
|
+
console.log(" " + cols.map(([h], i) => style.dim(h.padEnd(widths[i]!))).join(" "));
|
|
900
|
+
for (const t of table) {
|
|
901
|
+
console.log(" " + cols.map(([, k, align], i) => {
|
|
902
|
+
const cell = align === "right" ? t[k].padStart(widths[i]!) : t[k].padEnd(widths[i]!);
|
|
903
|
+
if (k === "decode") return style.green(style.bold(cell));
|
|
904
|
+
if (k === "model") return style.bold(cell);
|
|
905
|
+
if (k === "stack") return cell.trimEnd() === "mlx-bun" ? style.accent(cell) : style.dim(cell);
|
|
906
|
+
if (k === "commit" || k === "when") return style.dim(cell);
|
|
907
|
+
return cell;
|
|
908
|
+
}).join(" "));
|
|
909
|
+
}
|
|
910
|
+
console.log();
|
|
911
|
+
console.log(style.dim(" STACK engine that produced the number (mlx-bun = us; mlx-lm/optiq = Python references)"));
|
|
912
|
+
console.log(style.dim(" BENCH server e2e = full HTTP round-trip · direct decode = engine only, no server"));
|
|
913
|
+
console.log(style.dim(" decode script / cli benchmark = single-stack decode measurement"));
|
|
914
|
+
console.log(style.dim(" KV KV-cache quantization (off = bf16, config = per-layer kv_config.json)"));
|
|
915
|
+
console.log(style.dim(" PREFILL prompt ingestion tok/s (in-process benches) · DECODE generation tok/s"));
|
|
916
|
+
console.log(style.dim(" TTFT request → first token, incl. server overhead (server benches)"));
|
|
917
|
+
console.log(style.dim(" PEAK max memory during generation"));
|
|
918
|
+
console.log();
|
|
919
|
+
console.log(style.dim(` ${rows.length} run(s) · --limit <n> for more · --raw for full records · mlx-bun benchmark to add a run`));
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
case "pi": {
|
|
924
|
+
// mlx-bun consumes its own flags; the rest describe how the built-in
|
|
925
|
+
// agent runs. `mlx-bun pi` always drives pi's own TUI in-process
|
|
926
|
+
// (src/pi-terminal.ts) — pi is bundled, nothing to install. Users who
|
|
927
|
+
// already run their own pi connect it with `mlx-bun harness pi`.
|
|
928
|
+
const OURS_VAL = new Set([
|
|
929
|
+
"--query", "-q", "--port", "--host", "--memory-budget", "--prompt-cache", "--kv-quant",
|
|
930
|
+
"--compiled-decode", "--perf-kernel", "--fused-decode", "--fused-sdpa", "--thinking",
|
|
931
|
+
"--temperature", "--top-p", "--top-k",
|
|
932
|
+
]);
|
|
933
|
+
const OURS_BOOL = new Set(["--force-wire", "--expert-offload"]);
|
|
934
|
+
const passthrough: string[] = [];
|
|
935
|
+
for (let i = 1; i < argv.length; i++) {
|
|
936
|
+
const a = argv[i]!;
|
|
937
|
+
if (OURS_VAL.has(a)) { i++; continue; }
|
|
938
|
+
if (OURS_BOOL.has(a)) continue;
|
|
939
|
+
passthrough.push(a);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const { parsePiArgs } = await import("./pi-terminal");
|
|
943
|
+
const stdinPiped = !process.stdin.isTTY;
|
|
944
|
+
const parsed = parsePiArgs(passthrough, stdinPiped);
|
|
945
|
+
|
|
946
|
+
// Headless machine-readable runs (`-p`, `--mode json`, `--mode rpc`) must
|
|
947
|
+
// keep stdout pristine for the response/JSONL stream — route our bring-up
|
|
948
|
+
// logging to stderr. Interactive keeps the pretty banner on stdout.
|
|
949
|
+
const quiet = parsed.mode !== "interactive";
|
|
950
|
+
const origStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
951
|
+
if (quiet) {
|
|
952
|
+
(process.stdout as unknown as { write: typeof process.stderr.write }).write =
|
|
953
|
+
process.stderr.write.bind(process.stderr);
|
|
954
|
+
}
|
|
955
|
+
const restoreStdout = () => {
|
|
956
|
+
if (quiet) (process.stdout as unknown as { write: typeof origStdoutWrite }).write = origStdoutWrite;
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
const rt = serverRuntimeFlags();
|
|
960
|
+
const port = rt.port;
|
|
961
|
+
const baseUrl = `http://localhost:${port}/v1`;
|
|
962
|
+
const { probeServer } = await import("./harness-pi");
|
|
963
|
+
let models = await probeServer(baseUrl);
|
|
964
|
+
let startedServer = false;
|
|
965
|
+
if (models) {
|
|
966
|
+
console.log(`reusing running server at ${baseUrl} (${models.map((m) => m.id).join(", ")})`);
|
|
967
|
+
// Server-shaping flags can't apply to a server we didn't start —
|
|
968
|
+
// drop them loudly rather than silently or fatally.
|
|
969
|
+
const ignored = [...OURS_VAL, ...OURS_BOOL].filter((f) => f !== "--port" && argv.includes(f));
|
|
970
|
+
if (ignored.length > 0) {
|
|
971
|
+
const { style } = await import("./tui");
|
|
972
|
+
console.log(style.dim(` ignoring ${ignored.join(", ")} — the running server keeps its own configuration`));
|
|
973
|
+
console.log(style.dim(" (stop it or use --port <other> to start a fresh server with these flags)"));
|
|
974
|
+
}
|
|
975
|
+
try {
|
|
976
|
+
const stats = await (await fetch(`http://localhost:${port}/stats`)).json() as
|
|
977
|
+
{ server?: { owner?: string } };
|
|
978
|
+
if (stats.server?.owner === "pi-session") {
|
|
979
|
+
console.log("note: that server belongs to another `mlx-bun pi` session and ends when it does —");
|
|
980
|
+
console.log(" run `mlx-bun serve` for a persistent server.");
|
|
981
|
+
}
|
|
982
|
+
} catch {}
|
|
983
|
+
} else {
|
|
984
|
+
const { banner, step, style } = await import("./tui");
|
|
985
|
+
banner(pkg.version);
|
|
986
|
+
const { m, picked } = await resolveModelAuto(opt("query") ?? optShort("q"));
|
|
987
|
+
if (picked)
|
|
988
|
+
console.log(` ${style.dim(`auto-picked ${m.repoId} (largest supported model that fits; override with --query)`)}`);
|
|
989
|
+
const sNative = step("native runtime");
|
|
990
|
+
await ensureNative(sNative);
|
|
991
|
+
sNative.done("native runtime ready");
|
|
992
|
+
const { createServer, loadContext } = await import("./server");
|
|
993
|
+
const sLoad = step(`loading ${m.repoId}`);
|
|
994
|
+
const t0 = performance.now();
|
|
995
|
+
const ctx = await loadContext(m.path, m.repoId, { memoryBudgetBytes: rt.serverOptions.memoryBudgetBytes });
|
|
996
|
+
createServer(ctx, port, { ...rt.serverOptions, owner: "pi-session" });
|
|
997
|
+
startedServer = true;
|
|
998
|
+
sLoad.done(`serving ${style.bold(m.repoId)} ${style.dim(`at ${baseUrl} · ready in ${(performance.now() - t0).toFixed(0)} ms`)}`);
|
|
999
|
+
models = await probeServer(baseUrl);
|
|
1000
|
+
if (!models) { restoreStdout(); console.error("server started but /v1/models probe failed"); process.exit(1); }
|
|
1001
|
+
}
|
|
1002
|
+
const exitNote = () => {
|
|
1003
|
+
// The server we started lives exactly as long as the pi session — any
|
|
1004
|
+
// exit tears it down. A standalone server belongs to `mlx-bun serve`
|
|
1005
|
+
// (which we reuse and never stop).
|
|
1006
|
+
if (startedServer && parsed.mode === "interactive")
|
|
1007
|
+
console.log("pi exited — shutting down (use `mlx-bun serve` for a persistent server)");
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
// pi's own TUI embedded in-process — nothing to install.
|
|
1011
|
+
const { runEmbeddedPi } = await import("./pi-terminal");
|
|
1012
|
+
let message = parsed.message;
|
|
1013
|
+
if (parsed.mode === "print" && stdinPiped) {
|
|
1014
|
+
const piped = (await Bun.stdin.text()).trim();
|
|
1015
|
+
if (piped) message = message ? `${message}\n\n${piped}` : piped;
|
|
1016
|
+
}
|
|
1017
|
+
if (parsed.mode === "interactive")
|
|
1018
|
+
console.log(`launching mlx-bun pi (model ${models[0]!.id}) — /help for commands, double-Ctrl+C to exit`);
|
|
1019
|
+
restoreStdout();
|
|
1020
|
+
const code = await runEmbeddedPi({
|
|
1021
|
+
baseUrl,
|
|
1022
|
+
modelLabel: models[0]!.id,
|
|
1023
|
+
contextWindow: models[0]!.contextWindow,
|
|
1024
|
+
reasoning: models[0]!.reasoning,
|
|
1025
|
+
mode: parsed.mode,
|
|
1026
|
+
printFormat: parsed.printFormat,
|
|
1027
|
+
initialMessage: message,
|
|
1028
|
+
});
|
|
1029
|
+
exitNote();
|
|
1030
|
+
process.exit(code);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
case "harness": {
|
|
1034
|
+
const target = positional(0);
|
|
1035
|
+
if (target !== "pi") {
|
|
1036
|
+
console.error("usage: mlx-bun harness pi [--base-url <url>] [--remove]");
|
|
1037
|
+
process.exit(1);
|
|
1038
|
+
}
|
|
1039
|
+
const { detectPi, installPiExtension, removePiExtension, DEFAULT_BASE_URL } =
|
|
1040
|
+
await import("./harness-pi");
|
|
1041
|
+
if (flag("remove")) {
|
|
1042
|
+
console.log(removePiExtension() ? "mlx-bun provider extension removed" : "nothing to remove");
|
|
1043
|
+
break;
|
|
1044
|
+
}
|
|
1045
|
+
const pi = detectPi();
|
|
1046
|
+
if (!pi.found) {
|
|
1047
|
+
console.error("pi not found on PATH. Install it first:");
|
|
1048
|
+
console.error(" curl -fsSL https://pi.dev/install.sh | sh");
|
|
1049
|
+
console.error("then re-run: mlx-bun harness pi");
|
|
1050
|
+
process.exit(1);
|
|
1051
|
+
}
|
|
1052
|
+
const { step, box, style } = await import("./tui");
|
|
1053
|
+
const sPi = step("detecting pi");
|
|
1054
|
+
sPi.done(`pi ${style.dim(`${pi.binPath}${pi.version ? ` · v${pi.version}` : ""}`)}`);
|
|
1055
|
+
const baseUrl = opt("base-url", DEFAULT_BASE_URL)!;
|
|
1056
|
+
const sExt = step("installing provider extension");
|
|
1057
|
+
const result = await installPiExtension(baseUrl);
|
|
1058
|
+
sExt.done(`provider extension installed ${style.dim(`· ${result.path}`)}`);
|
|
1059
|
+
const sProbe = step("probing server");
|
|
1060
|
+
if (result.serverReachable) sProbe.done(`server live ${style.dim(`· ${result.bakedModels.join(", ")}`)}`);
|
|
1061
|
+
else sProbe.done(`server not running ${style.dim(`· models discovered when pi starts against ${baseUrl}`)}`);
|
|
1062
|
+
console.log();
|
|
1063
|
+
box([
|
|
1064
|
+
`${style.green("●")} ${style.bold("your pi is connected to mlx-bun")}`,
|
|
1065
|
+
"",
|
|
1066
|
+
`launch ${style.accent("pi --provider mlx-bun")}`,
|
|
1067
|
+
`select ${style.dim("/model inside pi · or scope cycling:")} ${style.accent('pi --models "mlx-bun/*"')}`,
|
|
1068
|
+
`undo ${style.dim("mlx-bun harness pi --remove")}`,
|
|
1069
|
+
]);
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
default:
|
|
1074
|
+
printHelp(cmd);
|
|
1075
|
+
}
|