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
@@ -1,13 +1,17 @@
1
1
  # Parallel slots — configurable batched serving
2
2
 
3
- Status: **design + phasing** (S0 landed; S1+ not started)
3
+ Status: **S0 + S1b SHIPPED** (S2 not started; S3+ deferred)
4
4
  Owner: serving layer
5
- Default: **off** (`--slots 1` = today's serialized path, untouched)
5
+ Default: **off** (`--batch 1` = today's serialized path, untouched)
6
+
7
+ > **Flag rename note:** This document was drafted with `--slots N` as the design name.
8
+ > The shipped flag is `--batch N` (`--decode-concurrency` accepted as an mlx_lm.server alias).
9
+ > Occurrences of `--slots` in pre-ship text below refer to today's `--batch N`.
6
10
 
7
11
  ## Goal
8
12
 
9
13
  Let the server process more than one generation request concurrently,
10
- gated entirely behind a `--slots N` flag. `N=1` runs exactly what we
14
+ gated entirely behind a `--batch N` flag (originally designed as `--slots N`). `N=1` runs exactly what we
11
15
  have today (the serialized promise chain). `N>1` loads a batch
12
16
  scheduler that runs multiple sequences through one forward pass.
13
17
 
@@ -78,7 +82,7 @@ concurrent ≈ arrival_rate × mean_generation_time).
78
82
 
79
83
  When it does **not** help: traffic so light that requests never overlap
80
84
  in time. Then there's only ever one live sequence and batching is idle
81
- — which is exactly why `--slots 1` is and stays the default.
85
+ — which is exactly why `--batch 1` is and stays the default.
82
86
 
83
87
  ## Current code: what's batch=1 and what isn't
84
88
 
@@ -98,7 +102,7 @@ Good news: the hot path is already written over a batch dim `B`.
98
102
  | `src/generate.ts:295,315,378` | prompt/decode tensors built `[1, L]` / `[1, 1]` |
99
103
  | `src/generate.ts:320,235` | `hLast` slice `[1, …]`; `sampleStep` reshape `[1, V]` |
100
104
  | `src/generate.ts:247` | sampler history `[1]`, one sampled token/step |
101
- | `src/model/gemma4.ts:647-653` | per-layer-input reshapes `[1, L, …]` |
105
+ | `src/model/gemma4.ts:647-653` | per-layer-input reshapes `[1, L, …]` *(pre-S1b inventory — this entry was fixed in S1b; e4b per-layer-input is now B-generic)* |
102
106
  | `src/model/gemma4-base.ts:210` | **one scalar `offset` per cache, shared across rows** |
103
107
  | `src/model/gemma4.ts:174,206` | **scalar RoPE offset** for the whole batch |
104
108
  | `src/model/gemma4-base.ts:256` | `makeMask` from that single offset (returns empty mask at N=1) |
@@ -237,7 +241,7 @@ Degradation levers: `--perf-kernel off` / `--fused-* off` drops L3→L2; bf16 KV
237
241
  drops L2→L1. Each layer must stand alone and stay reachable (don't-delete-
238
242
  optionality as a fallback ladder).
239
243
 
240
- So a feature like batched decode (`--slots`) is "done" only when green at
244
+ So a feature like batched decode (`--batch`) is "done" only when green at
241
245
  **every (model × layer)** cell — and each cell may need its own fix:
242
246
 
243
247
  | Model · path | L1 (bf16 / mlx-lm) | L2 (quant / optiq) | L3 (perf / KL) |
@@ -323,7 +327,7 @@ Batched-decode landmines found by the modes analysis (2026-06-14):
323
327
  required), so quant batched decode falls to `quantizedSdpaUnfused` — correct
324
328
  but the perf kernel is bypassed (perf debt; a batched-aware fused decode kernel
325
329
  is a later item). `quantizedSdpaUnfused` must be validated with a 4-D bool mask.
326
- - **All cells**: compiled-decode forced off under `--slots>1`; LoRA batches only
330
+ - **All cells**: compiled-decode forced off under `--batch>1`; LoRA batches only
327
331
  within one adapter group (else drain).
328
332
 
329
333
  ## KV memory: dynamic allocation, not static partition
@@ -357,11 +361,12 @@ Budget accounting is total-bytes from day one.
357
361
  **Current state (2026-06-14):** the batched *primitive* is **rung 2** —
358
362
  one shared `[B, H, S, D]` buffer per layer (`KVCache`/`RotatingKVCache`),
359
363
  growing in 256-token steps, width = longest live sequence, left-pad
