Stage 3: in-plugin autoencoder training + dataset building - #6
Conversation
…e 3) Port the upstream Python training pipeline into the live Rust plugin so users can build a dataset and train the autoencoder in-session, then play their own model. Previously only the frozen decoder shipped; training was offline-only. New modules: - autoencoder.rs: pure f64 full AE (encoder+decoder), manual backprop, Adam, BatchNorm. Fixes two upstream bugs: broken Adam bias correction (per-param step counter t) and no batch shuffle. Converges better; not a numeric match. - model_ops.rs: op-list export byte-compatible with decoder.json / train_export.py, so a trained model loads into decoder.rs unchanged. - dataset.rs: encode_onsets (exact corpus_encode.py port incl. banker's rounding) + encode_grid (capture live pattern). - audio.rs: symphonia decode + spectral-flux onset detection (rustfft). - training.rs: TrainShared + background task executor + arc-swap hot-swap. Wiring: - Decoder hot-swaps via ArcSwap (audio thread wait-free; never locks a Mutex). model_generation invalidates the regen cache on swap. - Trained model persists in DAW state via #[persist] trained_model; baked decoder.json stays the default/fallback. Restored in initialize(). - SharedState gains per-step substeps so the GUI can capture/encode patterns. - editor.rs: Training panel (capture / add audio / train / cancel / progress + loss / encode pattern->latent). Deps: arc-swap, rustfft, rfd, symphonia (wav+flac). Tests: 34 cargo tests (AE overfit gradient gate, Adam-fix, shuffle determinism, export<->decoder parity, corpus-encode parity with Python cases, onset sanity, end-to-end train->swap->persist->encode). clippy -D warnings clean; CLAP + VST3 bundle builds; both headless host scale-tests pass.
…init, identity_op) CI clippy (-D warnings) failed on 7 style lints that a stale local clippy cache had masked: col_mean and three test loops use enumerate instead of range-index; Autoencoder::new builds via vec![]/extend instead of init-then-push; the onset test uses usize::abs_diff; and a test mask drops a no-op `& 0xFFFF`. No behaviour change; 34 tests still pass.
Code reviewOverviewSolid, well-tested stage. Training is correctly isolated from the audio thread, the export format reuses the existing FindingsM1 — RT-safety: model hot-swap can deallocate the old decoder on the audio thread. ( M2 — L1 — encode→latent round-trip is lossy. The encoder ends at L2 — loss gradient isn't normalized by batch size. L3 — long audio files collapse to one bar. L4 — L5 — minor Train/Ingest enable race. L6 — L7 — Positives
VerdictApprove with nits. M1 (audio-thread dealloc) is the one I'd fix before relying on this in a live set; M2 and the L-items are cleanups. 🤖 Generated with Claude Code |
Address PR #6 review. M1 (RT-safety): `ArcSwap::load()` is wait-free but not allocation-free — after a `store`, the audio thread could hold the last reference to the old `Arc<Decoder>` and free its heap inside `process()` when the load guard dropped. Hot-swaps now go through `TrainShared::swap_model`, which uses `ArcSwap::swap` and parks the displaced decoder in a generation- tagged graveyard. The audio thread publishes `gen_acked` at the end of `maybe_regen` (after its load guard is dropped, Release-ordered); `collect_garbage` (GUI/background thread, Acquire) then drops only the retired decoders the audio thread has provably moved past, so the heap free never runs on the audio thread. Drained from the editor frame, the background executor, and `initialize`. New unit test covers retire/ack/ collect and the live decoder staying intact. M2: `run_ingest` now restores the captured prior status verbatim (so a prior `Done` survives an audio ingest) instead of collapsing everything non-`Running` to `Idle`; only a transient `Ingesting` falls back to Idle. L1: "Encode pattern → latent" gains a hover note that latents are clamped to 0..1 so the round-trip is approximate. L2: documented that batch size acts as a secondary LR knob in `fit`. L4: documented that `pick_files()` blocks only the editor thread. L6: `initialize` uses `swap_model` (retiring restore) and collects garbage; the redundant-swap concern is now bounded by the graveyard and never touches the audio thread.
Review addressed —
|
What
Ports the upstream Python training pipeline into the live Rust plugin. Previously only the frozen decoder shipped (inference-only); training was offline-only in Python. Now the user builds a dataset and trains the autoencoder in-session, then plays their own model.
Highlights
autoencoder.rs) — encoder + decoder, manual backprop, Adam, BatchNorm. Fixes two upstream bugs: broken Adam bias correction (per-param step countert) and no batch shuffle. Converges better; not a numeric match to Python (by design).Capture pattern(snapshot the live grid) andAdd audio…(decode.wav/.flacvia symphonia → spectral-flux onset detection via rustfft → 32-dim sample). Both sources accumulate.ArcSwap(process()does a wait-freeload(), never locks aMutex).model_generationinvalidates the regen cache on swap.Encode pattern → latentsets the 4 latent sliders to the current grid's latent code.#[persist]; bakeddecoder.jsonstays the default/fallback.Design doc:
docs/plans/2026-06-16-runtime-training-design.md.New deps
arc-swap,rustfft,rfd,symphonia(wav+flac).Testing
corpus_encodeparity with the Pythontest_corpus_encode.pycases, onset sanity, and an end-to-end train→hot-swap→persist→encode integration test.cargo clippy --all-targets -- -D warningsclean.pluginvalruns in CI.Manual verification still needed
GUI check in Carla (VST3): train a model, watch the loss bar, confirm the pattern swaps, test
Encode → latent, save/reload the project to verify persistence.