mlx-bun 0.0.4 → 0.0.6
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 +175 -93
- package/docs/dag/training-inference-map.html +575 -0
- 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 +193 -295
- package/docs/design/optimization_plan.md +4 -2
- package/docs/design/orpo-dynamic-lambda.md +179 -0
- package/docs/design/orpo-future-enhancements.md +340 -0
- package/docs/design/orpo-training.md +1212 -0
- package/docs/design/parallel-slots.md +34 -26
- package/docs/design/parity-tier-dag.md +143 -0
- 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/design/w4a16-compute-precision-spike.md +232 -0
- package/docs/investigations/orpo-base-uf-experiment-and-directions.md +120 -0
- package/docs/investigations/orpo-flash-cce-pin-leak.md +167 -0
- package/docs/investigations/orpo-uf-testing-handoff.md +126 -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 +489 -0
- package/docs/investigations/trainer-validation-experiment.md +173 -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/memory.md +77 -0
- package/docs/reference/orpo-quickstart.md +130 -0
- package/docs/reference/server-api.md +105 -7
- package/docs/reference/server-config.md +12 -5
- package/docs/reference/training.md +296 -0
- package/package.json +1 -1
- package/src/cli.ts +651 -18
- package/src/eval/ifeval.ts +138 -0
- package/src/eval/runner.ts +76 -6
- package/src/eval/tasks/bfcl.ts +49 -15
- package/src/eval/tasks/gsm8k.ts +21 -9
- package/src/eval/tasks/hashhop.ts +33 -4
- package/src/eval/tasks/humaneval.ts +30 -8
- package/src/eval/tasks/ifeval.ts +52 -18
- package/src/eval/tasks/mmlu.ts +85 -74
- package/src/eval/ultrafeedback.ts +60 -0
- package/src/jobs/runner.ts +6 -0
- package/src/lora.ts +12 -7
- package/src/memory/chunk.ts +25 -0
- package/src/memory/cluster.ts +24 -0
- package/src/memory/db.ts +114 -0
- package/src/memory/ingest.ts +22 -0
- package/src/memory/pipeline.ts +63 -0
- package/src/memory/prompts.ts +23 -0
- package/src/memory/schedule.ts +163 -0
- package/src/memory/synthesize.ts +29 -0
- package/src/memory/tools.ts +399 -0
- package/src/memory/vault.ts +694 -0
- package/src/memory/wikify.ts +23 -0
- package/src/mlx/metal-kernel.ts +31 -4
- package/src/model/flash-attention.ts +37 -19
- 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/pi-provider.ts +29 -13
- package/src/pi-session.ts +81 -0
- package/src/pi-terminal.ts +13 -24
- package/src/pi-web.ts +246 -166
- package/src/quantize/job.ts +17 -2
- package/src/quantize/quantizer.ts +26 -2
- package/src/server.ts +216 -47
- package/src/tokenizer.ts +6 -2
- package/src/train/dataset.ts +29 -6
- package/src/train/flash-cce.ts +1175 -0
- package/src/train/job.ts +64 -3
- package/src/train/lora-params.ts +53 -4
- package/src/train/loss.ts +947 -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 +570 -54
- package/src/train/watch.ts +542 -0
- package/src/web/app.html +285 -19
- package/src/web/skills/memory/SKILL.md +97 -0
- package/src/web/skills/web-research/SKILL.md +3 -1
- package/src/web/skills.ts +46 -8
- /package/{docs → src/assets}/curve-designer.html +0 -0
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
# Parallel slots — configurable batched serving
|
|
2
2
|
|
|
3
|
-
Status: **
|
|
3
|
+
Status: **S0 + S1b SHIPPED** (S2 not started; S3+ deferred)
|
|
4
4
|
Owner: serving layer
|
|
5
|
-
Default: **off** (`--
|
|
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 `--
|
|
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 `--
|
|
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 (`--
|
|
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 `--
|
|
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.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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.
|
|
470
|
+
- `ServerOptions.batch` (default 1); later `kvBudgetBytes`,
|
|
466
471
|
`promptConcurrency`.
|
|
467
|
-
- CLI: `--slots N`
|
|
468
|
-
- `
|
|
469
|
-
`
|
|
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
|
|
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)
|
|
481
|
-
|
|
482
|
-
`
|
|
483
|
-
|
|
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
|
|
488
|
-
queued requests prefill into freed rows. Dynamic byte-budget admission
|
|
489
|
-
|
|
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 `
|
|
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
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Parity-tier DAG → meaningful CLI flags
|
|
2
|
+
|
|
3
|
+
**Status:** vision / roadmap (2026-06-21). The first artifact exists — a zoomable,
|
|
4
|
+
tier-tagged map of the training + inference DAGs at
|
|
5
|
+
[`docs/dag/training-inference-map.html`](../dag/training-inference-map.html). This
|
|
6
|
+
doc records where it goes and *why*: to turn a sprawling pile of CLI flags into a
|
|
7
|
+
small, coherent, tier-organized surface.
|
|
8
|
+
|
|
9
|
+
## The framework (recap)
|
|
10
|
+
|
|
11
|
+
Every node in the compute graph sits in the **lowest parity tier whose oracle it
|
|
12
|
+
matches bit-for-bit**:
|
|
13
|
+
|
|
14
|
+
- **L1** — drop-in for **mlx-lm** (its op is the oracle; bit-exact or it's a bug).
|
|
15
|
+
- **L2** — drop-in for **mlx-optiq** (optiq's behavior is the oracle).
|
|
16
|
+
- **L3** — **our originals** (flash-CCE head, ORPO loss, prefix-share, segmented
|
|
17
|
+
backward, HLG sampler, expert offload). **No oracle ships**, so correctness is
|
|
18
|
+
gated by finite-difference / KL / quality, *not* parity.
|
|
19
|
+
|
|
20
|
+
**The gate is the oracle.** A node can only live in a lower tier if that oracle
|
|
21
|
+
exists AND it matches bit-exactly; otherwise it bubbles up. Note the subtlety the
|
|
22
|
+
map already shows: an *optimization* that still matches the oracle stays low —
|
|
23
|
+
`compiled-decode` is ours but is L1 because it replays the same ops bit-for-bit.
|
|
24
|
+
Only nodes with **no** oracle (flash-CCE) are *forced* to L3. (Background +
|
|
25
|
+
post-mortem: [orpo-flash-cce-pin-leak.md](../investigations/orpo-flash-cce-pin-leak.md).)
|
|
26
|
+
|
|
27
|
+
## Two axes: parity gates performance
|
|
28
|
+
|
|
29
|
+
Every optimization has **two independent coordinates**, and conflating them is what
|
|
30
|
+
made the flag surface unreadable:
|
|
31
|
+
|
|
32
|
+
1. **Parity** — *which reference does it reproduce bit-for-bit?* This sets the
|
|
33
|
+
**lowest level** it may live in: matches mlx-lm → L1; matches mlx-optiq but not
|
|
34
|
+
mlx-lm → L2; matches neither → L3 (gated by KL + tests, no oracle).
|
|
35
|
+
2. **Performance** — *is it the fastest correct way?* This sets whether it's the
|
|
36
|
+
**default (on)** within its level, or a kept-but-**off** opt-in.
|
|
37
|
+
|
|
38
|
+
**Parity gates performance.** A faster kernel that breaks mlx-lm parity is simply
|
|
39
|
+
*not in L1* — it bubbles up to whatever level its parity allows. You never trade the
|
|
40
|
+
guarantee for speed silently; you change levels, and the user opts in. The level is
|
|
41
|
+
your floor of trust; the default within it is the best way to honor that floor.
|
|
42
|
+
|
|
43
|
+
### Decision procedure for any new optimization
|
|
44
|
+
|
|
45
|
+
1. **Measure its parity** (run it both ways, compare — "one flag, one measurement
|
|
46
|
+
per lever"). The lowest oracle it matches bit-for-bit is its level ceiling.
|
|
47
|
+
2. **Measure its performance** vs that level's current default:
|
|
48
|
+
- **faster AND holds the guarantee** → it becomes the **default (on)** for that level.
|
|
49
|
+
- **slower but still correct** → **keep it as a documented default-off flag**
|
|
50
|
+
(optionality for A/B; never the default, never deleted).
|
|
51
|
+
- **breaks the guarantee but offers a real tradeoff** (e.g. 2× tok/s at KL
|
|
52
|
+
0.0015) → an **L3 opt-in**; the user chooses exact vs fast.
|
|
53
|
+
|
|
54
|
+
Worked examples:
|
|
55
|
+
- *5% less memory, still bit-for-bit with mlx-lm* → **default-on in L1.**
|
|
56
|
+
- *2× tok/s, but KL 0.0015 to mlx-lm* → **L3 opt-in** (user picks exact or 2×).
|
|
57
|
+
- *another way, 3% slower, still correct* → **kept, off in every level.**
|
|
58
|
+
|
|
59
|
+
### Our decode levers under this rule (today)
|
|
60
|
+
|
|
61
|
+
| lever | parity | perf | placement |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| `compiled-decode` | bit-exact w/ uncompiled (`compiled-decode.test`) | faster | **default-on, every level** |
|
|
64
|
+
| `perf-kernel` | matches optiq, not mlx-lm (`perf-kernel-oracle.test`) | faster | **default-on from L2** (the L2 bridge) |
|
|
65
|
+
| `fused-sdpa` | matches optiq, not mlx-lm (`fused-sdpa.test`) | faster | **default-on from L2** (the L2 bridge) |
|
|
66
|
+
| `fused-decode` | (would be L3) | ~3% slower (`optimization_plan.md`) | **kept, off everywhere** |
|
|
67
|
+
|
|
68
|
+
So `--l1/--l2/--l3` is the **parity** axis (the guarantee), and the defaults within
|
|
69
|
+
each tier are the **performance** axis (the fastest kernel that still holds it). For
|
|
70
|
+
the *decode path* L2 == L3 today (every decode kernel matches mlx-lm or optiq); L3's
|
|
71
|
+
real territory is the no-oracle **features** — HLG sampler, expert offload, batched
|
|
72
|
+
mixed-precision — which live off the decode axis and are gated by KL + quality.
|
|
73
|
+
|
|
74
|
+
## Why this matters for CLI flags
|
|
75
|
+
|
|
76
|
+
We are accumulating a **lot** of flags (`--grad-accum`, `--grad-clip`, `--seg`,
|
|
77
|
+
`--no-flash`, `--no-prefix`, `--no-segment`, `--lambda`, `--val-size`, kv-quant
|
|
78
|
+
knobs, sampler knobs, …). The problem isn't any one flag — it's the **combinatorics**
|
|
79
|
+
and the fact that **some are never really a choice** (always-on guards, or the only
|
|
80
|
+
viable route). The tier-tagged DAG makes each flag legible by asking: **what route
|
|
81
|
+
does this flag push the computation down, and what tier is that route?**
|
|
82
|
+
|
|
83
|
+
A flag falls into exactly one of three buckets:
|
|
84
|
+
|
|
85
|
+
| flag selects… | tier effect | what it IS | action |
|
|
86
|
+
|---|---|---|---|
|
|
87
|
+
| the only viable route (no alternative node) | — | not a choice | **always-on default; drop the flag** |
|
|
88
|
+
| between two routes at the SAME tier / same oracle | none | cosmetic / safety guard | **default it; expose only if it ever fails** |
|
|
89
|
+
| between a **lower-tier** route and an **L3** route | **L1/L2 ↔ L3** | a real **parity ⇄ optimization** knob | **keep, document as such, group by tier** |
|
|
90
|
+
|
|
91
|
+
So the DAG lets us answer "**what flags push down THAT route**" for any path, and
|
|
92
|
+
from that:
|
|
93
|
+
|
|
94
|
+
1. **Prune always-on flags to defaults** — anything whose alternative route is
|
|
95
|
+
never selected, or whose toggle doesn't change the tier, shouldn't be a flag.
|
|
96
|
+
2. **Surface the real axis.** The flags that genuinely matter all toggle
|
|
97
|
+
*bit-exact-lower-tier ⇄ our-L3-optimization*. Those could collapse into a small
|
|
98
|
+
number of intent-level switches (e.g. a single `--exact` that forces every node
|
|
99
|
+
down to its lowest tier, vs the optimized default) instead of N independent
|
|
100
|
+
kernel toggles the user has to understand individually.
|
|
101
|
+
3. **Group the rest by what they trade** — memory (`--seg`, all bit-exact → not a
|
|
102
|
+
parity choice, a memory↔compute knob), hyperparameters (`--lambda`, `--lr` →
|
|
103
|
+
training knobs, orthogonal to tier), etc.
|
|
104
|
+
|
|
105
|
+
### Worked classification (current training flags)
|
|
106
|
+
|
|
107
|
+
- `--grad-clip 1.0` → standard guard, L1, **always-on default** (not a route choice).
|
|
108
|
+
- `--seg N` / `--no-segment` → memory↔compute, **bit-exact either way** → a *memory*
|
|
109
|
+
knob, **not** a parity choice. Should read as "memory budget," not "exactness."
|
|
110
|
+
- `--grad-accum N` → changes the accumulation route (L1; smoother grads) → a real
|
|
111
|
+
*training-quality* knob.
|
|
112
|
+
- `--no-flash` → **L3 flash-CCE → L2 MLX fused head** → a genuine *parity ⇄ perf*
|
|
113
|
+
fallback (the one place the head leaves L3).
|
|
114
|
+
- `--no-prefix` → **L3 prefix-share → bit-exact two-forward** → a *perf* knob with no
|
|
115
|
+
numerical cost.
|
|
116
|
+
- `--lambda` → ORPO hyperparameter (pure L3) → a *training* knob.
|
|
117
|
+
|
|
118
|
+
The pattern: most flags are **memory/training knobs or always-on guards**; only a
|
|
119
|
+
couple are true **parity ⇄ optimization** toggles. Today they're a flat list the
|
|
120
|
+
user must reason about individually; tier-tagging lets us present them as what they
|
|
121
|
+
actually are.
|
|
122
|
+
|
|
123
|
+
## Roadmap to make it executable
|
|
124
|
+
|
|
125
|
+
The map is hand-authored today. To make the framework *enforce* itself:
|
|
126
|
+
|
|
127
|
+
1. **Derive the graph from code** — extract the call graph + per-node op/oracle tags
|
|
128
|
+
so it stays in sync and is **queryable** ("every L3 node", "paths that cross into
|
|
129
|
+
L3", "the pure-L1 subgraph", "flags that select route X").
|
|
130
|
+
2. **CI-enforce the gate** — a node tagged L1 must have a passing bit-exact parity
|
|
131
|
+
test vs mlx-lm; if it can't, it's a bug or must be re-tagged L3. "The oracle is
|
|
132
|
+
the gate" becomes a check, not a principle.
|
|
133
|
+
3. **Shrink the L3 surface** — audit whether anything in the L3 island is actually
|
|
134
|
+
bit-exact-able and just unproven (e.g. the prefix-shared *forward* is already
|
|
135
|
+
bit-exact — `prefix-shared-parity.ts` — so only the encode-once *fold* is truly
|
|
136
|
+
L3). Minimizing L3 minimizes what needs KL/quality gates.
|
|
137
|
+
4. **Generate the flag surface from the graph** — once flags map to routes, the CLI
|
|
138
|
+
help + defaults can be derived: always-on routes have no flag, parity⇄opt routes
|
|
139
|
+
collapse to intent switches, knobs group by what they trade.
|
|
140
|
+
|
|
141
|
+
End state: instead of a growing flat list of flags the user memorizes, a small set
|
|
142
|
+
of intent-level switches whose meaning is exactly "which tier route does the compute
|
|
143
|
+
take," backed by a DAG that's checked in CI.
|
|
@@ -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
|
|
19
|
-
|
|
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),
|
|
101
|
-
`
|
|
102
|
-
|
|
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
|
|
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
|
|
144
|
-
|
|
145
|
-
|
|
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)
|
|
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
|
|
238
|
-
- `src/model/minicpm5.ts` — MiniCPM5 (Phase A
|
|
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.
|
|
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,
|
|
343
|
-
isolation). e4b
|
|
344
|
-
per-layer-input boundary threading;
|
|
345
|
-
|
|
346
|
-
e4b
|
|
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` (
|
|
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) —
|
|
478
|
-
|
|
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`),
|
|
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
|
-
-
|
|
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
|
|
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):**
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
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.
|