360
- "waste" on short rows. But the **budget/admission half of rung 2 is NOT
361
- built** (no `B × S_max` projection, no KV budget enforcement) — that's
362
- scheduler work (S2), and the scheduler itself isn't built (`--slots`
363
- inert). And **rung 3 (paged) is a deferred spike**. So today: rung-2
364
- *allocation shape*, no *budget control*, not wired into serving.
364
+ "waste" on short rows. Rung-2 allocation IS wired into serving via
365
+ `BatchScheduler` + `GenerationGateway` (S1b SHIPPED). But the
366
+ **budget/admission half of rung 2 is NOT built** (no `B × S_max`
367
+ projection, no KV budget enforcement) — that's scheduler work (S2, NOT
368
+ STARTED). And **rung 3 (paged) is a deferred spike**. So today: rung-2
369
+ *allocation + scheduling wired*, no *budget/admission control* yet.
365
370
 
366
371
  KV quantization is a force multiplier: 4-bit KV (`generate.ts:67`,
367
372
  already supported) is ~4× smaller, so the same budget buys ~4× the slots
@@ -462,34 +467,37 @@ predicate (full-attention model + no vision/adapter/repetition-penalty/user-seed
462
467
 
463
468
  ## Config surface
464
469
 
465
- - `ServerOptions.slots` (default 1); later `kvBudgetBytes`,
470
+ - `ServerOptions.batch` (default 1); later `kvBudgetBytes`,
466
471
  `promptConcurrency`.
467
- - CLI: `--slots N` (S0); later `--kv-budget <GB>`.
468
- - `slots === 1` → today's serialized promise chain, untouched.
469
- `slots > 1` → scheduler.
472
+ - CLI: `--batch N` (shipped; `--slots N` was the original design name, `--decode-concurrency` accepted as mlx_lm.server alias); later `--kv-budget <GB>`.
473
+ - `batch === 1` → today's serialized promise chain, untouched.
474
+ `batch > 1` → scheduler.
470
475
 
471
476
  ## Phasing (incremental, parity-gated)
472
477
 
473
- - **S0 — config seam (DONE).** `--slots N` / `ServerOptions.slots`
478
+ - **S0 — config seam (DONE).** `--slots N` (now shipped as `--batch N`) / `ServerOptions.slots`
474
479
  plumbed, validated, surfaced (ready card + `/stats`). `N>1` warns that
475
480
  batched execution lands in S1 and runs serially. No behavior change.
476
- - **S1 — static 2-wide, base model only.** Split by the reuse finding:
481
+ - **S1 — static 2-wide, base model only. (DONE — S1b LIVE as `--batch N`)** Split by the reuse finding:
477
482
  - **S1a (prefill)** — reuse the training machinery (`buildBatchedPadMask`
478
483
  / `BatchedMaskCache`); wire the serving path to prefill B prompts in one
479
484
  forward. Mostly generalization + consolidating the two mask builders.
480
- - **S1b (decode)** — the new work: a batched KV cache that grows with
481
- per-row offsets, per-row RoPE via the array-offset path, and the per-row
482
- `[B,1,1,S]` decode mask (`buildBatchedDecodeMask`, landed 2026-06-14).
483
- Plus the B-token-per-step decode loop + per-row stream fan-out.
485
+ - **S1b (decode) — SHIPPED 2026-06-14.** `BatchScheduler`
486
+ (`src/serve/batch-scheduler.ts`) + `GenerationGateway`
487
+ (`src/serve/generation-gateway.ts`) wired into `createServer` behind
488
+ `--batch N`. L1 oracle-verified for CPM + all 3 Gemmas (full-attn +
489
+ sliding-window via `BatchedRotatingCache`). Per-row SSE fan-out live.
490
+ See STATUS block above for details.
484
491
  **Teacher-forced gate**: a 2-row batch must produce per-row logits matching
485
492
  two solo runs within bf16 tolerance (the train-batch-e2e methodology, now
486
493
  applied to decode).
487
- - **S2 — N-wide + continuous injection/eviction.** Rows retire on EOS;
488
- queued requests prefill into freed rows. Dynamic byte-budget admission.
489
- - **S3+ — paged KV** (custom paged-attention kernel + block manager),
494
+ - **S2 — N-wide + continuous injection/eviction. (NOT STARTED)** Rows retire on EOS;
495
+ queued requests prefill into freed rows. Dynamic byte-budget admission
496
+ (`B × S_max` projection, KV budget enforcement) still TODO.
497
+ - **S3+ — paged KV (DEFERRED)** (custom paged-attention kernel + block manager),
490
498
  KV-quant under batch, LoRA-group batching.
491
499
 
492
- Every phase ships default-off behind `slots=1`; the serialized path is
500
+ Every phase ships default-off behind `batch=1`; the serialized path is
493
501
  never removed (only bypassed).
494
502
 
495
503
  ## Open questions / to measure
@@ -1,6 +1,6 @@
1
1
  # Runtime isolation — the AI may crash, the UI never may
2
2
 
3
- Status: **design** (not started)
3
+ Status: **design** (not started — deferred future work; no scheduled PLAN.md phase)
4
4
  Owner: serving layer / runtime
5
5
  Default today: inference runs **in-process**, on the server's event-loop
6
6
  thread. This doc proposes moving it to a **subprocess**.
@@ -15,8 +15,10 @@ it holds all 42 layers' recompute activations at once, so e4b's training backwar
15
15
  spikes to **23 GB live @2048** (resting is only 6.95 GB) and **crashes at 4096+**.
16
16
  We proved a fix — **segmented backward** — that forces per-segment streaming:
17
17
  forward saves detached boundary activations at segment edges, then backprop runs
18
- segment-by-segment via a surrogate-loss `value_and_grad`. Only one segment's
19
- activations are ever live. On an activation-dominated toy it beat per-layer
18
+ segment-by-segment via `mlx_vjp` (the shipped production implementation — see §9.4;
19
+ an earlier surrogate-loss `value_and_grad` approach was tried but leaked ~32 MB/seg/step
20
+ and was replaced). Only one segment's activations are ever live.
21
+ On an activation-dominated toy it beat per-layer
20
22
  checkpointing (4.26 vs 5.97 GB) with **bit-exact gradients**. Extrapolated to e4b
21
23
  this lands at **~10 GB @8K** — fitting on the 32 GB M1 Max, where the optiq/mlx-lm
22
24
  reference cannot (it crashes on e4b @2048+). Next: wire it into the trainer,
@@ -97,12 +99,16 @@ Algorithm:
97
99
  small (one [B,L,hidden] per edge) and cheap.
98
100
  2. **Compute the loss** from the final hidden (LM head + response-only CE) and its
99
101
  gradient w.r.t. the final hidden — this `dh_out` seeds the last layer segment.
100
- 3. **Backward, reverse over segments.** For each segment k (last→first), build a
101
- `value_and_grad` over `[boundary_leaf_k, ...segment_LoRA_params]` whose loss is
102
- the surrogate `sum(stop_gradient(dh_out) ⊙ segment_forward(boundary_leaf, params))`
103
- (the optiq chunked-CE surrogate, generalized to the whole backward). It returns
102
+ 3. **Backward, reverse over segments.** For each segment k (last→first), call
103
+ **`mlx_vjp`** (bound as `Vjp` in `src/mlx/autograd.ts`) over
104
+ `[boundary_leaf_k, ...segment_LoRA_params]` with cotangent `dh_out`. It returns
104
105
  `[dh_in, ...dLoRA_segment]`. `dh_in` becomes `dh_out` for segment k−1; accumulate
105
- `dLoRA`. Only segment k's activations are live during its `value_and_grad`.
106
+ `dLoRA`. Only segment k's activations are live during its vjp.
107
+ **NOTE:** an earlier design used a surrogate-loss `value_and_grad` here
108
+ (`sum(stop_grad(dh)⊙segment_forward(...))`). That proved to leak ~32 MB per
109
+ segment per step (linear, no plateau) and was replaced by `mlx_vjp` in §9.4.
110
+ The production code uses `Vjp`/`mlx_vjp` throughout — not surrogate-loss
111
+ `value_and_grad`.
106
112
 
107
113
  Why it's exact: `d/dx sum(stop_grad(dh)⊙y) = (dy/dx)ᵀ dh = dLoss/dx`, and likewise
108
114
  for params — this is reverse-mode AD done in segments. Verified **0.000%** vs the
@@ -140,9 +146,14 @@ quant. The trivial case to validate the machinery on a real model.
140
146
  2. **Trainer:** add a `segmented` training path (flag `TrainConfig.segments` or auto;
141
147
  gate alongside the existing `gradCheckpoint`). It:
142
148
  - runs the segmented forward (save detached boundaries),
143
- - computes response-only CE loss + `dFinalHidden` (already memory-bounded),
144
- - runs the reverse per-segment surrogate `value_and_grad`, threading the LoRA
145
- params for each segment as that `value_and_grad`'s argnums, accumulating grads,
149
+ - computes response-only CE loss and `dFinalHidden` via a `Vjp` of the loss head
150
+ with scalar cotangent 1.0 (already memory-bounded),
151
+ - runs the backward per-segment using **`mlx_vjp`** (`Vjp` in
152
+ `src/mlx/autograd.ts`): for each segment k (last→first), calls
153
+ `vjp.apply([boundary_k, ...LoRA_k], [dh_out])` to get `[dh_in, ...dLoRA_k]`,
154
+ threads `dh_in` as `dh_out` for segment k−1, and accumulates `dLoRA` — no
155
+ surrogate scalar, no `value_and_grad` (see §9.4 for why that approach leaked
156
+ ~32 MB/seg/step and was replaced),
146
157
  - also handles the embedding + (e4b) per-layer-input grads from the first
147
158
  segment's `dh_in`.
148
159
  3. **Validate:** grads bit-match the current trainer (response-only CE, same seed);
@@ -232,17 +243,24 @@ returns the split indices + which donor K/V to save, computed from the above.
232
243
  - `scripts/ft-chunk-smoke.ts` — e4b train/fwd/infer smoke (GRAD_CKPT, MEM_LIMIT_GB, MLX_BUN_MEM_LOG).
233
244
  - `scripts/sdpa-vs-manual.ts`, `scripts/flash-dk-debug.ts`, `scripts/flash-grad-test.ts` — gradient correctness.
234
245
  - `scripts/chunk-{eval,finetune,filter}.ts` — the chunk experiment pipeline.
235
- - `src/train/trainer.ts` — training loop (combined-eval, mem-log, gradCkpt ctx). Add the segmented path here.
246
+ - `src/train/trainer.ts` — training loop (combined-eval, mem-log, gradCkpt ctx); contains the segmented path (`TrainConfig.segmentSize`).
236
247
  - `src/train/forward.ts`, `src/train/loss.ts` — training forward + response-only CE.
237
- - `src/model/gemma4.ts` — e4b model: `forwardLayers`, `runCheckpointedLayer`, `computePerLayerInputs`, KV-sharing (`numDonors`, `previousKvs`, `cacheIndex`), `layer_types`. Add `runLayerRange`.
238
- - `src/model/minicpm5.ts` — MiniCPM5 (Phase A target).
239
- - `src/mlx/autograd.ts` (`ValueAndGrad`), `src/mlx/checkpoint.ts` (`Checkpoint`), `src/mlx/ffi.ts` (memory fns, `stopGradient`, `evalAll`).
248
+ - `src/model/gemma4.ts` — e4b model: `forwardLayers`, `runCheckpointedLayer`, `computePerLayerInputs`, KV-sharing (`numDonors`, `previousKvs`, `cacheIndex`), `layer_types`, `runLayerRange` (added for segmented backward).
249
+ - `src/model/minicpm5.ts` — MiniCPM5 (Phase A).
250
+ - `src/mlx/autograd.ts` (`ValueAndGrad`, `Vjp`), `src/mlx/checkpoint.ts` (`Checkpoint`), `src/mlx/ffi.ts` (memory fns, `stopGradient`, `evalAll`, `mlx_vjp`).
240
251
  - Memory: `[[e4b-lora-training-seqlen-ceiling]]` (full findings), `[[opssdpa-dk-vjp-bug]]`, `[[three-level-fidelity-tree-model]]`.
241
252
 
242
- ## 8. First action on resume
243
- Phase A step 1: add `runLayerRange` to `MiniCPM5Model`, then a segmented training
253
+ ## 8. Original first action (COMPLETED)
254
+ ~~Phase A step 1: add `runLayerRange` to `MiniCPM5Model`, then a segmented training
244
255
  path in `trainer.ts`, validate bit-exact grads + measure peak on
245
- `scripts/ft-chunk-smoke.ts`-style MiniCPM5 run. Then Phase B (e4b) with the §5 plan.
256
+ `scripts/ft-chunk-smoke.ts`-style MiniCPM5 run. Then Phase B (e4b) with the §5 plan.~~
257
+
258
+ **Both Phase A and Phase B are done — see §9 and §10.** The genuine remaining work
259
+ is `planSegments` full-attention isolation for the ~10 GB @8K e4b target: put each
260
+ O(L²) full-attn layer (5,11,17,23,29,35,41) in its own short segment so it isn't
261
+ materialized alongside sliding-layer activations. Currently 17.5 GB @8K (seg2);
262
+ resting is ~6.6 GB, so ~3.4 GB of transient budget remains. The mechanism and tunable
263
+ segmentation knob are proven; this is a peak-optimization step. See §10 close.
246
264
 
247
265
  ---
248
266
 
@@ -339,11 +357,11 @@ memory win confirmed, no leak, trains end-to-end through `scripts/chunk-finetune
339
357
  baseline (11.89). json/schema/labels/nonempty 100%, anchors 86%. Segmented
340
358
  training produces a quality adapter. Adapter:
341
359
  `~/.cache/mlx-bun-finetunes/minicpm5-chunk-segmented`.
342
- 2. **Phase B (e4b)** with the §5 plan (KV-sharing, per-layer-input, full-attn O(L²)
343
- isolation). e4b adds `runLayerRange` to `Gemma4Model` with donor-KV /
344
- per-layer-input boundary threading; the `SegmentedBackward` + vjp machinery and
345
- the §5 segmentation strategy carry over. This is the path to the original goal:
346
- e4b @8K under ~10 GB where the optiq/mlx-lm reference crashes.
360
+ 2. ✅ **DONE — see §10.** **Phase B (e4b)** with the §5 plan (KV-sharing,
361
+ per-layer-input, full-attn O(L²) isolation). e4b `runLayerRange` added to
362
+ `Gemma4Model` with donor-KV / per-layer-input boundary threading;
363
+ `SegmentedBackwardGemma4` + vjp machinery wired into trainer, validated on real
364
+ e4b. Full details in §10.
347
365
 
348
366
  ---
349
367
 
@@ -467,42 +485,43 @@ are proven; this is a peak-optimization, not a blocker.
467
485
  (Config note: mlx-lm's default LoRA scale is 20.0; mlx-bun's `DEFAULT_TRAIN_CONFIG`
468
486
  is 1.0 — a config difference, not correctness. The chunk runs used 1.0.)
469
487
 
470
- **Trainer side — `SegmentedBackwardGemma4` (to build + validate):** same skeleton as
471
- the MiniCPM5 `SegmentedBackward`, with TWO additions.
488
+ **Trainer side — `SegmentedBackwardGemma4` (BUILT + VALIDATED, `src/train/segmented.ts`):**
489
+ same skeleton as the MiniCPM5 `SegmentedBackward`, with TWO additions — both are
490
+ implemented and verified in the Phase B results above.
472
491
 
473
492
  (a) **per-layer-input is a pure constant boundary — NO gradient threading.** The
474
493
  `per_layer_input_gate` / `per_layer_projection` LoRA live INSIDE the layer (applied
475
494
  to the sliced `pls`), so they're differentiated within each segment's vjp normally.
476
495
  The `perLayer` tensor itself feeds in as a constant (its grad would flow to the
477
- embed / `per_layer_model_projection`, neither a LoRA target) — so detach it once and
478
- slice per segment; discard its cotangent. Easy.
496
+ embed / `per_layer_model_projection`, neither a LoRA target) — detached once and
497
+ sliced per segment; its cotangent is discarded.
479
498
 
480
499
  (b) **the KV-shared donor K/V is a SECOND boundary stream WITH its own cotangent.**
481
500
  The donor's K/V depend on the donor layer's q/k/v LoRA and are attended by every
482
501
  sharer, so `dLoss/d(donor LoRA)` gets contributions from the donor's own attention
483
502
  AND all sharers. Threading:
484
503
  - **Forward (boundary-saving):** when a segment produces a reused donor's K/V
485
- (`donorKvOut`), detach it (rope-applied K + V, tiny — ~1 KB/tok) and save as a
486
- boundary; later sharer segments pass it in via `donorKvIn`.
504
+ (`donorKvOut`), it is detached (rope-applied K + V, tiny — ~1 KB/tok) and saved as
505
+ a boundary; later sharer segments pass it in via `donorKvIn`.
487
506
  - **Backward (reverse), two extra pieces:**
488
- - keep a cotangent accumulator `dKV[d]` per reused donor d (init zero).
507
+ - a cotangent accumulator `dKV[d]` per reused donor d (init zero).
489
508
  - a SHARER segment's vjp differentiates `[boundary_h, donorK, donorV, ...LoRA]`
490
509
  with cotangent `[dh]`; its vjps for `donorK/donorV` are ACCUMULATED into `dKV[d]`.
491
510
  - a DONOR segment (produces donor d's K/V) has a MULTI-OUTPUT forward
492
511
  `[h_out, donorK, donorV]`; its vjp takes cotangents `[dh, dKV[d].k, dKV[d].v]`
493
- (the `Vjp` class already supports nOut>1 + multiple cotangents) and returns
512
+ (the `Vjp` class supports nOut>1 + multiple cotangents) and returns
494
513
  `[dh_in, ...donor LoRA]`. This folds the sharers' gradient back into the donor.
495
514
  - donors 22 (sliding) and 23 (full) are each reused by all sharers of their type;
496
515
  if both live in one segment, that segment's forward outputs 5 arrays
497
516
  `[h, k22, v22, k23, v23]` with 5 cotangents.
498
- - **Segmentation (the §5 plan):** respect the donor/sharer split at layer 24 and
499
- ISOLATE the 7 full-attention layers (5,11,17,23,29,35,41) — each is O(L²) in the
500
- backward under ops.sdpa (~1 GB bf16 @8K), so put each full layer in a short
501
- segment and group the sliding layers. The natural first cut is the 6-layer
502
- `sssssF` period → 7 segments of 6 (one full layer each). `planSegments` from §5
503
- returns the split indices + which donor K/V to save.
517
+ - **Segmentation (the §5 plan):** the donor/sharer split at layer 24 is respected and
518
+ the 7 full-attention layers (5,11,17,23,29,35,41) — each O(L²) in the backward
519
+ under ops.sdpa (~1 GB bf16 @8K) — are candidates for isolation into short segments;
520
+ the natural first cut is the 6-layer `sssssF` period → 7 segments of 6 (one full
521
+ layer each). `planSegments` from §5 returns the split indices + which donor K/V to
522
+ save. Full-attention ISOLATION for the ~10 GB @8K target remains the next
523
+ optimization step (currently 17.5 GB @8K, seg2 — see §10 close above).
504
524
 
505
525
  Everything else (forward boundaries via `detachLeaf`, head vjp with cotangent 1.0,
506
- reverse `dh` chain, `mlx_vjp` not surrogate) is identical to the MiniCPM5 path.
507
- Target: e4b @8K ≤ ~10 GB, then `chunk-eval` quality on e4b (the original goal —
508
- chunk data is mostly 3.4–8.2K, useless at the ≤2K the reference is capped to).
526
+ reverse `dh` chain, `mlx_vjp` not surrogate) is identical to the MiniCPM5 path and
527
+ is verified end-to-end: peak 10 GB @2048, active flat (no leak), adapter saved.
@@ -0,0 +1,127 @@
1
+ # Design: mlx-bun desktop app (Tauri)
2
+
3
+ A native macOS desktop app that wraps the existing mlx-bun chat UI in a
4
+ window — double-click an icon, get local AI, no terminal. The app is a
5
+ **thin shell**: it spawns `mlx-bun serve` as a sidecar and points a
6
+ WKWebView at the chat UI the server already serves. Almost no new code;
7
+ the value is packaging + UX, not new functionality.
8
+
9
+ Companion docs: [embedding.md](../reference/embedding.md) (the sidecar
10
+ layout this builds on), [distribution.md](../reference/distribution.md)
11
+ (the sign/notarize pipeline this reuses).
12
+
13
+ ## Where it lives — separate repo (recommended)
14
+
15
+ Ship the app from its **own repo** (`mlx-bun-desktop`), not inside this
16
+ one.
17
+
18
+ - **Toolchain isolation.** This repo is Bun/TS. Tauri drags in Rust,
19
+ Cargo, and `src-tauri/target/` (heavy build artifacts). Keeping it out
20
+ preserves the core repo's clean dependency surface and scope.
21
+ - **Different product, cadence, audience.** The app's artifact is a
22
+ signed `.dmg` / Homebrew **Cask** for end users; the core repo ships a
23
+ CLI tarball + npm package for developers. They version independently.
24
+ - **The sidecar is already a published artifact.** The app pulls a
25
+ pinned, signed, notarized `mlx-bun` bundle from a core-repo release
26
+ (the versionless `mlx-bun-arm64.tar.gz`, or a tagged
27
+ `mlx-bun-v<ver>-arm64.tar.gz`) — no source coupling needed.
28
+ - **Precedent.** The Homebrew tap is already a separate repo; this fits.
29
+
30
+ In-repo alternative (a `desktop/` subdir) is viable if tight version
31
+ lockstep ever matters more than toolchain cleanliness — the mechanics
32
+ below are identical, only the sidecar source changes (local `dist/`
33
+ instead of a downloaded release). Default to separate.
34
+
35
+ ## Architecture
36
+
37
+ ```
38
+ mlx-bun-desktop.app
39
+ ├─ src-tauri (Rust shell)
40
+ │ on launch → pick a free port, spawn sidecar:
41
+ │ mlx-bun serve <model> --port <p> --no-open
42
+ │ readiness → poll GET http://127.0.0.1:<p>/v1/models until 200
43
+ │ window → WKWebView → http://127.0.0.1:<p>/#/chat
44
+ │ on quit → SIGKILL the sidecar (restart→ready is ~0.4s; safe)
45
+ └─ Contents/Resources/
46
+ mlx-bun + libmlxc.dylib + libmlx.dylib + libjaccl.dylib
47
+ + mlx.metallib + pi assets (the bundle build-binary.sh produces)
48
+ ```
49
+
50
+ The window renders the **same page** `mlx-bun serve` serves today
51
+ (`/#/chat`, the embedded pi web chat). No bespoke UI.
52
+
53
+ ## Components
54
+
55
+ 1. **Sidecar binary.** Tauri `externalBin` (target-triple-suffixed, e.g.
56
+ `mlx-bun-aarch64-apple-darwin`); the dylibs + metallib + pi assets go
57
+ in `bundle.resources` beside it. mlx-bun resolves its libs via
58
+ `dirname(realpath(execPath))`, so the flat Resources layout works
59
+ unchanged (verified: Bun realpaths execPath through symlinks).
60
+ 2. **Spawn / teardown** (`main.rs`). Use Tauri's shell/sidecar API (or
61
+ `std::process::Command`) to launch on `setup`, kill on window close /
62
+ app exit. One generation runs at a time server-side, so no client
63
+ concurrency to manage.
64
+ 3. **Port.** Bind an ephemeral port in Rust, pass it to both the spawn
65
+ (`--port`) and the webview URL, to avoid 8090 collisions with a
66
+ user's own `mlx-bun serve`.
67
+ 4. **Readiness gate.** Show a lightweight "starting…/downloading model…"
68
+ splash until `/v1/models` returns 200. Warm start ~0.4s; first-ever
69
+ run downloads the starter model (~0.9 GB) — the existing onboarding
70
+ page already narrates this, so the splash can hand off to it.
71
+
72
+ ## Distribution
73
+
74
+ A `.app` is a better Gatekeeper citizen than the flat CLI binary:
75
+
76
+ - **Stapleable.** Unlike a bare Mach-O, a `.app`/`.dmg` accepts a stapled
77
+ notarization ticket → works **offline**, no online check needed.
78
+ - **Channels:** signed `.dmg` direct download, and/or a Homebrew **Cask**
79
+ (`brew install --cask mlx-bun`) — a natural 5th install path.
80
+ - **Not App Store.** JIT + a spawned child process + the Metal model
81
+ don't fit the sandbox. Developer-ID + notarized + DMG/Cask outside the
82
+ store is the same flow this project already uses.
83
+
84
+ ### Signing / entitlements
85
+
86
+ Reuse the core pipeline. The nested sidecar `mlx-bun` still needs the
87
+ same entitlements as the standalone build
88
+ ([packaging/entitlements.plist](../../packaging/entitlements.plist)):
89
+
90
+ - `com.apple.security.cs.allow-jit` + `allow-unsigned-executable-memory`
91
+ (JavaScriptCore JIT) — without these the sidecar is killed on launch.
92
+ - `com.apple.security.cs.disable-library-validation` — the sidecar
93
+ dlopens the bundled MLX dylibs.
94
+
95
+ Tauri's bundler signs nested binaries during the `.app` signing pass;
96
+ point these entitlements at the sidecar specifically. Notarize the
97
+ `.app` (or `.dmg`) with the existing `AC_PROFILE` notary credentials,
98
+ then `xcrun stapler staple`.
99
+
100
+ ## Milestones
101
+
102
+ 1. **Spike** — `tauri init` in a new repo; hardcode port 8090; spawn a
103
+ locally-built `mlx-bun serve --no-open`; load `/#/chat` in the window;
104
+ `cargo tauri dev`. Proves the wrap end-to-end.
105
+ 2. **Robust runtime** — ephemeral port; readiness poll + splash;
106
+ clean teardown on quit; surface sidecar stderr to a log.
107
+ 3. **Bundle + sidecar** — wire `externalBin` + `bundle.resources` to a
108
+ pinned core-repo release tarball (download in a build script, or check
109
+ in a fetch step); first-run model-download UX.
110
+ 4. **Sign + notarize + package** — Developer-ID sign with nested
111
+ entitlements, notarize, staple, emit `.dmg`.
112
+ 5. **Cask** — formula in a tap pointing at the `.dmg` release asset;
113
+ `brew install --cask`.
114
+
115
+ ## Open questions / risks
116
+
117
+ - **Tauri sidecar entitlement plumbing** — confirm Tauri applies the
118
+ per-binary entitlements to `externalBin` (may need a custom signing
119
+ hook in the bundler config). Validate early in milestone 4.
120
+ - **Window vs. menubar** — a normal window is the MVP; a menubar/tray app
121
+ ("always-on local model") is a possible later mode.
122
+ - **Model picker** — MVP serves one model (the starter). Switching models
123
+ means restarting the sidecar; defer a UI for that.
124
+ - **Version pinning** — decide whether the app tracks `latest` or pins a
125
+ specific `mlx-bun` release per app version (pin is safer for repro).
126
+ - **Universal binary** — arm64-only today; Tauri can target both, but
127
+ MLX is Apple-Silicon-only, so x86 is moot.
@@ -1,7 +1,7 @@
1
1
  # Handoff — segmented backward training (2026-06-16)
