mlx-bun 0.0.7 → 0.0.9
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-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/diffusion-gemma-port.md +602 -0
- 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/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/spec-decode-larger-targets.md +229 -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 +262 -0
- package/docs/investigations/dspark-handoff.md +58 -0
- package/docs/investigations/kernel-perf-review-2026-07.md +92 -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.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 +40 -9
- package/docs/reference/training.md +25 -11
- 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 +1029 -86
- package/src/config.ts +54 -3
- package/src/diffusion/diffusion-generate.ts +436 -0
- package/src/download.ts +99 -8
- package/src/embed.ts +77 -0
- package/src/eval/perplexity.ts +164 -0
- package/src/eval/runner.ts +19 -7
- package/src/eval/tasks/bfcl.ts +1 -1
- package/src/generate.ts +196 -19
- package/src/harness-pi.ts +43 -17
- package/src/index.ts +2 -0
- 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/ffi.ts +11 -0
- package/src/mlx/ops.ts +113 -0
- package/src/model/compiled-decode.ts +43 -8
- package/src/model/diffusion-gemma.ts +1026 -0
- package/src/model/factory.ts +35 -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 +62 -6
- package/src/model/gemma4.ts +33 -2
- package/src/model/minicpm5.ts +106 -8
- package/src/model/qwen3.ts +276 -0
- package/src/model/steel-linear-kernel.ts +96 -0
- package/src/model/support.ts +45 -7
- 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/quantize/config-writer.ts +1 -1
- package/src/registry.ts +215 -7
- package/src/responses.ts +23 -0
- package/src/sampler.ts +220 -9
- package/src/serve/batch-scheduler.ts +272 -80
- package/src/serve/generation-gateway.ts +90 -18
- package/src/server.ts +643 -50
- 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/status-page.html +1 -1
- package/src/tokenizer.ts +7 -0
- package/src/train/diffusion-lora.ts +216 -0
- package/src/train/flash-cce.ts +70 -15
- 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 +289 -30
- package/src/train/prefix-shared.ts +33 -5
- package/src/train/segmented.ts +67 -11
- package/src/train/trainer.ts +92 -28
- package/src/vision/diffusion-vision.ts +620 -0
- package/src/web/skills/memory/SKILL.md +40 -17
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Model management: get / scan / ls / gc
|
|
2
|
+
|
|
3
|
+
How mlx-bun downloads, indexes, lists, and reclaims models. The store is
|
|
4
|
+
the standard Hugging Face hub cache — nothing proprietary; `hf` and
|
|
5
|
+
mlx-lm read/write the same tree.
|
|
6
|
+
|
|
7
|
+
## The cache layout (and why duplicates happen)
|
|
8
|
+
|
|
9
|
+
Default root: `~/.cache/huggingface/hub` (override with `HF_HUB_CACHE`,
|
|
10
|
+
or `HF_HOME` — the root becomes `$HF_HOME/hub`). Per repo:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
models--<org>--<name>/
|
|
14
|
+
blobs/<digest> # verified content, one file per blob
|
|
15
|
+
snapshots/<commit>/<file> → ../../blobs/<digest> # symlink farm
|
|
16
|
+
refs/<revision> # e.g. refs/main = the commit sha
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Snapshot-per-commit semantics:** every revision you download gets its
|
|
20
|
+
own `snapshots/<commit>` directory, and nothing ever deletes old ones.
|
|
21
|
+
When upstream pushes a new commit and you `get` again, you gain a new
|
|
22
|
+
snapshot; the previous one — and any blobs only it references, i.e. the
|
|
23
|
+
old weights — stays on disk forever. That is why one repo can appear
|
|
24
|
+
several times in a raw snapshot listing, and how tens of GB of dead
|
|
25
|
+
blobs accumulate. `refs/main` always names the current (canonical)
|
|
26
|
+
snapshot; the registry and every query verb resolve through it.
|
|
27
|
+
|
|
28
|
+
## get — download (resumable, verified)
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
mlx-bun get <org/repo> [--revision main]
|
|
32
|
+
mlx-bun get <substring> # no "/" = registry query, re-gets the match
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- Plain HTTPS resolve/CDN (no Xet), sequential files, `Range`-resume of
|
|
36
|
+
partial blobs (`<blob>.incomplete`). Auth: `HF_TOKEN` env, then the
|
|
37
|
+
token `hf auth login` writes. 401/403 answers get a "gated repo" hint.
|
|
38
|
+
- A substring argument (no `/`) resolves against already-downloaded
|
|
39
|
+
repos — `mlx-bun get 12B` refreshes the 12B to upstream's latest. An
|
|
40
|
+
unknown substring errors with a pointer to `mlx-bun ls <q>`.
|
|
41
|
+
- Concurrent-writer safety: an `O_EXCL` lockfile (`<blob>.lock`, pid +
|
|
42
|
+
timestamp) serializes two processes downloading the same blob (e.g. a
|
|
43
|
+
foreground `get` racing the server's background auto-download). Locks
|
|
44
|
+
from dead pids, or older than ~1 h, are stolen; a live lock fails fast
|
|
45
|
+
with "another download … is in progress".
|
|
46
|
+
- If the download created a new snapshot (upstream pushed), `get` says
|
|
47
|
+
so and estimates what `mlx-bun gc` would reclaim.
|
|
48
|
+
|
|
49
|
+
### Verification story
|
|
50
|
+
|
|
51
|
+
Every blob is checksummed **while streaming** (a resume re-hashes the
|
|
52
|
+
existing prefix in chunks — no whole-file allocation):
|
|
53
|
+
|
|
54
|
+
- **LFS files** (weights): sha256 must equal the API's `lfs.oid` — which
|
|
55
|
+
is also the blob's filename.
|
|
56
|
+
- **Small files** (configs, tokenizer): git blob identity,
|
|
57
|
+
`sha1("blob <size>\0" + content)`, must equal the API's `blobId`.
|
|
58
|
+
|
|
59
|
+
A mismatch deletes the partial (never resume corrupt bytes); a short
|
|
60
|
+
read keeps the `.incomplete` for resume.
|
|
61
|
+
|
|
62
|
+
## scan + the registry
|
|
63
|
+
|
|
64
|
+
`mlx-bun scan` walks `models--*/snapshots/*` reading only `config.json`
|
|
65
|
+
+ safetensors **headers** (never tensor bytes) into
|
|
66
|
+
`~/.cache/mlx-bun/registry.sqlite`. The registry is a derived cache: on
|
|
67
|
+
schema drift it rebuilds; rows whose snapshot dir vanished are reaped.
|
|
68
|
+
|
|
69
|
+
Schema (table `models`, one row **per snapshot**, keyed on `path`):
|
|
70
|
+
|
|
71
|
+
| column | meaning |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `path` (PK) | `<hub>/models--…/snapshots/<commit>` |
|
|
74
|
+
| `repo_id`, `model_type` | identity |
|
|
75
|
+
| `param_count`, `num_layers`, `hidden_size`, `vocab_size` | shape |
|
|
76
|
+
| `size_bytes` | language-model weights (sidecar excluded) |
|
|
77
|
+
| `sidecar_bytes` | `optiq_vision.safetensors` (bf16 SigLIP sidecar) |
|
|
78
|
+
| `experts_bytes` | `.experts.` tensor bytes (MoE fit math) |
|
|
79
|
+
| `quant_bits`, `quant_group_size`, `quant_mode` | quantization |
|
|
80
|
+
| `has_vision_sidecar` | sidecar file present |
|
|
81
|
+
| `vision_config_type` | `config.json`'s `vision_config.model_type` when it names a vision tower (`*_vision`) |
|
|
82
|
+
| `has_kv_config`, `has_tool_template`, `license` | capabilities/terms |
|
|
83
|
+
| `scanned_at` | freshness (canonical tie-break) |
|
|
84
|
+
|
|
85
|
+
**Vision capability** is `has_vision_sidecar OR vision_config_type =
|
|
86
|
+
*unified_vision`: the SigLIP models (e2b/e4b/26B/31B,
|
|
87
|
+
`gemma4_vision`) need the bf16 sidecar; the unified 12B
|
|
88
|
+
(`gemma4_unified_vision`) declares its encoder-free tower in config.
|
|
89
|
+
Presence of a `vision_config` key alone is **not** a signal — Qwen3.5
|
|
90
|
+
nests a copy of its own text config there.
|
|
91
|
+
|
|
92
|
+
## Query resolution rules (per verb)
|
|
93
|
+
|
|
94
|
+
Every verb resolves fuzzy queries the same way, with revision collapsing
|
|
95
|
+
so a stale snapshot never makes a repo "ambiguous":
|
|
96
|
+
|
|
97
|
+
- **`ls [query]`** — substring match on repo id / model type; shows **one
|
|
98
|
+
row per repo** (the canonical revision). `--all-revisions` shows the
|
|
99
|
+
per-snapshot truth with the canonical marked `*`. Capabilities column
|
|
100
|
+
labels the support tier: `supported (targeted)` (dedicated forward,
|
|
101
|
+
L2/L3 paths) vs `supported (generic)` (Tier-0 universal module).
|
|
102
|
+
- **`fit` / `serve` / `train` / … (single-model verbs)** — `resolve()`:
|
|
103
|
+
the query must match exactly one **repo**; multiple cached revisions of
|
|
104
|
+
that repo collapse to the canonical snapshot (refs/main, else most
|
|
105
|
+
recently scanned). Drafter companions (`*_assistant`) never count.
|
|
106
|
+
- **`get <substring>`** — same `resolve()`, then re-downloads that repo id.
|
|
107
|
+
|
|
108
|
+
Canonical = the snapshot `refs/main` points at; if no `refs/main`, the
|
|
109
|
+
most recently scanned snapshot (stable path tie-break).
|
|
110
|
+
|
|
111
|
+
## gc — reclaim superseded snapshots + dead blobs
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
mlx-bun gc # plan + per-repo reclaim summary, deletes nothing
|
|
115
|
+
mlx-bun gc --yes # actually delete (destructive)
|
|
116
|
+
mlx-bun gc --dry-run # never delete, even with --yes
|
|
117
|
+
mlx-bun gc --yes --force # also prune warned snapshots (see below)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Per repo: keep every snapshot a `refs/*` file points at, delete the
|
|
121
|
+
rest, then delete blobs no surviving snapshot symlinks to. Prints
|
|
122
|
+
keep/prune/skip counts and reclaimable bytes per repo.
|
|
123
|
+
|
|
124
|
+
Safety rails:
|
|
125
|
+
|
|
126
|
+
- **Deletion requires `--yes`.** The default run is a report.
|
|
127
|
+
- **File-loss tripwire:** a superseded snapshot containing files the
|
|
128
|
+
kept snapshots *lack* is skipped with a warning naming those files —
|
|
129
|
+
deleting it would delete the machine's only copy (live example: a
|
|
130
|
+
stale gemma-4-12B snapshot holds an `optiq_vision.safetensors` that
|
|
131
|
+
the canonical revision dropped). `--force` overrides, deliberately.
|
|
132
|
+
- Repos with no usable refs (no `refs/`, or refs naming a missing
|
|
133
|
+
snapshot) are left untouched.
|
|
134
|
+
- `.incomplete` / `.lock` resume artifacts in `blobs/` are never touched.
|
|
135
|
+
|
|
136
|
+
After a real deletion, gc re-scans so the registry drops the reaped
|
|
137
|
+
rows immediately.
|
|
@@ -37,6 +37,11 @@ mlx-bun train e4b --data ./prefs --resume ~/.cache/mlx-bun/mlx-bun-finetunes/orp
|
|
|
37
37
|
| `--rank` / `--scale` | orpo 16 / 2.0 | LoRA rank (by_bits scaled) / scale |
|
|
38
38
|
| `--seq` | e4b 8192 · cpm 4096 | max sequence length |
|
|
39
39
|
| `--lambda` | 0.1 | ORPO odds-ratio weight |
|
|
40
|
+
| `--sft-scope` | `full` | ORPO chosen-NLL scope (`sft_scope`): `full` = paper/TRL-faithful prompt+response CE · `response` = response-only, bit-exact to pre-2026-07 runs |
|
|
41
|
+
| `--grad-accum` | 1 | gradient accumulation steps (effective batch = batch × grad-accum at batch-size-1 memory) |
|
|
42
|
+
| `--grad-clip` | 1.0 | gradient-norm clip (0 = off) |
|
|
43
|
+
| `--seed` | 0 | data-shuffle / init seed |
|
|
44
|
+
| `--val-size` | 256 | max validation examples per eval |
|
|
40
45
|
| `--seg` | 2 | layers/segment (segmented backward); `--no-segment` to disable |
|
|
41
46
|
| `--save-every` | off | mountable checkpoint every N steps |
|
|
42
47
|
| `--resume <dir>` | — | warm-start LoRA weights from a checkpoint/adapter |
|
|
@@ -65,6 +70,7 @@ MODEL=/path/to/snapshot DATA=/path/to/datadir bun scripts/train-orpo.ts
|
|
|
65
70
|
| `LR` | 1e-5 | learning rate (cosine + short warmup) |
|
|
66
71
|
| `RANK` / `SCALE` | 16 / 2.0 | LoRA rank (by_bits scaled) / scale |
|
|
67
72
|
| `LAMBDA` | 0.1 | ORPO odds-ratio weight |
|
|
73
|
+
| `SFT_SCOPE` | `full` | chosen-NLL scope: `full` (paper/TRL-faithful, prompt+response) · `response` (pre-2026-07 response-only, reproduces old runs) |
|
|
68
74
|
| `FLASH=0` | (on) | use the MLX fused head instead of the flash Metal head |
|
|
69
75
|
| `PREFIX=0` | (on) | disable prefix-sharing (plain two-forward) |
|
|
70
76
|
| `SEGOFF=1` | (off) | disable the segmented backward (more memory) |
|
|
@@ -11,7 +11,9 @@ For the full set of start flags (`--port`, `--memory-budget`,
|
|
|
11
11
|
defaults, and the perf levers) and a compatibility matrix of which
|
|
12
12
|
combinations compose, see [server-config.md](./server-config.md). This
|
|
13
13
|
doc covers the request/response wire format. LoRA adapters are mounted at
|
|
14
|
-
runtime via `POST /v1/adapters` (below),
|
|
14
|
+
runtime via `POST /v1/adapters` (below), or at startup with
|
|
15
|
+
`serve --adapter <dir>` (which also makes that adapter the default for
|
|
16
|
+
requests that don't select one).
|
|
15
17
|
|
|
16
18
|
## POST /v1/chat/completions
|
|
17
19
|
|
|
@@ -25,7 +27,25 @@ Request body (OpenAI chat schema; unknown fields ignored):
|
|
|
25
27
|
"temperature": 0.7, // 0 = greedy
|
|
26
28
|
"top_p": 0, "top_k": 0, // 0 = off
|
|
27
29
|
"seed": 1234, // omit for time-derived
|
|
30
|
+
"min_p": 0.05, // min-p sampling (0 = off)
|
|
31
|
+
"xtc_probability": 0.5, // XTC sampling: chance per step of dropping
|
|
32
|
+
"xtc_threshold": 0.1, // every token above threshold except the
|
|
33
|
+
// least likely (EOS + newline exempt,
|
|
34
|
+
// matching mlx_lm.server)
|
|
35
|
+
"logit_bias": { "42": -5.0 }, // additive per-token-id logit bias
|
|
28
36
|
"repetition_penalty": 1.1, // optional
|
|
37
|
+
"repetition_context_size": 20, // recent-token window (0 = whole history)
|
|
38
|
+
"presence_penalty": 0.0, // subtracted once if the token occurred
|
|
39
|
+
"presence_context_size": 20, // in the window (mlx-lm extension)
|
|
40
|
+
"frequency_penalty": 0.0, // subtracted per occurrence in the window
|
|
41
|
+
"frequency_context_size": 20, // (mlx-lm extension)
|
|
42
|
+
"logprobs": false, // BOOL (mlx_lm.server's type, even on
|
|
43
|
+
// /v1/completions): return each emitted
|
|
44
|
+
// token's logprob
|
|
45
|
+
"top_logprobs": -1, // int in [0, 11] or -1 = unset (mlx-lm's
|
|
46
|
+
// validation — its cap is 11, not
|
|
47
|
+
// OpenAI's 20); k > 0 returns the top-k
|
|
48
|
+
// (token, logprob) pairs per position
|
|
29
49
|
"stop": "\n\n", // or ["###", "\n\n"] (spec: up to 4)
|
|
30
50
|
"tools": [ /* OpenAI function tools */ ],
|
|
31
51
|
"tool_choice": "auto", // "none" disables tools
|
|
@@ -88,6 +108,43 @@ Non-streaming response:
|
|
|
88
108
|
}
|
|
89
109
|
```
|
|
90
110
|
|
|
111
|
+
### logprobs / top_logprobs
|
|
112
|
+
|
|
113
|
+
With `"logprobs": true` and/or `"top_logprobs": k` (1–11), the
|
|
114
|
+
non-streaming response's `choices[0].logprobs` carries
|
|
115
|
+
**`mlx_lm.server`'s block, not OpenAI's** (server.py
|
|
116
|
+
`generate_response`): entries are keyed by token *id*, token strings
|
|
117
|
+
are the raw vocab pieces (`convert_ids_to_tokens` — e.g. `"▁Hello"`),
|
|
118
|
+
and there is no `bytes` field. The same block appears on
|
|
119
|
+
`/v1/completions` (mlx-lm shares the response builder; there is no
|
|
120
|
+
legacy `{tokens, token_logprobs}` text-completion shape).
|
|
121
|
+
|
|
122
|
+
```jsonc
|
|
123
|
+
// top_logprobs: k > 0 (wins over logprobs when both are set — mlx-lm's if/elif)
|
|
124
|
+
"logprobs": { "content": [
|
|
125
|
+
{ "id": 9906, "token": "▁Hello", "logprob": -0.02, // = top-1, merged from
|
|
126
|
+
"top_logprobs": [ // dict(top[0], top_logprobs=top)
|
|
127
|
+
{ "id": 9906, "token": "▁Hello", "logprob": -0.02 },
|
|
128
|
+
{ "id": 13347, "token": "▁Hi", "logprob": -4.1 } // … k entries, sorted desc
|
|
129
|
+
] },
|
|
130
|
+
// … one entry per generated token (reasoning/tool tokens included)
|
|
131
|
+
] }
|
|
132
|
+
|
|
133
|
+
// logprobs: true only
|
|
134
|
+
"logprobs": { "content": [ { "id": 9906, "logprob": -0.02 }, … ] }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The distribution matches mlx-lm `generate_step` exactly: full-vocab
|
|
138
|
+
log-softmax of the logits **after** logits processors (logit_bias,
|
|
139
|
+
penalties), **before** the sampler's temperature/top-p/top-k/min-p/XTC
|
|
140
|
+
(mlx_lm/generate.py L409–422). mlx-lm's top-k order is unspecified
|
|
141
|
+
(argpartition); ours is sorted descending — the same set, so the entry
|
|
142
|
+
is deterministically the argmax. Stream chunks never carry logprobs
|
|
143
|
+
(mlx-lm's streaming responses don't either). Requests with logprobs
|
|
144
|
+
run on the serial lane under `--batch N` (like the other mlx-lm
|
|
145
|
+
sampler extensions). Invalid values are rejected with mlx-lm's exact
|
|
146
|
+
messages (see Errors).
|
|
147
|
+
|
|
91
148
|
Streaming (`"stream": true`) is SSE: `data: <chunk>\n\n` per event,
|
|
92
149
|
terminated by `data: "[DONE]"`. Chunks are `chat.completion.chunk`
|
|
93
150
|
objects whose `choices[0].delta` carries `{role}`, then `{content}`
|
|
@@ -118,7 +175,12 @@ back to plain content.
|
|
|
118
175
|
All errors are `{ "error": { "message": …, ... } }`.
|
|
119
176
|
|
|
120
177
|
- `400` — malformed JSON, empty `messages`, unknown adapter id, vision
|
|
121
|
-
request on a model without a sidecar, prompt build failures
|
|
178
|
+
request on a model without a sidecar, prompt build failures,
|
|
179
|
+
non-numeric `logit_bias` keys/values (`logit_bias must be a dict of
|
|
180
|
+
int to float`, mlx-lm's coercion error), invalid logprobs params
|
|
181
|
+
(mlx-lm's exact validation: `logprobs must be of type bool`,
|
|
182
|
+
`top_logprobs must be of type int` / `at least 0` / `at most 11`;
|
|
183
|
+
`top_logprobs: -1` is the accepted "unset" sentinel).
|
|
122
184
|
- `400` with `"type": "memory_admission"`, `"code":
|
|
123
185
|
"context_over_budget"` — `prompt + max_tokens` exceeds the memory
|
|
124
186
|
budget's max safe context (only when serving with `--memory-budget`;
|
|
@@ -126,11 +188,56 @@ All errors are `{ "error": { "message": …, ... } }`.
|
|
|
126
188
|
up front). Lower `max_tokens` or shorten the prompt; the ceiling is
|
|
127
189
|
visible at `/stats`.
|
|
128
190
|
|
|
191
|
+
## POST /v1/completions (raw text completion)
|
|
192
|
+
|
|
193
|
+
`mlx_lm.server`'s text-completion endpoint: **no chat template** — the
|
|
194
|
+
`prompt` string is tokenized directly (the tokenizer's own BOS handling,
|
|
195
|
+
exactly mlx-lm's `tokenizer.encode(request.prompt)`) and the model
|
|
196
|
+
continues it. Same generation gateway, admission control, prompt cache,
|
|
197
|
+
and adapter selection as chat.
|
|
198
|
+
|
|
199
|
+
```jsonc
|
|
200
|
+
{
|
|
201
|
+
"prompt": "Once upon a time", // REQUIRED, string only (token arrays
|
|
202
|
+
// rejected, matching mlx_lm.server)
|
|
203
|
+
"max_tokens": 512, // default 512 (mlx_lm.server's default) —
|
|
204
|
+
// NOT the chat lane's generous default
|
|
205
|
+
"stream": false,
|
|
206
|
+
"stop": "\n\n", // string or array, decoded-text matching
|
|
207
|
+
// plus every sampling/penalty field from /v1/chat/completions:
|
|
208
|
+
// temperature, top_p, top_k, seed, min_p, xtc_probability,
|
|
209
|
+
// xtc_threshold, logit_bias, repetition_penalty,
|
|
210
|
+
// repetition_context_size, presence_penalty, presence_context_size,
|
|
211
|
+
// frequency_penalty, frequency_context_size, adapter,
|
|
212
|
+
// logprobs (BOOL, mlx-lm's type — not OpenAI's legacy int),
|
|
213
|
+
// top_logprobs (0-11 or -1) — same response block as chat, see
|
|
214
|
+
// "logprobs / top_logprobs" above (mlx-lm shares the builder)
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Response is the OpenAI text-completion object:
|
|
219
|
+
|
|
220
|
+
```jsonc
|
|
221
|
+
{
|
|
222
|
+
"id": "cmpl-…", "object": "text_completion", "created": 1760000000,
|
|
223
|
+
"model": "<loaded model id>",
|
|
224
|
+
"choices": [{ "index": 0, "text": "…", "finish_reason": "stop" | "length" }],
|
|
225
|
+
"usage": { "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0,
|
|
226
|
+
"prompt_tokens_details": { "cached_tokens": 0 } }
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Streaming (`"stream": true`) is SSE: `text_completion` chunks whose
|
|
231
|
+
`choices[0].text` carries the delta; the final chunk carries
|
|
232
|
+
`finish_reason` and `usage` (always attached — mlx-lm gates usage behind
|
|
233
|
+
`stream_options.include_usage`; ours is an additive superset), then
|
|
234
|
+
`data: [DONE]`. There is no `echo` parameter (mlx_lm.server has none).
|
|
235
|
+
|
|
129
236
|
## POST /v1/messages (Anthropic Messages API)
|
|
130
237
|
|
|
131
238
|
Anthropic-protocol surface over the same engine — on by default, like
|
|
132
239
|
`optiq serve`. Point any Anthropic-SDK tool at the server
|
|
133
|
-
(`ANTHROPIC_BASE_URL=http://localhost:
|
|
240
|
+
(`ANTHROPIC_BASE_URL=http://localhost:8080`, any `x-api-key`) — Claude
|
|
134
241
|
Code works as a client this way.
|
|
135
242
|
|
|
136
243
|
- `system` (string or text blocks), `messages` with string or
|
|
@@ -141,7 +248,11 @@ Code works as a client this way.
|
|
|
141
248
|
- `tools` (`{name, description, input_schema}`) map to function tools;
|
|
142
249
|
server-tool types (web_search, …) are dropped silently.
|
|
143
250
|
- `max_tokens`, `temperature`, `top_p`, `top_k`, `stop_sequences`,
|
|
144
|
-
`stream` as in the Anthropic spec.
|
|
251
|
+
`stream` as in the Anthropic spec. The mlx-lm sampler/penalty
|
|
252
|
+
extensions (`min_p`, `xtc_probability`, `xtc_threshold`,
|
|
253
|
+
`repetition_penalty`, `presence_penalty`, `frequency_penalty` + their
|
|
254
|
+
`*_context_size` windows) pass through as extras; `logit_bias` does
|
|
255
|
+
not exist in this protocol and is not accepted here.
|
|
145
256
|
- Response: `{id: "msg_…", type: "message", content: [{type: "text"} |
|
|
146
257
|
{type: "tool_use"}…], stop_reason, usage: {input_tokens,
|
|
147
258
|
output_tokens, cache_read_input_tokens}}` —
|
|
@@ -162,7 +273,11 @@ OpenAI SDK speak this now). Oracle: optiq responses shim.
|
|
|
162
273
|
`function_call_output`), `instructions` (merged with any
|
|
163
274
|
system/developer items into one leading system message),
|
|
164
275
|
`max_output_tokens`, `temperature`, `top_p`, `top_k`, flat
|
|
165
|
-
`tools`/`tool_choice` (built-in tool types dropped), `stream`.
|
|
276
|
+
`tools`/`tool_choice` (built-in tool types dropped), `stream`. The
|
|
277
|
+
mlx-lm sampler/penalty extensions (`min_p`, `xtc_probability`,
|
|
278
|
+
`xtc_threshold`, `logit_bias`, `repetition_penalty`,
|
|
279
|
+
`presence_penalty`, `frequency_penalty` + `*_context_size`) pass
|
|
280
|
+
through as extras.
|
|
166
281
|
- **`previous_response_id` resumption**: pass a prior response id
|
|
167
282
|
instead of resending the conversation; the server splices the stored
|
|
168
283
|
input + output back in (instructions carry forward when omitted).
|
|
@@ -177,9 +292,46 @@ OpenAI SDK speak this now). Oracle: optiq responses shim.
|
|
|
177
292
|
response.completed` (+ `response.function_call_arguments.delta/.done`
|
|
178
293
|
for tool calls).
|
|
179
294
|
|
|
295
|
+
## POST /v1/embeddings (OpenAI Embeddings API)
|
|
296
|
+
|
|
297
|
+
Text embeddings, available when the **served model is an embedding model**
|
|
298
|
+
(plain Qwen3 / Qwen3-Embedding) — consistent with the single-model server:
|
|
299
|
+
`mlx-bun serve mlx-community/Qwen3-Embedding-4B-4bit-DWQ`, then POST here. On
|
|
300
|
+
any other served model the route returns `400 invalid_request_error`.
|
|
301
|
+
|
|
302
|
+
- `input` (string or array of strings), `model` (ignored — the served model
|
|
303
|
+
is used), and a **non-standard** `instruction` (optional): applies
|
|
304
|
+
Qwen3-Embedding's query format `Instruct: {instruction}\nQuery:{text}`,
|
|
305
|
+
which steers *which* similarity axis the geometry reflects. Omit it for
|
|
306
|
+
plain document embeddings.
|
|
307
|
+
- Pooling is last-token + L2-normalization (the vectors are unit-length);
|
|
308
|
+
similarity is the dot product. Bit-exact vs the mlx-lm `qwen3` reference
|
|
309
|
+
(`tests/qwen3-embed-parity.test.ts`).
|
|
310
|
+
- Response: `{ "object": "list", "data": [{ "object": "embedding", "index":
|
|
311
|
+
0, "embedding": [float…] }, …], "model": "<served id>", "usage": {
|
|
312
|
+
"prompt_tokens": N, "total_tokens": N } }`. Embedding is a single forward
|
|
313
|
+
(no decode loop), so it runs inline — not through the generation gateway.
|
|
314
|
+
|
|
315
|
+
```sh
|
|
316
|
+
curl localhost:8080/v1/embeddings -H 'content-type: application/json' \
|
|
317
|
+
-d '{"input": ["the cat sat on the mat", "a kitten by the window"]}'
|
|
318
|
+
```
|
|
319
|
+
|
|
180
320
|
## GET /v1/models
|
|
181
321
|
|
|
182
|
-
`{ "object": "list", "data": [{ "id": "<model id>", "object": "model",
|
|
322
|
+
`{ "object": "list", "data": [{ "id": "<model id>", "object": "model",
|
|
323
|
+
"created": <unix s>, … }] }`
|
|
324
|
+
|
|
325
|
+
The served model is FIRST (with extra capability fields:
|
|
326
|
+
`context_window`, `reasoning`, `vision`, `owned_by`), followed by every other
|
|
327
|
+
servable model the local registry knows (mlx-lm scans the HF cache
|
|
328
|
+
here; the registry is that scan, filtered to supported architectures).
|
|
329
|
+
`GET /v1/models/<id>` filters the list to that id — same list shape,
|
|
330
|
+
matching `mlx_lm.server`.
|
|
331
|
+
|
|
332
|
+
## GET /health
|
|
333
|
+
|
|
334
|
+
`{"status": "ok"}` — byte-for-byte what `mlx_lm.server` returns.
|
|
183
335
|
|
|
184
336
|
## GET /stats
|
|
185
337
|
|
|
@@ -301,15 +453,37 @@ Select per request with the `adapter` body field. Prompt-cache entries
|
|
|
301
453
|
are namespaced per adapter spec, so switching adapters never reuses
|
|
302
454
|
another adapter's KV.
|
|
303
455
|
|
|
456
|
+
`serve --adapter <dir>` (alias `--adapter-path`, mlx_lm.server's
|
|
457
|
+
spelling) mounts an adapter at startup through this same machinery and
|
|
458
|
+
makes it the default for requests that send no `adapter` field; an
|
|
459
|
+
explicit `adapter` (including `"none"`) always wins, and hot-swap via
|
|
460
|
+
these endpoints is unchanged.
|
|
461
|
+
|
|
304
462
|
## Client setup: pi
|
|
305
463
|
|
|
306
|
-
|
|
464
|
+
The supported one-command path:
|
|
465
|
+
|
|
466
|
+
```bash
|
|
467
|
+
mlx-bun harness pi # install; then: pi --provider mlx-bun
|
|
468
|
+
mlx-bun harness pi --remove # undo
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
It writes a self-contained extension to
|
|
472
|
+
`~/.pi/agent/extensions/mlx-bun-provider.ts` that registers the `mlx-bun`
|
|
473
|
+
provider with live discovery: at pi startup it fetches `/v1/models` from the
|
|
474
|
+
running server, so the stable `mlx-bun/local` model handle always resolves to
|
|
475
|
+
whatever the server is actually serving (context window, reasoning, and
|
|
476
|
+
vision capability included), and never goes stale across model swaps. The
|
|
477
|
+
model list baked at install time is only the fallback for when the server is
|
|
478
|
+
down.
|
|
479
|
+
|
|
480
|
+
Manual fallback — `~/.pi/agent/models.json`:
|
|
307
481
|
|
|
308
482
|
```jsonc
|
|
309
483
|
{
|
|
310
484
|
"providers": {
|
|
311
485
|
"mlx-bun": {
|
|
312
|
-
"baseUrl": "http://
|
|
486
|
+
"baseUrl": "http://127.0.0.1:8080/v1",
|
|
313
487
|
"api": "openai-completions",
|
|
314
488
|
"apiKey": "sk-anything-nonempty",
|
|
315
489
|
"models": [{ "id": "<model id from /v1/models>" }]
|
|
@@ -318,5 +492,9 @@ another adapter's KV.
|
|
|
318
492
|
}
|
|
319
493
|
```
|
|
320
494
|
|
|
321
|
-
|
|
495
|
+
Note the manual route bakes a concrete model id, which goes stale when you
|
|
496
|
+
serve a different model — the staleness `harness pi`'s `local` id exists to
|
|
497
|
+
solve.
|
|
498
|
+
|
|
499
|
+
Any OpenAI SDK works the same way: `baseURL: "http://127.0.0.1:8080/v1"`,
|
|
322
500
|
any non-empty `apiKey`.
|
|
@@ -19,16 +19,18 @@ the design rationale is in
|
|
|
19
19
|
|
|
20
20
|
| Flag | Arg | Default | What it does |
|
|
21
21
|
| --- | --- | --- | --- |
|
|
22
|
-
| `--host` | addr |
|
|
23
|
-
| `--port` | n | `
|
|
22
|
+
| `--host` | addr | `127.0.0.1` | Interface to bind. Loopback-only by default (mlx_lm.server parity); pass `--host 0.0.0.0` to expose the server on your network. |
|
|
23
|
+
| `--port` | n | `8080` | Listen port. A pre-flight probe refuses to start if the port is already serving. |
|
|
24
24
|
| `--memory-budget` | GB | machine RAM × 0.75 | Admission ceiling. Requests whose `prompt + max_tokens` exceed the budget's max safe context are **rejected with 400** (`type: memory_admission`) instead of risking an uncatchable GPU OOM. Also caps the mlx allocator (`mlx_set_memory_limit`) as defense in depth. **Decimal GB (×10⁹).** |
|
|
25
|
-
| `--prompt-cache` | GB | `2` (≈2e9 bytes) | Byte cap for the prompt (prefix-reuse KV) cache. **Binary GiB (×2³⁰)** on input.
|
|
26
|
-
| `--batch` | n | `1` (serial) | Max concurrent requests batched through the mlx-lm-parity engine. `>1` switches the **whole server** into bf16 continuous batching — a *mode*, not a load fallback. See [Execution modes](#execution-modes-serial-vs---batch-n).
|
|
25
|
+
| `--prompt-cache` | GB | `2` (≈2e9 bytes) | Byte cap for the prompt (prefix-reuse KV) cache. **Binary GiB (×2³⁰)** on input. Unset ⇒ the 2 GB default; an explicit `--prompt-cache 0` **disables** the cache (maxBytes 0 evicts every entry). |
|
|
26
|
+
| `--batch` | n | `1` (serial) | Max concurrent requests batched through the mlx-lm-parity engine. `>1` switches the **whole server** into bf16 continuous batching — a *mode*, not a load fallback. See [Execution modes](#execution-modes-serial-vs---batch-n). `--decode-concurrency` is accepted for drop-in compatibility, but the semantics differ: in mlx_lm.server it caps per-BatchGenerator decode parallelism (default 32); in mlx-bun it enables continuous batching with this cap (mlx-bun's default is the optimized serial path). |
|
|
27
27
|
| `--kv-quant` | `config`\|`off`\|`4`\|`8` | `config` serial / `off`(bf16) under `--batch N` | KV-cache quantization. `config` = per-layer `kv_config.json` (optiq parity); `off` = bf16; `4`/`8` = uniform bits (group 64, start 0). Under `--batch N`, an explicit value routes those requests to the **serial** lane (batched is bf16-only). |
|
|
28
|
+
| `--adapter` | dir | none | Mount a LoRA adapter at startup (same machinery as `POST /v1/adapters`; the adapter id is the directory's basename) and make it the **default** for requests that send no `adapter` field. A request's explicit `adapter` — including `"none"` — always wins, and hot-swap via `/v1/adapters` is unchanged. `--adapter-path` is accepted as the mlx_lm.server-named alias. A bad adapter fails startup loudly rather than silently serving the base model. This is the flag `mlx-bun train`'s completion message points at. |
|
|
28
29
|
| `--thinking` | `true`\|`false` | model's own (false for CPM) | Server-wide default for the chat template's `enable_thinking` (MiniCPM5/CPM and Qwen3.5 hybrid reasoning). A request's `chat_template_kwargs.enable_thinking` overrides it. |
|
|
29
|
-
| `--temperature` | n ∈ [0,5] | `generation_config.json` | Server-wide sampling default. Per-request `temperature` still wins; the browser chat (sends none) inherits this. |
|
|
30
|
+
| `--temperature` | n ∈ [0,5] | `generation_config.json` | Server-wide sampling default. Per-request `temperature` still wins; the browser chat (sends none) inherits this. `--temp` is accepted as an alias (mlx_lm.server compat); explicit `--temperature` wins if both are given. **Migration note:** mlx_lm.server's `--temp` *default* is `0.0` (unset-temperature requests are greedy there); mlx-bun falls back to the model's `generation_config.json`, then `0.7` — pass `--temp 0` for mlx-lm's behavior. |
|
|
30
31
|
| `--top-p` | n ∈ [0,1] | `generation_config.json` | Server-wide top-p default (per-request `top_p` wins). |
|
|
31
32
|
| `--top-k` | n ∈ [0,1e6] | `generation_config.json` | Server-wide top-k default (per-request `top_k` wins). |
|
|
33
|
+
| `--max-tokens` | n | `65536` chat / `512` raw completion | Completion cap when a request omits `max_tokens` (mlx_lm.server flag; its default there is 512 — `--max-tokens 512` reproduces mlx_lm.server exactly). |
|
|
32
34
|
| `--no-open` | (bool) | off | Skip the automatic browser open on start. By default an interactive terminal session opens `http://<host>:<port>/#/chat` once the server is ready; pass this flag to suppress it (e.g. headless or non-TTY environments already skip it). |
|
|
33
35
|
| `--hlg-sampling` | `on`\|`off` | off | Piecewise tone-curve (HLG) sampling: rolls off the top-token region, boosts the mids, gentles the tail. The overall gain folds from `--temperature`. See [docs/design/hlg-sampling.md](../design/hlg-sampling.md). |
|
|
34
36
|
| `--hlg-width` | nats | `4` | HLG mid-region half-width (nats). Only meaningful with `--hlg-sampling on`. |
|
|
@@ -36,12 +38,18 @@ the design rationale is in
|
|
|
36
38
|
| `--hlg-toe` | nats | `6` | HLG shadow rolloff scale (nats). Only meaningful with `--hlg-sampling on`. |
|
|
37
39
|
| `--hlg-pivot-offset` | nats | `6` | HLG pivot point: nats below the top token. Only meaningful with `--hlg-sampling on`. |
|
|
38
40
|
| `--expert-offload` | (bool) | off | **MoE models only.** Serve experts from a page-aligned file mmap (built on first use). Keeps the model out of memory pressure — physical footprint ≈ active params. Ignored with a warning on dense models. Bit-exact with the resident path. |
|
|
41
|
+
| `--l1` | (bool) | — | **Parity tier preset:** bit-for-bit IDENTICAL to mlx-lm (bf16 KV, unfused SDPA, no perf kernel). Sets the whole decode route; any explicit per-fork flag (`--kv-quant`/`--perf-kernel`/…) overrides the preset. See [docs/design/parity-tier-dag.md](../design/parity-tier-dag.md). |
|
|
42
|
+
| `--l2` | (bool) | — | **Parity tier preset:** bit-for-bit IDENTICAL to mlx-optiq (quantized KV per `kv_config.json` + fused N-tiled prefill SDPA + **stock unfused decode** — the composition the optiq goldens track, `scripts/regen-kvq-goldens.ts`). The perf kernel stays **off**: it is envelope-gated, not bit-exact (see `--l3`), and opting into it (`--l2 --perf-kernel on`) is an explicit choice that leaves the bare-tier guarantee. |
|
|
43
|
+
| `--l3` | (bool) | — | **Parity tier preset:** best performance, no bit-exact oracle (KL + test gated). On the decode path L3 = L2 + the envelope-gated perf kernel; L3 also owns the no-oracle features (HLG sampler, expert offload). No tier given ⇒ the per-flag defaults below, which equal `--l3`. |
|
|
39
44
|
| `--compiled-decode` | on\|off | on | Replay the per-step decode graph in C++ (`MLX_BUN_COMPILED_DECODE`). Bit-exact A/B lever. **Serial lane only** (see note below). **Gemma4-dense only** — LoRA, MoE, and non-Gemma4 models (MiniCPM5 / Qwen3.5) run eager; an unsupported step falls back to eager for the rest of that generation. |
|
|
40
|
-
| `--perf-kernel` | on\|off | **on** | Fused quantized-KV decode-SDPA Metal kernel (`MLX_BUN_PERF_KERNEL`), the perf side of the compat A/B. Engages on quantized caches at decode. **Serial lane only.** |
|
|
45
|
+
| `--perf-kernel` | on\|off | **on** | Fused quantized-KV decode-SDPA Metal kernel (`MLX_BUN_PERF_KERNEL`), the perf side of the compat A/B. **Not bit-exact** — envelope-gated (≥56/64 teacher-forced argmax vs the frozen compat trajectory, `tests/perf-kernel-oracle.test.ts`), so it is an **L3 lever**: on in `--l3`/no-tier default, **off in bare `--l1`/`--l2`**. Engages on quantized caches at decode. **Serial lane only.** |
|
|
41
46
|
| `--fused-decode` | on\|off | off | Experimental: tile the quantized decode SDPA (`MLX_BUN_FUSED_DECODE`). **Serial lane only.** |
|
|
42
47
|
| `--fused-sdpa` | on\|off | on | Fused SDPA path for quantized prefill/continuation (inverted env `MLX_BUN_NO_FUSED_SDPA`). **Serial lane only.** |
|
|
43
48
|
| `--force-wire` | (bool) | off | Wire weights into memory for the whole generation (`MLX_BUN_FORCE_WIRE`). Near-ceiling models (e.g. 26B) need it. **Serial lane only.** |
|
|
44
49
|
|
|
50
|
+
The default host/port (`127.0.0.1:8080`) match `mlx_lm.server`, so running
|
|
51
|
+
mlx-bun alongside the Python reference server needs an explicit `--port`.
|
|
52
|
+
|
|
45
53
|
The five performance levers are A/B knobs whose defaults are the measured
|
|
46
54
|
winners; flip them to compare. They are set as `MLX_BUN_*` env vars
|
|
47
55
|
before the model loads, so they apply to `mlx-bun pi` too. They affect
|
|
@@ -62,7 +70,9 @@ Most quality knobs can be set per request in the chat body and override
|
|
|
62
70
|
the server-wide default. Precedence, highest first:
|
|
63
71
|
|
|
64
72
|
1. explicit request field (`temperature`, `top_p`, `top_k`, `seed`,
|
|
65
|
-
`
|
|
73
|
+
`min_p`, `xtc_probability`/`xtc_threshold`, `logit_bias`,
|
|
74
|
+
`repetition_penalty`, `presence_penalty`/`frequency_penalty` (+ their
|
|
75
|
+
`*_context_size` windows), `max_tokens`/`max_completion_tokens`, `stop`,
|
|
66
76
|
`chat_template_kwargs.enable_thinking`, `adapter`, `tools`)
|
|
67
77
|
2. the matching `--temperature`/`--top-p`/`--top-k`/`--thinking` server default
|
|
68
78
|
3. the model's `generation_config.json`
|
|
@@ -128,16 +138,36 @@ concurrently with each other.
|
|
|
128
138
|
| vision (image parts) | ❌ serial — needs offset-0 single-seq prefill + bidirectional image mask |
|
|
129
139
|
| LoRA `adapter` (resolves to ≥1) | ❌ serial — `loraState.active` is one per-generation field; per-row adapters unsupported |
|
|
130
140
|
| `repetition_penalty` | ❌ serial — per-row logits processors are a later refinement |
|
|
141
|
+
| `min_p` / `xtc_*` / `logit_bias` / `presence_penalty` / `frequency_penalty` | ❌ serial — one gate for the whole sampler/processor family (safe v1; min_p/XTC are per-row samplers and could batch later) |
|
|
131
142
|
| explicit `seed` | ❌ serial — reproducibility ⇒ solo (matches mlx-lm) |
|
|
132
143
|
| KV quant active (explicit `--kv-quant`) | ❌ serial — batched is bf16-only in v1 |
|
|
144
|
+
| hybrid-cache model (Qwen3.5 gated-DeltaNet) | ❌ serial — model-level cache-capability gate (see below) |
|
|
133
145
|
| `temperature` / `top_p` / `top_k` | ✅ batches (each row samples with its own seed) |
|
|
134
146
|
| `stop` sequences | ✅ batches (per-row `StopMatcher` in the onToken closure) |
|
|
135
147
|
| `tools` / `tool_choice` | ✅ batches (per-row tool router; decode-layer parse) |
|
|
136
148
|
| `--thinking` / `enable_thinking` | ✅ batches (template-render concern, lane-independent) |
|
|
137
149
|
| multi-turn / long prompt | ✅ batches, but **no prompt-cache reuse** (`cached_tokens=0`) |
|
|
138
150
|
|
|
139
|
-
|
|
140
|
-
scheduler assembles each layer's cache by attention type.
|
|
151
|
+
Full-attention (CPM) and sliding-window (Gemma) models batch — the
|
|
152
|
+
scheduler assembles each layer's cache by attention type. Hybrid
|
|
153
|
+
gated-DeltaNet models (Qwen3.5) **route serial**: their `SSMCache` has no
|
|
154
|
+
dynamic-B merge/filter, so the gateway's cache-capability gate (the
|
|
155
|
+
mirror of mlx-lm server.py's all-caches-have-`merge` check) sends every
|
|
156
|
+
request down the serial lane with the standard drain behavior. A batched
|
|
157
|
+
SSM path is planned (mlx-lm's `ArraysCache` is the oracle) — see
|
|
158
|
+
[batching-v2-plan.md](../design/batching-v2-plan.md) item (h).
|
|
159
|
+
|
|
160
|
+
A non-batchable request **drains** the batch: while it waits, the
|
|
161
|
+
scheduler stops admitting new rows, finishes the running ones, and
|
|
162
|
+
releases the GPU so the serial request runs (mlx-lm's `drain_batch`);
|
|
163
|
+
admission then resumes. So a steady stream of batchable traffic cannot
|
|
164
|
+
starve a serial-lane request.
|
|
165
|
+
|
|
166
|
+
`--batch N` is a **mode switch, not a load-dependent fallback**:
|
|
167
|
+
auto-batching "when >1 request arrives" was considered and rejected —
|
|
168
|
+
an idle vs. loaded server would produce different numerics for the same
|
|
169
|
+
request, breaking determinism and the drop-in-for-`mlx_lm.server`
|
|
170
|
+
promise.
|
|
141
171
|
|
|
142
172
|
## Compatibility matrix
|
|
143
173
|
|
|
@@ -157,6 +187,7 @@ inside it per the table above).
|
|
|
157
187
|
| vision request | ✅ | ✅ via serial lane (in bf16 under Option B) |
|
|
158
188
|
| LoRA `adapter` | ✅ | ✅ via serial lane |
|
|
159
189
|
| `repetition_penalty` | ✅ | ✅ via serial lane |
|
|
190
|
+
| `min_p` / `xtc_*` / `logit_bias` / presence+frequency penalties | ✅ | ✅ via serial lane |
|
|
160
191
|
| `seed` | ✅ | ✅ via serial lane |
|
|
161
192
|
| `tools` / `stop` | ✅ | ✅ (batches) |
|
|
162
193
|
| `--compiled-decode`/`--perf-kernel`/`--fused-*`/`--force-wire` | ✅ (serial perf tree) | **n/a — compat mode, no perf flags by design** |
|
|
@@ -22,7 +22,7 @@ Training is reachable several ways — all of them drive the **same** runner
|
|
|
22
22
|
|
|
23
23
|
| Path | How | Use when |
|
|
24
24
|
|---|---|---|
|
|
25
|
-
| **CLI** | `mlx-bun train <model> --data <dir> [options]` — foreground, full ORPO stack on by default; auto-detects e4b/Gemma + sets its env; `--save-every`, `--resume`, `--dry-run`; streams loss to the terminal. `mlx-bun help train` for all flags. Run long jobs detached: `nohup mlx-bun train … &` | **The default for CLI users** |
|
|
25
|
+
| **CLI** | `mlx-bun train <model> --data <dir> [options]` — foreground, full ORPO stack on by default; auto-detects e4b/Gemma + sets its env; `--save-every`, `--resume`, `--dry-run`, `--sft-scope full\|response`, `--grad-accum`, `--grad-clip`, `--seed`, `--val-size`; streams loss to the terminal. `mlx-bun help train` for all flags. Run long jobs detached: `nohup mlx-bun train … &` | **The default for CLI users** |
|
|
26
26
|
| **Web UI** | `mlx-bun serve`, open `/finetune` — pick model → dataset → hyperparameters → train; watch live train/val loss; merge/export the adapter | Interactive, in the browser |
|
|
27
27
|
| **HTTP API** | `POST /api/finetune/submit` (job id + SSE events); `POST /api/finetune/inspect-dataset` to probe a file; `POST /api/finetune/merge` to fold an adapter into base weights | Scripted / remote |
|
|
28
28
|
| **ORPO launcher** | [`scripts/train-orpo.ts`](../../scripts/train-orpo.ts) — the same stack driven by env vars (`MODEL=`, `DATA=`, `RANK=`, `RESUME=`, …). See [orpo-quickstart](./orpo-quickstart.md) | Env-var scripting |
|
|
@@ -36,7 +36,7 @@ isolation + a single GPU lease across concurrent server requests).
|
|
|
36
36
|
### Quick start (HTTP API)
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
curl -s localhost:
|
|
39
|
+
curl -s localhost:8080/api/finetune/submit -X POST -H 'content-type: application/json' -d '{
|
|
40
40
|
"model_dir": "/path/to/snapshot",
|
|
41
41
|
"data_dir": "/path/to/dataset", // dir with train.jsonl (+ optional valid.jsonl)
|
|
42
42
|
"adapter_path": "/path/to/output-adapter",
|
|
@@ -81,7 +81,7 @@ differ from the trainer/API defaults in the table below:
|
|
|
81
81
|
|
|
82
82
|
The script hard-codes `method=sft`, `batch_size=1`, `steps_per_report=1`, and
|
|
83
83
|
uses the default `ops.sdpa` training attention (set `MLX_BUN_TRAIN_ATTN=flash`
|
|
84
|
-
to override —
|
|
84
|
+
to override — experimental, much slower; see Methodology).
|
|
85
85
|
|
|
86
86
|
## Data formats
|
|
87
87
|
|
|
@@ -112,12 +112,19 @@ Probe a file before submitting: `POST /api/finetune/inspect-dataset` with
|
|
|
112
112
|
and a preference term, `L = L_NLL(chosen) + λ·L_OR`, with
|
|
113
113
|
`L_OR = -log σ(log_odds)` and
|
|
114
114
|
`log_odds = (ℓ_w − ℓ_r) − (log1mexp(ℓ_w) − log1mexp(ℓ_r))`, where `ℓ` is the
|
|
115
|
-
**length-normalized** (mean over response tokens) log-prob.
|
|
115
|
+
**length-normalized** (mean over response tokens) log-prob. The SFT term's
|
|
116
|
+
scope is `sft_scope`: **`full`** (default; paper/TRL-faithful) computes
|
|
117
|
+
`L_NLL` as the token-mean cross-entropy over the **full prompt+response** of
|
|
118
|
+
the chosen sequence (only padding excluded — TRL's `chosen_nll_loss`),
|
|
119
|
+
from the same chosen forward (no extra forward); **`response`** uses
|
|
120
|
+
`L_NLL = −ℓ_w` (response-only — the pre-2026-07 mlx-bun behavior, kept for
|
|
121
|
+
reproducing old runs). The odds-ratio `ℓ_w/ℓ_r` stay response-only in **both**
|
|
122
|
+
modes (that also matches TRL). Uses the same
|
|
116
123
|
`{prompt, chosen, rejected}` preference data as DPO but needs **no reference
|
|
117
124
|
model** (2 forwards/step vs DPO's 4). Default LR `1e-5` (lower than DPO — the
|
|
118
125
|
loss carries a full SFT NLL term a high LR destabilizes). Tune with
|
|
119
|
-
`orpo_lambda`, `orpo_warmup_iters`, `orpo_lr_schedule`. Design +
|
|
120
|
-
roadmap: [orpo-training](../design/orpo-training.md).
|
|
126
|
+
`orpo_lambda`, `orpo_warmup_iters`, `orpo_lr_schedule`, `sft_scope`. Design +
|
|
127
|
+
optimization roadmap: [orpo-training](../design/orpo-training.md).
|
|
121
128
|
|
|
122
129
|
The ORPO head + long-context machinery compose into one stack (all B=1), each
|
|
123
130
|
piece independently toggled and each falling back cleanly:
|
|
@@ -179,6 +186,7 @@ are `DEFAULT_TRAIN_CONFIG` (trainer.ts:89).
|
|
|
179
186
|
| `orpo_fused_ce` | bool | `false` | Fused linear-CE head: one CustomVjp with an analytic softmax−onehot backward (MLX `quantizedMatmul` both ways) — no autograd through the head, no retained `[M,vocab]`. Exact; `[chunk,vocab]` transient (orpo only) |
|
|
180
187
|
| `orpo_flash_ce` | bool | `false` | Route the fused head through the **flash-CCE Metal kernel** (verbatim MLX steel GEMM + ORPO epilogue, fwd **and** bwd): neither `[M,vocab]` nor a dequantized head touches HBM → `[M,vocab]`-free, fastest on large vocab (e4b bwd 754 ms, 0.93 GB flat @ M=8192). Implies the fused head. The Apple-CCE coeff filter / blockMax skip are opt-in via `MLX_BUN_CCE_BWD_FILTER_EPS` / `_BLOCK_EPS` (default off — exact). B=1 (orpo only) |
|
|
181
188
|
| `orpo_prefix_shared` | bool | `false` | Shared prompt-prefix: one forward over `[prompt; chosen; rejected]` with a block-sparse mask + block-wise RoPE, so the shared prompt is encoded **once** (a big win when the prompt dominates). Falls back to two-forward per row on prompt mismatch. Composes with the flash head and the segmented backward (MiniCPM5 + e4b). B=1 (orpo only) |
|
|
189
|
+
| `sft_scope` | `full` \| `response` | `full` | Scope of ORPO's chosen-NLL term. `full` (paper/TRL-faithful): token-mean CE over the **full prompt+response** (only padding excluded), from the same chosen forward. `response`: `L_NLL = −ℓ_w` — reproduces pre-2026-07 runs bit-exactly. The odds-ratio `ℓ_w/ℓ_r` are response-only in both modes. Applies to every ORPO path (naive/chunked/fused/flash/prefix/segmented) (orpo only). CLI spelling: `mlx-bun train … --sft-scope full\|response` |
|
|
182
190
|
|
|
183
191
|
### Environment variables (training)
|
|
184
192
|
|
|
@@ -186,7 +194,7 @@ are `DEFAULT_TRAIN_CONFIG` (trainer.ts:89).
|
|
|
186
194
|
|---|---|---|---|
|
|
187
195
|
| `MLX_BUN_FUSED_GELU` | **`0` (required for Gemma)** | on | The fused GeGLU is a **CustomKernel with no gradient (vjp)** ([`fused-geglu-kernel.ts`](../../src/model/fused-geglu-kernel.ts)); the Gemma forward uses it ([`gemma4.ts:277`](../../src/model/gemma4.ts)), so a Gemma (e4b/12B/26B) backward fails unless it's off. MiniCPM5 (Llama-arch SwiGLU) never hits it, so the `.ts` script's MiniCPM5 default doesn't need it. |
|
|
188
196
|
| `MLX_BUN_PERF_KERNEL` | **`0` for training** | on | The fused quantized-decode kernel likewise has no vjp. It only fires at decode L=1 (rare in the L>1 training forward), but the e4b recipe sets `0` to be safe. |
|
|
189
|
-
| `MLX_BUN_TRAIN_ATTN` | **leave unset** | unset → `ops.sdpa` | Default `ops.sdpa` **is** mlx's fused flash-attention kernel — the correct, working path. `flash` selects a *different* hand-rolled
|
|
197
|
+
| `MLX_BUN_TRAIN_ATTN` | **leave unset** | unset → `ops.sdpa` | Default `ops.sdpa` **is** mlx's fused flash-attention kernel — the correct, working path. `flash` selects a *different* hand-rolled O(L)-memory kernel: its two port bugs (dK buffer-transpose, dQ divergent causal barrier) are fixed and FD-validated (`tests/flash-attention.test.ts`, T≤256), but it is ~30× slower and the historical e4b ≥2K crash has **not been re-validated at that scale** since the fix — leave unset unless memory-bound and re-verified. |
|
|
190
198
|
| `MLX_BUN_MEM_LOG` | `1` to profile | off | Print per-step peak/active/cache memory |
|
|
191
199
|
|
|
192
200
|
> Important: the trainer itself ([`trainer.ts`](../../src/train/trainer.ts))
|
|
@@ -221,7 +229,7 @@ What the recipe pins (and why it differs from the bare defaults):
|
|
|
221
229
|
| `RANK` / `SCALE` / `LR` | `16` / `20` / `1e-5` | task-tuned |
|
|
222
230
|
| `ITERS` | `2` (probe) / `900` (train) | ~2 epochs over 450 examples |
|
|
223
231
|
| `MLX_BUN_PERF_KERNEL` / `MLX_BUN_FUSED_GELU` | `0` / `0` | **required** — the fused kernels have no vjp (see env table) |
|
|
224
|
-
| attention | default `ops.sdpa` | mlx's fused flash kernel; **not** `MLX_BUN_TRAIN_ATTN=flash` (
|
|
232
|
+
| attention | default `ops.sdpa` | mlx's fused flash kernel; **not** `MLX_BUN_TRAIN_ATTN=flash` (~30× slower; e4b ≥2K unvalidated since the kernel fix) |
|
|
225
233
|
|
|
226
234
|
The two non-negotiables for e4b: **segmented backward** (`SEG>0`, so the
|
|
227
235
|
long-context activations fit) and the **fused kernels off** (so the backward
|
|
@@ -265,9 +273,15 @@ At long `max_seq_length`, activation memory dominates. Two levers:
|
|
|
265
273
|
### Training attention kernel (`MLX_BUN_TRAIN_ATTN`)
|
|
266
274
|
- **default `ops.sdpa`** — mlx's fused SDPA; correct (0.00% vs autograd),
|
|
267
275
|
O(L²) backward memory. Use this.
|
|
268
|
-
- **`flash`** — opt-in O(L)
|
|
269
|
-
|
|
270
|
-
|
|
276
|
+
- **`flash`** — opt-in O(L)-memory path. Its two port bugs (dK
|
|
277
|
+
buffer-transpose, dQ divergent causal barrier) were **fixed** and
|
|
278
|
+
FD-validated (`scripts/experiments/flash-fd-check.ts`; regression test
|
|
279
|
+
`tests/flash-attention.test.ts`, T≤256), but it remains ~30× slower than
|
|
280
|
+
`ops.sdpa`, and the historical e4b multi-K (≥2K) SIGTRAP has **not been
|
|
281
|
+
re-validated at that scale since the fix**
|
|
282
|
+
(`scripts/experiments/segmented-grad-test-e4b.ts` is the repro harness).
|
|
283
|
+
Prefer the default; treat `flash` as experimental on e4b long context.
|
|
284
|
+
Detail in segmented-backward-training §6.
|
|
271
285
|
|
|
272
286
|
## Outputs
|
|
273
287
|
|