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.
- package/README.md +166 -93
- package/docs/design/adapters-end-to-end.md +50 -46
- package/docs/design/compat-cli-surface-design.md +10 -8
- package/docs/design/docs-reorg-plan.md +14 -1
- package/docs/design/hlg-sampling.md +12 -2
- package/docs/design/memory-system.md +1 -1
- package/docs/design/optimization_plan.md +4 -2
- package/docs/design/orpo-future-enhancements.md +298 -0
- package/docs/design/orpo-training.md +1199 -0
- package/docs/design/parallel-slots.md +34 -26
- package/docs/design/runtime-isolation.md +1 -1
- package/docs/design/segmented-backward-training.md +58 -39
- package/docs/design/tauri-desktop-app.md +127 -0
- package/docs/{segmented-backward-handoff.md → investigations/segmented-backward-handoff.md} +1 -1
- package/docs/investigations/starter-model-port-handoff.md +32 -13
- package/docs/investigations/steel-flash-cce-handoff.md +431 -0
- package/docs/planning/IDEAS.md +29 -0
- package/docs/planning/PRODUCT_ROADMAP.md +147 -27
- package/docs/planning/chunk-task-roadmap.md +1 -1
- package/docs/planning/journal.md +161 -0
- package/docs/reference/distribution.md +61 -5
- package/docs/reference/library-api.md +29 -14
- package/docs/reference/orpo-quickstart.md +91 -0
- package/docs/reference/server-api.md +105 -7
- package/docs/reference/server-config.md +12 -5
- package/docs/reference/training.md +292 -0
- package/package.json +1 -1
- package/src/eval/ifeval.ts +138 -0
- package/src/eval/ultrafeedback.ts +60 -0
- package/src/lora.ts +12 -7
- package/src/mlx/metal-kernel.ts +18 -4
- package/src/model/flash-attention.ts +32 -16
- package/src/model/fused-geglu-kernel.ts +111 -0
- package/src/model/gemma4-base.ts +39 -1
- package/src/model/gemma4.ts +196 -21
- package/src/model/minicpm5.ts +44 -3
- package/src/server.ts +3 -3
- package/src/train/dataset.ts +18 -3
- package/src/train/flash-cce.ts +1162 -0
- package/src/train/job.ts +26 -2
- package/src/train/lora-params.ts +53 -4
- package/src/train/loss.ts +941 -0
- package/src/train/optimizer.ts +10 -3
- package/src/train/prefix-shared.ts +435 -0
- package/src/train/rank.ts +36 -0
- package/src/train/segmented.ts +1148 -3
- package/src/train/steel-qmm-header.ts +1461 -0
- package/src/train/trainer.ts +509 -52
- package/src/web/app.html +21 -5
- /package/{docs → src/assets}/curve-designer.html +0 -0
|
@@ -19,9 +19,34 @@ provider config before they can try local AI. They should install one
|
|
|
19
19
|
binary, run one command, wait for one resumable model download, and get a
|
|
20
20
|
working local chat UI plus a local AI server.
|
|
21
21
|
|
|
22
|
+
The intended first-run shape is product, not demo: start with the tiny
|
|
23
|
+
welcoming model quickly, begin the stronger recommended model download in the
|
|
24
|
+
background, and graduate the user into the best default profile for their
|
|
25
|
+
actual Mac without requiring them to learn the model zoo first.
|
|
26
|
+
|
|
22
27
|
The runtime can still expose every advanced knob. The default path should
|
|
23
28
|
not require them.
|
|
24
29
|
|
|
30
|
+
## Four product faces
|
|
31
|
+
|
|
32
|
+
The same runtime has to support four related but distinct jobs:
|
|
33
|
+
|
|
34
|
+
1. **Local AI product** — the one-command experience for someone who just wants
|
|
35
|
+
useful local AI. This path should be opinionated, benchmarked, and boringly
|
|
36
|
+
reliable.
|
|
37
|
+
2. **Library for app developers** — the embeddable TypeScript/MLX layer for
|
|
38
|
+
Bun, Tauri, Electron, and desktop tools that want local inference without a
|
|
39
|
+
Python sidecar.
|
|
40
|
+
3. **AI lab for researchers** — the place where parity, evals, kernels,
|
|
41
|
+
compiled graphs, ORPO, LoRA adapters, and model experiments can happen in
|
|
42
|
+
the same stack that serves the result.
|
|
43
|
+
4. **Autonomous background agent runtime** — the always-available local server
|
|
44
|
+
for scripts, recurring tasks, personal tools, and per-request adapter
|
|
45
|
+
routing.
|
|
46
|
+
|
|
47
|
+
These are not separate products. They are one runtime with progressive
|
|
48
|
+
disclosure: appliance defaults at the front, sharp instruments underneath.
|
|
49
|
+
|
|
25
50
|
## The three modes (the organizing design law)
|
|
26
51
|
|
|
27
52
|
mlx-bun has not published anything: the entire CLI, command, and flag
|
|
@@ -72,7 +97,7 @@ verbs and one flag vocabulary, not three separate CLIs:
|
|
|
72
97
|
serving, latency prediction, the Lab UI) graft onto the mlx-lm spine
|
|
73
98
|
as additive flags and a few new verbs.
|
|
74
99
|
|
|
75
|
-
### Status (2026-06-12)
|
|
100
|
+
### Status (2026-06-12; compat surface design remains open as of 2026-06-17)
|
|
76
101
|
|
|
77
102
|
The compat layer does not exist yet: today's commands (`serve`, `fit`,
|
|
78
103
|
`benchmark`, `evals`, `get`/`scan`/`ls`, `pi`, `harness`) are ad-hoc,
|
|
@@ -83,27 +108,42 @@ reconcile the existing commands into it, and fill the capability matrix
|
|
|
83
108
|
behind it (convert/quantize, LoRA training, eval/perplexity, fuse,
|
|
84
109
|
cache_prompt, upload/share). Decomposed into phases in PLAN.md.
|
|
85
110
|
|
|
86
|
-
## Core idea
|
|
111
|
+
## Core idea: finite matrix runtime
|
|
87
112
|
|
|
88
|
-
The local-AI problem is not continuous for Mac users.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
and preselect the best known model artifact,
|
|
92
|
-
memory budget, and runtime
|
|
113
|
+
The local-AI problem is not continuous for Mac users. MLX means Apple Silicon,
|
|
114
|
+
and Apple ships a finite set of real chip/RAM/bandwidth combinations. The
|
|
115
|
+
supported model universe is also curated. For each physical device profile and
|
|
116
|
+
model snapshot, we can research and preselect the best known model artifact,
|
|
117
|
+
context target, KV mode, memory budget, kernel route, graph route, and runtime
|
|
118
|
+
flags.
|
|
93
119
|
|
|
94
|
-
That means first-run behavior can be a lookup table, not a wizard or
|
|
95
|
-
benchmark:
|
|
120
|
+
That means first-run behavior can be a measured lookup table, not a wizard or
|
|
121
|
+
an exploratory benchmark:
|
|
96
122
|
|
|
97
123
|
```text
|
|
98
|
-
detect Mac profile
|
|
99
|
-
|
|
124
|
+
detect exact Mac profile
|
|
125
|
+
fingerprint the model snapshot
|
|
126
|
+
choose the task mode: chat, vision, tool loop, adapter, training, eval
|
|
127
|
+
look up the measured execution plan
|
|
100
128
|
download/resume the exact model artifact
|
|
101
129
|
start the tuned local server
|
|
102
130
|
open the built-in UI
|
|
103
131
|
show how to connect external tools and harnesses
|
|
104
132
|
```
|
|
105
133
|
|
|
106
|
-
The
|
|
134
|
+
The current `fit` endpoint is the nucleus of this idea: it already reasons over
|
|
135
|
+
real Apple Silicon profiles, model bytes, KV bytes, context, and predicted
|
|
136
|
+
decode. The product version should evolve from "does this fit?" into:
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
device profile + model checksum + task mode + live memory pressure
|
|
140
|
+
-> recommended execution plan
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Execution plans can eventually include specialized graph variants, fused kernel
|
|
144
|
+
routes, prompt-cache policy, adapter policy, safe context limits, expected
|
|
145
|
+
decode/TTFT, and user-facing rationale. The research work feeds the table. The
|
|
146
|
+
table feeds the user.
|
|
107
147
|
|
|
108
148
|
## Recommended profiles
|
|
109
149
|
|
|
@@ -143,6 +183,31 @@ changes a device-tier decision, unlocks a larger useful context window,
|
|
|
143
183
|
keeps decode above a user-visible threshold, improves quality at the same
|
|
144
184
|
budget, or prevents failure. Saving memory for its own sake is secondary.
|
|
145
185
|
|
|
186
|
+
## Specialization strategy
|
|
187
|
+
|
|
188
|
+
For the curated product path, it is acceptable to trade more work up front for a
|
|
189
|
+
better hot path later. The Mac is known, the model snapshot is known, and the
|
|
190
|
+
task mode is known. That lets us precompute or cache exact plans instead of
|
|
191
|
+
shipping every decision to runtime.
|
|
192
|
+
|
|
193
|
+
Candidate plan fields:
|
|
194
|
+
|
|
195
|
+
- Model checksum and architecture fingerprint.
|
|
196
|
+
- Apple Silicon profile: chip, unified memory, bandwidth tier, GPU/CPU shape.
|
|
197
|
+
- Context buckets and fixed-shape cache layouts.
|
|
198
|
+
- Kernel choices: MLX built-ins, MLX fast paths, custom Metal kernels, or
|
|
199
|
+
eager fallback.
|
|
200
|
+
- Graph choices: eager, compiled exact-shape graph, compiled bucketed graph,
|
|
201
|
+
or prebuilt graph table.
|
|
202
|
+
- Adapter choices: none, single LoRA, stacked LoRA, task-routed LoRA.
|
|
203
|
+
- Measured results: TTFT, decode tok/s, peak memory, failure envelope.
|
|
204
|
+
|
|
205
|
+
This is where loop unrolling, branch deletion, static DAG extraction, fused
|
|
206
|
+
kernels, and shape-specialized compiled graphs become product features rather
|
|
207
|
+
than abstract optimizations. They matter when they let the default profile be
|
|
208
|
+
faster, larger, more stable, or better aligned without asking the user to tune
|
|
209
|
+
anything.
|
|
210
|
+
|
|
146
211
|
## First-run UX
|
|
147
212
|
|
|
148
213
|
The no-args command should be the friendly path:
|
|
@@ -153,17 +218,40 @@ mlx-bun
|
|
|
153
218
|
|
|
154
219
|
Likely behavior:
|
|
155
220
|
|
|
156
|
-
1. Detect the hardware profile.
|
|
157
|
-
2. Pick the recommended runtime profile.
|
|
158
|
-
3. Show the selected model and why it was chosen.
|
|
159
|
-
4. Start or resume the background download.
|
|
160
|
-
5. Start the local server when ready.
|
|
161
|
-
6. Open or serve the built-in chat UI.
|
|
162
|
-
7. Show connection details for OpenAI/Anthropic-compatible clients.
|
|
163
|
-
8. Offer harness setup actions for Pi, Hermes, OpenClaw, and others.
|
|
221
|
+
1. Detect the hardware profile. *(not yet: `recommendedRepoId()` at `src/fit.ts:86` returns `DEFAULT_REPO_ID` unconditionally — device-profile lookup table is unimplemented)*
|
|
222
|
+
2. Pick the recommended runtime profile. *(same caveat as above)*
|
|
223
|
+
3. Show the selected model and why it was chosen. *(not yet: model is printed, rationale is not)*
|
|
224
|
+
4. Start or resume the background download. **Shipped** — auto-download on first run; resumable.
|
|
225
|
+
5. Start the local server when ready. **Shipped.**
|
|
226
|
+
6. Open or serve the built-in chat UI. **Shipped** — browser tab opened automatically; URL printed either way; `--no-open` skips the tab.
|
|
227
|
+
7. Show connection details for OpenAI/Anthropic-compatible clients. **Shipped** — printed at startup.
|
|
228
|
+
8. Offer harness setup actions for Pi, Hermes, OpenClaw, and others. *Partially shipped: `mlx-bun harness pi` configures an existing Pi install; Hermes and OpenClaw adapters do not exist.*
|
|
164
229
|
|
|
165
230
|
This should feel like an appliance, not a framework tutorial.
|
|
166
231
|
|
|
232
|
+
## Adapter catalog vision
|
|
233
|
+
|
|
234
|
+
The average user should not have to train adapters to benefit from them. The
|
|
235
|
+
product path can ship or offer a curated catalog of lightweight LoRA adapters:
|
|
236
|
+
assistant, coding, writing, research, planning, personal-ops, and other
|
|
237
|
+
well-scoped modes. A user can install one small adapter and get a better local
|
|
238
|
+
assistant without changing the base model.
|
|
239
|
+
|
|
240
|
+
Advanced users can still train their own adapters with mlx-bun. The broader
|
|
241
|
+
product loop is:
|
|
242
|
+
|
|
243
|
+
```text
|
|
244
|
+
curate conversations and task data
|
|
245
|
+
train / ORPO task adapters
|
|
246
|
+
publish small adapter artifacts
|
|
247
|
+
route requests through the right adapter per task
|
|
248
|
+
keep the base model resident
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
This makes ORPO performance directly product-relevant. If training is cheap and
|
|
252
|
+
repeatable enough, the project can keep improving small local models through
|
|
253
|
+
adapter updates instead of asking every user to download a larger base model.
|
|
254
|
+
|
|
167
255
|
## Built-in UI
|
|
168
256
|
|
|
169
257
|
Bun can serve the UI directly from the same executable. The default UI
|
|
@@ -213,6 +301,27 @@ The UI should not hide that this is local. It should make "local" feel
|
|
|
213
301
|
easy: predictable memory, transparent downloads, offline-capable serving,
|
|
214
302
|
and clear integration instructions.
|
|
215
303
|
|
|
304
|
+
### SigLIP vision sidecar — image input for gemma e4b (shipped, 2026-06-17)
|
|
305
|
+
|
|
306
|
+
The Gemma 4 e4b vision encoder (`src/vision/siglip.ts`) is a faithful port
|
|
307
|
+
of the optiq SigLIP tower. It auto-loads on first image input and falls back
|
|
308
|
+
to text-only mode when no image is present, so the chat UI gains image support
|
|
309
|
+
with no startup cost. Features are within ~1% rel-RMSE of the optiq oracle;
|
|
310
|
+
every MLX primitive (rms_norm, gelu, matmul, SDPA, RoPE, pooling) is bit-exact.
|
|
311
|
+
The residual is sub-bf16 floating-point accumulation across the deep encoder,
|
|
312
|
+
not a porting gap. L1/L2/L3 paths all work; vision falls back to the monolith
|
|
313
|
+
path on L1 and L2.
|
|
314
|
+
|
|
315
|
+
### Adapter web selector and CLI /v1/adapters (shipped)
|
|
316
|
+
|
|
317
|
+
Trained LoRA adapters can be selected from the chat UI via a per-conversation
|
|
318
|
+
picker (`#chat-adapter` in `src/web/app.html`). The picker lists adapters
|
|
319
|
+
discovered by `GET /v1/adapters/available`, mounts the chosen one via
|
|
320
|
+
`POST /v1/adapters`, and sends a `set_adapter` message to Pi-web so that
|
|
321
|
+
every subsequent turn is routed through the right adapter — including the
|
|
322
|
+
`before_provider_request` hook in `src/pi-web.ts`. Adapters are base-model-scoped
|
|
323
|
+
and dropped on model swap.
|
|
324
|
+
|
|
216
325
|
### The UI must never lag while the AI is busy (candidate, 2026-06-16)
|
|
217
326
|
|
|
218
327
|
A product guarantee: **the AI may crash, the UI never may** — and the UI
|
|
@@ -413,14 +522,25 @@ reuse across pipeline stages.
|
|
|
413
522
|
|
|
414
523
|
## Open questions
|
|
415
524
|
|
|
416
|
-
- What exact profile table should ship first?
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
- What is the minimum first-run UI that feels good enough?
|
|
421
|
-
- What is the right install path: Homebrew, curl installer, GitHub
|
|
422
|
-
release, or all of the above?
|
|
525
|
+
- What exact profile table should ship first? *(The device-profile lookup
|
|
526
|
+
table is unimplemented — `recommendedRepoId()` returns `DEFAULT_REPO_ID`
|
|
527
|
+
for every SKU. The profile shape in this doc is the spec; the research
|
|
528
|
+
work and the fill remain open.)*
|
|
423
529
|
- How should background downloads interact with server startup and UI
|
|
424
530
|
readiness?
|
|
425
531
|
- How should profile recommendations be versioned as benchmarks improve?
|
|
426
532
|
- When Qwen lands, what evals decide whether it replaces a Gemma default?
|
|
533
|
+
|
|
534
|
+
### Resolved
|
|
535
|
+
|
|
536
|
+
- **Should no-args open the UI automatically, print a local URL, or both?**
|
|
537
|
+
Both: the browser tab is opened automatically on start; the URL is always
|
|
538
|
+
printed; `--no-open` suppresses the tab without hiding the URL.
|
|
539
|
+
- **Which UI stack should be embedded? / How much of Pi should be bundled?**
|
|
540
|
+
Resolved by harness-pi + pi-web shipping: the web UI is the bundled
|
|
541
|
+
`src/web/app.html` chat page talking to Pi via `pi-web.ts`; Pi is
|
|
542
|
+
configured externally via `mlx-bun harness pi`, or launched embedded via
|
|
543
|
+
`mlx-bun pi`.
|
|
544
|
+
- **What is the right install path?** All of the above — shipped: npm
|
|
545
|
+
publish (v0.0.4, `bunx mlx-bun`), Homebrew release pipeline (sign,
|
|
546
|
+
notarize, `joshuarossi/homebrew-tap`), and GitHub release binary.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Chunk-task quality roadmap (2026-06-17)
|
|
2
2
|
|
|
3
3
|
Where the MiniCPM5 chunking adapter stands and what raises real segmentation
|
|
4
|
-
quality. Companion to
|
|
4
|
+
quality. Companion to the chunk-eval metric vs. accuracy finding (score = well-formedness, not gold accuracy) and
|
|
5
5
|
[`docs/investigations/chunk-finetune-experiment.md`](../investigations/chunk-finetune-experiment.md).
|
|
6
6
|
|
|
7
7
|
## Where we are
|
package/docs/planning/journal.md
CHANGED
|
@@ -153,3 +153,164 @@ the what/when (phases, exit criteria); this is the narrative behind it.
|
|
|
153
153
|
3. **Tool param values were JSON-coerced regardless of schema.** `<param name="path">2025</param>` became the number 2025. Oracle (`_decode_tool_parameter_value` + `_tool_parameter_schema`) keeps string-typed params as raw text. Ported; also made the `<param>` regex CDATA-safe (a `</param>` inside CDATA no longer truncates the value). Unit tests added.
|
|
154
154
|
4. **Sampling ignored the model's `generation_config.json`.** The server hardcoded temp 0.7 / no top-p; optiq serve injects the author-recommended sampling (`optiq/runtime/gen_config.py`) as defaults. Ported: `loadContext` reads `generation_config.json` (MiniCPM5: temp 0.9, top_p 0.95; Gemma: temp 1.0, top_k 64, top_p 0.95) and `toOptions` uses it when the request omits the field. Explicit request values still win. This was the likely "quality" gap on a 1B model.
|
|
155
155
|
- Verification: MiniCPM5 parity (bf16 + mixed-KV) still 100/100 bit-exact; minicpm5-tools, tool-call, chat-template, tools-template, parity (Gemma 12B), server-tools, and server suites all green. Live two-turn agent round-trip (tool call → tool result → final answer) confirmed working in-process.
|
|
156
|
+
|
|
157
|
+
## 2026-06-16 — Segmented-backward LoRA training (PRs #9–10)
|
|
158
|
+
|
|
159
|
+
Long-context LoRA training was OOMing: MiniCPM5 @4096 spiked to ~25.5 GB full
|
|
160
|
+
backward because all activations had to stay live simultaneously. The
|
|
161
|
+
segmented-backward design threads the backward segment-by-segment so only one
|
|
162
|
+
segment's activations are resident at a time.
|
|
163
|
+
|
|
164
|
+
The key implementation decision was to drive the per-segment backward with
|
|
165
|
+
`mlx_vjp` (a new `Vjp` binding in `autograd.ts`) rather than a surrogate
|
|
166
|
+
`value_and_grad` over a reconstructed loss. The surrogate path leaked roughly
|
|
167
|
+
one activation buffer per segment per step because mlx `eval` does not detach;
|
|
168
|
+
`mlx_vjp` is explicit about what it differentiates and stops the leak.
|
|
169
|
+
Segment-boundary tensors are materialized as graph-free leaves via
|
|
170
|
+
`fromBytesCopy` for the same reason.
|
|
171
|
+
|
|
172
|
+
Phase A (MiniCPM5, PR #9) landed first: grads bit-exact vs the full backward
|
|
173
|
+
(relNorm 0.000%), peak 10.91 → 3.29 GB @2048, no leak, trains end-to-end via
|
|
174
|
+
`TrainConfig.segmentSize`. The default training attention is `ops.sdpa`
|
|
175
|
+
(correct gradients, one fused kernel); `MLX_BUN_TRAIN_ATTN=flash` is an
|
|
176
|
+
opt-in for bit-exact grad comparison against the full backward only, because
|
|
177
|
+
`ops.sdpa` has a pre-existing eager-vs-autograd asymmetry in bf16 (~6%) that
|
|
178
|
+
is unrelated to the segmentation mechanism — the segmented path works
|
|
179
|
+
correctly under either setting.
|
|
180
|
+
|
|
181
|
+
Phase B (e4b / Gemma4, PR #10) was harder: e4b's KV-shared donor pattern
|
|
182
|
+
means multiple sharer segments reference the same K/V tensors produced by a
|
|
183
|
+
single producer segment, requiring cotangent accumulation across sharers before
|
|
184
|
+
the producer vjp can run. Implemented `SegmentedBackwardGemma4` with a
|
|
185
|
+
two-boundary stream (per-layer-input + donor K/V) and a multi-output producer
|
|
186
|
+
vjp `[h, donorK, donorV]` with accumulated `[dh, dKV.k, dKV.v]` cotangents.
|
|
187
|
+
Grads are bit-exact for single-consumer donor reuse and 0.97% bf16-class for
|
|
188
|
+
multi-consumer accumulation (confirmed the full backward also sums in bf16, so
|
|
189
|
+
this is order-of-operations noise, not a logic bug). e4b trains at 8 K context
|
|
190
|
+
(~17.5 GB, segSize=2) where the full backward would OOM (~70 GB).
|
|
191
|
+
|
|
192
|
+
A contiguity fix was also required: donor K/V are transposed views, so
|
|
193
|
+
`detachLeaf` must row-major-copy them before `rawBytes` reads linearly.
|
|
194
|
+
|
|
195
|
+
## 2026-06-16 — Qwen3.5 / MiniCPM5 reasoning-channel fix (PR #12)
|
|
196
|
+
|
|
197
|
+
Two serving-layer bugs left reasoning-capable models unusable in chat:
|
|
198
|
+
|
|
199
|
+
The first was a role-name mismatch. pi-ai renames the system role to
|
|
200
|
+
`developer` for reasoning models. Our chat templates only accepted
|
|
201
|
+
system/user/assistant/tool and returned 400. The turn completed with
|
|
202
|
+
`stopReason: "error"` without throwing, so the browser saw an empty turn with no
|
|
203
|
+
error shown. Fix: `normalizeMessages` maps developer→system (genuine OpenAI
|
|
204
|
+
compat — OpenAI itself uses the developer role), and `mapEventToFrames` surfaces
|
|
205
|
+
a `stopReason:"error"` turn as a visible error frame instead of silently
|
|
206
|
+
discarding it.
|
|
207
|
+
|
|
208
|
+
The second was reasoning leaking into content. Qwen3.5 and MiniCPM5 templates
|
|
209
|
+
prime an *open* `<think>` tag in the generation prompt, so the model's first
|
|
210
|
+
token is the closing `</think>` — the opening tag never appears in the decoded
|
|
211
|
+
stream. `ThinkingTagSplitter` was waiting for the open tag before routing
|
|
212
|
+
reasoning content into the OpenAI `reasoning_content` channel, so all of it
|
|
213
|
+
leaked into `content` instead. Fix: `promptEndsInOpenThink` detects this
|
|
214
|
+
condition and seeds `ThinkingTagSplitter` via `startInThinking` so the splitter
|
|
215
|
+
begins in the reasoning state.
|
|
216
|
+
|
|
217
|
+
## 2026-06-16 — Adapters end-to-end (PR #13)
|
|
218
|
+
|
|
219
|
+
The goal was to make LoRA adapter switching a first-class operation in both the
|
|
220
|
+
web chat UI and the Pi CLI, without building a custom protocol — lean on Pi's
|
|
221
|
+
`before_provider_request` hook instead.
|
|
222
|
+
|
|
223
|
+
Server-side: `GET /v1/adapters/available` scans the two adapter stores
|
|
224
|
+
(`~/.cache/mlx-bun-finetunes`, `~/.cache/mlx-bun/adapters`) and returns
|
|
225
|
+
`{id, path, rank, scale, mounted}` for weight-bearing checkpoints (dataset-only
|
|
226
|
+
dirs are skipped). `lora.ts` gained `listAvailableAdapters` for the scan and
|
|
227
|
+
base-model detection so the web selector can filter to compatible adapters only.
|
|
228
|
+
|
|
229
|
+
Pi hook: `pi-web.ts` registers `pi.on("before_provider_request")` to inject the
|
|
230
|
+
per-connection selected adapter into every outgoing provider payload using the
|
|
231
|
+
existing inline-extension pattern. A `set_adapter` WebSocket message sets the
|
|
232
|
+
selection; default none leaves the payload unchanged so the base model runs.
|
|
233
|
+
|
|
234
|
+
Web chat: an adapter dropdown was added next to the Thinking pill, populated
|
|
235
|
+
from `/v1/adapters/available` on ready. Selecting fires an idempotent `POST
|
|
236
|
+
/v1/adapters` then a `set_adapter` WS message.
|
|
237
|
+
|
|
238
|
+
CLI: `extensions/mlx-bun-adapter.ts` is a shipped Pi extension exposing
|
|
239
|
+
`/adapter <id> | off | list` and the same hook. Both UIs control adapters
|
|
240
|
+
through Pi; no separate plumbing.
|
|
241
|
+
|
|
242
|
+
## 2026-06-17 — First-run onboarding UX
|
|
243
|
+
|
|
244
|
+
The previous startup story required knowing the right model ID, running a
|
|
245
|
+
separate download command, and manually opening a browser. The new path:
|
|
246
|
+
`mlx-bun serve` (or bare `mlx-bun`) auto-downloads the starter if absent,
|
|
247
|
+
starts the server, and opens the chat UI. If a localhost tab is already open it
|
|
248
|
+
focuses that tab rather than opening a duplicate (`--no-open` to skip).
|
|
249
|
+
|
|
250
|
+
The web chat empty state was updated with mlx-bun-centric starter chips and a
|
|
251
|
+
download-aware greeting. A live background-download pill shows progress. The
|
|
252
|
+
system prompt briefs the model on mlx-bun and its own identity, with framing
|
|
253
|
+
appropriate to MiniCPM5 being a fast starter while a larger model optionally
|
|
254
|
+
loads in the background.
|
|
255
|
+
|
|
256
|
+
`chat-template.ts` also tightened `supportsThinking` to require the `<think>`
|
|
257
|
+
channel we actually split, so e4b / harmony-format models no longer leak raw
|
|
258
|
+
reasoning markers into content for users who haven't configured a thinkingFormat.
|
|
259
|
+
|
|
260
|
+
The runtime-isolation design doc (`docs/design/runtime-isolation.md`) was also
|
|
261
|
+
written this session: inference running in-process on the Bun event loop is a
|
|
262
|
+
known UI-lag footgun when the GPU is busy. The doc proposes moving inference to
|
|
263
|
+
a subprocess; the work is explicitly deferred (design, not started) — written
|
|
264
|
+
now so the root-cause analysis is available cold.
|
|
265
|
+
|
|
266
|
+
## 2026-06-17 — SigLIP vision sidecar for gemma-4 e4b
|
|
267
|
+
|
|
268
|
+
Ported optiq's `vlm/gemma4` SigLIP encoder as `src/vision/siglip.ts`: a 16-layer
|
|
269
|
+
vision transformer with clippable linears (trained clip bounds), manual-f32
|
|
270
|
+
RMS norms on q/k/v, on-device 2D RoPE, fused SDPA at scale=1.0, and GeGLU →
|
|
271
|
+
3×3 avg-pool → `MultimodalEmbedder` → `/embed_scale`.
|
|
272
|
+
|
|
273
|
+
The tower selection is driven by `vision_config.model_type`: e2b/e4b get
|
|
274
|
+
SigLIP, the 12B gets the existing encoder-free tower. Lazy loading means
|
|
275
|
+
text-only sessions never pay for it. `gemma4.ts`'s `forwardEmbeddings` was
|
|
276
|
+
extended to support per-layer-input models (e2b/e4b) — it previously threw for
|
|
277
|
+
that path — threading zeroed image-token ids into the per-layer forward matching
|
|
278
|
+
optiq's `where(text_mask, ids, 0)`.
|
|
279
|
+
|
|
280
|
+
Parity: every mlx primitive is bit-exact vs the optiq oracle (verified
|
|
281
|
+
model-free via `op-parity-{dump.py,check.ts}` — rms_norm, gelu, matmul, clip,
|
|
282
|
+
cos, sin, full multidim RoPE, sdpa, pool). The residual ~1% feature divergence
|
|
283
|
+
is sub-bf16 full-graph composition order amplified by the encoder's scale=1.0
|
|
284
|
+
peaked softmax (the topped-out softmax distribution is highly sensitive to small
|
|
285
|
+
pre-softmax shifts in bf16). The acceptance bar was exact spliced ids + greedy
|
|
286
|
+
prefix + grounded output, which passes. Driving the residual to zero would
|
|
287
|
+
require matching optiq's exact op ordering across the full encoder; deferred as
|
|
288
|
+
a TODO.
|
|
289
|
+
|
|
290
|
+
## 2026-06-17 — Distribution: npm, bunx, Homebrew signed/notarized, direct download
|
|
291
|
+
|
|
292
|
+
The question was how to ship a standalone binary that runs on a user's Mac
|
|
293
|
+
without requiring Bun, Python, or anything else. We settled on four parallel
|
|
294
|
+
install paths rather than picking one.
|
|
295
|
+
|
|
296
|
+
**npm / bunx**: `bunx mlx-bun` for users already in the Bun ecosystem. A
|
|
297
|
+
`publish-release.sh` script and `bun run publish` alias handle the npm side.
|
|
298
|
+
Published as v0.0.4.
|
|
299
|
+
|
|
300
|
+
**Homebrew**: `scripts/release-binary.sh` builds a standalone binary, signs it
|
|
301
|
+
with a Developer ID (nested Mach-O first, then the executable with JIT
|
|
302
|
+
entitlements from `packaging/entitlements.plist`), notarizes via `AC_PROFILE`, and
|
|
303
|
+
produces a clean tarball + sha256 for the formula.
|
|
304
|
+
`packaging/homebrew/mlx-bun.rb` is the formula source of truth (self-contained
|
|
305
|
+
`libexec` bundle + `bin` symlink; Bun `realpath`s `execPath` so sibling dylibs
|
|
306
|
+
resolve after relocation). The tap lives at `joshuarossi/homebrew-tap`.
|
|
307
|
+
`scripts/publish-release.sh` auto-syncs the formula sha256 on each release.
|
|
308
|
+
One fix was required: `--disable-library-validation` in the entitlements so
|
|
309
|
+
brew-relocated dylibs load without `CS_RESTRICT` blocking them.
|
|
310
|
+
|
|
311
|
+
**Direct download**: the notarized tarball passes Gatekeeper even when
|
|
312
|
+
downloaded via browser + Finder (quarantine worst case), so a stable versionless
|
|
313
|
+
URL (`releases/latest/download/mlx-bun-arm64.tar.gz`) is published alongside the
|
|
314
|
+
versioned asset and documented as a one-liner curl install.
|
|
315
|
+
|
|
316
|
+
**Embedded library**: existing path, unchanged (`docs/reference/embedding.md`).
|
|
@@ -54,7 +54,7 @@ and notarized as one artifact.
|
|
|
54
54
|
This runs [build-binary.sh](../../scripts/build-binary.sh), then:
|
|
55
55
|
- signs every nested Mach-O (dylibs, any `.node`) with the hardened
|
|
56
56
|
runtime, then the executable with the JIT
|
|
57
|
-
[entitlements](../../
|
|
57
|
+
[entitlements](../../packaging/entitlements.plist) (JavaScriptCore JITs —
|
|
58
58
|
without `allow-jit` the binary is killed on launch);
|
|
59
59
|
- notarizes the zipped bundle via `AC_PROFILE` and waits;
|
|
60
60
|
- emits `dist-release/mlx-bun-v<ver>-arm64.tar.gz` (+ `.sha256`) and
|
|
@@ -75,19 +75,20 @@ gh release create v0.0.4 dist-release/mlx-bun-v0.0.4-arm64.tar.gz \
|
|
|
75
75
|
|
|
76
76
|
## The tap
|
|
77
77
|
|
|
78
|
-
Homebrew taps must be a repo named `homebrew-<name
|
|
78
|
+
Homebrew taps must be a repo named `homebrew-<name>`. The canonical tap is
|
|
79
|
+
**`joshuarossi/homebrew-tap`** (Homebrew shorthand: `joshuarossi/tap`).
|
|
79
80
|
|
|
80
|
-
1. Create a public repo **`joshuarossi/homebrew-
|
|
81
|
+
1. Create a public repo **`joshuarossi/homebrew-tap`** (one-time).
|
|
81
82
|
2. Copy [packaging/homebrew/mlx-bun.rb](../../packaging/homebrew/mlx-bun.rb)
|
|
82
83
|
to `Formula/mlx-bun.rb` in it, updating `version`/`url`/`sha256` from
|
|
83
84
|
the release-script output. Commit and push.
|
|
84
85
|
3. Install:
|
|
85
86
|
```sh
|
|
86
|
-
brew install joshuarossi/
|
|
87
|
+
brew install joshuarossi/tap/mlx-bun
|
|
87
88
|
mlx-bun --version
|
|
88
89
|
```
|
|
89
90
|
|
|
90
|
-
`brew audit --strict --new joshuarossi/
|
|
91
|
+
`brew audit --strict --new joshuarossi/tap/mlx-bun` before pushing
|
|
91
92
|
catches most formula issues; `brew install --build-from-source` +
|
|
92
93
|
`brew test` runs the formula's `test do` locally.
|
|
93
94
|
|
|
@@ -119,3 +120,58 @@ Users then upgrade with:
|
|
|
119
120
|
```sh
|
|
120
121
|
brew upgrade joshuarossi/tap/mlx-bun
|
|
121
122
|
```
|
|
123
|
+
|
|
124
|
+
## Direct download (stable URL)
|
|
125
|
+
|
|
126
|
+
`publish-release.sh` uploads two assets per release: the versioned tarball
|
|
127
|
+
(`mlx-bun-v<ver>-arm64.tar.gz`) **and** a versionless stable copy
|
|
128
|
+
(`mlx-bun-arm64.tar.gz`) pinned to `releases/latest`. This lets the
|
|
129
|
+
install script target a URL that never changes:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
https://github.com/joshuarossi/mlx-bun/releases/latest/download/mlx-bun-arm64.tar.gz
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The user-facing install path is the script at
|
|
136
|
+
[`website/public/install.sh`](../../website/public/install.sh) (served at
|
|
137
|
+
`mlx-bun.dev/install.sh`), which resolves that URL, extracts the bundle, and
|
|
138
|
+
links `mlx-bun` onto `PATH`:
|
|
139
|
+
```sh
|
|
140
|
+
curl -fsSL https://mlx-bun.dev/install.sh | sh
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Without the script, the same stable URL streams straight to `tar`:
|
|
144
|
+
```sh
|
|
145
|
+
curl -fsSL https://github.com/joshuarossi/mlx-bun/releases/latest/download/mlx-bun-arm64.tar.gz \
|
|
146
|
+
| tar -xz -C /usr/local/bin mlx-bun
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Note: **browser downloads and `curl`-to-file** (e.g. `curl -O ...`) are
|
|
150
|
+
quarantined by Gatekeeper — macOS sets the `com.apple.quarantine` xattr on
|
|
151
|
+
the saved file. The piped one-liner above streams directly to `tar` and
|
|
152
|
+
never writes a quarantined file, so it runs without any Gatekeeper prompt.
|
|
153
|
+
For the browser/curl-to-file path, the notarization ticket resolves the
|
|
154
|
+
Gatekeeper check online; `xattr -d com.apple.quarantine mlx-bun` is the
|
|
155
|
+
manual escape hatch if offline.
|
|
156
|
+
|
|
157
|
+
## npm / bunx
|
|
158
|
+
|
|
159
|
+
mlx-bun is published to npm as `mlx-bun` (current: 0.0.4). Install or
|
|
160
|
+
run without a permanent install:
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
# one-off (no install)
|
|
164
|
+
bunx mlx-bun
|
|
165
|
+
|
|
166
|
+
# permanent global install
|
|
167
|
+
bun install -g mlx-bun
|
|
168
|
+
mlx-bun --version
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The npm package wraps the same signed binary via a postinstall download
|
|
172
|
+
step (it does not bundle the ~228 MB dylibs/metallib in the npm tarball
|
|
173
|
+
itself). Publish to npm as part of a release:
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
bun publish # from repo root; package.json version must already be bumped
|
|
177
|
+
```
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
The HTTP server is one consumer of a library-first API. Everything it
|
|
4
4
|
does — generation, sampling, KV schemes, LoRA, vision, caching, memory
|
|
5
|
-
admission — is importable directly into a Bun process.
|
|
6
|
-
|
|
5
|
+
admission — is importable directly into a Bun process. Published as
|
|
6
|
+
`mlx-bun` on npm (0.0.4); `bunx mlx-bun` works out of the box, or
|
|
7
|
+
`import … from "mlx-bun"` for programmatic use.
|
|
7
8
|
|
|
8
9
|
One rule everywhere: **one generation at a time per process** (one GPU,
|
|
9
10
|
batch=1). The server serializes through a queue; do the same.
|
|
@@ -11,17 +12,24 @@ batch=1). The server serializes through a queue; do the same.
|
|
|
11
12
|
## Quick start
|
|
12
13
|
|
|
13
14
|
```ts
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
import {
|
|
16
|
+
loadModelConfig,
|
|
17
|
+
Weights,
|
|
18
|
+
createModel, // dispatches to Gemma4Model / MiniCPM5Model / Qwen35Model
|
|
19
|
+
loadTokenizer,
|
|
20
|
+
ChatTemplate,
|
|
21
|
+
generate,
|
|
22
|
+
} from "mlx-bun"; // or "./src/index" in-repo
|
|
20
23
|
|
|
21
24
|
const dir = "/path/to/hf-snapshot"; // mlx-bun ls prints these
|
|
22
|
-
const
|
|
25
|
+
const config = await loadModelConfig(dir);
|
|
26
|
+
const model = createModel(await Weights.open(dir), config); // returns RuntimeModel
|
|
23
27
|
const tok = await loadTokenizer(dir);
|
|
24
28
|
const template = await ChatTemplate.load(dir);
|
|
29
|
+
// Gemma4Model and MiniCPM5Model can still be imported directly from "mlx-bun"
|
|
30
|
+
// if you need the concrete type. Qwen35Model is NOT exported from the public
|
|
31
|
+
// package — use createModel/RuntimeModel (preferred dispatch), or import from
|
|
32
|
+
// "./src/model/qwen35" in-repo.
|
|
25
33
|
|
|
26
34
|
const ids = tok.encode(template.render([{ role: "user", content: "hi" }]));
|
|
27
35
|
const promptIds = ids[0] === ids[1] && ids[0] === tok.bosTokenId ? ids.slice(1) : ids;
|
|
@@ -67,7 +75,7 @@ prompt time), the decode clock from there.
|
|
|
67
75
|
## Serving pieces
|
|
68
76
|
|
|
69
77
|
```ts
|
|
70
|
-
import { loadContext, createServer } from "
|
|
78
|
+
import { loadContext, createServer } from "mlx-bun";
|
|
71
79
|
|
|
72
80
|
const ctx = await loadContext(dir, "my-model", { memoryBudgetBytes: 12e9 });
|
|
73
81
|
const server = createServer(ctx, 8090, {
|
|
@@ -84,6 +92,12 @@ completions, Anthropic `/v1/messages`, OpenAI Responses, adapters, and
|
|
|
84
92
|
|
|
85
93
|
## Prompt cache (in-process prefix reuse)
|
|
86
94
|
|
|
95
|
+
> **Note:** `PromptCache`, `AdapterManager`, `saveKvCache`, and
|
|
96
|
+
> `loadKvCache` are **not** re-exported from the public `"mlx-bun"`
|
|
97
|
+
> entry point (`src/index.ts`). Import them from their internal paths
|
|
98
|
+
> as shown below; this is intentional — they are implementation-level
|
|
99
|
+
> pieces rather than stable public API surface.
|
|
100
|
+
|
|
87
101
|
```ts
|
|
88
102
|
import { PromptCache } from "./src/prompt-cache";
|
|
89
103
|
const cache = new PromptCache(2e9); // BYTE-capped LRU
|
|
@@ -102,9 +116,11 @@ under one adapter must not seed another's prefill).
|
|
|
102
116
|
```ts
|
|
103
117
|
import { saveKvCache, loadKvCache } from "./src/kv-store";
|
|
104
118
|
saveKvCache("/tmp/prefix.kv", tokens, caches); // page-aligned file
|
|
105
|
-
const { tokens, caches, mmap } = loadKvCache("/tmp/prefix.kv", model);
|
|
119
|
+
const { tokens, caches, mmap } = loadKvCache("/tmp/prefix.kv", model as Gemma4Model);
|
|
106
120
|
// reload is a zero-copy MAP_PRIVATE mmap straight to the GPU (~1 ms);
|
|
107
121
|
// keep `mmap` referenced as long as the caches live
|
|
122
|
+
// Note: loadKvCache is typed to accept Gemma4Model, but only uses model.layers.length
|
|
123
|
+
// at runtime — a cast from RuntimeModel is safe for any model that has a layers array.
|
|
108
124
|
```
|
|
109
125
|
|
|
110
126
|
Quantized caches are not persistable yet (documented gap).
|
|
@@ -112,8 +128,7 @@ Quantized caches are not persistable yet (documented gap).
|
|
|
112
128
|
## Registry + fit (model discovery and memory math)
|
|
113
129
|
|
|
114
130
|
```ts
|
|
115
|
-
import { Registry } from "
|
|
116
|
-
import { fit, skuMatrix } from "./src/fit";
|
|
131
|
+
import { Registry, fit, skuMatrix } from "mlx-bun";
|
|
117
132
|
|
|
118
133
|
const reg = new Registry(); // bun:sqlite, scans the HF cache
|
|
119
134
|
await reg.scan();
|
|
@@ -150,4 +165,4 @@ any amount of switching.
|
|
|
150
165
|
handler → process death). Use `fit`/admission before generating, not
|
|
151
166
|
try/catch around it.
|
|
152
167
|
- Never read a typed array that native code wrote in a hot path; use
|
|
153
|
-
the `read.*` helpers (bun#32054 — see PLAN.md Phase 4 findings).
|
|
168
|
+
the `read.*` helpers (bun#32054 — see PLAN-archive.md Phase 4 findings).
|