2
2
 
3
3
  Current-state report for the segmented-backward LoRA training work. Deep dossier
4
- (design, proofs, every measurement): [`docs/design/segmented-backward-training.md`](design/segmented-backward-training.md)
4
+ (design, proofs, every measurement): [`docs/design/segmented-backward-training.md`](../design/segmented-backward-training.md)
5
5
  §9 (MiniCPM5) and §10 (e4b). Branch: `segmented-backward-training`,
6
6
  PR [#9](https://github.com/joshuarossi/mlx-bun/pull/9).
7
7
 
@@ -52,6 +52,22 @@ QK-norm against the oracle, llama has none).
52
52
  Josh decides; both tracks are specified below. The shared recipe
53
53
  (§2) applies to either.
54
54
 
55
+ > **Outcome (2026-06-17, resolved).** Both tracks proceeded as
56
+ > recommended. **Track A (MiniCPM5-1B) was chosen as the publishing
57
+ > gate and shipped** (open question 1 confirmed): `src/model/minicpm5.ts`
58
+ > is live, parity suite green, PLAN.md updated. The starter runs
59
+ > non-thinking (open question 2 confirmed). Weights are kept on disk as a
60
+ > permanent fallback and tour-guide model — not deleted after the big model
61
+ > lands (open question 3 confirmed). Publishing mechanics resolved
62
+ > (open question 4 green-lit): the package is published as
63
+ > `mlx-bun@0.0.4` on npm, installable via `bunx mlx-bun`, and a
64
+ > Homebrew release pipeline (sign/notarize/tap, default tap
65
+ > `joshuarossi/homebrew-tap`) is in place. First-run onboarding
66
+ > (auto-download + browser open) shipped alongside. **Track B
67
+ > (Qwen3.5-0.8B) is underway** as Phase 14 proper on its own clock,
68
+ > with reasoning fixes already merged. All four open questions are
69
+ > resolved; see §6.
70
+
55
71
  ---
