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
@@ -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. People own a small
89
- set of real Apple Silicon SKUs: 16 GB, 24 GB, 32 GB, 36 GB, 48 GB,
90
- 64 GB, and larger workstation tiers. For each profile, we can research
91
- and preselect the best known model artifact, context target, KV mode,
92
- memory budget, and runtime flags.
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 a
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
- look up recommended runtime profile
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 research work feeds the table. The table feeds the user.
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
- - Should no-args open the UI automatically, print a local URL, or both?
418
- - Which UI stack should be embedded?
419
- - How much of Pi should be bundled, embedded, or configured externally?
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 [[chunk-eval-metric-vs-accuracy]] and
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
@@ -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](../../scripts/entitlements.plist) (JavaScriptCore JITs —
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-mlx-bun`**.
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/mlx-bun/mlx-bun
87
+ brew install joshuarossi/tap/mlx-bun
87
88
  mlx-bun --version
88
89
  ```
89
90
 
90
- `brew audit --strict --new joshuarossi/mlx-bun/mlx-bun` before pushing
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. Not yet
6
- published to npm; import from `src/`.
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 { loadModelConfig } from "./src/config";
15
- import { Weights } from "./src/weights";
16
- import { Gemma4Model } from "./src/model/gemma4";
17
- import { loadTokenizer } from "./src/tokenizer";
18
- import { ChatTemplate } from "./src/chat-template";
19
- import { generate } from "./src/generate";
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 model = new Gemma4Model(await Weights.open(dir), await loadModelConfig(dir));
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 "./src/server";
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 "./src/registry";
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).