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/README.md
CHANGED
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
# mlx-bun
|
|
2
2
|
|
|
3
|
+
**One command, chatting in under a minute** — a fully local AI on your
|
|
4
|
+
Apple Silicon Mac's GPU, no Python, no API key:
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
curl -fsSL https://mlx-bun.dev/install.sh | sh
|
|
8
|
+
mlx-bun
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Docs: **[mlx-bun.dev](https://mlx-bun.dev)**
|
|
12
|
+
|
|
3
13
|
Native MLX inference for Bun, and the beginning of a one-command local AI
|
|
4
14
|
product for Apple Silicon Macs. `mlx-bun` detects the machine, starts a useful
|
|
5
15
|
local assistant, exposes OpenAI/Anthropic-compatible APIs, and gives
|
|
6
16
|
TypeScript apps direct access to MLX without Python or a sidecar server.
|
|
7
17
|
|
|
18
|
+
Six goals, in one binary:
|
|
19
|
+
|
|
20
|
+
1. **A drop-in replacement for `mlx_lm.server`** — same port, endpoints,
|
|
21
|
+
request fields, and flags; stop the Python server, start `mlx-bun serve`,
|
|
22
|
+
the same curl works ([site guide](https://mlx-bun.dev/guides/drop-in-mlx-lm/)).
|
|
23
|
+
2. **A TypeScript library** — embed MLX generation in Bun/Tauri/Electron apps,
|
|
24
|
+
or ship it as a signed single-binary sidecar
|
|
25
|
+
([library](./docs/reference/library-api.md), [embedding](./docs/reference/embedding.md)).
|
|
26
|
+
3. **Bit-exact, fast, and memory-honest** — logit parity with the Python
|
|
27
|
+
reference as the correctness oracle, measured speed wins, and a
|
|
28
|
+
deterministic `fit` memory contract ([benchmarks](#benchmarks),
|
|
29
|
+
[correctness](#correctness)).
|
|
30
|
+
4. **One command, under a minute** — the appliance path above; the default
|
|
31
|
+
experience needs zero configuration ([getting started](#getting-started)).
|
|
32
|
+
5. **A personal memory** — a local, git-tracked wiki your assistant reads and
|
|
33
|
+
(via local synthesis) writes ([memory](#personal-memory)).
|
|
34
|
+
6. **An AI lab** — training, speculative decoding, and sampling research kept
|
|
35
|
+
next to the runtime that serves them ([the lab](#the-lab)).
|
|
36
|
+
|
|
8
37
|
The product bet is that local AI on Mac is a finite optimization problem, not
|
|
9
38
|
an infinite configuration maze: Apple ships a known set of chip/RAM/bandwidth
|
|
10
39
|
SKUs, the supported model list is curated, and the task mode is explicit. That
|
|
@@ -36,7 +65,7 @@ mlx-bun
|
|
|
36
65
|
|
|
37
66
|
Self-contained (binary + MLX runtime) and notarized, so it runs without a
|
|
38
67
|
Gatekeeper prompt. Installs to `~/.mlx-bun` by default — override with
|
|
39
|
-
`MLX_BUN_INSTALL_DIR`, or pin a release with `MLX_BUN_VERSION=v0.0.
|
|
68
|
+
`MLX_BUN_INSTALL_DIR`, or pin a release with `MLX_BUN_VERSION=v0.0.8`.
|
|
40
69
|
|
|
41
70
|
### Homebrew
|
|
42
71
|
|
|
@@ -93,7 +122,7 @@ model named, it does everything for you:
|
|
|
93
122
|
3. starts downloading `gemma-4-e4b` (the stronger 4B model) in the
|
|
94
123
|
background — it becomes the default on your next `mlx-bun serve`;
|
|
95
124
|
4. opens the chat UI in your browser at
|
|
96
|
-
[`http://localhost:
|
|
125
|
+
[`http://localhost:8080/#/chat`](http://localhost:8080/#/chat)
|
|
97
126
|
(pass `--no-open` to skip).
|
|
98
127
|
|
|
99
128
|
Want a specific model instead? Name it — `mlx-bun serve e4b` (substring
|
|
@@ -105,7 +134,7 @@ your machine.
|
|
|
105
134
|
**Prefer the API?** It's OpenAI-compatible — any OpenAI client works:
|
|
106
135
|
|
|
107
136
|
```sh
|
|
108
|
-
curl http://localhost:
|
|
137
|
+
curl http://localhost:8080/v1/chat/completions \
|
|
109
138
|
-H 'Content-Type: application/json' \
|
|
110
139
|
-d '{
|
|
111
140
|
"messages": [{"role": "user", "content": "Hello!"}],
|
|
@@ -117,9 +146,13 @@ That's it. The server serves the one model it was started with (the
|
|
|
117
146
|
request's `model` field is ignored and the loaded model id is echoed
|
|
118
147
|
back in responses).
|
|
119
148
|
|
|
149
|
+
Longer walkthrough on the site:
|
|
150
|
+
[Installation](https://mlx-bun.dev/getting-started/installation/) and
|
|
151
|
+
[Quickstart](https://mlx-bun.dev/getting-started/quickstart/).
|
|
152
|
+
|
|
120
153
|
## What mlx-bun is
|
|
121
154
|
|
|
122
|
-
`mlx-bun` has
|
|
155
|
+
`mlx-bun` has several faces, all sharing one native runtime:
|
|
123
156
|
|
|
124
157
|
- **The local AI product** — run `mlx-bun` or `bunx mlx-bun` and get a
|
|
125
158
|
working local chat UI plus a local server. The default path should feel like
|
|
@@ -128,25 +161,57 @@ back in responses).
|
|
|
128
161
|
- **The app developer library** — embed local MLX inference inside Bun,
|
|
129
162
|
Tauri, Electron, and other TypeScript applications without running Python in
|
|
130
163
|
the background.
|
|
131
|
-
- **The AI lab** — keep parity, evals, kernels, LoRA/ORPO training, adapter
|
|
132
|
-
routing, and performance experiments close to the runtime that will actually
|
|
133
|
-
serve them.
|
|
134
164
|
- **The background agent runtime** — leave a local OpenAI-compatible endpoint
|
|
135
165
|
available for scripts, recurring jobs, app integrations, and per-request
|
|
136
166
|
adapter routing.
|
|
137
|
-
- **The local memory layer** —
|
|
138
|
-
|
|
139
|
-
symlinks to mlx-bun's own docs, and the built-in pi agents read both those
|
|
140
|
-
docs and your articles quietly (`memory_search`, `memory_section`, etc.) and can open it in Obsidian for you
|
|
141
|
-
(`mlx-bun memory open [article]`). Chat-time memory is read-only; synthesis is
|
|
142
|
-
the separate writer path. The planned synthesis pipeline uses the same local
|
|
143
|
-
models with stage-specific LoRA adapters (chunk, bucket, synthesize, edit)
|
|
144
|
-
trained by the project creator and distributed from Hugging Face.
|
|
167
|
+
- **The local memory layer** and **the AI lab** — each gets its own section
|
|
168
|
+
below.
|
|
145
169
|
|
|
146
170
|
The default experience is intentionally opinionated. Power users can override
|
|
147
171
|
models, budgets, KV modes, adapters, and flags; the average user should not
|
|
148
172
|
need to know those knobs exist.
|
|
149
173
|
|
|
174
|
+
## Personal memory
|
|
175
|
+
|
|
176
|
+
`mlx-bun memory init` creates a git-tracked Markdown wiki at
|
|
177
|
+
`~/.mlx-bun/wiki`; `Reference/` is seeded with read-only symlinks to
|
|
178
|
+
mlx-bun's own docs, and the built-in pi agents read both those docs and your
|
|
179
|
+
articles quietly (`memory_search`, `memory_section`, etc.) and can open it in
|
|
180
|
+
Obsidian for you (`mlx-bun memory open [article]`). Chat-time memory is
|
|
181
|
+
read-only; synthesis is the separate writer path: `mlx-bun memory synthesize`
|
|
182
|
+
runs the built-in local pipeline (segment → extract → route → create/patch →
|
|
183
|
+
reconcile → link → wikify) that turns your conversations into cross-linked
|
|
184
|
+
subject articles, using the same local models with a fine-tuned chunking LoRA
|
|
185
|
+
adapter.
|
|
186
|
+
|
|
187
|
+
It's your data in your format: plain Markdown, git history, editable in any
|
|
188
|
+
tool, never leaves the machine. Reference:
|
|
189
|
+
[docs/reference/memory.md](./docs/reference/memory.md); synthesis design:
|
|
190
|
+
[docs/design/memory-synthesis.md](./docs/design/memory-synthesis.md); on the
|
|
191
|
+
site: [Personal memory](https://mlx-bun.dev/guides/memory/).
|
|
192
|
+
|
|
193
|
+
## The lab
|
|
194
|
+
|
|
195
|
+
Parity, evals, kernels, training, and sampling research live next to the
|
|
196
|
+
runtime that serves them:
|
|
197
|
+
|
|
198
|
+
- **LoRA fine-tuning on your Mac** (SFT / DPO / ORPO) — `mlx-bun train
|
|
199
|
+
<model> --data <dir>`, with a flash-CCE Metal head, segmented backward, and
|
|
200
|
+
prefix-sharing so long-context preference training fits laptop memory;
|
|
201
|
+
watch runs live with `mlx-bun train-watch`
|
|
202
|
+
([training](./docs/reference/training.md),
|
|
203
|
+
[quickstart](./docs/reference/orpo-quickstart.md)).
|
|
204
|
+
- **The curve designer** — HLG tone-curve sampling with an interactive
|
|
205
|
+
designer in the server UI at `/curves`
|
|
206
|
+
([design](./docs/design/hlg-sampling.md)).
|
|
207
|
+
- **Speculative decoding research (DSpark)** — a KV-injection drafter
|
|
208
|
+
([design](./docs/design/dspark-speculative-decoding.md)).
|
|
209
|
+
- The full research trail — including dead ends — is tracked in
|
|
210
|
+
[docs/design/](./docs/design/) and
|
|
211
|
+
[docs/investigations/](./docs/investigations/).
|
|
212
|
+
|
|
213
|
+
On the site: [The lab](https://mlx-bun.dev/about/lab/).
|
|
214
|
+
|
|
150
215
|
## Supported models
|
|
151
216
|
|
|
152
217
|
Scope is deliberate: a few model families held to **bit-exact** logit
|
|
@@ -164,7 +229,9 @@ Currently MiniCPM5, the Gemma-4 OptiQ quants, and Qwen3.5-4B:
|
|
|
164
229
|
Not sure what fits your machine? `bun src/cli.ts fit <model> --ctx 8192`
|
|
165
230
|
gives a deterministic answer (see below). The larger
|
|
166
231
|
`Qwen3.6-27B-OptiQ-4bit` is still in bring-up — parity and serving polish
|
|
167
|
-
remain (see [PLAN.md](./PLAN.md)).
|
|
232
|
+
remain (see [PLAN.md](./PLAN.md)). Downloading, cache layout, and reclaiming
|
|
233
|
+
disk: [docs/reference/models.md](./docs/reference/models.md); on the site:
|
|
234
|
+
[Choosing a model](https://mlx-bun.dev/getting-started/models/).
|
|
168
235
|
|
|
169
236
|
## Why
|
|
170
237
|
|
|
@@ -204,8 +271,12 @@ bun src/cli.ts ls # list models (size, params, quant, capabili
|
|
|
204
271
|
bun src/cli.ts ls --vision --max-size 10GB
|
|
205
272
|
bun src/cli.ts fit gemma --ctx 32768 # memory contract: fits? max context? predicted tok/s
|
|
206
273
|
bun src/cli.ts fit gemma --ctx 8192 --skus # ...same, across the Apple Silicon lineup
|
|
207
|
-
bun src/cli.ts serve gemma --port
|
|
274
|
+
bun src/cli.ts serve gemma --port 8080 # OpenAI-compatible server
|
|
208
275
|
bun src/cli.ts serve gemma --memory-budget 18 # ...with admission control (GB)
|
|
276
|
+
bun src/cli.ts generate gemma "a haiku about metal shaders" # raw one-shot generation
|
|
277
|
+
bun src/cli.ts train e4b --data ./pref-data --method orpo # LoRA fine-tune (SFT/DPO/ORPO)
|
|
278
|
+
bun src/cli.ts memory init # git-tracked Markdown wiki at ~/.mlx-bun/wiki
|
|
279
|
+
bun src/cli.ts memory synthesize # conversations -> cross-linked wiki articles, all local
|
|
209
280
|
bun src/cli.ts pi # built-in agentic coding CLI (pi's TUI, in-process)
|
|
210
281
|
bun src/cli.ts evals # recorded benchmark runs
|
|
211
282
|
bun src/cli.ts harness pi # connect your own pi install to the local server
|
|
@@ -223,11 +294,14 @@ MoE active-expert bytes for decode), calibrated prefill transient, and
|
|
|
223
294
|
the machine's wired-memory ceiling. Predictions are recorded next to
|
|
224
295
|
measured peaks in the eval DB.
|
|
225
296
|
|
|
297
|
+
Every verb, with flags: [docs/reference/cli.md](./docs/reference/cli.md);
|
|
298
|
+
on the site: [CLI reference](https://mlx-bun.dev/reference/cli/).
|
|
299
|
+
|
|
226
300
|
## HTTP API
|
|
227
301
|
|
|
228
302
|
`serve` exposes an OpenAI-compatible API. Anything that speaks the
|
|
229
303
|
OpenAI protocol can point at it — the OpenAI SDK
|
|
230
|
-
(`baseURL: "http://localhost:
|
|
304
|
+
(`baseURL: "http://localhost:8080/v1"`, any non-empty `apiKey`), or
|
|
231
305
|
agent CLIs like pi/OpenClaw via their provider config.
|
|
232
306
|
|
|
233
307
|
- **`POST /v1/chat/completions`** — streaming (SSE) and non-streaming;
|
|
@@ -278,21 +352,30 @@ agent CLIs like pi/OpenClaw via their provider config.
|
|
|
278
352
|
up front is the only defense. Ceiling visible at `GET /stats`.
|
|
279
353
|
- **Anthropic Messages API** (`POST /v1/messages`, on by default) —
|
|
280
354
|
point any Anthropic-SDK tool at the server
|
|
281
|
-
(`ANTHROPIC_BASE_URL=http://localhost:
|
|
355
|
+
(`ANTHROPIC_BASE_URL=http://localhost:8080`); Claude Code runs
|
|
282
356
|
against it as a fully local backend. Streaming event grammar, native
|
|
283
357
|
tool_use/tool_result mapping, image blocks via the vision path.
|
|
284
358
|
- **OpenAI Responses API** (`POST /v1/responses`) — the protocol
|
|
285
359
|
Codex/Cursor/Continue speak; includes `previous_response_id`
|
|
286
360
|
resumption backed by a TTL + byte-capped store (visible in /stats).
|
|
361
|
+
- **OpenAI Embeddings API** (`POST /v1/embeddings`) — when the served
|
|
362
|
+
model is an embedding model (`mlx-bun serve Qwen3-Embedding-4B-4bit-DWQ`):
|
|
363
|
+
last-token pooled, L2-normalized vectors, bit-exact vs mlx-lm. Also a
|
|
364
|
+
one-shot CLI: `mlx-bun embed <model> --text "…"`.
|
|
287
365
|
- **`GET /v1/models`**, **`GET /stats`** (cache hit rates, bytes,
|
|
288
366
|
active KV scheme, response store), **`GET/POST/DELETE /v1/adapters`**,
|
|
289
367
|
plus **`GET /library`**, **`GET /fit`**, **`GET /downloads`**, and
|
|
290
368
|
**`GET /v1`** (a self-describing index of every endpoint).
|
|
291
369
|
|
|
370
|
+
Guide with examples on the site:
|
|
371
|
+
[The HTTP API](https://mlx-bun.dev/guides/http-api/); coming from
|
|
372
|
+
`mlx_lm.server`:
|
|
373
|
+
[Drop-in for mlx-lm](https://mlx-bun.dev/guides/drop-in-mlx-lm/).
|
|
374
|
+
|
|
292
375
|
## Library
|
|
293
376
|
|
|
294
377
|
The server is one consumer of a library-first API. Published to npm as
|
|
295
|
-
`mlx-bun` (current: 0.0.
|
|
378
|
+
`mlx-bun` (current: 0.0.8) — import from the package (`from "mlx-bun"`)
|
|
296
379
|
or `./src/index` in a clone; `bunx mlx-bun` runs the CLI without
|
|
297
380
|
installing. Full reference:
|
|
298
381
|
[docs/reference/library-api.md](./docs/reference/library-api.md). For shipping inside a
|
|
@@ -326,6 +409,9 @@ KV caches can be persisted to disk (page-aligned files that reload as
|
|
|
326
409
|
zero-copy GPU-safe mmaps — see `src/kv-store.ts`) so a standard agent
|
|
327
410
|
preamble prefills once, ever.
|
|
328
411
|
|
|
412
|
+
On the site: [Using the library](https://mlx-bun.dev/guides/library/) and
|
|
413
|
+
[Embedding in a Mac app](https://mlx-bun.dev/guides/embedding/).
|
|
414
|
+
|
|
329
415
|
## Benchmarks
|
|
330
416
|
|
|
331
417
|
Head-to-head against the Python stacks (mlx-lm 0.31.3, mlx-optiq 0.2.1),
|
|
@@ -368,6 +454,9 @@ both served it from the same machine state). One further optiq cell
|
|
|
368
454
|
(12B/kv=config) is blocked on an upstream `quantized_matmul` bug. Both
|
|
369
455
|
are documented in the results file.
|
|
370
456
|
|
|
457
|
+
On the site (with the honest negatives spelled out):
|
|
458
|
+
[Benchmarks](https://mlx-bun.dev/about/benchmarks/).
|
|
459
|
+
|
|
371
460
|
## Correctness
|
|
372
461
|
|
|
373
462
|
Logit parity with mlx-lm (same weights, Python reference) is the
|
|
@@ -387,6 +476,9 @@ bun test # fast tier runs everywhere; model-loaded tests run only
|
|
|
387
476
|
# when the reference snapshot is in your HF cache
|
|
388
477
|
```
|
|
389
478
|
|
|
479
|
+
On the site: [Correctness](https://mlx-bun.dev/about/correctness/) and
|
|
480
|
+
[How it compares](https://mlx-bun.dev/about/comparison/).
|
|
481
|
+
|
|
390
482
|
## Troubleshooting
|
|
391
483
|
|
|
392
484
|
- **`dlopen` / `libmlxc.dylib` not found** — the MLX runtime
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Batching perf path — promote continuous batching out of compat mode
|
|
2
|
+
|
|
3
|
+
Status: P5 + two gate fixes LANDED (2026-07-02, same day); rest PLANNED.
|
|
4
|
+
|
|
5
|
+
**Headline result (M1 Max 32 GB, loaded machine, 4 concurrent × 128 tok,
|
|
6
|
+
median of 3, wall-clock aggregate; oMLX 0.4.5.dev1, same OptiQ snapshots):**
|
|
7
|
+
|
|
8
|
+
| Model | oMLX | mlx-bun `--batch 4` | mean TTFT (theirs/ours) |
|
|
9
|
+
| --- | --- | --- | --- |
|
|
10
|
+
| MiniCPM5-1B | 339.0 | **345.4** | 397 / **119 ms** |
|
|
11
|
+
| gemma-4-e4b | **89.8** | 87.1 (−3%) | 815 / **397 ms** |
|
|
12
|
+
| Qwen3.5-4B | **100.9** | 99.5 (−1%) | 848 / **368 ms** |
|
|
13
|
+
|
|
14
|
+
What landed (all bit-gated, `tsc` 0, suites green):
|
|
15
|
+
1. **P5 — SSM batched path** (`SSMCache.mergeRows`/`filter` in
|
|
16
|
+
`src/model/qwen3-delta.ts`; "ssm" kind in `batch-scheduler.ts` merge/wrap/
|
|
17
|
+
filter; per-row RoPE via `ropeOffsetArr` in `qwen3_5.ts`; gate relaxed in
|
|
18
|
+
`generation-gateway.ts`, kill switch `MLX_BUN_BATCH_SSM=0`). Oracle:
|
|
19
|
+
`tests/qwen35-batched-parity.test.ts` — solo-prefill+merge batched greedy
|
|
20
|
+
is **token-for-token identical** to mlx-lm B=2 left-padded batch
|
|
21
|
+
(`goldens/apple-m1-max/batched-golden-qwen35.json`). No ssm_mask needed:
|
|
22
|
+
rows solo-prefill unpadded, decode feeds one real token per row.
|
|
23
|
+
2. **Logits processors batch** (`generation-gateway.ts`): the per-row sampler
|
|
24
|
+
folds `makeLogitsProcessors` over a per-row device-side token history
|
|
25
|
+
(generate()'s pushHistory), so repetition/presence/frequency penalties +
|
|
26
|
+
logit_bias + min_p/XTC no longer route serial. Load-bearing discovery:
|
|
27
|
+
**Qwen3.5 ships a default repetition penalty in generation_config.json**,
|
|
28
|
+
which routed EVERY request serial under `--batch N` — the SSM port alone
|
|
29
|
+
changed nothing over HTTP until this landed.
|
|
30
|
+
3. Diagnostic breadcrumb: `MLX_BUN_LANE_DEBUG=1` logs per-request lane
|
|
31
|
+
decisions + shape (server.ts) — how (2) was found.
|
|
32
|
+
|
|
33
|
+
Bench-method traps found while measuring — **both FIXED 2026-07-02**:
|
|
34
|
+
- `serve`/`bench` accepted `--model` but IGNORED it (positional-only fuzzy
|
|
35
|
+
query; auto-pick silently loaded the wrong model). Now: `--model
|
|
36
|
+
<path-or-query>` is an explicit override — a directory with config.json
|
|
37
|
+
loads via `scanSnapshot` (no registry dependency), anything else resolves
|
|
38
|
+
like the positional. Precedence: `--model` > positional > `--query` >
|
|
39
|
+
auto-pick.
|
|
40
|
+
- Event-loop starvation during generation was the SERIAL lane, not batch
|
|
41
|
+
(batch's drive loop yields per step — `/stats` answered in ~8 ms mid-
|
|
42
|
+
generation once measured properly): serial decode is an unbroken microtask
|
|
43
|
+
chain, so `/stats`//health/new accepts stalled for the whole generation
|
|
44
|
+
(measured 2.5 s on a 512-token run). Fixed with a ≥25 ms rate-limited
|
|
45
|
+
macrotask hop wrapped around onToken in `GenerationGateway.run`'s serial
|
|
46
|
+
branch — covers all four API surfaces, streaming and not. After: `/stats`
|
|
47
|
+
10–44 ms mid-generation, decode unchanged (cpm5 serial 193 tok/s). Motivated by the oMLX head-to-head (M1 Max,
|
|
48
|
+
2026-07-02): at 4 concurrent requests oMLX's continuous batching gets
|
|
49
|
+
1.46–1.89× our aggregate throughput (Qwen3.5-4B 97.3 vs 66.5 tok/s;
|
|
50
|
+
MiniCPM5 328 vs 173; e4b 85 vs 52) and our mean TTFT queue-inflates to
|
|
51
|
+
1.1–3.8 s. Single-stream we match or beat them (e4b 55.8 vs 53.7,
|
|
52
|
+
Qwen3.5 66.3 vs 63.9, TTFT 3–7× better everywhere); the gap is entirely
|
|
53
|
+
the serial lane going flat under parallel load. Full comparison + numbers:
|
|
54
|
+
the 2026-07-02 oMLX report (Claude artifact) and `~/.omlx` bench scripts.
|
|
55
|
+
|
|
56
|
+
Design stance: the batch lane grows the **same parity-tier DAG as the
|
|
57
|
+
serial lane** (docs/design/parity-tier-dag.md). `--l1 --batch N` stays
|
|
58
|
+
bit-exact mlx-lm-B=N compat mode forever (the oracle checkpoint); the
|
|
59
|
+
default/`--l3` batch path becomes the optimized one. `--batch` remains a
|
|
60
|
+
mode switch, not a load fallback.
|
|
61
|
+
|
|
62
|
+
## 1. Current state
|
|
63
|
+
|
|
64
|
+
`src/serve/batch-scheduler.ts` (`BatchScheduler`) is a real
|
|
65
|
+
continuous-batching engine: iteration-level scheduling, pipelined decode
|
|
66
|
+
(`#step` via `ops.asyncEvalAll`, mlx-lm `GenerationBatch._step`
|
|
67
|
+
semantics), chunked joiner prefill (2048-token chunks) interleaved with
|
|
68
|
+
decode, per-row eviction (`filterKVRows` / `BatchedRotatingCache.filter`),
|
|
69
|
+
per-row failure containment, drain-on-serial-waiter, `clearCache` every
|
|
70
|
+
256 steps. Bit-parity with mlx-lm B=N verified at B=2 across all 4 model
|
|
71
|
+
paths (`tests/batched-decode-parity.test.ts`, `tests/batched-rotating.test.ts`,
|
|
72
|
+
`tests/batch-scheduler.test.ts`).
|
|
73
|
+
|
|
74
|
+
`src/serve/generation-gateway.ts` (`GenerationGateway.willBatch`) routes;
|
|
75
|
+
one `AsyncMutex` is the GPU/`loraState` exclusion domain. The batch lane
|
|
76
|
+
is deliberately compat mode: it calls `model.forwardHidden` /
|
|
77
|
+
`logitsFromHidden` directly, bypassing `generate()` and every perf lever.
|
|
78
|
+
|
|
79
|
+
### Levers the batch lane lacks, and why
|
|
80
|
+
|
|
81
|
+
| Lever | Serial mechanism | Why batch can't use it today |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| Quantized KV (`kv_config`) | `maybeQuantizeKv` (`src/generate.ts:101`) | `QuantizedKVCache` (`src/model/gemma4-base.ts:419`) has no dynamic-B ops (no merge/filter/extend/temporalView over the packed/scales/biases triples); `willBatch` hard-gates `kvQuant` → serial |
|
|
84
|
+
| Perf-kernel fused decode SDPA | dispatched from `quantizedSdpa` (`gemma4-base.ts:1560`) | kernel M=1-specialized: `fused-decode-kernel.ts:262` rejects `B !== 1 \|\| L !== 1`, and requires empty mask (batched decode needs per-row left-pad validity) |
|
|
85
|
+
| Fused N-tiled SDPA (`--fused-decode`) | `quantizedSdpaTiled` | needs 2-D mask; batched masks are 4-D `[B,1,1,S]` (`buildBatchedDecodeMask`); quantized-only anyway |
|
|
86
|
+
| Compiled decode | `CompiledDecode.for()` (`generate.ts:518-525`) | trace is `[1,1]`; B changes every join/evict |
|
|
87
|
+
| Prompt-cache prefix reuse | `PromptCache.take/put` in `runGeneration` | bypassed by design in v1; every batched row solo-prefills (`cachedTokens: 0`) |
|
|
88
|
+
| Vectorized sampling | n/a | `#step` samples per row (B slice+sample+concat graphs per token) |
|
|
89
|
+
|
|
90
|
+
Two more gap sources specific to the benchmark:
|
|
91
|
+
|
|
92
|
+
- **Qwen3.5 never batches at all**: `SSMCache` (`src/model/qwen3-delta.ts`)
|
|
93
|
+
has no merge/filter, so the capability gate routes every request serial.
|
|
94
|
+
The Qwen3.5 concurrency loss is pure serial queueing. mlx-lm batches
|
|
95
|
+
Mamba-family state via `ArraysCache` — oracle exists.
|
|
96
|
+
- Joins re-merge the whole batch (O(B·S) slice storm, `#mergeJoiner`)
|
|
97
|
+
instead of mlx-lm's `extend`.
|
|
98
|
+
|
|
99
|
+
## 2. Gap analysis: quantized KV + fused kernels at B>1
|
|
100
|
+
|
|
101
|
+
- `QuantizedKVCache` already stores `[B,H,S,D']` triples; missing is
|
|
102
|
+
dynamic-B surgery. New `src/model/batched-quant.ts`:
|
|
103
|
+
`BatchedQuantizedKVCache` = quantized twin of `BatchedRotatingCache`
|
|
104
|
+
(merge/extend/filter/extract/temporalView = the `mergeKVRows`/`filterKVRows`
|
|
105
|
+
B-axis ops from `src/model/batched-mask.ts:171,196`, applied to all three
|
|
106
|
+
triples). Left-pad must align to quantization group boundaries — pad in
|
|
107
|
+
whole 256-token STEP granules (both caches already grow in STEP=256).
|
|
108
|
+
- Conversion point mirrors `maybeQuantizeKv`: row solo-prefills bf16,
|
|
109
|
+
`toQuantized` per layer per `kv_config` before merge.
|
|
110
|
+
- **Scope cut**: shipped `kv_config.json` quantize full-attention layers
|
|
111
|
+
only ⇒ P1 = quantized full layers + bf16 `BatchedRotatingCache` sliding
|
|
112
|
+
layers (the serial `config` composition). Batched rotating-quant deferred.
|
|
113
|
+
- Correctness fallback: `quantizedSdpaUnfused` (`gemma4-base.ts:1273`) is
|
|
114
|
+
already B-generic with `mask.mode === "array"`; needs the batched mask
|
|
115
|
+
broadcast as `[B,1,1,1,N]` for the 5-D GQA score reshape.
|
|
116
|
+
- Perf kernel at B>1: (1) drop the `B !== 1` gate (keep `L !== 1`);
|
|
117
|
+
(2) grid gains a B axis (`[TG_THREADS, H·B, G]`), `SCORE_ROW` offsets
|
|
118
|
+
index `((b·KV + kv)·N + n)`; (3) replace the empty-mask requirement with
|
|
119
|
+
a per-row `leftPad` int32 `[B]` input — each row's n-loop starts at
|
|
120
|
+
`max(blockStart, leftPad[b])`, so pad bytes are never read.
|
|
121
|
+
- RoPE per-row positions already solved (`ropeOffsetArr`, the S1b.1 fix).
|
|
122
|
+
|
|
123
|
+
## 3. Phases
|
|
124
|
+
|
|
125
|
+
### P0 — Baseline + engine quick wins (no new numerics) [S–M, 2–4 days]
|
|
126
|
+
- Write `scripts/bench-serving-load.ts` (N concurrent vs live server,
|
|
127
|
+
aggregate tok/s + TTFT p50/p95). Re-measure vs oMLX **with `--batch 4`
|
|
128
|
+
actually on** to size the true engine gap vs the serial-queueing gap.
|
|
129
|
+
- Land `extend` join (full-attention twin + `BatchedRotatingCache.extend`);
|
|
130
|
+
oracle: extend `scripts/gen-batched-dynamic-golden.py` (mlx-lm `extend`).
|
|
131
|
+
- Vectorize the homogeneous-sampler fast path in `#step` (all-greedy →
|
|
132
|
+
one `ops.argmaxAxis` on `[B,V]`); mixed rows keep the loop.
|
|
133
|
+
- Exit: gated suites unchanged; B=4 baseline recorded; joins not O(B·S).
|
|
134
|
+
|
|
135
|
+
### P1 — Quantized KV at B>1 [M–L, 1–1.5 wk]
|
|
136
|
+
- `src/model/batched-quant.ts` + scheduler wiring (layer kind
|
|
137
|
+
`"full" | "rot" | "qfull"`); solo-prefill bf16 → `toQuantized` → extend.
|
|
138
|
+
- Batched mask 5-D broadcast; validate `quantizedSdpaUnfused`.
|
|
139
|
+
- `willBatch`: `kvQuant` stops being a serial gate when quant layers are
|
|
140
|
+
full-attention-only.
|
|
141
|
+
- Exit (parity ladder; no external oracle — mlx-lm quant batching NYI):
|
|
142
|
+
(1) B=1-row batched-quant **bit-exact vs serial `--kv-quant config`**;
|
|
143
|
+
(2) row-isolation within the calibrated bf16-reduction envelope;
|
|
144
|
+
(3) KL vs batched-bf16 ≤ serial quant-vs-bf16 KL; 6-task eval unchanged;
|
|
145
|
+
(4) `--l1 --batch N` suites untouched.
|
|
146
|
+
|
|
147
|
+
### P2 — Perf kernel at B>1 + fallback matrix [M, ~1 wk]
|
|
148
|
+
- Kernel edits per §2; dispatch when every row supported, else unfused.
|
|
149
|
+
- Fallback matrix documented in server-config.md.
|
|
150
|
+
- Batched frozen-oracle envelope (B=2/4 `freeze-perf-oracle` variant,
|
|
151
|
+
≥56/64 per row, `tests/perf-kernel-oracle.test.ts` pattern).
|
|
152
|
+
- Exit: dispatch counter proves engagement; envelope green; measured
|
|
153
|
+
uplift at B=4 vs P1 unfused on a clean machine; if <5%, ship the
|
|
154
|
+
fallback matrix only and close honestly.
|
|
155
|
+
|
|
156
|
+
### P3 — Admission, prompt cache, adapters, defaults [M–L, 1–1.5 wk]
|
|
157
|
+
- KV-budget admission: `projectKvBytes(model, promptLen, maxTokens)`
|
|
158
|
+
beside `fit()`; `--kv-budget`, `--prompt-concurrency`;
|
|
159
|
+
`/stats.batch.{pending_rows,kv_bytes,kv_budget_bytes}`.
|
|
160
|
+
- Prompt-cache reuse under batching: `take` at admit, forward uncached
|
|
161
|
+
tail; on finish/evict `extract(row)` → solo caches → `put`. Quantized
|
|
162
|
+
entries are non-trimmable mid-group — match on full-prefix or STEP
|
|
163
|
+
boundaries. Real `prefillMs/decodeMs`; `BatchStats.cachedTokens` real.
|
|
164
|
+
- Single-adapter batches: adapter-spec = batch compatibility key
|
|
165
|
+
(join if match, drain if not; `loraState.active` set once per epoch).
|
|
166
|
+
- Default review: if batch lane at 1 live row is within ~2% of serial,
|
|
167
|
+
propose flipping the shipped default to `--batch 4` (Josh decision,
|
|
168
|
+
numerics change vs serial default); else README recommends `--batch 4`
|
|
169
|
+
for agent workloads.
|
|
170
|
+
- Exit: TTFT p50 <500 ms / p95 <1.5 s at 4 concurrent short prompts;
|
|
171
|
+
`cached_tokens>0` on repeats under `--batch 2`; oversized-request
|
|
172
|
+
queueing test (no OOM); B=4 aggregate within 15% of oMLX on cpm5 + e4b.
|
|
173
|
+
|
|
174
|
+
### P4 — Burst decode [REVISED after reading + porting oMLX's source]
|
|
175
|
+
- **oMLX-style event-loop bursting: BUILT AND REFUTED (2026-07-02).** Read
|
|
176
|
+
their actual implementation (`omlx/engine_core.py _step_burst` — full
|
|
177
|
+
source ships in `/Applications/oMLX.app/Contents/Resources/omlx/`): it is
|
|
178
|
+
NOT a GPU trick — it runs K `scheduler.step()` calls per
|
|
179
|
+
`run_in_executor` hand-off with adaptive time budgets (100 ms single /
|
|
180
|
+
30 ms concurrent, 64-step cap) because each per-token hand-off ping-pongs
|
|
181
|
+
the GIL with asyncio/uvicorn (~1 ms/token; their comment: 80 vs 74 tok/s).
|
|
182
|
+
Ported faithfully to `BatchScheduler.#drive` and measured: **regression**
|
|
183
|
+
— cpm5 B=4 345→289, batch-lane B=1 149→121, TTFT +~100 ms (SSE flushes
|
|
184
|
+
wait out the budget). Bun's `setImmediate` hop costs microseconds — no
|
|
185
|
+
GIL to dodge — so the port kept the cost and lost the win. Reverted;
|
|
186
|
+
numbers restored (cpm5 B=4 349.1). Breadcrumb comment left at the drive
|
|
187
|
+
loop. This also explains their ~21-token SSE bursts (measurement trap in
|
|
188
|
+
the comparison report).
|
|
189
|
+
- **Still open (different mechanism, not refuted): device-side step
|
|
190
|
+
chaining** — generalize `generateInner`'s pipeline to depth k (k chained
|
|
191
|
+
step graphs fed device-side, one host sync per k tokens). That attacks
|
|
192
|
+
OUR real per-token cost (FFI graph-build + sync readback), which is not
|
|
193
|
+
what oMLX's burst attacks. Exit unchanged: bit-exact vs depth-1, cpm5
|
|
194
|
+
single-stream gap vs oMLX (178 vs ~214) is the target; no regression at
|
|
195
|
+
k=1.
|
|
196
|
+
|
|
197
|
+
### P5 — SSM batched path (Qwen3.5) [DONE 2026-07-02 — see header]
|
|
198
|
+
- Landed in one day, much cheaper than the ~1 wk estimate: `ssm_mask` under
|
|
199
|
+
left-padding turned out unnecessary (our join model solo-prefills unpadded,
|
|
200
|
+
so SSM state never sees a pad token; mlx-lm's masked-pad prefill is
|
|
201
|
+
equivalent — proven by the token-exact oracle match). Batch ops are plain
|
|
202
|
+
B-axis concat/take on the two state slots.
|
|
203
|
+
- Bonus fix that the exit benchmark forced out: logits processors now batch
|
|
204
|
+
(per-row fold; Qwen3.5's default repetition penalty had been routing every
|
|
205
|
+
request serial). Exit numbers in the header table.
|
|
206
|
+
|
|
207
|
+
Ordering: P1→P2 strictly ordered; P4 and P5 independent (P5 first if the
|
|
208
|
+
Qwen3.5 headline matters most). Total ~5–6 wk serial, ~4 wk with P4/P5
|
|
209
|
+
parallel.
|
|
210
|
+
|
|
211
|
+
## 4. Risks
|
|
212
|
+
|
|
213
|
+
- **No external oracle for quant-batched** — mitigated by the P1 parity
|
|
214
|
+
ladder; `--l1 --batch N` stays the always-reachable bit-exact checkpoint.
|
|
215
|
+
- **Metal kernel at B>1**: leftPad start-index never touches pad bytes;
|
|
216
|
+
no atomics (deterministic); gate = batched frozen-oracle envelope +
|
|
217
|
+
dispatch assertions + A/B vs unfused.
|
|
218
|
+
- **Memory**: B× quant caches + prompt cache + weights on 24 GB — P3
|
|
219
|
+
admission is the backstop; mirror mlx-lm's
|
|
220
|
+
`trim_to(total − activeBatchBytes)` so LRU and live batch share one
|
|
221
|
+
ceiling. GPU OOM is uncatchable — queueing test before default changes.
|
|
222
|
+
- **Ring-wrap at B>1 short-context-verified only**: land a long-context
|
|
223
|
+
bf16 batched golden before any P3 default flip. Quantized rotating out
|
|
224
|
+
of scope.
|
|
225
|
+
- **Default flip changes numerics** vs today's serial default — explicit
|
|
226
|
+
Josh decision with doc trail.
|
|
227
|
+
- **Benchmark hygiene**: quotable numbers via `bench-serving-load.ts` on a
|
|
228
|
+
clean machine; TTFT at the SSE first-token event, not server-side
|
|
229
|
+
(oMLX's own server log over-reports by excluding ~180 ms/req cache
|
|
230
|
+
bookkeeping — measure them wall-clock only).
|
|
231
|
+
- **Regression fence**: all existing gated batch suites + the serial suite
|
|
232
|
+
green unchanged at every phase.
|