@@ -191,6 +191,70 @@ Sample run on **M4 Pro / 48 GB**:
191191└───────────┴─────────┴─────────┴───────────┴───────────┴────────────┘
192192```
193193
194+ ## LoRA training
195+
196+ Finetune SA3 on your own audio, entirely in MLX (no PyTorch) — a full training
197+ CLI that replicates [ underfit] ( https://github.com/dada-bots/underfit ) 's
198+ conventions, defaults, and checkpoint format (so it can run as underfit's
199+ Apple-Silicon backend). Three steps: ** pre-encode → train → generate** . See
200+ ` TRAINING_CONVENTIONS.md ` for the complete convention inventory and the
201+ torch(MPS)-vs-MLX forward+backward parity results.
202+
203+ ** 1. Pre-encode** your audio to SAME latents (once, offline — torch-free):
204+
205+ ``` bash
206+ uv run python scripts/pre_encode_mlx.py \
207+ --audio-dir ~ /my-clips --output-dir ~ /my-latents --codec same-s
208+ ```
209+
210+ Each file becomes ` <stem>.npy ` latents ` [D, T] ` + a ` <stem>.json ` sidecar
211+ (duration, padding mask, tags) — the exact format underfit's dataset uses.
212+ Use ` same-s ` for sm-music/sm-sfx, ` same-l ` for medium.
213+
214+ ** 2. Train.** Training uses the ** BASE** checkpoint (` stabilityai/stable-audio-3-*-base ` ,
215+ rectified_flow), * not* the shipped ARC weights inference uses. The base-model npz is
216+ ** auto-downloaded** from HuggingFace on first run — no flag needed (override with
217+ ` --dit-weights <path> ` ; regenerate one yourself from a ` -base ` repo with
218+ ` scripts/export_base_npz.py ` ):
219+
220+ ``` bash
221+ uv run python scripts/lora_train_mlx.py \
222+ --dit sm-music --latents-dir ~ /my-latents --lr 1e-4 --name my-lora \
223+ --adapter-type dora-rows --rank 16 --max-steps 2000
224+ ```
225+
226+ Defaults mirror underfit (dora-rows / rank 16, AdamW, uniform sampler + the
227+ "full" distribution shift, CFG-dropout 0.1, signal-only masked rectified-flow
228+ loss, checkpoint every 1000 steps). To match the full dashboard template config,
229+ add `--timestep-sampler trunc_logit_normal --use-effective-length --beta2 0.95
230+ --weight-decay 0.01 --lr-scheduler inverse --lr-warmup 0.995`. Checkpoints land at
231+ ` output/runs/<name>/<uuid>/checkpoints/<name>-step=S-epoch=E.safetensors ` with
232+ ` lora_config ` metadata; resume with ` --lora-ckpt-path <ckpt> ` .
233+
234+ Optional training-time demos (RF-Euler inference → mp3, underfit's on-disk
235+ format): ` --demo-every 1000 --demo-config demos.json ` , where ` demos.json ` is a
236+ list of ` {"prompt", "cfg", "seed", "steps", "lora_strength"?, "lora_interval_max"?} ` .
237+
238+ ** 3. Generate** with your adapter — the checkpoint applies to the shipped ARC
239+ model at inference via ` --lora ` (see "Apply a LoRA finetune" above; also
240+ ` scripts/sa3_gradio.py --lora <ckpt> ` to open the web UI with it preloaded):
241+
242+ ``` bash
243+ ./sa3 --dit sm-music --decoder same-s --prompt " ..." --out finetuned.wav \
244+ --lora " output/runs/my-lora/" * " /checkpoints/my-lora-step=2000-epoch=" * .safetensors
245+ ```
246+
247+ ### Building your own loop
248+
249+ The CLI is assembled from reusable pieces: ` inject_trainable_lora ` /
250+ ` save_lora_checkpoint ` / ` apply_lora_checkpoint ` (` models/defs/lora.py ` — all 9
251+ LoRA/DoRA/BoRA/-XS types with the no-weight-materialization forward), the RF
252+ loss + samplers + distribution shift (` models/defs/training.py ` ), the
253+ pre-encoded dataset + prompt templates (` models/defs/latent_dataset.py ` ), and
254+ ` encode_audio ` (` models/defs/audio_encoding.py ` ). Checkpoints use the same
255+ safetensors keys + ` lora_config ` metadata as the PyTorch trainer, so adapters
256+ are interchangeable in either direction.
257+
194258## Flag reference
195259
196260| Flag | Default | Notes |
@@ -221,6 +285,7 @@ sa3_mlx/
221285├── sa3 ← shell wrapper (use this)
222286├── install.sh ← uv bootstrap (run once)
223287├── README.md
288+ ├── TRAINING_CONVENTIONS.md ← LoRA-training conventions + MPS-vs-MLX parity
224289├── requirements.txt
225290├── output/ ← default landing zone for generated WAVs
226291├── scripts/
@@ -229,15 +294,26 @@ sa3_mlx/
229294│ ├── examples.py ← shared examples block (--help + post-install)
230295│ ├── install.py ← install.sh's Python half (bundle picker)
231296│ ├── test_all_configs.py ← npz + CLI config sanity tests
232- │ └── benchmark.py ← wall-time + peak-RAM matrix across model × duration
297+ │ ├── benchmark.py ← wall-time + peak-RAM matrix across model × duration
298+ │ ├── lora_train_mlx.py ← LoRA training CLI (underfit conventions)
299+ │ ├── pre_encode_mlx.py ← audio → SAME-latent pre-encode (torch-free)
300+ │ ├── sa3_gradio.py ← web UI (invoked by ./sa3-gradio; --lora preload)
301+ │ ├── test_lora_merge.py ← adapter-math tests (weight-free; run in CI)
302+ │ └── parity_forward_{torch,mlx}.py ← torch(MPS)-vs-MLX fwd+bwd parity harness
233303└── models/
234304 ├── defs/
235305 │ ├── sa3_pipeline.py ← sampler + conditioner + unpatch
236306 │ ├── t5gemma_mlx.py ← T5Gemma encoder + SentencePiece wrapper
237307 │ ├── dit_mlx.py ← small DiT (sm-music + sm-sfx)
238308 │ ├── dit_mlx_medium.py ← medium DiT (differential attention)
239309 │ ├── same_s_{encoder,decoder}.py ← small codec
240- │ └── same_l_{encoder,decoder}.py ← large codec
310+ │ ├── same_l_{encoder,decoder}.py ← large codec
311+ │ ├── lora.py ← trainable LoRA/DoRA/BoRA adapters (9 types)
312+ │ ├── lora_merge.py ← inference-time LoRA merge + per-step gating
313+ │ ├── training.py ← RF loss + timestep samplers + distribution shift
314+ │ ├── latent_dataset.py ← pre-encoded dataset + underfit prompt templates
315+ │ ├── audio_encoding.py ← waveform → SAME latents (pre-encode bridge)
316+ │ └── demo_mlx.py ← training-time RF-Euler demos → mp3
241317 └── mlx/ ← .npz weights (auto-downloaded; ~8.4 GB total)
242318 ├── t5gemma_f16.npz 541 MB text encoder + tokenizer
243319 ├── dit_sm-music_f16.npz 877 MB DiT + conditioner baked in
0 commit comments