mlx-bun 0.0.4 → 0.0.5

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.
Files changed (50) hide show
  1. package/README.md +166 -93
  2. package/docs/design/adapters-end-to-end.md +50 -46
  3. package/docs/design/compat-cli-surface-design.md +10 -8
  4. package/docs/design/docs-reorg-plan.md +14 -1
  5. package/docs/design/hlg-sampling.md +12 -2
  6. package/docs/design/memory-system.md +1 -1
  7. package/docs/design/optimization_plan.md +4 -2
  8. package/docs/design/orpo-future-enhancements.md +298 -0
  9. package/docs/design/orpo-training.md +1199 -0
  10. package/docs/design/parallel-slots.md +34 -26
  11. package/docs/design/runtime-isolation.md +1 -1
  12. package/docs/design/segmented-backward-training.md +58 -39
  13. package/docs/design/tauri-desktop-app.md +127 -0
  14. package/docs/{segmented-backward-handoff.md → investigations/segmented-backward-handoff.md} +1 -1
  15. package/docs/investigations/starter-model-port-handoff.md +32 -13
  16. package/docs/investigations/steel-flash-cce-handoff.md +431 -0
  17. package/docs/planning/IDEAS.md +29 -0
  18. package/docs/planning/PRODUCT_ROADMAP.md +147 -27
  19. package/docs/planning/chunk-task-roadmap.md +1 -1
  20. package/docs/planning/journal.md +161 -0
  21. package/docs/reference/distribution.md +61 -5
  22. package/docs/reference/library-api.md +29 -14
  23. package/docs/reference/orpo-quickstart.md +91 -0
  24. package/docs/reference/server-api.md +105 -7
  25. package/docs/reference/server-config.md +12 -5
  26. package/docs/reference/training.md +292 -0
  27. package/package.json +1 -1
  28. package/src/eval/ifeval.ts +138 -0
  29. package/src/eval/ultrafeedback.ts +60 -0
  30. package/src/lora.ts +12 -7
  31. package/src/mlx/metal-kernel.ts +18 -4
  32. package/src/model/flash-attention.ts +32 -16
  33. package/src/model/fused-geglu-kernel.ts +111 -0
  34. package/src/model/gemma4-base.ts +39 -1
  35. package/src/model/gemma4.ts +196 -21
  36. package/src/model/minicpm5.ts +44 -3
  37. package/src/server.ts +3 -3
  38. package/src/train/dataset.ts +18 -3
  39. package/src/train/flash-cce.ts +1162 -0
  40. package/src/train/job.ts +26 -2
  41. package/src/train/lora-params.ts +53 -4
  42. package/src/train/loss.ts +941 -0
  43. package/src/train/optimizer.ts +10 -3
  44. package/src/train/prefix-shared.ts +435 -0
  45. package/src/train/rank.ts +36 -0
  46. package/src/train/segmented.ts +1148 -3
  47. package/src/train/steel-qmm-header.ts +1461 -0
  48. package/src/train/trainer.ts +509 -52
  49. package/src/web/app.html +21 -5
  50. /package/{docs → src/assets}/curve-designer.html +0 -0
package/README.md CHANGED
@@ -1,21 +1,44 @@
1
1
  # mlx-bun
2
2
 
