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
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# Training / fine-tuning reference
|
|
2
|
+
|
|
3
|
+
How to fine-tune a served model with LoRA adapters: the entry points, the
|
|
4
|
+
data formats, every config flag, and the methodology behind the knobs.
|
|
5
|
+
|
|
6
|
+
mlx-bun trains **LoRA adapters** (the base quantized weights stay frozen),
|
|
7
|
+
supports **SFT**, **DPO**, and **ORPO**, and runs on a single Apple-Silicon GPU. The
|
|
8
|
+
output is a PEFT-compatible adapter you hot-swap into the server — see
|
|
9
|
+
[adapters-end-to-end](../design/adapters-end-to-end.md) for the serving side
|
|
10
|
+
and [segmented-backward-training](../design/segmented-backward-training.md)
|
|
11
|
+
for the long-context memory mechanism.
|
|
12
|
+
|
|
13
|
+
> Source of truth: the config schema is `FinetuneSubmit` in
|
|
14
|
+
> [`src/train/job.ts`](../../src/train/job.ts); defaults are
|
|
15
|
+
> `DEFAULT_TRAIN_CONFIG` in [`src/train/trainer.ts`](../../src/train/trainer.ts).
|
|
16
|
+
> This doc is generated against those — if they drift, the code wins.
|
|
17
|
+
|
|
18
|
+
## Entry points
|
|
19
|
+
|
|
20
|
+
Training is reachable several ways — all of them drive the **same** runner
|
|
21
|
+
(`finetuneRunner` in `src/train/job.ts`):
|
|
22
|
+
|
|
23
|
+
| Path | How | Use when |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| **CLI** | `mlx-bun train <model> --data <dir> [options]` — foreground, full ORPO stack on by default; auto-detects e4b/Gemma + sets its env; `--save-every`, `--resume`, `--dry-run`; streams loss to the terminal. `mlx-bun help train` for all flags. Run long jobs detached: `nohup mlx-bun train … &` | **The default for CLI users** |
|
|
26
|
+
| **Web UI** | `mlx-bun serve`, open `/finetune` — pick model → dataset → hyperparameters → train; watch live train/val loss; merge/export the adapter | Interactive, in the browser |
|
|
27
|
+
| **HTTP API** | `POST /api/finetune/submit` (job id + SSE events); `POST /api/finetune/inspect-dataset` to probe a file; `POST /api/finetune/merge` to fold an adapter into base weights | Scripted / remote |
|
|
28
|
+
| **ORPO launcher** | [`scripts/train-orpo.ts`](../../scripts/train-orpo.ts) — the same stack driven by env vars (`MODEL=`, `DATA=`, `RANK=`, `RESUME=`, …). See [orpo-quickstart](./orpo-quickstart.md) | Env-var scripting |
|
|
29
|
+
| **Shell recipe** | [`scripts/ft-e4b-v2.sh`](../../scripts/ft-e4b-v2.sh) `probe`\|`train` — the actual e4b run we use; sets the required env (see [What we actually run](#what-we-actually-run-the-e4b-recipe)) | Reproducing our e4b fine-tune |
|
|
30
|
+
| **Library** | `import { finetuneRunner } from "./src/train/job"` and call it with a config + emitter | Embedding training in your own TS |
|
|
31
|
+
|
|
32
|
+
The CLI and the launcher run the runner **in-process / foreground** (stream to the
|
|
33
|
+
terminal, `nohup`-able); the Web UI and HTTP API run it as a **subprocess job** (crash
|
|
34
|
+
isolation + a single GPU lease across concurrent server requests).
|
|
35
|
+
|
|
36
|
+
### Quick start (HTTP API)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
curl -s localhost:8090/api/finetune/submit -X POST -H 'content-type: application/json' -d '{
|
|
40
|
+
"model_dir": "/path/to/snapshot",
|
|
41
|
+
"data_dir": "/path/to/dataset", // dir with train.jsonl (+ optional valid.jsonl)
|
|
42
|
+
"adapter_path": "/path/to/output-adapter",
|
|
43
|
+
"method": "sft",
|
|
44
|
+
"rank": 16, "iters": 300, "learning_rate": 2e-4, "max_seq_length": 2048
|
|
45
|
+
}'
|
|
46
|
+
# → { "jobId": "..." } then stream events from the jobs SSE endpoint
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`model_dir`, `data_dir`, and `adapter_path` are **required**; everything else
|
|
50
|
+
falls back to the defaults below.
|
|
51
|
+
|
|
52
|
+
### Quick start (script)
|
|
53
|
+
|
|
54
|
+
[`scripts/chunk-finetune.ts`](../../scripts/chunk-finetune.ts) is the worked
|
|
55
|
+
example (MiniCPM5 on the chunking task). It calls `finetuneRunner` directly,
|
|
56
|
+
driven by env vars:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# MODEL unset → defaults to the MiniCPM5-1B-OptiQ-4bit snapshot
|
|
60
|
+
DATA=/path/to/chunk ITERS=300 RANK=16 SEQ=2048 SEG=4 \
|
|
61
|
+
bun scripts/chunk-finetune.ts
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Env knobs — note the script applies its **own task-tuned defaults**, which
|
|
65
|
+
differ from the trainer/API defaults in the table below:
|
|
66
|
+
|
|
67
|
+
| Env | Maps to | Script default |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `MODEL` | `model_dir` | MiniCPM5-1B-OptiQ-4bit snapshot (a **path** if set, not a name) |
|
|
70
|
+
| `DATA` | `data_dir` | the lucien chunk dataset path |
|
|
71
|
+
| `SEQ` | `max_seq_length` | `8192` |
|
|
72
|
+
| `ITERS` | `iters` | `2` (probe; use 300 for a real run) |
|
|
73
|
+
| `RANK` | `rank` | `16` |
|
|
74
|
+
| `LR` | `learning_rate` | `1e-5` |
|
|
75
|
+
| `SCALE` | `scale` | `20` |
|
|
76
|
+
| `SEG` | `segment_size` | `0` (off) |
|
|
77
|
+
| `EVAL_EVERY` | `steps_per_eval` | auto from `ITERS` |
|
|
78
|
+
| `ADAPTER` | `adapter_path` | `~/.cache/mlx-bun-finetunes/minicpm5-chunk-seq<SEQ>` |
|
|
79
|
+
| `CKPT` | `save_checkpoints` | on (`CKPT=0` disables) |
|
|
80
|
+
| `GRAD_CKPT` | `grad_checkpoint` | off (`GRAD_CKPT=1` enables) |
|
|
81
|
+
|
|
82
|
+
The script hard-codes `method=sft`, `batch_size=1`, `steps_per_report=1`, and
|
|
83
|
+
uses the default `ops.sdpa` training attention (set `MLX_BUN_TRAIN_ATTN=flash`
|
|
84
|
+
to override — but flash crashes e4b at multi-K; see Methodology).
|
|
85
|
+
|
|
86
|
+
## Data formats
|
|
87
|
+
|
|
88
|
+
Each row of `train.jsonl` (and optional `valid.jsonl`) is auto-detected by its
|
|
89
|
+
keys ([`src/train/dataset.ts`](../../src/train/dataset.ts)):
|
|
90
|
+
|
|
91
|
+
| Format | Shape | Loss boundary |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `messages` | `{"messages": [{"role","content"}, …]}` | response-only — loss on the final turn, prompt = chat-template render of all prior turns |
|
|
94
|
+
| `prompt-completion` | `{"prompt": "...", "completion": "..."}` | loss on the completion only |
|
|
95
|
+
| `text` | `{"text": "..."}` | full-sequence (no prompt mask) |
|
|
96
|
+
| `dpo` *(method=dpo or orpo)* | `{"prompt", "chosen", "rejected"}` | preference loss on chosen vs rejected (the same preference format serves both DPO and ORPO) |
|
|
97
|
+
|
|
98
|
+
Probe a file before submitting: `POST /api/finetune/inspect-dataset` with
|
|
99
|
+
`{"path": "..."}` returns `{ ok, n_train, n_valid, format }`.
|
|
100
|
+
|
|
101
|
+
## SFT vs DPO vs ORPO
|
|
102
|
+
|
|
103
|
+
- **SFT** (`method: "sft"`, default) — supervised fine-tune; response-only
|
|
104
|
+
cross-entropy. Default LR `2e-4`. For instruction-following, formatting,
|
|
105
|
+
task adaptation.
|
|
106
|
+
- **DPO** (`method: "dpo"`) — Direct Preference Optimization on
|
|
107
|
+
chosen/rejected pairs; loss `-log σ(β·((π_c − ref_c) − (π_r − ref_r)))`
|
|
108
|
+
with reference log-probs computed at LoRA scale 0. Default LR `5e-5`. Tune
|
|
109
|
+
with `dpo_beta`, `dpo_warmup_iters`, `dpo_lr_schedule`.
|
|
110
|
+
- **ORPO** (`method: "orpo"`) — Odds Ratio Preference Optimization (Hong et
|
|
111
|
+
al. 2024): a **reference-free** monolithic objective combining the SFT term
|
|
112
|
+
and a preference term, `L = L_NLL(chosen) + λ·L_OR`, with
|
|
113
|
+
`L_OR = -log σ(log_odds)` and
|
|
114
|
+
`log_odds = (ℓ_w − ℓ_r) − (log1mexp(ℓ_w) − log1mexp(ℓ_r))`, where `ℓ` is the
|
|
115
|
+
**length-normalized** (mean over response tokens) log-prob. Uses the same
|
|
116
|
+
`{prompt, chosen, rejected}` preference data as DPO but needs **no reference
|
|
117
|
+
model** (2 forwards/step vs DPO's 4). Default LR `1e-5` (lower than DPO — the
|
|
118
|
+
loss carries a full SFT NLL term a high LR destabilizes). Tune with
|
|
119
|
+
`orpo_lambda`, `orpo_warmup_iters`, `orpo_lr_schedule`. Design + optimization
|
|
120
|
+
roadmap: [orpo-training](../design/orpo-training.md).
|
|
121
|
+
|
|
122
|
+
The ORPO head + long-context machinery compose into one stack (all B=1), each
|
|
123
|
+
piece independently toggled and each falling back cleanly:
|
|
124
|
+
|
|
125
|
+
- **`orpo_flash_ce`** — the [M,vocab]-free flash-CCE Metal head (steel GEMM fwd
|
|
126
|
+
**and** bwd; e4b backward 754 ms, peak 0.93 GB flat at M=8192). Implies the
|
|
127
|
+
fused head. Falls back to the MLX fused head when the tiling isn't clean.
|
|
128
|
+
- **`segment_size`** — segmented backward (gradient-checkpointed layer
|
|
129
|
+
activations) for long context; the head is ~free in memory, the activations
|
|
130
|
+
are the wall.
|
|
131
|
+
- **`orpo_prefix_shared`** — encode the shared prompt once; composes with both
|
|
132
|
+
the flash head and the segmented backward (MiniCPM5 **and** e4b/Gemma4). Per-row
|
|
133
|
+
two-forward fallback when chosen/rejected prompts differ.
|
|
134
|
+
- **warm-start** — `scripts/train-orpo.ts RESUME=<adapter-dir>` continues from a
|
|
135
|
+
checkpoint's weights (optimizer + LR schedule restart).
|
|
136
|
+
|
|
137
|
+
The one-command way to run all of it is the **ORPO launcher**
|
|
138
|
+
([`scripts/train-orpo.ts`](../../scripts/train-orpo.ts)) — see
|
|
139
|
+
[orpo-quickstart](./orpo-quickstart.md) for the command, the per-model defaults,
|
|
140
|
+
and the measured memory/throughput (e.g. e4b full stack @ 8192 ≈ 13 GB,
|
|
141
|
+
~70 s/step on the M1 Max dev box). Run long jobs detached from your shell
|
|
142
|
+
(`nohup … &`), not as a session-spawned background task.
|
|
143
|
+
|
|
144
|
+
## Configuration reference
|
|
145
|
+
|
|
146
|
+
All fields optional except `model_dir` / `data_dir` / `adapter_path`. Defaults
|
|
147
|
+
are `DEFAULT_TRAIN_CONFIG` (trainer.ts:89).
|
|
148
|
+
|
|
149
|
+
| Field (API) | Type | Default | Effect |
|
|
150
|
+
|---|---|---|---|
|
|
151
|
+
| `method` | `sft` \| `dpo` \| `orpo` | `sft` | Training objective (see above) |
|
|
152
|
+
| `rank` | int ≥2 | `8` | LoRA rank per adapted linear |
|
|
153
|
+
| `scale` | float >0 | `1.0` | LoRA α (effective update = α·BA) |
|
|
154
|
+
| `rank_scaling` | `constant` \| `by_bits` \| `by_kl` | `by_bits` | Per-layer rank policy (see Methodology) |
|
|
155
|
+
| `target_modules` | string[] | `q,k,v,o,gate,up,down _proj` | Which linears get adapters |
|
|
156
|
+
| `num_layers` | int | `-1` | `-1` = all layers; `N` = last N only |
|
|
157
|
+
| `iters` | int >0 | `100` | Total training steps |
|
|
158
|
+
| `learning_rate` | float >0 | `2e-4` (sft) / `5e-5` (dpo) | AdamW LR |
|
|
159
|
+
| `max_seq_length` | int >0 | `512` | Truncate/pad sequences to this |
|
|
160
|
+
| `batch_size` | int ≥1 | `1` | Rows per step (B=1 is the safe path; B>1 length-sorts + pads to 32) |
|
|
161
|
+
| `grad_accumulation_steps` | int ≥1 | `1` | Accumulate grads over N micro-steps |
|
|
162
|
+
| `seed` | int | `0` | RNG for shuffling + LoRA init |
|
|
163
|
+
| `steps_per_report` | int >0 | `10` | Emit a train-loss metric every N steps |
|
|
164
|
+
| `steps_per_eval` | int >0 | `50` | Eval on `valid.jsonl` every N steps |
|
|
165
|
+
| `weight_decay` | float ≥0 | `0.01` | AdamW weight decay (β = `[0.9, 0.999]`, fixed) |
|
|
166
|
+
| `lora_dropout` | float [0,1) | `0.0` | LoRA-input dropout (PEFT-style), training-only regularizer for small sets. Recompute-safe (mask keyed by step+layer, so segmented/checkpointed backward reproduces it) |
|
|
167
|
+
| `rs_lora` | bool | `false` | rsLoRA — scale the update by α/√rank instead of α, so `rank_scaling` changes capacity not step size. Recorded in the adapter config; inference applies the same per-layer scale |
|
|
168
|
+
| `lora_plus_ratio` | float ≥1 | `1.0` | LoRA+ — LR multiplier for the B leaves (A stays at base LR). >1 speeds the B-driven early learning (B is zero-init). 1 = off. Wired in the ORPO loop |
|
|
169
|
+
| `grad_checkpoint` | bool | `false` | Recompute layer activations in backward (memory↔compute; bit-identical) |
|
|
170
|
+
| `segment_size` | int | `0` (off) | `>0` enables segmented backward — layers per segment (see below) |
|
|
171
|
+
| `save_checkpoints` | bool | `false` | Save every eval-step checkpoint + write `metrics.json` |
|
|
172
|
+
| `dpo_beta` | float >0 | `0.1` | DPO strength (dpo only) |
|
|
173
|
+
| `dpo_warmup_iters` | int ≥0 | `0` | DPO LR warmup (dpo only) |
|
|
174
|
+
| `dpo_lr_schedule` | `constant` \| `cosine` | `cosine` | DPO LR schedule (dpo only) |
|
|
175
|
+
| `orpo_lambda` | float >0 | `0.1` | ORPO λ — weights **only** the odds-ratio term; the SFT-NLL term stays unweighted (orpo only) |
|
|
176
|
+
| `orpo_warmup_iters` | int ≥0 | `0` | ORPO LR warmup (orpo only) |
|
|
177
|
+
| `orpo_lr_schedule` | `constant` \| `cosine` | `cosine` | ORPO LR schedule (orpo only) |
|
|
178
|
+
| `orpo_chunk_size` | int | `0` (off) | Token-chunk the response head, bounding the `[M,vocab]` logits to `[chunk,vocab]` (defaults to 512 when a fused head is on). Exact (orpo only) |
|
|
179
|
+
| `orpo_fused_ce` | bool | `false` | Fused linear-CE head: one CustomVjp with an analytic softmax−onehot backward (MLX `quantizedMatmul` both ways) — no autograd through the head, no retained `[M,vocab]`. Exact; `[chunk,vocab]` transient (orpo only) |
|
|
180
|
+
| `orpo_flash_ce` | bool | `false` | Route the fused head through the **flash-CCE Metal kernel** (verbatim MLX steel GEMM + ORPO epilogue, fwd **and** bwd): neither `[M,vocab]` nor a dequantized head touches HBM → `[M,vocab]`-free, fastest on large vocab (e4b bwd 754 ms, 0.93 GB flat @ M=8192). Implies the fused head. The Apple-CCE coeff filter / blockMax skip are opt-in via `MLX_BUN_CCE_BWD_FILTER_EPS` / `_BLOCK_EPS` (default off — exact). B=1 (orpo only) |
|
|
181
|
+
| `orpo_prefix_shared` | bool | `false` | Shared prompt-prefix: one forward over `[prompt; chosen; rejected]` with a block-sparse mask + block-wise RoPE, so the shared prompt is encoded **once** (a big win when the prompt dominates). Falls back to two-forward per row on prompt mismatch. Composes with the flash head and the segmented backward (MiniCPM5 + e4b). B=1 (orpo only) |
|
|
182
|
+
|
|
183
|
+
### Environment variables (training)
|
|
184
|
+
|
|
185
|
+
| Env var | Set for training | Default | Why |
|
|
186
|
+
|---|---|---|---|
|
|
187
|
+
| `MLX_BUN_FUSED_GELU` | **`0` (required for Gemma)** | on | The fused GeGLU is a **CustomKernel with no gradient (vjp)** ([`fused-geglu-kernel.ts`](../../src/model/fused-geglu-kernel.ts)); the Gemma forward uses it ([`gemma4.ts:277`](../../src/model/gemma4.ts)), so a Gemma (e4b/12B/26B) backward fails unless it's off. MiniCPM5 (Llama-arch SwiGLU) never hits it, so the `.ts` script's MiniCPM5 default doesn't need it. |
|
|
188
|
+
| `MLX_BUN_PERF_KERNEL` | **`0` for training** | on | The fused quantized-decode kernel likewise has no vjp. It only fires at decode L=1 (rare in the L>1 training forward), but the e4b recipe sets `0` to be safe. |
|
|
189
|
+
| `MLX_BUN_TRAIN_ATTN` | **leave unset** | unset → `ops.sdpa` | Default `ops.sdpa` **is** mlx's fused flash-attention kernel — the correct, working path. `flash` selects a *different* hand-rolled custom kernel that crashes e4b at multi-K; do not set it. |
|
|
190
|
+
| `MLX_BUN_MEM_LOG` | `1` to profile | off | Print per-step peak/active/cache memory |
|
|
191
|
+
|
|
192
|
+
> Important: the trainer itself ([`trainer.ts`](../../src/train/trainer.ts))
|
|
193
|
+
> only reads `MLX_BUN_TRAIN_ATTN` and `MLX_BUN_MEM_LOG` — but the **model
|
|
194
|
+
> forward** it runs reads `MLX_BUN_FUSED_GELU` / `MLX_BUN_PERF_KERNEL`. The
|
|
195
|
+
> trainer does **not** disable those itself, so **the caller must** export
|
|
196
|
+
> them. The [e4b recipe](#what-we-actually-run-the-e4b-recipe) does this; if
|
|
197
|
+
> you train a Gemma model by hand, set `MLX_BUN_FUSED_GELU=0` yourself.
|
|
198
|
+
|
|
199
|
+
## What we actually run (the e4b recipe)
|
|
200
|
+
|
|
201
|
+
Everything above is the full surface (*what you can do*). In practice the
|
|
202
|
+
fine-tune we run is [`scripts/ft-e4b-v2.sh`](../../scripts/ft-e4b-v2.sh):
|
|
203
|
+
e4b (gemma-4-e4b-it-OptiQ-4bit, pinned snapshot) on the lucien `chunk-v2-500`
|
|
204
|
+
curated set (450 train convs) through the segmented-backward trainer. It wraps
|
|
205
|
+
`chunk-finetune.ts` with the e4b-required env and a two-step workflow:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
scripts/ft-e4b-v2.sh probe # 2-iter memory/stability check (~1 min) — RUN FIRST
|
|
209
|
+
scripts/ft-e4b-v2.sh train # the real run (~900 iters ≈ 2 epochs, batch_size 1)
|
|
210
|
+
ITERS=750 SEQ=4096 SEG=1 scripts/ft-e4b-v2.sh train # override any knob inline
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
What the recipe pins (and why it differs from the bare defaults):
|
|
214
|
+
|
|
215
|
+
| Knob | Recipe value | Why |
|
|
216
|
+
|---|---|---|
|
|
217
|
+
| model | e4b OptiQ-4bit, pinned snapshot `fcdb12d7…` | the validated e4b snapshot |
|
|
218
|
+
| data | `chunk-v2-500` (450 train convs) | the curated chunk set |
|
|
219
|
+
| `SEQ` | `8192` | long context |
|
|
220
|
+
| `SEG` | `4` | **segmented backward**, 4 layers/segment — so 8K-ctx activations fit |
|
|
221
|
+
| `RANK` / `SCALE` / `LR` | `16` / `20` / `1e-5` | task-tuned |
|
|
222
|
+
| `ITERS` | `2` (probe) / `900` (train) | ~2 epochs over 450 examples |
|
|
223
|
+
| `MLX_BUN_PERF_KERNEL` / `MLX_BUN_FUSED_GELU` | `0` / `0` | **required** — the fused kernels have no vjp (see env table) |
|
|
224
|
+
| attention | default `ops.sdpa` | mlx's fused flash kernel; **not** `MLX_BUN_TRAIN_ATTN=flash` (that one crashes e4b) |
|
|
225
|
+
|
|
226
|
+
The two non-negotiables for e4b: **segmented backward** (`SEG>0`, so the
|
|
227
|
+
long-context activations fit) and the **fused kernels off** (so the backward
|
|
228
|
+
has gradients). Always run `probe` before `train`.
|
|
229
|
+
|
|
230
|
+
## Methodology
|
|
231
|
+
|
|
232
|
+
### LoRA
|
|
233
|
+
Adapters attach to the target linears; A is initialized uniform, B is zeros,
|
|
234
|
+
so the adapted model equals the base model at step 0. Only A/B are
|
|
235
|
+
differentiated — base quantized weights are frozen. Default targets are the 7
|
|
236
|
+
attention+MLP projections per block (`q/k/v/o_proj`, `gate/up/down_proj`),
|
|
237
|
+
following Unsloth. See [`src/train/lora-params.ts`](../../src/train/lora-params.ts).
|
|
238
|
+
|
|
239
|
+
### Rank scaling (`rank_scaling`)
|
|
240
|
+
- `constant` — every target gets `rank`.
|
|
241
|
+
- `by_bits` *(default)* — `rank × (bits / 4)`, clamped ≥2; gives **wider
|
|
242
|
+
adapters to the more-sensitive (higher-bit) layers** (mixed precision: a
|
|
243
|
+
4-bit layer gets the base rank, an 8-bit layer 2×). This is optiq's "one
|
|
244
|
+
sensitivity signal, two optimizations" — the same KL pass that assigns the
|
|
245
|
+
bits also sets the rank. The per-layer bits are read straight from the loaded
|
|
246
|
+
model's quant specs (auto-wired; no metadata file needed).
|
|
247
|
+
- `by_kl` — scales by per-layer KL importance, clamped to [0.5×, 2×]; reads the
|
|
248
|
+
KL values from `optiq_metadata.json` and falls back to `by_bits` if none are
|
|
249
|
+
recorded.
|
|
250
|
+
|
|
251
|
+
### Long-context memory: segmented backward vs gradient checkpointing
|
|
252
|
+
At long `max_seq_length`, activation memory dominates. Two levers:
|
|
253
|
+
|
|
254
|
+
- `grad_checkpoint: true` — recompute each layer's activations during
|
|
255
|
+
backward. Bit-identical; trades compute for memory.
|
|
256
|
+
- `segment_size: N` — **segmented backward**: run the layer stack forward
|
|
257
|
+
detaching the residual stream into graph-free boundary leaves every N
|
|
258
|
+
layers, then backprop segment-by-segment via `mlx_vjp` (cotangent passed
|
|
259
|
+
directly, *not* a surrogate-loss `value_and_grad`, which leaked). Only one
|
|
260
|
+
segment's activations live at a time. This is the path to multi-K context;
|
|
261
|
+
full mechanism, proofs, and measured peaks (e.g. MiniCPM5 10.91→3.29 GB
|
|
262
|
+
@2048) are in
|
|
263
|
+
[segmented-backward-training](../design/segmented-backward-training.md).
|
|
264
|
+
|
|
265
|
+
### Training attention kernel (`MLX_BUN_TRAIN_ATTN`)
|
|
266
|
+
- **default `ops.sdpa`** — mlx's fused SDPA; correct (0.00% vs autograd),
|
|
267
|
+
O(L²) backward memory. Use this.
|
|
268
|
+
- **`flash`** — opt-in O(L) memory path, but the hand-rolled dK kernel is
|
|
269
|
+
slow and **crashes e4b at multi-K (≥2K)**; do not use it for e4b LoRA
|
|
270
|
+
training. Detail in segmented-backward-training §6.
|
|
271
|
+
|
|
272
|
+
## Outputs
|
|
273
|
+
|
|
274
|
+
A finished run writes a PEFT-compatible adapter directory:
|
|
275
|
+
|
|
276
|
+
- `adapters.safetensors` — the `lora_a` / `lora_b` tensors
|
|
277
|
+
- `optiq_lora_config.json` — mlx-bun/optiq adapter metadata (per-layer ranks)
|
|
278
|
+
- `adapter_config.json` — PEFT-compatible config
|
|
279
|
+
|
|
280
|
+
When `save_checkpoints: true`, each eval step also writes
|
|
281
|
+
`checkpoints/step-<NNNNN>-val<loss>/` and a durable `metrics.json` (config,
|
|
282
|
+
wall seconds, peak GB, final/best train+val loss, full val trajectory).
|
|
283
|
+
|
|
284
|
+
**Serving the adapter:** hot-swap it into a running server via the adapter
|
|
285
|
+
API and select it per-request — see
|
|
286
|
+
[adapters-end-to-end](../design/adapters-end-to-end.md) and the adapter
|
|
287
|
+
endpoints in [server-api](server-api.md). Or fold it into the base weights
|
|
288
|
+
with `POST /api/finetune/merge`.
|
|
289
|
+
|
|
290
|
+
## Memory & performance tips
|
|
291
|
+
|
|
292
|
+
- Start at `batch_size: 1` (the no-padding path); raise only with headroom.
|
|
293
|
+
- OOM → lower `max_seq_length`, set `segment_size` (e.g. 2–4), or reduce
|
|
294
|
+
`rank` / `num_layers`.
|
|
295
|
+
- Set `MLX_BUN_MEM_LOG=1` to watch per-step peak memory.
|
|
296
|
+
- `grad_accumulation_steps` raises effective batch without the memory cost.
|
package/package.json
CHANGED