56
72
 
57
73
  ## 1. Machine + reference environment logistics
@@ -308,16 +324,19 @@ isolates the novel machinery so parity bisects cleanly.
308
324
  - PLAN.md updated with findings (negative results included — e.g.
309
325
  any oracle deviations discovered, like the Phase 11 ones).
310
326
 
311
- ## 6. Open questions for Josh
312
-
313
- 1. **Track A vs B for the gate** — recommendation is A
314
- (MiniCPM5) for publishing speed; B proceeds afterward as Phase
315
- 14 on its own clock. Decide before work starts.
316
- 2. MiniCPM5's hybrid `<think>` mode (the OptiQ page's
317
- `enable_thinking` note): starter ships non-thinking — confirm.
318
- 3. Should the starter stay on disk as the permanent "fallback +
319
- tour guide" (current plan) or be deleted once the big model
320
- lands? (Current code keeps it; ~1 GB.)
321
- 4. Publishing mechanics can proceed in parallel with the port
322
- (npm `files` allowlist, un-`private` the package, repo
323
- visibility decision for the native pack) — green-light?
327
+ ## 6. Resolved questions
328
+
329
+ All four questions were resolved by 2026-06-17 (see outcome box above).
330
+
331
+ 1. **Track A vs B for the gate** — **Track A (MiniCPM5-1B) chosen.**
332
+ `src/model/minicpm5.ts` shipped as the publishing gate; Track B
333
+ (Qwen3.5-0.8B) is underway as Phase 14 proper on its own clock.
334
+ 2. **Thinking mode for the starter** — **Non-thinking confirmed.**
335
+ The starter runs with thinking disabled; no `<think>` tokens in the
336
+ first-run flow.
337
+ 3. **Keep weights on disk?** — **Yes, kept as permanent fallback + tour
338
+ guide.** The ~1 GB artifact is not deleted once the big model lands.
339
+ 4. **Publishing mechanics green-lit?** — **Yes, shipped.**
340
+ Package published as `mlx-bun@0.0.4` on npm; installable via
341
+ `bunx mlx-bun`; Homebrew release pipeline (sign/notarize/tap,
342
+ default tap `joshuarossi/homebrew-tap`) is in place.