Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
91e3a28
Add MLX LoRA training and audio encoding primitives
betweentwomidnights Jun 14, 2026
e5af3b4
Clarify odd-overlap chunk stitching
betweentwomidnights Jun 15, 2026
669618c
Cover optimized small and medium DiT LoRA injection
betweentwomidnights Jun 26, 2026
c05cbbd
Match PyTorch masked loss averaging
betweentwomidnights Jun 26, 2026
7d59118
docs: underfit training-convention inventory + port gap analysis
Jul 14, 2026
06fe643
mlx lora: underfit checkpoint conventions — key roots, conditioner ad…
Jul 14, 2026
dbea9ac
mlx training data: pre-encode CLI + latent dataset with underfit sema…
Jul 14, 2026
3d4ca37
mlx trainer: lora_train_mlx.py — underfit loop conventions end to end
Jul 14, 2026
db8c55c
mlx trainer: base-ckpt training + the two parity-critical forward con…
Jul 14, 2026
a13e9ba
mlx lora: reformulate DoRA training forward — no weight materialization
Jul 14, 2026
b446231
mlx trainer: compiled step, T5 conditioning cache, wired limit, --gra…
Jul 14, 2026
577a041
mlx lora: BoRA speed-mode reformulation + --bora-mode {speed,memory}
Jul 14, 2026
d39980b
mlx trainer: training-time demos (RF Euler + CFG) + gradio LoRA preload
Jul 15, 2026
2aac00a
mlx trainer: full default-config support (effective-length, optimizer…
Jul 15, 2026
35c7dee
mlx: §9 forward-parity harness (torch MPS vs MLX, bug-localizing)
Jul 15, 2026
a80e1c7
mlx: §9 harness also checks the backward (d loss/d noised)
Jul 15, 2026
42ed8f6
ci: MLX test job on Apple Silicon (macos-latest / arm64)
Jul 15, 2026
98d4d2f
docs: README LoRA training section (pre-encode → train → generate)
Jul 15, 2026
fb2d51c
mlx: auto-download base-model npz for training + export_base_npz.py c…
Jul 15, 2026
cf8194c
mlx pre-encode: print engine + device banner
Jul 15, 2026
ba8fed8
mlx trainer: print engine + device banner (Engine: MLX / device: ...)
Jul 15, 2026
78ddeca
mlx pre-encode: --exclude-file support (skip relpaths, torch pre_enco…
Jul 15, 2026
ccadf8a
MLX trainer: torch-identical tqdm output + ARC demos
Jul 15, 2026
71061c5
MLX demos: honor per-entry length; make the DiT length-agnostic
Jul 15, 2026
abac31b
MLX demos: live step-progress so slow full-length demos don't look hung
Jul 15, 2026
4527a75
MLX demos: report per-demo wall-clock in the ♪ line
Jul 15, 2026
9fba072
MLX trainer: note the SAME-S demo-decoder substitution in the console
Jul 15, 2026
0f5c31c
MLX trainer: write the ARC-demo scratch LoRA outside the checkpoints dir
Jul 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/mlx.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: MLX (Apple Silicon)

# GitHub-hosted `macos-latest` runners are Apple Silicon (arm64), which the MLX
# runtime requires. Scoped to optimized/mlx changes so it doesn't spend macOS
# runner minutes on unrelated PRs.
on:
pull_request:
paths:
- "optimized/mlx/**"
- ".github/workflows/mlx.yml"

jobs:
test:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install MLX deps
run: |
python -m pip install --upgrade pip
pip install -r optimized/mlx/requirements.txt pytest
- name: Adapter math — all 9 LoRA/DoRA/BoRA types (weight-free)
run: pytest optimized/mlx/scripts/test_lora_merge.py -q
- name: Import + CLI smoke (no weights downloaded)
working-directory: optimized/mlx
run: |
python -c "import mlx.core; from models.defs import lora, training, demo_mlx, latent_dataset, lora_merge, sa3_pipeline; print('core imports OK')"
python scripts/lora_train_mlx.py --help > /dev/null
python scripts/pre_encode_mlx.py --help > /dev/null
echo "CLI smoke OK"
3 changes: 3 additions & 0 deletions optimized/mlx/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ output/gradio/

# Local LoRA library scanned by the gradio UI (user adapters, never committed)
loras/

# Trainer telemetry (written to cwd, underfit convention)
loss_by_timestep.bin
80 changes: 78 additions & 2 deletions optimized/mlx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,70 @@ Sample run on **M4 Pro / 48 GB**:
└───────────┴─────────┴─────────┴───────────┴───────────┴────────────┘
```

## LoRA training

Finetune SA3 on your own audio, entirely in MLX (no PyTorch) — a full training
CLI that replicates [underfit](https://github.com/dada-bots/underfit)'s
conventions, defaults, and checkpoint format (so it can run as underfit's
Apple-Silicon backend). Three steps: **pre-encode → train → generate**. See
`TRAINING_CONVENTIONS.md` for the complete convention inventory and the
torch(MPS)-vs-MLX forward+backward parity results.

**1. Pre-encode** your audio to SAME latents (once, offline — torch-free):

```bash
uv run python scripts/pre_encode_mlx.py \
--audio-dir ~/my-clips --output-dir ~/my-latents --codec same-s
```

Each file becomes `<stem>.npy` latents `[D, T]` + a `<stem>.json` sidecar
(duration, padding mask, tags) — the exact format underfit's dataset uses.
Use `same-s` for sm-music/sm-sfx, `same-l` for medium.

**2. Train.** Training uses the **BASE** checkpoint (`stabilityai/stable-audio-3-*-base`,
rectified_flow), *not* the shipped ARC weights inference uses. The base-model npz is
**auto-downloaded** from HuggingFace on first run — no flag needed (override with
`--dit-weights <path>`; regenerate one yourself from a `-base` repo with
`scripts/export_base_npz.py`):

```bash
uv run python scripts/lora_train_mlx.py \
--dit sm-music --latents-dir ~/my-latents --lr 1e-4 --name my-lora \
--adapter-type dora-rows --rank 16 --max-steps 2000
```

Defaults mirror underfit (dora-rows / rank 16, AdamW, uniform sampler + the
"full" distribution shift, CFG-dropout 0.1, signal-only masked rectified-flow
loss, checkpoint every 1000 steps). To match the full dashboard template config,
add `--timestep-sampler trunc_logit_normal --use-effective-length --beta2 0.95
--weight-decay 0.01 --lr-scheduler inverse --lr-warmup 0.995`. Checkpoints land at
`output/runs/<name>/<uuid>/checkpoints/<name>-step=S-epoch=E.safetensors` with
`lora_config` metadata; resume with `--lora-ckpt-path <ckpt>`.

Optional training-time demos (RF-Euler inference → mp3, underfit's on-disk
format): `--demo-every 1000 --demo-config demos.json`, where `demos.json` is a
list of `{"prompt", "cfg", "seed", "steps", "lora_strength"?, "lora_interval_max"?}`.

**3. Generate** with your adapter — the checkpoint applies to the shipped ARC
model at inference via `--lora` (see "Apply a LoRA finetune" above; also
`scripts/sa3_gradio.py --lora <ckpt>` to open the web UI with it preloaded):

```bash
./sa3 --dit sm-music --decoder same-s --prompt "..." --out finetuned.wav \
--lora "output/runs/my-lora/"*"/checkpoints/my-lora-step=2000-epoch="*.safetensors
```

### Building your own loop

The CLI is assembled from reusable pieces: `inject_trainable_lora` /
`save_lora_checkpoint` / `apply_lora_checkpoint` (`models/defs/lora.py` — all 9
LoRA/DoRA/BoRA/-XS types with the no-weight-materialization forward), the RF
loss + samplers + distribution shift (`models/defs/training.py`), the
pre-encoded dataset + prompt templates (`models/defs/latent_dataset.py`), and
`encode_audio` (`models/defs/audio_encoding.py`). Checkpoints use the same
safetensors keys + `lora_config` metadata as the PyTorch trainer, so adapters
are interchangeable in either direction.

## Flag reference

| Flag | Default | Notes |
Expand Down Expand Up @@ -221,6 +285,7 @@ sa3_mlx/
├── sa3 ← shell wrapper (use this)
├── install.sh ← uv bootstrap (run once)
├── README.md
├── TRAINING_CONVENTIONS.md ← LoRA-training conventions + MPS-vs-MLX parity
├── requirements.txt
├── output/ ← default landing zone for generated WAVs
├── scripts/
Expand All @@ -229,15 +294,26 @@ sa3_mlx/
│ ├── examples.py ← shared examples block (--help + post-install)
│ ├── install.py ← install.sh's Python half (bundle picker)
│ ├── test_all_configs.py ← npz + CLI config sanity tests
│ └── benchmark.py ← wall-time + peak-RAM matrix across model × duration
│ ├── benchmark.py ← wall-time + peak-RAM matrix across model × duration
│ ├── lora_train_mlx.py ← LoRA training CLI (underfit conventions)
│ ├── pre_encode_mlx.py ← audio → SAME-latent pre-encode (torch-free)
│ ├── sa3_gradio.py ← web UI (invoked by ./sa3-gradio; --lora preload)
│ ├── test_lora_merge.py ← adapter-math tests (weight-free; run in CI)
│ └── parity_forward_{torch,mlx}.py ← torch(MPS)-vs-MLX fwd+bwd parity harness
└── models/
├── defs/
│ ├── sa3_pipeline.py ← sampler + conditioner + unpatch
│ ├── t5gemma_mlx.py ← T5Gemma encoder + SentencePiece wrapper
│ ├── dit_mlx.py ← small DiT (sm-music + sm-sfx)
│ ├── dit_mlx_medium.py ← medium DiT (differential attention)
│ ├── same_s_{encoder,decoder}.py ← small codec
│ └── same_l_{encoder,decoder}.py ← large codec
│ ├── same_l_{encoder,decoder}.py ← large codec
│ ├── lora.py ← trainable LoRA/DoRA/BoRA adapters (9 types)
│ ├── lora_merge.py ← inference-time LoRA merge + per-step gating
│ ├── training.py ← RF loss + timestep samplers + distribution shift
│ ├── latent_dataset.py ← pre-encoded dataset + underfit prompt templates
│ ├── audio_encoding.py ← waveform → SAME latents (pre-encode bridge)
│ └── demo_mlx.py ← training-time RF-Euler demos → mp3
└── mlx/ ← .npz weights (auto-downloaded; ~8.4 GB total)
├── t5gemma_f16.npz 541 MB text encoder + tokenizer
├── dit_sm-music_f16.npz 877 MB DiT + conditioner baked in
Expand Down
Loading
Loading