mlx-bun 0.0.9 → 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/docs/design/batching-perf-path.md +232 -0
- package/docs/design/omlx-adoption-map.md +90 -0
- package/docs/design/segmented-backward-training.md +15 -0
- package/docs/design/ssd-kv-cold-tier.md +158 -0
- package/docs/investigations/decode-roofline-lookagain.md +92 -6
- package/docs/investigations/kernel-perf-review-2026-07.md +105 -26
- package/docs/planning/release-notes-v0.0.10.md +57 -0
- package/docs/reference/server-config.md +3 -0
- package/docs/reference/training.md +6 -1
- package/package.json +1 -1
- package/src/cli.ts +49 -5
- package/src/generate.ts +8 -0
- package/src/kv-store.ts +240 -98
- package/src/mlx/array.ts +21 -0
- package/src/mlx/ops.ts +26 -0
- package/src/model/gemma4-base.ts +34 -1
- package/src/model/qwen3-delta.ts +48 -1
- package/src/model/qwen3_5.ts +11 -2
- package/src/prompt-cache.ts +55 -9
- package/src/registry.ts +4 -1
- package/src/serve/batch-scheduler.ts +41 -9
- package/src/serve/generation-gateway.ts +78 -19
- package/src/server.ts +135 -25
- package/src/ssd-cache.ts +213 -0
- package/src/train/flash-cce.ts +27 -24
- package/src/train/loss.ts +53 -11
- package/src/train/prefix-shared.ts +30 -7
- package/src/train/segmented.ts +150 -46
- package/src/train/trainer.ts +21 -11
|
@@ -12,7 +12,7 @@ area reviews + full mechanisms): `reports/kernel-perf-review-2026-07-01.json`
|
|
|
12
12
|
vendored MLX steel machinery, exact-by-default backward with autograd + FD
|
|
13
13
|
parity — marred by decision drift: the PLAN-decided 1.35× filter win was
|
|
14
14
|
never landed and the in-code comments argued three contradictory positions
|
|
15
|
-
(comments fixed 2026-07-01; the FLIP
|
|
15
|
+
(comments fixed 2026-07-01; the FLIP LANDED 2026-07-02 — see backlog #1).
|
|
16
16
|
- **Training flash-attention kernel**: faithful port with exemplary post-bug
|
|
17
17
|
discipline, correctly opt-in — but it copies a naive oracle (~30× slower)
|
|
18
18
|
and its tests covered none of the production configs (extended 2026-07-01:
|
|
@@ -47,35 +47,109 @@ evalSftLoss error swallowing · tokens_per_sec metric · inverted comments
|
|
|
47
47
|
(gemma4.ts:298, flash-cce filter trio) · silent steel fallback (now warns;
|
|
48
48
|
H%128 throw narrowed) · flash-attn test blindness.
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
tile loop at trace-time N — q-cat adapters grow N per step under shapeless
|
|
52
|
-
replay, so the newest KV rows
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
FIXED 2026-07-02: **FUSED_DECODE=1 inside whole-graph compiled decode** baked
|
|
51
|
+
the tile loop at trace-time N — q-cat adapters grow N per step under shapeless
|
|
52
|
+
replay, so the newest KV rows were silently never attended. REPRODUCED on e4b
|
|
53
|
+
(42 layers traced tile=true at N=17; diverges within 3 tokens then
|
|
54
|
+
repetition-loops). The 12B was never affected — its segmented form runs concat
|
|
55
|
+
layers as uncompiled js layers. Fix: generate() refuses to compile under
|
|
56
|
+
MLX_BUN_FUSED_DECODE=1 (explicit opt-in wins, like LoRA/MoE) + quantizedSdpa
|
|
57
|
+
throws if the tiled path is ever selected inside a trace (lands in the
|
|
58
|
+
transactional fallback, never silent). Regression in
|
|
59
|
+
tests/compiled-decode.test.ts, whose e4b block also now uses the dynamic
|
|
60
|
+
snapshot resolver — the old hard-coded hash silently skipped it on this
|
|
61
|
+
machine, which is how the combo went unexercised.
|
|
62
|
+
|
|
63
|
+
NEW BUG (found 2026-07-02 by the backlog-#8 gate test; FIXED in mlx-bun,
|
|
64
|
+
UPSTREAM in mlx): **mx.quantized_matmul(transpose=false) returns wrong values
|
|
65
|
+
at row count 2 or 3** — relnorm ~0.33-0.39 on our dylib, ~0.98-1.14 on stock
|
|
66
|
+
mlx 0.31.2 (oracle venv repro); M=1 and M>=4 exact, transpose=true exact at
|
|
67
|
+
every M. mlx's autograd computes dh for a transpose=true head qmm via an
|
|
68
|
+
internal transpose=false call at the cotangent's row count, so EVERY training
|
|
69
|
+
head silently produced wrong dh for 2-3-token responses, 2-3-token tail
|
|
70
|
+
chunks, and tiny prompt spans (f32 ground truth: dh relnorm up to 0.99 at the
|
|
71
|
+
affected positions, cos ~0.2). Serving/decode unaffected (transpose=true).
|
|
72
|
+
Fix: (a) loss.ts `logitsFromHiddenPadM` pads differentiated head spans to 4
|
|
73
|
+
rows (pad cotangents are exactly zero) — applied at all seven head sites;
|
|
74
|
+
(b) ops.quantizedMatmul pads direct transpose=false calls (the fused head's
|
|
75
|
+
analytic backward). Verified vs f32-dequant ground truth (chunk-4 dh
|
|
76
|
+
0.47→0.006) + M-sweeps. Worth filing upstream with the 10-line python repro.
|
|
55
77
|
|
|
56
78
|
## The ranked optimization backlog (task #12)
|
|
57
79
|
|
|
58
|
-
1.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
E4B=1
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
80
|
+
1. ~~**[S] Land the coeff filter at eps~1e-5**~~ **LANDED 2026-07-02** — BOTH
|
|
81
|
+
skips default 1e-5 (coeff filter + blockMax block skip; env=0 restores exact).
|
|
82
|
+
All gates run on the M1 Max with real chunk-ORPO data
|
|
83
|
+
(flash-cce-filter-realdata.ts, extended: E4B=1 + blockEps sweep + Vjp
|
|
84
|
+
full-logits fidelity): filter CPM5 0.343%/1.41×, e4b 0.158%/1.70×; blockMax
|
|
85
|
+
skip ~lossless (≤0.004%) and real text DOES go cold (the synthetic probe was
|
|
86
|
+
the artifact) — **combined 1.71× (CPM5) / 3.16× (e4b)**; teacher-forced
|
|
87
|
+
fidelity cosine ≥0.99993, filter adds ≤0.07% over the eps=0 reassociation
|
|
88
|
+
floor (standing test in tests/train-orpo-fused-ce.test.ts). The "eps=0
|
|
89
|
+
byte-identical" gate resolved structurally: eps=0 compiles the identical
|
|
90
|
+
pre-flip kernel, but run-to-run dh is never byte-stable (atomic-add
|
|
91
|
+
reassociation) — byte-replay was the wrong spelling of that gate.
|
|
92
|
+
2. ~~**[M] planSegments full-attention isolation**~~ **REFUTED BY MEASUREMENT
|
|
93
|
+
2026-07-02** — built, A/B'd @8K on real chunk rows: ZERO peak win (18.09 vs
|
|
94
|
+
18.02 GB; scripts/experiments/seg-isolation-smoke.ts + MLX_BUN_SEG_MEM_LOG
|
|
95
|
+
phase probes). The §5 cost-model premise is false: mlx's sdpa BACKWARD
|
|
96
|
+
materializes O(L²) scores for EVERY layer (~3.5 GB/layer @8K e4b — the
|
|
97
|
+
sliding window is just an additive mask; sliding pair +7.1 GB ≈ full+sliding
|
|
98
|
+
+7.15). Worst segment = layer count alone, so **segment_size is the whole
|
|
99
|
+
knob: seg1 measures 14.59 GB @8K (+3% step time, loss identical) — which
|
|
100
|
+
DOES fit the 24 GB M4 Pro today, no code needed.** Inter-segment clearCache
|
|
101
|
+
also measured no-effect (watermark is live memory). Real next levers: the
|
|
102
|
+
head vjp's ~3 GB [M,V] spike (backlog #8) and an O(L)-memory attention
|
|
103
|
+
backward (the flash-attention track).
|
|
104
|
+
3. ~~**[S] Auto-dispatch the training head by M**~~ **LANDED 2026-07-02** —
|
|
105
|
+
fusedRespLogpMean routes requested-flash batches with M <
|
|
106
|
+
MLX_BUN_FLASH_MIN_M (default 1024) to the exact fused head. Fresh crossover
|
|
107
|
+
sweep (head-dispatch-sweep.ts, M1 Max — the review's 1.9× @M=512 was
|
|
108
|
+
pre-filter-flip): fused wins e4b M=64 1.6× / M=512 1.13×, tie @1024; the
|
|
109
|
+
#1-flip made flash FASTER at long M too (e4b ≥2048; 8K 10.7 vs 13.2 s) on
|
|
110
|
+
top of its memory win. CPM5: fused faster at every M — flash stays a pure
|
|
111
|
+
memory lever there (≥1024 honors the caller's intent). Dispatch proven by
|
|
112
|
+
bit-identical-to-fused loss in tests/train-orpo-fused-ce.test.ts.
|
|
113
|
+
4. ~~**[S] Fused decode full-capacity KV buffers + activeN**~~ **REFUTED BY
|
|
114
|
+
MEASUREMENT 2026-07-02** — the variant was built (an int32 activeN input,
|
|
115
|
+
capacity-strided rows, fetch attaches full buffers), gated BYTE-IDENTICAL
|
|
116
|
+
on all three recorded dispatch shapes and perf-oracle-green — and then
|
|
117
|
+
measured SLOWER end-to-end: interleaved in-process A/B on the 12B uniform
|
|
118
|
+
kv4 (tokens identical across arms, M1 Max) gave 24.64 vs 26.18 tok/s @8k
|
|
119
|
+
(0.94) and 23.07 vs 23.83 @22k (0.97) — the "~4× at 32k" scaling is
|
|
120
|
+
refuted too. Two holes in the estimate: the 12B's full-attention dispatch
|
|
121
|
+
shape is KV=1 (H16 D512), where the fetch view is a CONTIGUOUS PREFIX and
|
|
122
|
+
the six-copies model never applied; and kernel-only micro-A/Bs proved
|
|
123
|
+
unstable (±50% run-to-run) — the numbers behind "1-3%" were never
|
|
124
|
+
end-to-end. REVERTED; evidence + re-application recipe:
|
|
125
|
+
scripts/experiments/fused-decode-activen-ab.ts.
|
|
69
126
|
5. **[M] Collapse per-token host syncs in the spec/DSpark loop** — ~2γ+1
|
|
70
127
|
round-trips → ~2 per verify cycle; directly attacks DSpark's fixed draft
|
|
71
128
|
overhead.
|
|
72
129
|
6. **[M] Pipeline the batched decode loop + clearCache cadence** — folded into
|
|
73
130
|
the batching-v2 wave (docs/design/batching-v2-plan.md).
|
|
74
131
|
7. **[S] Enable the 26B perf kernel** (open PLAN Phase E box; oracle frozen).
|
|
75
|
-
8.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
132
|
+
8. ~~**[S] Bound the SFT segmented head**~~ **LANDED 2026-07-02** — both SFT
|
|
133
|
+
segmented classes now use boundedSftCe (token-chunked Checkpoint head, the
|
|
134
|
+
proven ORPO plumbing; MLX_BUN_SEG_HEAD/MLX_BUN_SEG_HEAD_CHUNK knobs).
|
|
135
|
+
Head-only A/B at e4b M=6000: whole-vocab head 16.60 GB peak vs bounded
|
|
136
|
+
6.60 GB (**~10 GB head-vjp transient eliminated**), loss identical, dh
|
|
137
|
+
relnorm 0.00000; head time +45% (Checkpoint recompute, small share of a
|
|
138
|
+
step). The @8K smoke's short responses were never head-bound (peak set by
|
|
139
|
+
the per-layer sdpa backward — see #2), so its wall peak moved little; the
|
|
140
|
+
win is exactly the long-response regime the review named. Gate test:
|
|
141
|
+
tests/train-orpo-fused-ce.test.ts "bounded SFT head".
|
|
142
|
+
**Landing this exposed a NEW upstream mlx bug (see ledger below).**
|
|
143
|
+
9. ~~**[S] Segmented-step overhead pass**~~ **LANDED 2026-07-02** — all three
|
|
144
|
+
fixes: MlxArray.detachCopy (ONE mlx→mlx copy; the old rawBytes+
|
|
145
|
+
fromBytesCopy route was two copies via the JS heap), detachLeaves (ONE
|
|
146
|
+
evalAll barrier per segment's whole vjp set, applied in all six segmented
|
|
147
|
+
backward loops), and a step-shared block-sparse-mask memo for the CPM5
|
|
148
|
+
prefix classes (prefixSharedCaches — makeMask hands out slice views).
|
|
149
|
+
Gate: loss + grads BYTE-IDENTICAL on CPM5-SFT / CPM5-ORPO-prefix / e4b-SFT
|
|
150
|
+
fixed steps. Step time (short-seq fixtures, M1 Max): CPM5 ORPO-prefix
|
|
151
|
+
238→158 ms (−34%), e4b SFT 2065→1272 ms (−38%); e4b @8K flat (GPU-bound,
|
|
152
|
+
as predicted) with peak unchanged (14.49 GB).
|
|
79
153
|
10. **[M] e4b prefill gap** — profile-first; NOTE: the decode look-again could
|
|
80
154
|
NOT reproduce RESULTS.md's 304-vs-373 row on the M1 Max (no eval-DB
|
|
81
155
|
backing) — re-bench on the M4 Pro before building anything.
|
|
@@ -85,8 +159,13 @@ or make the tile loop trace-safe).
|
|
|
85
159
|
The review's framing assumed the megakernel post-mortem's "M=1 decode is at
|
|
86
160
|
the floor". The follow-up investigation (docs/investigations/
|
|
87
161
|
decode-roofline-lookagain.md) OVERTURNED that: only the 12B is at the wall
|
|
88
|
-
(~92-93% of measured roofline); CPM5 58-64%, e4b 64-70%, 26B 60-62
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
162
|
+
(~92-93% of measured roofline); CPM5 58-64%, e4b 64-70%, 26B 60-62%.
|
|
163
|
+
**2026-07-02 second correction (lookagain §7):** the "host JS graph build =
|
|
164
|
+
#1 recoverable term" part of that verdict was itself wrong — the overlap
|
|
165
|
+
spike (decode-overlap-probe.ts, spin-injection + serial anchor) proved the
|
|
166
|
+
pipelined loop ALREADY hides the build; wall = GPU step time. The lookagain's
|
|
167
|
+
host-side fixes (1a–1d: CPM5 compiled-decode port, build-ahead spike, SharedKv
|
|
168
|
+
segmented-compile, 12B concat-phase compile) are refuted or de-prioritized;
|
|
169
|
+
its GPU-side fixes (26B gather-qmm bandwidth, e4b dispatch batching, CPM5
|
|
170
|
+
KV-path, backlog #4 contiguity, spec decode) are now the ENTIRE recoverable
|
|
171
|
+
gap and still compose with the backlog above.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# mlx-bun v0.0.10 — batching parity, SSD KV cache, sharper serving
|
|
2
|
+
|
|
3
|
+
## Highlights
|
|
4
|
+
|
|
5
|
+
**Concurrent serving now matches the fastest MLX batching servers.** With
|
|
6
|
+
`--batch 4`, aggregate throughput at 4 parallel requests is at parity with
|
|
7
|
+
(or ahead of) oMLX on every shared model we benchmarked on an M1 Max —
|
|
8
|
+
MiniCPM5-1B 349 vs 339 tok/s, gemma-4-e4b within 3%, Qwen3.5-4B within 1% —
|
|
9
|
+
with 2–3× better time-to-first-token under load, and per-token SSE
|
|
10
|
+
streaming (no burst batching of your tokens).
|
|
11
|
+
|
|
12
|
+
- **Qwen3.5 joins the batch lane.** Hybrid gated-DeltaNet (SSM) caches grew
|
|
13
|
+
the dynamic-batching ops; the batched decode path is verified
|
|
14
|
+
token-for-token identical against mlx-lm's B=2 batched oracle.
|
|
15
|
+
- **Sampler features batch too.** Repetition/presence/frequency penalties,
|
|
16
|
+
`logit_bias`, `min_p`, and XTC now run per-row inside the batch instead
|
|
17
|
+
of forcing requests onto the serial path. (Qwen3.5 ships a default
|
|
18
|
+
repetition penalty — before this fix it silently serialized every
|
|
19
|
+
request under `--batch N`.)
|
|
20
|
+
|
|
21
|
+
**SSD cold tier for the prompt cache** (`--ssd-cache <dir>`). Long-context
|
|
22
|
+
prefix KV now spills to disk on eviction, is snapshotted after requests
|
|
23
|
+
settle, and **survives server restarts**: re-attaching a 13.7k-token
|
|
24
|
+
coding-agent conversation went from a 12.1 s full re-prefill to a 0.24 s
|
|
25
|
+
zero-copy mmap restore in our measurements — with 0% steady-state decode
|
|
26
|
+
overhead (nothing runs on the token loop). Entries are keyed by model
|
|
27
|
+
fingerprint + KV scheme + tokenizer hash + adapter namespace; corrupt or
|
|
28
|
+
incompatible files self-quarantine; the tier is always safe to delete.
|
|
29
|
+
Companion flags: `--ssd-cache-max <GB>` (default 32), `--ssd-cache-verify`.
|
|
30
|
+
|
|
31
|
+
## Fixes
|
|
32
|
+
|
|
33
|
+
- `serve`/`benchmark` now honor `--model <path-or-query>` as an explicit
|
|
34
|
+
override (it was previously accepted but ignored — auto-pick could
|
|
35
|
+
silently load a different model). A directory containing `config.json`
|
|
36
|
+
loads directly from that path.
|
|
37
|
+
- The server stays responsive during long serial generations: `/stats`,
|
|
38
|
+
`/health`, and new connections previously stalled until the generation
|
|
39
|
+
finished (measured 2.5 s on a 512-token run); they now answer in tens of
|
|
40
|
+
milliseconds, with no measurable decode cost.
|
|
41
|
+
|
|
42
|
+
## Internals
|
|
43
|
+
|
|
44
|
+
- KV persistence format v2: all cache kinds round-trip (bf16, per-layer
|
|
45
|
+
quantized, sliding-window, quantized sliding-window, SSM), with
|
|
46
|
+
integrity hashes, invalidation metadata, and atomic writes.
|
|
47
|
+
- Golden `.bin` parity fixtures are no longer tracked in git (they are
|
|
48
|
+
machine-specific and regenerable); JSON manifests remain.
|
|
49
|
+
- New design docs: `docs/design/omlx-adoption-map.md` (feature adoption
|
|
50
|
+
scoreboard), `docs/design/batching-perf-path.md`,
|
|
51
|
+
`docs/design/ssd-kv-cold-tier.md`.
|
|
52
|
+
|
|
53
|
+
Full details and measured numbers: STATUS.md and the design docs above.
|
|
54
|
+
Credit where due: the SSD-tier and batching comparisons were driven by
|
|
55
|
+
studying [oMLX](https://github.com/jundot/omlx) (Apache 2.0) — a strong
|
|
56
|
+
project whose ideas we benchmarked and adapted where they made mlx-bun
|
|
57
|
+
faster.
|
|
@@ -23,6 +23,9 @@ the design rationale is in
|
|
|
23
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
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
|
+
| `--ssd-cache` | dir | off | SSD cold tier under the prompt cache ([docs/design/ssd-kv-cold-tier.md](../design/ssd-kv-cold-tier.md)): prefix KV spills to disk on RAM eviction, is snapshotted after requests settle (debounced 1 s), and **survives restarts** — a long-context agent re-attach restores via zero-copy mmap instead of re-prefilling (measured 13.7k-token prefix: 12 s → 0.24 s TTFT; 0% decode overhead). Entries are keyed by model fingerprint **+ effective kv scheme** + tokenizer hash + adapter ns; incompatible/corrupt files self-quarantine. Serial lane only. Requires the RAM cache. |
|
|
27
|
+
| `--ssd-cache-max` | GB | `32` | SSD tier byte cap (binary GiB); oldest-mtime entries evicted at the cap. |
|
|
28
|
+
| `--ssd-cache-verify` | (bool) | off | Verify every tensor hash on restore. Reads all bytes eagerly (defeats lazy page fault-in) — integrity paranoia only; the header hash is always verified. |
|
|
26
29
|
| `--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
30
|
| `--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
31
|
| `--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. |
|
|
@@ -132,6 +132,11 @@ piece independently toggled and each falling back cleanly:
|
|
|
132
132
|
- **`orpo_flash_ce`** — the [M,vocab]-free flash-CCE Metal head (steel GEMM fwd
|
|
133
133
|
**and** bwd; e4b backward 754 ms, peak 0.93 GB flat at M=8192). Implies the
|
|
134
134
|
fused head. Falls back to the MLX fused head when the tiling isn't clean.
|
|
135
|
+
**Auto-dispatch by M** (2026-07-02): batches with M below `MLX_BUN_FLASH_MIN_M`
|
|
136
|
+
(default 1024) take the exact MLX fused head instead — it is faster at short
|
|
137
|
+
M (e4b M=64 1.6×, M=512 1.13×; CPM5 at every M), while flash wins memory
|
|
138
|
+
everywhere and time at long M on e4b (M≥2048; 8K: 10.7 vs 13.2 s with the
|
|
139
|
+
filter/blockMax defaults). `MLX_BUN_FLASH_MIN_M=0` always honors flash.
|
|
135
140
|
- **`segment_size`** — segmented backward (gradient-checkpointed layer
|
|
136
141
|
activations) for long context; the head is ~free in memory, the activations
|
|
137
142
|
are the wall.
|
|
@@ -184,7 +189,7 @@ are `DEFAULT_TRAIN_CONFIG` (trainer.ts:89).
|
|
|
184
189
|
| `orpo_lr_schedule` | `constant` \| `cosine` | `cosine` | ORPO LR schedule (orpo only) |
|
|
185
190
|
| `orpo_chunk_size` | int | `0` (off) | Token-chunk the response head, bounding the `[M,vocab]` logits to `[chunk,vocab]` (defaults to 512 when a fused head is on). Exact (orpo only) |
|
|
186
191
|
| `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) |
|
|
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
|
|
192
|
+
| `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 (e4b 0.93 GB flat @ M=8192). Implies the fused head. The Apple-CCE coeff filter + blockMax skip default ON at 1e-5 (2026-07-02; combined bwd 1.71× CPM5 / 3.16× e4b vs exact — `MLX_BUN_CCE_BWD_FILTER_EPS=0` / `_BLOCK_EPS=0` restore exact). Batches with M < `MLX_BUN_FLASH_MIN_M` (default 1024) auto-take the exact fused head (faster at short M; `=0` always honors flash). B=1 (orpo only) |
|
|
188
193
|
| `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
194
|
| `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` |
|
|
190
195
|
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// mlx-bun evals recent benchmark runs
|
|
10
10
|
// mlx-bun harness pi [--base-url <url>] [--remove] connect your own pi to the local server
|
|
11
11
|
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { basename, join, resolve } from "node:path";
|
|
12
14
|
import { Registry } from "./registry";
|
|
13
15
|
import { loadModelConfig } from "./config";
|
|
14
16
|
import { fit, skuMatrix, thisMachine } from "./fit";
|
|
@@ -66,6 +68,10 @@ Examples:
|
|
|
66
68
|
mlx-bun get mlx-community/gemma-4-12B-it-OptiQ-4bit`;
|
|
67
69
|
|
|
68
70
|
const SERVER_FLAGS = `Server options:
|
|
71
|
+
--model <path|query> Explicit model override (mlx_lm.server spelling):
|
|
72
|
+
a directory containing config.json loads from that
|
|
73
|
+
path; anything else resolves like the positional
|
|
74
|
+
query. Wins over the positional and auto-pick.
|
|
69
75
|
--host <addr> Interface to bind [default: 127.0.0.1, loopback
|
|
70
76
|
only; use 0.0.0.0 to expose on your network]
|
|
71
77
|
--port <n> Listen port [default: 8080]
|
|
@@ -74,6 +80,14 @@ const SERVER_FLAGS = `Server options:
|
|
|
74
80
|
GPU [default: machine RAM × 0.75, check-only]
|
|
75
81
|
--prompt-cache <GB> Prompt (KV) cache byte cap [default: 2 GB;
|
|
76
82
|
--prompt-cache 0 disables the cache]
|
|
83
|
+
--ssd-cache <dir> SSD cold tier for the prompt cache: prefix KV
|
|
84
|
+
spills to disk on eviction and persists across
|
|
85
|
+
restarts (long-context agent TTFT drops from a
|
|
86
|
+
full re-prefill to a zero-copy mmap restore).
|
|
87
|
+
Off unless set.
|
|
88
|
+
--ssd-cache-max <GB> SSD tier byte cap [default: 32 GB]
|
|
89
|
+
--ssd-cache-verify Verify tensor hashes on every restore (reads all
|
|
90
|
+
bytes eagerly — integrity paranoia only)
|
|
77
91
|
--batch <n> Max concurrent requests batched through the
|
|
78
92
|
mlx-lm-parity engine [default: 1 = serial].
|
|
79
93
|
>1 opts the whole server into bf16 continuous
|
|
@@ -285,6 +299,9 @@ and runs the mlx-lm/optiq comparison legs.
|
|
|
285
299
|
positional) [default: auto-pick]
|
|
286
300
|
|
|
287
301
|
Options:
|
|
302
|
+
--model <path|query> Explicit model override: a directory containing
|
|
303
|
+
config.json loads from that path; anything else
|
|
304
|
+
resolves like the positional query. Wins over both.
|
|
288
305
|
--tokens <n> Tokens to decode per run [default: 256]
|
|
289
306
|
--runs <n> Runs (median reported) [default: 3]
|
|
290
307
|
--prompt-tokens <n> Pad the prompt to ~n tokens (long-context decode)
|
|
@@ -785,6 +802,14 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
|
|
|
785
802
|
if (budgetGB > 0) serverOptions.memoryBudgetBytes = budgetGB * 1e9;
|
|
786
803
|
const pcRaw = opt("prompt-cache"); // null if absent; an explicit value (incl. 0) wins → `--prompt-cache 0` DISABLES the cache
|
|
787
804
|
if (pcRaw !== null) serverOptions.promptCacheBytes = Math.max(0, Number(pcRaw)) * 2 ** 30;
|
|
805
|
+
// SSD cold tier (docs/design/ssd-kv-cold-tier.md) — off unless a dir is given.
|
|
806
|
+
const ssdDir = opt("ssd-cache");
|
|
807
|
+
if (ssdDir !== null) {
|
|
808
|
+
serverOptions.ssdCacheDir = ssdDir;
|
|
809
|
+
const maxRaw = opt("ssd-cache-max");
|
|
810
|
+
if (maxRaw !== null) serverOptions.ssdCacheMaxBytes = Math.max(1, Number(maxRaw)) * 2 ** 30;
|
|
811
|
+
if (flag("ssd-cache-verify")) serverOptions.ssdCacheVerify = true;
|
|
812
|
+
}
|
|
788
813
|
// --batch N: max concurrent requests batched through the mlx-lm-parity
|
|
789
814
|
// engine (N=1 = today's serial path). --decode-concurrency is accepted for
|
|
790
815
|
// drop-in compatibility with mlx_lm.server, but the semantics differ: there
|
|
@@ -893,8 +918,21 @@ function runtimeSummary(o: import("./server").ServerOptions): string {
|
|
|
893
918
|
|
|
894
919
|
/** Shared model resolution: explicit query wins; otherwise e4b (the default
|
|
895
920
|
* model everywhere) if it's downloaded, else the largest supported model that
|
|
896
|
-
* fits this machine. Downloads the recommended model first on a fresh install.
|
|
921
|
+
* fits this machine. Downloads the recommended model first on a fresh install.
|
|
922
|
+
* A query that is a DIRECTORY containing config.json loads directly via
|
|
923
|
+
* scanSnapshot (no registry dependency) — the `--model <path>` bench/CI shape. */
|
|
897
924
|
async function resolveModelAuto(query: string | null): Promise<{ m: import("./registry").ModelRecord; picked: boolean }> {
|
|
925
|
+
if (query && existsSync(join(query, "config.json"))) {
|
|
926
|
+
const { scanSnapshot } = await import("./registry");
|
|
927
|
+
const dir = resolve(query);
|
|
928
|
+
// Recover the repo id from an HF-cache path (…/models--org--name/snapshots/<rev>);
|
|
929
|
+
// otherwise the directory basename is the display id.
|
|
930
|
+
const hf = /models--([^/]+)--([^/]+)\/snapshots\//.exec(dir);
|
|
931
|
+
const repoId = hf ? `${hf[1]}/${hf[2]}` : basename(dir);
|
|
932
|
+
const m = await scanSnapshot(dir, repoId);
|
|
933
|
+
if (!m) throw new Error(`${dir} has config.json but no weights (*.safetensors)`);
|
|
934
|
+
return { m, picked: false };
|
|
935
|
+
}
|
|
898
936
|
const reg = new Registry();
|
|
899
937
|
if (reg.list().length === 0) await reg.scan();
|
|
900
938
|
if (query) return { m: reg.resolve(query), picked: false };
|
|
@@ -1220,7 +1258,12 @@ switch (cmd) {
|
|
|
1220
1258
|
process.exit(1);
|
|
1221
1259
|
}
|
|
1222
1260
|
}
|
|
1223
|
-
|
|
1261
|
+
// --model <path-or-query> is the explicit override (mlx_lm.server's
|
|
1262
|
+
// spelling — previously accepted-but-IGNORED here, which silently
|
|
1263
|
+
// auto-picked the wrong model). A directory with config.json loads
|
|
1264
|
+
// straight from that path; anything else resolves like the positional
|
|
1265
|
+
// fuzzy query. Precedence: --model > positional > --query > auto-pick.
|
|
1266
|
+
const { m, picked } = await resolveModelAuto(opt("model") ?? positional(0) ?? opt("query"));
|
|
1224
1267
|
const sFit = step("assessing fit");
|
|
1225
1268
|
const config = await loadModelConfig(m.path);
|
|
1226
1269
|
const report = fit(config, m.sizeBytes, 8192, undefined, undefined, m.expertsBytes);
|
|
@@ -1385,7 +1428,7 @@ switch (cmd) {
|
|
|
1385
1428
|
const { banner, step, box, style } = await import("./tui");
|
|
1386
1429
|
banner(pkg.version);
|
|
1387
1430
|
serverRuntimeFlags(); // env levers (--compiled-decode, --perf-kernel, ...) apply to the run
|
|
1388
|
-
const { m, picked } = await resolveModelAuto(positional(0) ?? opt("query"));
|
|
1431
|
+
const { m, picked } = await resolveModelAuto(opt("model") ?? positional(0) ?? opt("query"));
|
|
1389
1432
|
const tokens = Number(opt("tokens", "256"));
|
|
1390
1433
|
const runs = Number(opt("runs", "3"));
|
|
1391
1434
|
const promptTokens = Number(opt("prompt-tokens", "0"));
|
|
@@ -1573,13 +1616,14 @@ switch (cmd) {
|
|
|
1573
1616
|
// (src/pi-terminal.ts) — pi is bundled, nothing to install. Users who
|
|
1574
1617
|
// already run their own pi connect it with `mlx-bun harness pi`.
|
|
1575
1618
|
const OURS_VAL = new Set([
|
|
1576
|
-
"--query", "-q", "--port", "--host", "--memory-budget", "--prompt-cache",
|
|
1619
|
+
"--query", "-q", "--model", "--port", "--host", "--memory-budget", "--prompt-cache",
|
|
1620
|
+
"--ssd-cache", "--ssd-cache-max", "--kv-quant",
|
|
1577
1621
|
"--batch", "--decode-concurrency", "--adapter", "--adapter-path",
|
|
1578
1622
|
"--compiled-decode", "--perf-kernel", "--fused-decode", "--fused-sdpa", "--thinking",
|
|
1579
1623
|
"--temperature", "--temp", "--top-p", "--top-k", "--max-tokens",
|
|
1580
1624
|
"--hlg-sampling", "--hlg-width", "--hlg-shoulder", "--hlg-toe", "--hlg-pivot-offset",
|
|
1581
1625
|
]);
|
|
1582
|
-
const OURS_BOOL = new Set(["--force-wire", "--expert-offload", "--no-open", "--l1", "--l2", "--l3"]);
|
|
1626
|
+
const OURS_BOOL = new Set(["--force-wire", "--expert-offload", "--no-open", "--ssd-cache-verify", "--l1", "--l2", "--l3"]);
|
|
1583
1627
|
const passthrough: string[] = [];
|
|
1584
1628
|
for (let i = 1; i < argv.length; i++) {
|
|
1585
1629
|
const a = argv[i]!;
|
package/src/generate.ts
CHANGED
|
@@ -508,8 +508,16 @@ async function* generateInner(
|
|
|
508
508
|
// 0.6.0, and shapeless replay re-infers the whole tape whenever the
|
|
509
509
|
// growing attention windows change shape (= every step). Remove this
|
|
510
510
|
// when upstream implements GatherQMM::output_shapes.
|
|
511
|
+
// MLX_BUN_FUSED_DECODE=1 (the N-tiled fused decode experiment) cannot
|
|
512
|
+
// compose with compiled decode: the tile loop's bounds bake at trace-time
|
|
513
|
+
// N, so growing quantized caches silently stop attending their newest
|
|
514
|
+
// rows under shapeless replay (kernel-perf-review 2026-07; reproduced on
|
|
515
|
+
// e4b). The explicit opt-in wins over the default optimization — same
|
|
516
|
+
// policy as LoRA and MoE below. quantizedSdpa also throws on the combo
|
|
517
|
+
// as a backstop.
|
|
511
518
|
let compiled =
|
|
512
519
|
process.env.MLX_BUN_COMPILED_DECODE !== "0" &&
|
|
520
|
+
process.env.MLX_BUN_FUSED_DECODE !== "1" &&
|
|
513
521
|
!options.adapters?.length &&
|
|
514
522
|
model.config.modelType.startsWith("gemma4") &&
|
|
515
523
|
!model.config.text.enableMoeBlock
|