3
- Native MLX inference for Bun. Run quantized LLMs on Apple Silicon from
4
- TypeScript — no Python, no sidecar server, one runtime.
5
-
6
- Measured head-to-head on an M4 Pro (24 GB), same models, same day:
7
- logits **bit-exact** against the Python reference; served over HTTP the
8
- **fastest stack on every model tested** (TTFT 45–89 ms vs python's
9
- 220–327 ms, zero server overhead vs mlx-lm's −5–6%); prefill up to
10
- **1.8×** mlx-lm at 8k context; cold start → first token of a cached
11
- prompt in **394 ms**. Full table: [benchmarks](#benchmarks).
3
+ Native MLX inference for Bun, and the beginning of a one-command local AI
4
+ product for Apple Silicon Macs. `mlx-bun` detects the machine, starts a useful
5
+ local assistant, exposes OpenAI/Anthropic-compatible APIs, and gives
6
+ TypeScript apps direct access to MLX without Python or a sidecar server.
7
+
8
+ The product bet is that local AI on Mac is a finite optimization problem, not
9
+ an infinite configuration maze: Apple ships a known set of chip/RAM/bandwidth
10
+ SKUs, the supported model list is curated, and the task mode is explicit. That
11
+ means defaults can be chosen from measured device × model × mode profiles
12
+ instead of asking every user to become an ML systems engineer.
13
+
14
+ Measured head-to-head on an M4 Pro (24 GB), same models, same machine
15
+ state: logits **bit-exact** against the Python reference; served over
16
+ HTTP the **fastest stack on every model tested** — fastest decode, plus
17
+ TTFT **34–85 ms** vs python's 218–326 ms at **~0% server overhead**
18
+ (mlx-lm pays −5–6%); server start to ready in **0.17–0.47 s**, 2–5×
19
+ faster than the Python servers. Full table: [benchmarks](#benchmarks).
12
20
 
13
21
  ## Getting started
14
22
 
15
23
  You need an Apple Silicon Mac (MLX is Metal-only, so macOS only by
16
- design). Three ways in.
24
+ design). Four ways in.
25
+
26
+ ### Direct download (recommended)
27
+
28
+ No Homebrew, Bun, or git — just curl the install script. It downloads the
29
+ signed, notarized self-contained binary from the latest release and puts
30
+ `mlx-bun` on your PATH:
31
+
32
+ ```sh
33
+ curl -fsSL https://mlx-bun.dev/install.sh | sh
34
+ mlx-bun
35
+ ```
17
36
 
18
- ### Homebrew (recommended)
37
+ Self-contained (binary + MLX runtime) and notarized, so it runs without a
38
+ 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.4`.
40
+
41
+ ### Homebrew
19
42
 
20
43
  A self-contained, signed + notarized binary — no toolchain to install by
21
44
  hand:
@@ -68,8 +91,7 @@ model named, it does everything for you:
68
91
  2. downloads the sub-GB `MiniCPM5-1B` starter and serves it, so you're
69
92
  chatting in well under a minute;
70
93
  3. starts downloading `gemma-4-e4b` (the stronger 4B model) in the
71
- background — it becomes the default on your next `mlx-bun serve`, on a
72
- 16 GB+ Mac;
94
+ background — it becomes the default on your next `mlx-bun serve`;
73
95
  4. opens the chat UI in your browser at
74
96
  [`http://localhost:8090/#/chat`](http://localhost:8090/#/chat)
75
97
  (pass `--no-open` to skip).
@@ -95,22 +117,46 @@ That's it. The server serves the one model it was started with (the
95
117
  request's `model` field is ignored and the loaded model id is echoed
96
118
  back in responses).
97
119
 
120
+ ## What mlx-bun is
121
+
122
+ `mlx-bun` has four faces, all sharing one native runtime:
123
+
124
+ - **The local AI product** — run `mlx-bun` or `bunx mlx-bun` and get a
125
+ working local chat UI plus a local server. The default path should feel like
126
+ an appliance: download the starter model, start chatting quickly, then move
127
+ to the stronger recommended model as it becomes available.
128
+ - **The app developer library** — embed local MLX inference inside Bun,
129
+ Tauri, Electron, and other TypeScript applications without running Python in
130
+ 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
+ - **The background agent runtime** — leave a local OpenAI-compatible endpoint
135
+ available for scripts, recurring jobs, app integrations, and per-request
136
+ adapter routing.
137
+
138
+ The default experience is intentionally opinionated. Power users can override
139
+ models, budgets, KV modes, adapters, and flags; the average user should not
140
+ need to know those knobs exist.
141
+
98
142
  ## Supported models
99
143
 
100
144
  Scope is deliberate: a few model families held to **bit-exact** logit
101
145
  parity with the Python reference, rather than dozens held to none.
102
- Currently MiniCPM5 plus the Gemma-4 OptiQ quants:
146
+ Currently MiniCPM5, the Gemma-4 OptiQ quants, and Qwen3.5-4B:
103
147
 
104
148
  | Model | Download | Fits on | Vision | Notes |
105
149
  |---|---|---|---|---|
106
150
  | [`mlx-community/MiniCPM5-1B-OptiQ-4bit`](https://huggingface.co/mlx-community/MiniCPM5-1B-OptiQ-4bit) | 0.92 GB | 8 GB | — | Sub-GB option for 8 GB machines; bit-exact 100-step oracle parity (bf16 + mixed KV), tool calling + agent loop verified |
107
- | [`mlx-community/gemma-4-e4b-it-OptiQ-4bit`](https://huggingface.co/mlx-community/gemma-4-e4b-it-OptiQ-4bit) | 7.0 GB | 16 GB | — | **Recommended starter** (16 GB+); ~54 tok/s |
108
- | [`mlx-community/gemma-4-12B-it-OptiQ-4bit`](https://huggingface.co/mlx-community/gemma-4-12B-it-OptiQ-4bit) | 8.4 GB | 16 GB | ✓ | Vision sidecar + tool calling, both verified end-to-end |
109
- | [`mlx-community/gemma-4-26B-A4B-it-OptiQ-4bit`](https://huggingface.co/mlx-community/gemma-4-26B-A4B-it-OptiQ-4bit) | 18 GB | 24 GB | — | MoE (top-8 of 128 experts); ~54 tok/s — the python servers crash loading it on 24 GB |
151
+ | [`mlx-community/Qwen3.5-4B-OptiQ-4bit`](https://huggingface.co/mlx-community/Qwen3.5-4B-OptiQ-4bit) | 3.1 GB | 8 GB | — | bf16-KV bit-exact parity (vs mlx-lm); thinking + tool calling; ~74 tok/s predicted |
152
+ | [`mlx-community/gemma-4-e4b-it-OptiQ-4bit`](https://huggingface.co/mlx-community/gemma-4-e4b-it-OptiQ-4bit) | 7.0 GB | 16 GB | ✓ | **Recommended starter** (16 GB+); ~56 tok/s |
153
+ | [`mlx-community/gemma-4-12B-it-OptiQ-4bit`](https://huggingface.co/mlx-community/gemma-4-12B-it-OptiQ-4bit) | 8.4 GB | 16 GB | ✓ | Vision + tool calling, both verified end-to-end |
154
+ | [`mlx-community/gemma-4-26B-A4B-it-OptiQ-4bit`](https://huggingface.co/mlx-community/gemma-4-26B-A4B-it-OptiQ-4bit) | 18 GB | 24 GB | — | MoE (top-8 of 128 experts); ~54 tok/s |
110
155
 
111
156
  Not sure what fits your machine? `bun src/cli.ts fit <model> --ctx 8192`
112
- gives a deterministic answer (see below). Qwen 3.x is next on the
113
- roadmap (see [PLAN.md](./PLAN.md)).
157
+ gives a deterministic answer (see below). The larger
158
+ `Qwen3.6-27B-OptiQ-4bit` is still in bring-up — parity and serving polish
159
+ remain (see [PLAN.md](./PLAN.md)).
114
160
 
115
161
  ## Why
116
162
 
@@ -131,7 +177,7 @@ one:
131
177
  - **`bun:ffi`** — lowest-overhead FFI of any JS runtime; binds `mlx-c`
132
178
  directly, no node-gyp, no binding compilation.
133
179
  - **Lazy native weight loading** — mlx's loader materializes tensors on
134
- first use; opening an 8.9 GB model takes milliseconds, and warm
180
+ first use; opening an 8.4 GB model takes milliseconds, and warm
135
181
  restarts are near-instant on cached pages.
136
182
  - **`Bun.Image`** — native OS image codecs (HEIC, AVIF, WebP, JPEG, …)
137
183
  for the vision path, EXIF auto-orientation included.
@@ -152,9 +198,11 @@ bun src/cli.ts fit gemma --ctx 32768 # memory contract: fits? max conte
152
198
  bun src/cli.ts fit gemma --ctx 8192 --skus # ...same, across the Apple Silicon lineup
153
199
  bun src/cli.ts serve gemma --port 8090 # OpenAI-compatible server
154
200
  bun src/cli.ts serve gemma --memory-budget 18 # ...with admission control (GB)
201
+ bun src/cli.ts pi # built-in agentic coding CLI (pi's TUI, in-process)
155
202
  bun src/cli.ts evals # recorded benchmark runs
203
+ bun src/cli.ts harness pi # connect your own pi install to the local server
156
204
  ./benchmark.sh # head-to-head matrix vs mlx-lm/optiq (reboot first;
157
- # preflight-gated, resumable, writes benchmarks-h2h-<date>.md)
205
+ # preflight-gated, resumable, writes benchmarks-h2h-<date>-<host>.md)
158
206
  ```
159
207
 
160
208
  Model arguments are substring queries against the registry (`e4b`,
@@ -177,15 +225,18 @@ agent CLIs like pi/OpenClaw via their provider config.
177
225
  - **`POST /v1/chat/completions`** — streaming (SSE) and non-streaming;
178
226
  `temperature`, `top_p`, `top_k`, `max_tokens`, `seed`,
179
227
  `repetition_penalty`, `stop` (string or array, matched on decoded
180
- text with streaming hold-back); omitted sampling fields default to
181
- the model's own `generation_config.json` recipe; usage includes
182
- `cached_tokens`. Full schemas in
228
+ text with streaming hold-back), `reasoning_effort` (thinking on/off for
229
+ Qwen3.5 / MiniCPM5 — `none` disables, any level enables), and an `hlg`
230
+ tone-curve sampling override;
231
+ omitted sampling fields default to the model's own
232
+ `generation_config.json` recipe; usage includes `cached_tokens`.
233
+ Full schemas in
183
234
  [docs/reference/server-api.md](./docs/reference/server-api.md); start
184
235
  flags and the `--batch N` compatibility matrix in
185
236
  [docs/reference/server-config.md](./docs/reference/server-config.md).
186
237
  - **Tool calling** — pass OpenAI `tools`; each family's native format
187
238
  is parsed into `tool_calls` JSON with `finish_reason: "tool_calls"`
188
- (Gemma 4 `<|tool_call>` sentinel tokens; MiniCPM5
239
+ (Gemma 4 `<|tool_call>` sentinel tokens; MiniCPM5 / Qwen3.5
189
240
  `<function name=…>` XML with schema-aware argument decoding);
190
241
  `role: "tool"` round-trips, including multi-turn agent loops.
191
242
  - **Vision** — `image_url` content parts (data: URLs or http/s), on
@@ -195,16 +246,17 @@ agent CLIs like pi/OpenClaw via their provider config.
195
246
  common token prefix across requests (multi-turn conversations
196
247
  re-prefill only the new turn) — automatic, no client changes.
197
248
  Entries are adapter-specific when LoRA adapters are in play.
198
- - **LoRA hot-swap** — mount adapters at startup (`--adapter id=dir`)
199
- or at runtime (`POST /v1/adapters`), select per request with the
200
- `adapter` body field (`"id"`, stacked `"a+b"`, or `"none"`). The
201
- base model never reloads; an unselected adapter costs nothing.
249
+ - **LoRA hot-swap** — mount adapters at runtime (`POST /v1/adapters`)
250
+ and discover on-disk ones (`GET /v1/adapters/available`), select per
251
+ request with the `adapter` body field (`"id"`, stacked `"a+b"`, or
252
+ `"none"`). The base model never reloads; an unselected adapter costs
253
+ nothing.
202
254
  - **Mixed-precision KV** — when the model repo ships `kv_config.json`
203
255
  (every Gemma-4 OptiQ repo does), per-layer KV quantization applies
204
- automatically (full-attention layers; sliding layers stay bf16).
205
- Measured decode-neutral at 8k on the 12B with KV bytes ÷4 on the
206
- quantized layers. `--no-kv-quant` forces bf16; `--kv-bits N` forces
207
- uniform bits. Long prefills over quantized caches run a fused
256
+ automatically; the config sets bits per layer for both full-attention
257
+ and sliding-window (rotating) layers (Phase 9). Measured decode-neutral
258
+ at 8k on the 12B with KV bytes ÷4 on the quantized layers. `--kv-quant off` forces bf16; `--kv-quant 4|8`
259
+ forces uniform bits. Long prefills over quantized caches run a fused
208
260
  FlashAttention-2 tiling that never materializes the full scores
209
261
  matrix (bounded transient; `MLX_BUN_NO_FUSED_SDPA=1` to disable).
210
262
  `MLX_BUN_FUSED_DECODE=1` extends the tiling to single-token decode —
@@ -225,27 +277,30 @@ agent CLIs like pi/OpenClaw via their provider config.
225
277
  Codex/Cursor/Continue speak; includes `previous_response_id`
226
278
  resumption backed by a TTL + byte-capped store (visible in /stats).
227
279
  - **`GET /v1/models`**, **`GET /stats`** (cache hit rates, bytes,
228
- active KV scheme, response store), **`GET/POST/DELETE /v1/adapters`**.
280
+ active KV scheme, response store), **`GET/POST/DELETE /v1/adapters`**,
281
+ plus **`GET /library`**, **`GET /fit`**, **`GET /downloads`**, and
282
+ **`GET /v1`** (a self-describing index of every endpoint).
229
283
 
230
284
  ## Library
231
285
 
232
- The server is one consumer of a library-first API. Not yet published to
233
- npm — clone the repo and import from `src/`. Full reference:
286
+ The server is one consumer of a library-first API. Published to npm as
287
+ `mlx-bun` (current: 0.0.4) — import from the package (`from "mlx-bun"`)
288
+ or `./src/index` in a clone; `bunx mlx-bun` runs the CLI without
289
+ installing. Full reference:
234
290
  [docs/reference/library-api.md](./docs/reference/library-api.md). For shipping inside a
235
291
  Mac app (Tauri/Electron sidecar), `./scripts/build-binary.sh` produces
236
292
  a relocatable single-binary bundle — recipe incl. signing/notarization
237
293
  in [docs/reference/embedding.md](./docs/reference/embedding.md).
238
294
 
239
295
  ```ts
240
- import { loadModelConfig } from "./src/config";
241
- import { Weights } from "./src/weights";
242
- import { Gemma4Model } from "./src/model/gemma4";
243
- import { generate } from "./src/generate";
244
- import { loadTokenizer } from "./src/tokenizer";
245
- import { ChatTemplate } from "./src/chat-template";
296
+ import {
297
+ createModel, // dispatches to Gemma4 / MiniCPM5 / Qwen3.5
298
+ Weights, loadModelConfig, loadTokenizer, ChatTemplate, generate,
299
+ } from "mlx-bun"; // or "./src/index" in a clone
246
300
 
247
301
  const dir = "/path/to/hf/snapshot";
248
- const model = new Gemma4Model(await Weights.open(dir), await loadModelConfig(dir));
302
+ const config = await loadModelConfig(dir);
303
+ const model = createModel(await Weights.open(dir), config); // RuntimeModel
249
304
  const tok = await loadTokenizer(dir);
250
305
  const template = await ChatTemplate.load(dir);
251
306
 
@@ -266,43 +321,44 @@ preamble prefills once, ever.
266
321
  ## Benchmarks
267
322
 
268
323
  Head-to-head against the Python stacks (mlx-lm 0.31.3, mlx-optiq 0.2.1),
269
- same machine (M4 Pro 24 GB), same day, same HF snapshots, preflight-gated
270
- clean machine, median-of-N with warmups discarded. Full curated table
271
- (parity / performance / quality) with per-row provenance:
272
- [benchmarks/RESULTS.md](./benchmarks/RESULTS.md).
324
+ same machine (M4 Pro 24 GB), same HF snapshots, the 2026-06-14
325
+ cleared-machine run, preflight-gated, median-of-N with warmups
326
+ discarded. Full curated table (parity / performance / quality) with
327
+ per-row provenance: [benchmarks/RESULTS.md](./benchmarks/RESULTS.md).
328
+
329
+ **Served (warm)** — how agents actually use a local model.
330
+ decode tok/s · TTFT ms · start s:
273
331
 
274
- | | mlx-bun | mlx-lm | optiq |
332
+ | model | mlx-bun | mlx-lm | optiq |
275
333
  |---|---|---|---|
276
- | **TTFT, served (warm)** | **45–90 ms** | 219–224 ms | 222–331 ms |
277
- | **server start → ready** | **0.36–0.47 s** | 0.76–0.98 s | 0.79–1.00 s |
278
- | **decode through HTTP** (e4b / 12B / 26B) | **54.5** / 25.2 / **54.9** | 53.5 / — / 52.2 | 53.5 / **25.5** / † |
279
- | **server tax vs own direct decode** | **≈ 0%** | −5…−7% | ≈ 0% |
280
- | **direct decode** (engine only) | −1.9…−4.4% vs mlx-lm | baseline | −0.8…−1.2% |
281
- | **12B decode @8k context** | 23.3 (23.0 kv-mixed) | **24.4** | 23.2 kv-mixed |
282
-
283
- Honest negatives, same table: in this matrix our direct decode trailed
284
- mlx-lm on every model (12B −1.9%, 26B −2.9%, e4b −4.4% at short
285
- context; the 12B gap grew to −4.5% @8k), and optiq's served 12B edges
286
- ours by ~1% (25.5 vs 25.2) while paying 3.7× the TTFT. **Post-matrix
287
- (2026-06-11) the decode gap was root-caused and fixed** — a
288
- prefill→decode allocator-reclaim stall that mlx-lm clears with
289
- `mx.clear_cache` and bills to prompt time (we billed it to decode);
290
- after the reference-faithful fix, same-session paired runs put the 12B
291
- *ahead* at short context (25.1 vs 24.0) and at parity @8k (23.8 vs
292
- 23.9). Clean-machine re-measure pending; e4b retains a ~5% per-step
293
- host-overhead residual (see PLAN.md "Decode gap RESOLVED"). Served through HTTP — how agents actually use a local
294
- model — mlx-bun has the fastest decode on e4b and the 26B, and the
295
- fastest TTFT and startup everywhere by 2–5×. † = cell failed: optiq
296
- serve produced no output on the 26B (the Metal OOM crash class from
297
- python's non-lazy load transient — reproduced in isolation; mlx-bun
298
- and mlx-lm both served the same model from the same machine state).
299
- One further optiq cell is blocked on an upstream optiq bug; both are
300
- documented in the results file.
301
-
302
- These numbers are the 2026-06-11 cleared-machine re-run with the
303
- long-context guard active (every @8k row verified at its requested
304
- context — an earlier harness bug had fed python @8k baselines
305
- ~31-token prompts; fixed, guarded, and re-measured).
334
+ | MiniCPM5-1B | **252.9** · 34 · 0.17 | — | 223.6 · 64 · 0.84 |
335
+ | gemma-4-e4b | **55.7** · 44 · 0.36 | 53.5 · 218 · 0.98 | 53.4 · 221 · 0.78 |
336
+ | gemma-4-12B | **25.9** · 85 · 0.38 | — | 25.5 · 326 · 1.24 |
337
+ | gemma-4-26B | **54.2** · 45 · 0.47 | 52.3 · 228 · 0.77 | — |
338
+
339
+ Across every served model mlx-bun has the fastest decode and the fastest
340
+ TTFT/startup (2–5×), at **~0% server tax** vs its own direct engine —
341
+ mlx-lm pays −5–6%, optiq −1 to −10%.
342
+
343
+ **Direct (engine only).** Decode is at parity-to-slightly-behind mlx-lm
344
+ (e4b +1.1%, 12B +0.4%, 26B −1.1%, MiniCPM5 −0.9% — within the per-step
345
+ host-overhead residual), and prefill leads on the larger models (12B
346
+ 1.2×, 26B 1.1×, MiniCPM5 2.3×; e4b trails at 0.8×). The earlier decode
347
+ deficit was root-caused and fixed (2026-06-11) — a prefill→decode
348
+ allocator-reclaim stall mlx-lm clears with `mx.clear_cache` and bills to
349
+ prompt time; see PLAN.md "Decode gap RESOLVED".
350
+
351
+ **Long context (12B).** At 16k and 64k mlx-bun holds decode parity with
352
+ mlx-lm on bf16 (16k 23.9 = 23.9; 64k 20.9 = 20.9) while optiq drops to
353
+ 21.6 then collapses to 12.3 at 64k; mixed-KV trades ~2 tok/s for ~5 GB
354
+ lower peak.
355
+
356
+ Two cross-stack served cells are absent: no mlx-lm 12B row, and optiq
357
+ produced no output on the 26B (the Metal-OOM class from python's
358
+ non-lazy load transient — reproduced in isolation; mlx-bun and mlx-lm
359
+ both served it from the same machine state). One further optiq cell
360
+ (12B/kv=config) is blocked on an upstream `quantized_matmul` bug. Both
361
+ are documented in the results file.
306
362
 
307
363
  ## Correctness
308
364
 
@@ -319,8 +375,8 @@ findings). Golden files are regenerated only by explicit scripts
319
375
  (`scripts/regen-*.ts`) running the Python oracle.
320
376
 
321
377
  ```sh
322
- bun test # fast tier runs everywhere; model-loaded tests auto-skip
323
- # unless the reference snapshot is in your HF cache
378
+ bun test # fast tier runs everywhere; model-loaded tests run only
379
+ # when the reference snapshot is in your HF cache
324
380
  ```
325
381
 
326
382
  ## Troubleshooting
@@ -343,19 +399,36 @@ bun test # fast tier runs everywhere; model-loaded tests auto-skip
343
399
  ## Status
344
400
 
345
401
  Pre-alpha, moving fast. See [PLAN.md](./PLAN.md) for phases, exit
346
- criteria, measured numbers, and the findings log. Complete: load path,
347
- bit-exact model port (12B dense / e4b per-layer-input / 26B MoE),
348
- sampling + serving (tools, vision, prompt cache), registry/fit/KV
349
- persistence, quantized + mixed-precision KV serving (rotating-cache
350
- KV-quant included, Phase 9) with fused quantized prefill (Phase 10),
351
- LoRA hot-swap with per-request selection, resumable verified downloads
352
- (`mlx-bun get`), memory admission control, and the head-to-head
353
- benchmark harness, and the decode-gap root-cause fix (clear_cache
354
- placement + boundary accounting, 2026-06-11 — 12B now at-or-above
355
- mlx-lm decode in paired runs). Open: e4b's ~5% host-overhead decode
356
- residual (Phase 7), Anthropic messages + Responses API (Phase 11),
357
- SigLIP vision for e4b/26B, Qwen 3.x, the embeddable single-binary
358
- build.
402
+ criteria, measured numbers, and the findings log.
403
+
404
+ **Complete** — load path; bit-exact model port (MiniCPM5, Qwen3.5-4B,
405
+ e4b per-layer-input, 12B dense, 26B MoE); sampling + serving (tools, vision,
406
+ prompt cache); registry / fit / KV persistence; quantized +
407
+ mixed-precision KV serving (rotating-cache KV-quant, Phase 9) with fused
408
+ quantized prefill (Phase 10); LoRA hot-swap with per-request selection;
409
+ segmented-backward LoRA training (SFT / DPO / ORPO, with an `[M,vocab]`-free
410
+ flash-CCE head + prefix-sharing for long-context preference data — see
411
+ [training guide](docs/reference/training.md)); resumable
412
+ verified downloads (`mlx-bun get`); memory admission control; the
413
+ head-to-head benchmark harness; the decode-gap root-cause fix
414
+ (2026-06-11); Anthropic Messages + Responses API (`/v1/messages` and
415
+ `/v1/responses`, Phase 11); SigLIP vision sidecar for e4b
416
+ (commit 4625fe5); the embeddable single-binary build (signed +
417
+ notarized — Homebrew, direct-download, and npm/bunx).
418
+
419
+ **In progress** — `Qwen3.6-27B` bring-up (Phase 14f): same architecture
420
+ as the verified 4B (untied + larger geometry); parity and serving polish
421
+ remain. MTP speculation and Qwen3-VL vision deferred.
422
+
423
+ **Experimental** — opt-in, default-off, still being hardened: transparent
424
+ expert offload for MoE models (`serve --expert-offload`, Phase 20:
425
+ page-aligned mmap-backed experts, bit-exact — 26B-A4B 17.1→4.2 GB
426
+ resident, decode unregressed); batched serving (`--batch N`, Phase 18:
427
+ continuous-batching bf16 B=N decode, B=2 bit-parity verified for
428
+ MiniCPM5/12B/e4b/26B; throughput polish remains).
429
+
430
+ **Open** — e4b's ~5% per-step host-overhead decode residual (Phase 7);
431
+ SigLIP vision for 26B.
359
432
 
360
433
  ## License
361
434
 
@@ -1,5 +1,16 @@
1
1
  # Adapters end-to-end — notes / TODO (2026-06-16)
2
2
 
3
+ **Status legend:** ✅ DONE — shipped and verified in code | ⏳ PENDING — not yet implemented
4
+
5
+ | Section | Status |
6
+ | --- | --- |
7
+ | §A Pi CLI extension (`extensions/mlx-bun-adapter.ts`) | ✅ DONE |
8
+ | §B Web chat adapter selector (`src/web/app.html` + `before_provider_request` hook in `pi-web.ts`) | ✅ DONE |
9
+ | §C `/v1/adapters/available` discovery (`src/server.ts:1182`, `src/lora.ts:139`) | ✅ DONE |
10
+ | §D Overnight training from the web UI — monitor/stream endpoint + UI | ⏳ PENDING |
11
+ | §E `--force-adapter` CLI flag (absent from `src/cli.ts`) | ⏳ PENDING |
12
+ | Repo-id keying written into adapter configs at save time | ⏳ PENDING |
13
+
3
14
  Working notes for wiring trained LoRA adapters through the whole stack: Pi (CLI +
4
15
  web), the web Chat page UI, and the server's discovery surface. Captured from the
5
16
  2026-06-16 session (serving verified working; pi-extension mechanism confirmed from
@@ -31,16 +42,16 @@ API should let you move an adapter between these states explicitly.
31
42
  So **loaded** and **selected** are done. The gap is **available** (discovery) plus
32
43
  the two front-ends (Pi extension, web Chat selector).
33
44
 
34
- ## TODO
45
+ ## Shipped (§A–§C) ✅
35
46
 
36
- ### A. Pi CLI/terminal — adapter via extension *(default none)*
47
+ ### A. Pi CLI/terminal — adapter via extension *(default none)* ✅ DONE
37
48
 
38
- Mechanism (confirmed from the pi docs in-session): a Pi extension using the
39
- `before_provider_request` hook to inject the field into the outgoing
40
- OpenAI-compatible payload. `models.json` can't add arbitrary body fields — the hook
41
- is the right tool.
49
+ Wired a Pi extension using the `before_provider_request` hook to inject the `adapter`
50
+ field into the outgoing OpenAI-compatible payload. `models.json` can't add arbitrary
51
+ body fields — the hook was the right tool.
42
52
 
43
- Create `~/.pi/agent/extensions/mlx-bun-adapter.ts`:
53
+ Shipped as `~/.pi/agent/extensions/mlx-bun-adapter.ts` (copy in-repo + one-liner
54
+ installer):
44
55
 
45
56
  ```ts
46
57
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
@@ -60,53 +71,44 @@ export default function (pi: ExtensionAPI) {
60
71
  - Selection via `PI_MLX_ADAPTER=chunk pi --provider mlx-bun --model local -p "..."`
61
72
  (no native `-p --adapter` flag; unknown CLI flags get rejected). Optional shell
62
73
  helper: `pi-chunk() { PI_MLX_ADAPTER=chunk pi --provider mlx-bun --model local -p "$@"; }`.
63
- - Put it in the global `~/.pi/agent/extensions/` dir so it loads for non-interactive
64
- `-p` too. `/reload` to pick it up.
65
- - **Ship this** as part of mlx-bun (a copy in-repo + a one-liner installer), so users
66
- don't hand-write it. Consider also reading the *selected* adapter from a small
67
- state file the web UI / a `mlx-bun adapter use <id>` command writes, so CLI and web
68
- share one selection.
74
+ - Lives in the global `~/.pi/agent/extensions/` dir so it loads for non-interactive
75
+ `-p` too. `/reload` picks it up.
76
+ - Also reads the *selected* adapter from a small state file the web UI / a
77
+ `mlx-bun adapter use <id>` command writes, so CLI and web share one selection.
69
78
 
70
- ### B. Web Chat page — selector + load/unload *(default none)*
79
+ ### B. Web Chat page — selector + load/unload *(default none)* ✅ DONE
71
80
 
72
81
  Path: `src/web/app.html` (UI) ↔ `/ws/chat` (`src/pi-web.ts`) ↔ in-process Pi ↔ model.
73
82
 
74
83
  - **Selector** in `app.html`: a dropdown of *available* adapters with **"none"
75
84
  selected by default**. Changing it sets the active selection for subsequent
76
- messages. Allow multi-select → `"a+b"` (resolveSpec already supports stacking).
85
+ messages. Multi-select → `"a+b"` (resolveSpec already supports stacking).
77
86
  - **Load / unload controls**: buttons that call `POST /v1/adapters` (load) and
78
- `DELETE /v1/adapters/:id` (unload). Show each adapter's state (available / loaded /
79
- selected) so the three concepts are visible. An adapter must be **loaded** before
80
- it can be **selected** (selecting an unloaded id errors) — UI should either
81
- auto-load on select or gate selection to loaded ones.
82
- - **Inject via the `before_provider_request` hook — the primary mechanism, identical
83
- to the CLI (§A).** `pi-web.ts` ALREADY registers an inline extension this way
84
- (`pi.on("tool_call", …)` wired into `createAgentSession`, pi-web.ts:469/605), so
85
- `pi.on("before_provider_request", (e) => ({ ...e.payload, adapter }))` drops in the
86
- same way and reads the current selection from the WS session state. **No custom
87
+ `DELETE /v1/adapters/:id` (unload). Each adapter's state (available / loaded /
88
+ selected) is shown so the three concepts are visible. Selecting an unloaded adapter
89
+ auto-loads it (fires `POST /v1/adapters`) before injecting the id.
90
+ - **Injected via the `before_provider_request` hook — identical mechanism to the CLI
91
+ (§A).** `pi-web.ts` registers the inline extension in `createAgentSession`
92
+ (pi-web.ts:469/605), so `pi.on("before_provider_request", (e) => ({ ...e.payload,
93
+ adapter }))` reads the current selection from WS session state. **No custom
87
94
  provider plumbing, no new model-call wiring.** The hook is the spine for BOTH UIs.
88
- - **Selection → hook**: the dropdown just updates the session's selected-adapter value
89
- that the hook reads — a tiny client→server message (or reuse the `set_thinking`-style
90
- toggle path), NOT a heavyweight protocol. Default none → hook returns the payload
91
- unchanged → base model.
92
- - **Mounting is the only thing the hook can't do**: the injected id must be mounted
93
- server-side or `resolveSpec` rejects it. Auto-load-on-select handles it — selecting
94
- fires one `POST /v1/adapters`, then the hook injects the id. Discovery (§C) and a
95
- richer protocol are optional polish; the dropdown can start from `GET /v1/adapters`.
96
-
97
- ### C. Server — adapter **discovery** (the missing "available" list)
98
-
99
- - Add a store the server scans for available adapters:
100
- - `~/.cache/mlx-bun/adapters/` (the trainer already defaults saves here —
101
- `src/server.ts:1653`) and/or a **sidecar next to the model dir**.
102
- - New endpoint, e.g. `GET /v1/adapters/available` →
95
+ - **Selection → hook**: the dropdown updates the session's selected-adapter value via
96
+ a small client→server message (reusing the `set_thinking`-style toggle path). Default
97
+ none → hook returns the payload unchanged → base model.
98
+
99
+ ### C. Server — adapter **discovery** (`GET /v1/adapters/available`) ✅ DONE
100
+
101
+ - Scans `~/.cache/mlx-bun-finetunes/` and `~/.cache/mlx-bun/adapters/`
102
+ (`src/server.ts:1192`) and any sidecar next to the model dir.
103
+ - `GET /v1/adapters/available` (`src/server.ts:1182`, `src/lora.ts:139`) →
103
104
  `[{ id, path, source_repo, rank, scale, compatible }]`, where each entry is read
104
105
  from the adapter's `optiq_lora_config.json` (`source_model`, `rank`, `scale`).
105
- - **Filter by the served model.** Today `source_model` is a *machine/snapshot-specific
106
- path* (`/Users/joshrossi/.cache/huggingface/.../snapshots/<hash>`), which won't
107
- match across machines/snapshots. Key compatibility on the **repo id**
108
- (`mlx-community/MiniCPM5-1B-OptiQ-4bit`) instead — see "repo-id keying" below. The
109
- server already tracks `repoId` per model; `compatible = adapter.source_repo === served.repoId`.
106
+ - **Filters by the served model** on **repo id**
107
+ (`mlx-community/MiniCPM5-1B-OptiQ-4bit`) rather than the fragile
108
+ machine/snapshot-specific path. The server already tracks `repoId` per model;
109
+ `compatible = adapter.source_repo === served.repoId`.
110
+
111
+ ## TODO (§D–§E)
110
112
 
111
113
  ### D. Overnight training from the web UI
112
114
 
@@ -117,8 +119,10 @@ Training must be fully drivable from the web UI — not just a script.
117
119
  (rank / scale / num_layers / LR / dropout / iters / seq / segmentSize — the
118
120
  configurable-params task, mlx-lm defaults), **start**, and leave it running.
119
121
  - **Monitor**: live loss / iter / ETA, and survives the browser closing (the run is
120
- server-side; the page reconnects). `POST /v1/fit` already exists
121
- (`src/server.ts:1652`) — needs a status/stream endpoint + a UI.
122
+ server-side; the page reconnects). The training backend already exists:
123
+ `POST /api/finetune/submit` (`src/server.ts:1781`). What §D still needs is a
124
+ status/stream endpoint + the overnight-monitoring UI. (Note: `GET /fit` is the
125
+ unrelated memory-fit solver in `src/fit.ts` — not a training endpoint.)
122
126
  - **Completion**: the finished adapter lands in the store and shows up as *available*
123
127
  (state A in the three-state model), ready to load/select or auto-serve.
124
128
  - This closes the loop with the chunk-task roadmap: iterate data → train overnight →
@@ -6,6 +6,8 @@ modes"). This doc is the surface contract: every verb, the flag
6
6
  vocabulary, how optiq is absorbed, the parity harness, and **the
7
7
  matrix of what we still lack**.
8
8
 
9
+ **Update 2026-06-17:** Several items have shipped since the 2026-06-12 snapshot: **LoRA training** is live (`src/train/`, `scripts/chunk-finetune.ts`, segmented-backward Phase A MiniCPM5 + Phase B e4b both merged); the **web UI fine-tune tab** (`src/web/app.html`) is live with SFT/DPO workflow, loss curve, merge, and export; the **chat adapter selector** (web UI) is live; and **adapters end-to-end** (web selector + CLI `/adapter`) are done — the matrix below reflects these as ✅. The **Phase 17 compat parity harness itself** (the verb router, dotted aliases, flag-surface parity tests) remains **unbuilt**.
10
+
9
11
  ## Goal
10
12
 
11
13
  Design mlx-bun's entire CLI/flag surface from scratch (nothing is
@@ -54,7 +56,7 @@ shared subset; optiq-only features are additive (marked ⊕).
54
56
  | `chat` | mlx_lm.chat | engine ✅, web chat ✅; no terminal REPL (`tui.ts` is help-formatting) | build REPL frontend over `generate` |
55
57
  | `server` | mlx_lm.server / optiq serve | ✅ `serve` | rename flags; ⊕ `--kv-config`, `--anthropic` honored (default on) |
56
58
  | `convert` | mlx_lm.convert + optiq convert | ❌ | quant pipeline; ⊕ `--target-bpw/--candidate-bits/--reference` |
57
- | `lora` | mlx_lm.lora + optiq lora | serve/hot-swap ✅, train ❌ | `--train/--test`; ⊕ `--rank-scaling by_bits\|by_kl`; `lora info` |
59
+ | `lora` | mlx_lm.lora + optiq lora | serve/hot-swap ✅, train ✅ | `--train/--test`; ⊕ `--rank-scaling by_bits\|by_kl`; `lora info` |
58
60
  | `fuse` | mlx_lm.fuse | ❌ | fuse adapters, dequant, GGUF export, upload |
59
61
  | `cache_prompt` | mlx_lm.cache_prompt | runtime ✅, CLI ❌ | precompute + save reusable KV cache file |
60
62
  | `benchmark` | mlx_lm.benchmark / optiq benchmark | ✅ `benchmark` | flag-name parity (`-p/-g/-b/-n`); ppl half ❌ |
@@ -83,7 +85,7 @@ mlx-lm" half.
83
85
 
84
86
  ### pi — the built-in agentic coding CLI
85
87
 
86
- (Phase 16; full plan in **docs/pi-builtin-investigation.md**.) pi is an
88
+ (Phase 16; full plan in **docs/investigations/pi-builtin-investigation.md**.) pi is an
87
89
  MIT, Bun-native (`bun build --compile`) coding agent — the "CLI
88
90
  experience inside our package" that neither Python package has. Two
89
91
  commands, one for each audience:
@@ -111,8 +113,8 @@ split.
111
113
  The web UI's agent backbone is pi's RPC / `AgentSession.subscribe()`
112
114
  event stream (investigation Option 4) — the SAME events drive the
113
115
  embedded TUI and the browser, so tool calls / steering / progress come
114
- for free. Tiles: chat ✅; quantize / fine-tune / training-data gate on
115
- the capability matrix.
116
+ for free. Tiles: chat ✅; fine-tune ✅ (shipped: SFT/DPO workflow, loss curve, merge,
117
+ export); quantize / training-data gate on the capability matrix.
116
118
 
117
119
  ## Flag-vocabulary reconciliation (where we currently diverge)
118
120
 
@@ -150,7 +152,7 @@ column; the 🟥 column is the capability matrix that fills in behind it.
150
152
  | KV-cache quant: bf16 / uniform / **mixed per-layer** (kv_config.json) | `server` (`off`/`N`/`config`) | ✅ | done (Phase 9/10; config.ts reads, generate.ts applies per-layer) |
151
153
  | KV sensitivity *profiler* (authors a NEW kv_config.json) | `kv-cache` | 🟥 | model-prep, low pri (shipped artifacts already include one) |
152
154
  | TurboQuant — optional extra quant *method* (rotation VQ) | scheme, not a verb | 🟥 | Phase 13 (KV plumbing already exists) |
153
- | **LoRA / DoRA / full training** | `lora --train` | 🟥 | Training (new phase) |
155
+ | **LoRA / DoRA / full training** | `lora --train` | ✅ | Training (done: Phase A MiniCPM5 + Phase B e4b) |
154
156
  | **Sensitivity-aware rank scaling** | `lora --rank-scaling` | 🟥 | Training |
155
157
  | **Fuse adapters (+GGUF/upload)** | `fuse` | 🟥 | Training |
156
158
  | **lm-eval-harness tasks** | `evaluate` | 🟥 | Eval (new phase) |
@@ -159,7 +161,7 @@ column; the 🟥 column is the capability matrix that fills in behind it.
159
161
  | **HF upload** | `upload` | 🟥 | Distribution |
160
162
  | **Distributed share** | `share` | 🟥 | Distribution (lowest pri) |
161
163
  | **Web UI: quantize** | `lab`/UI | 🟥 | gated on Quantize |
162
- | **Web UI: fine-tune** | `lab`/UI | 🟥 | gated on Training |
164
+ | **Web UI: fine-tune** | `lab`/UI | ✅ | done (SFT/DPO workflow, loss curve, merge, export) |
163
165
  | **Web UI: training-data template + gen** | `lab`/UI | 🟥 | new (no engine dep) |
164
166
  | Web UI: chat | `/chat` | ✅ | done (publishing gate 2) |
165
167
  | **Native: pi embedded** (`mlx-bun pi`, in-process TUI) | — | ✅ (P3/P4) | Phase 16 (flagship) |
@@ -177,7 +179,7 @@ does. The real capability work splits along two axes:
177
179
  optional and not inference plumbing: a *profiler* to author a new
178
180
  kv_config.json (model-prep, low pri) and extra methods like TurboQuant
179
181
  (Phase 13).
180
- - **Training**: LoRA/DoRA/full + `fuse`.
182
+ - **Training**: LoRA/DoRA/full training ✅ (shipped); `fuse` verb (adapter merge, GGUF export, upload) 🟥 remaining.
181
183
  - **Eval**: harness + `perplexity`.
182
184
  - **Distribution**: `upload`/`share`.
183
185
 
@@ -221,7 +223,7 @@ correctness diffs need no clean machine.
221
223
  3. Inference-time KV quant → already done (bf16 / uniform / mixed
222
224
  per-layer via kv_config.json, Phase 9/10). Optional later: a profiler
223
225
  to author new configs (low pri) + the TurboQuant method (Phase 13).
224
- 4. Training → `lora --train`/`fuse` + web-UI fine-tune tile.
226
+ 4. Training → ✅ `lora --train` + web-UI fine-tune tile (both shipped); remaining: `fuse` verb (adapter merge, GGUF export, upload).
225
227
  5. Eval → `evaluate`/`perplexity`.
226
228
  6. Distribution → `upload`/`share`.
227
229
  7. Web UI training-data tile (independent of engine work).