The model code was spread across eight crates that had grown into each other:
ggml and cuda and mlx each owned part of a tensor runtime, llama and tts and
voice2 each owned part of a model, and libs/diffusion owned everything else.
They are now one tree with an explicit shape:
libs/ai/cuda — kernels and launch surface
libs/ai/metal — Metal shaders and the shim
libs/ai/llm — the language-model runtime (sessions, lanes, contexts,
the CUDA and Metal executors, the compiled Metal path)
libs/ai/models/ — common, flux, h3, music, paint, speech, stems, vision
libs/diffusion is not deleted but demoted: what remains is the VALIDATOR
crate — several dozen `*_validate.rs` oracles that check a native
implementation against a reference, which is where they belong now that the
implementations live next door.
The functional work inside the move is mostly in the LLM runtime: N lanes that
draft while one verify batch serves all of them, per-slot prefill over a shared
folded attention arena, speculation that survives batching, and a scheduler
that reports rather than publishes. And in the CUDA build: a machine without
usable CUDA must still LINK (and say so), the default kernel arch is the
building machine's GPU, `NO_CUDA` forces the stub even where the toolkit
exists, and kernels compile in parallel with progress.
libs/video_flow is new here: classical optical flow estimation and the `mkfl`
motion-field payload — a flow field measured from a clip without a model,
which is what drives free-rate bounce-looping playback and the uprez/tween
enhance pipe.
229 lines
8.4 KiB
Rust
229 lines
8.4 KiB
Rust
//! Parity against the PyTorch reference (the "oracle").
|
|
//!
|
|
//! The oracle lives at `local/stems_ref/` (gitignored: a pinned checkout of
|
|
//! ZFTurbo/Music-Source-Separation-Training @ 2ba884c2, the published
|
|
//! checkpoint, and `oracle.py` which dumps per-stage taps as `.npy`). Every
|
|
//! test here SKIPS when that tree is absent, so CI on a machine without the
|
|
//! 527 MB checkpoint stays green; on a machine that has it, they are the
|
|
//! contract.
|
|
//!
|
|
//! Regenerate the fixtures with:
|
|
//! ```text
|
|
//! cd local/stems_ref
|
|
//! ./venv/bin/python oracle.py taps --in fixtures/music_11s.wav --out taps_cpu --device cpu
|
|
//! ```
|
|
|
|
use makepad_ai_stems::config::{AUDIO_CHANNELS, CHUNK_SAMPLES, NUM_STEMS, STEM_NAMES};
|
|
use makepad_ai_stems::{StemsModel, StereoBuf};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
fn oracle_root() -> PathBuf {
|
|
PathBuf::from(concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/../../../../local/stems_ref"
|
|
))
|
|
}
|
|
|
|
fn checkpoint() -> PathBuf {
|
|
oracle_root().join("ckpt/model_bs_roformer_ep_17_sdr_9.6568.ckpt")
|
|
}
|
|
|
|
/// Minimal `.npy` reader: little-endian f32, C order — everything `oracle.py`
|
|
/// writes.
|
|
fn read_npy(path: &Path) -> (Vec<usize>, Vec<f32>) {
|
|
let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
|
|
assert_eq!(&bytes[0..6], b"\x93NUMPY", "{} is not a .npy", path.display());
|
|
let header_len = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
|
|
let header = std::str::from_utf8(&bytes[10..10 + header_len]).unwrap();
|
|
assert!(
|
|
header.contains("'<f4'") || header.contains("\"<f4\""),
|
|
"{} is not float32: {header}",
|
|
path.display()
|
|
);
|
|
assert!(
|
|
header.contains("'fortran_order': False"),
|
|
"{} is fortran-ordered",
|
|
path.display()
|
|
);
|
|
let open = header.find('(').unwrap();
|
|
let close = header[open..].find(')').unwrap() + open;
|
|
let shape: Vec<usize> = header[open + 1..close]
|
|
.split(',')
|
|
.map(str::trim)
|
|
.filter(|s| !s.is_empty())
|
|
.map(|s| s.parse().unwrap())
|
|
.collect();
|
|
let data_at = 10 + header_len;
|
|
let values: Vec<f32> = bytes[data_at..]
|
|
.chunks_exact(4)
|
|
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
|
|
.collect();
|
|
(shape, values)
|
|
}
|
|
|
|
struct Diff {
|
|
max_abs: f32,
|
|
rms_error: f64,
|
|
rms_signal: f64,
|
|
}
|
|
|
|
impl Diff {
|
|
fn of(got: &[f32], want: &[f32]) -> Diff {
|
|
assert_eq!(got.len(), want.len());
|
|
let mut max_abs = 0.0f32;
|
|
let mut err = 0.0f64;
|
|
let mut sig = 0.0f64;
|
|
for (g, w) in got.iter().zip(want) {
|
|
assert!(g.is_finite(), "non-finite output value {g}");
|
|
let d = (g - w).abs();
|
|
max_abs = max_abs.max(d);
|
|
err += (d as f64) * (d as f64);
|
|
sig += (*w as f64) * (*w as f64);
|
|
}
|
|
Diff {
|
|
max_abs,
|
|
rms_error: (err / got.len() as f64).sqrt(),
|
|
rms_signal: (sig / got.len() as f64).sqrt(),
|
|
}
|
|
}
|
|
|
|
/// Signal-to-noise ratio of our output against the oracle's, in dB.
|
|
fn snr_db(&self) -> f64 {
|
|
if self.rms_error <= 0.0 {
|
|
return f64::INFINITY;
|
|
}
|
|
20.0 * (self.rms_signal / self.rms_error).log10()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn one_chunk_matches_the_pytorch_reference() {
|
|
let root = oracle_root();
|
|
let taps = root.join("taps_cpu");
|
|
let ckpt = checkpoint();
|
|
if !ckpt.is_file() || !taps.join("00_input.npy").is_file() {
|
|
eprintln!(
|
|
"SKIP: oracle tree absent (want {} and {}/00_input.npy)",
|
|
ckpt.display(),
|
|
taps.display()
|
|
);
|
|
return;
|
|
}
|
|
|
|
let (in_shape, input) = read_npy(&taps.join("00_input.npy"));
|
|
assert_eq!(in_shape, vec![1, AUDIO_CHANNELS, CHUNK_SAMPLES]);
|
|
let chunk = StereoBuf {
|
|
left: input[..CHUNK_SAMPLES].to_vec(),
|
|
right: input[CHUNK_SAMPLES..2 * CHUNK_SAMPLES].to_vec(),
|
|
};
|
|
|
|
let load = std::time::Instant::now();
|
|
let mut model = match StemsModel::load(&ckpt) {
|
|
Ok(model) => model,
|
|
Err(e) => {
|
|
// A box with no usable device runtime is a valid skip for this
|
|
// suite (the arithmetic contract is what is under test, and it
|
|
// needs a device to produce anything at all).
|
|
eprintln!("SKIP: could not build the separator: {e}");
|
|
return;
|
|
}
|
|
};
|
|
eprintln!("load+compile: {:.2}s", load.elapsed().as_secs_f64());
|
|
|
|
let run = std::time::Instant::now();
|
|
let stems = model.separate_chunk(&chunk).expect("separate_chunk");
|
|
let chunk_secs = run.elapsed().as_secs_f64();
|
|
eprintln!(
|
|
"one chunk: {chunk_secs:.3}s for {:.2}s of audio ({:.2}x realtime)",
|
|
CHUNK_SAMPLES as f64 / 44100.0,
|
|
CHUNK_SAMPLES as f64 / 44100.0 / chunk_secs
|
|
);
|
|
|
|
let (recon_shape, recon) = read_npy(&taps.join("09_recon.npy"));
|
|
assert_eq!(recon_shape, vec![1, NUM_STEMS, AUDIO_CHANNELS, CHUNK_SAMPLES]);
|
|
|
|
let mut worst_snr = f64::INFINITY;
|
|
let mut worst_max = 0.0f32;
|
|
let mut loudest_rms = 0.0f64;
|
|
for stem in 0..NUM_STEMS {
|
|
for ch in 0..AUDIO_CHANNELS {
|
|
let at = (stem * AUDIO_CHANNELS + ch) * CHUNK_SAMPLES;
|
|
let want = &recon[at..at + CHUNK_SAMPLES];
|
|
let diff = Diff::of(stems[stem].channel(ch), want);
|
|
eprintln!(
|
|
" {:>6} ch{ch}: max_abs {:.3e} snr {:.1} dB (rms {:.6})",
|
|
STEM_NAMES[stem],
|
|
diff.max_abs,
|
|
diff.snr_db(),
|
|
diff.rms_signal
|
|
);
|
|
worst_max = worst_max.max(diff.max_abs);
|
|
loudest_rms = loudest_rms.max(diff.rms_signal);
|
|
// Per-stem SNR is only meaningful for a stem that carries signal.
|
|
// On a classical fixture the drums and bass stems are ~1e-6 RMS,
|
|
// where a 1e-7 absolute deviation reads as a poor "SNR" while
|
|
// actually being an exact match; those are gated on max_abs below.
|
|
if diff.rms_signal > 1e-3 {
|
|
worst_snr = worst_snr.min(diff.snr_db());
|
|
} else {
|
|
assert!(
|
|
diff.max_abs < 1e-5,
|
|
"near-silent stem {} ch{ch} deviates by {:.3e}",
|
|
STEM_NAMES[stem],
|
|
diff.max_abs
|
|
);
|
|
}
|
|
}
|
|
}
|
|
eprintln!(
|
|
"worst: max_abs {worst_max:.3e}, snr {worst_snr:.1} dB (loudest stem rms {loudest_rms:.4})"
|
|
);
|
|
|
|
// 16 transformer layers of f32 arithmetic reordered by a different kernel
|
|
// set will not be bit-identical; what must hold is that the difference is
|
|
// numerical noise, not a wrong graph. 55 dB is ~0.2% of a stem's own RMS —
|
|
// a convention or layout bug lands far below that (a transposed axis or
|
|
// the wrong RoPE flavour scores single-digit dB), so this is a sharp gate.
|
|
assert!(
|
|
worst_snr > 55.0,
|
|
"reconstruction SNR vs the oracle is only {worst_snr:.1} dB"
|
|
);
|
|
// Absolute ceiling relative to full scale, so a loud stem cannot hide a
|
|
// localized blow-up behind a good average.
|
|
assert!(worst_max < 2e-3, "max abs deviation {worst_max:.3e}");
|
|
}
|
|
|
|
#[test]
|
|
fn stems_sum_back_to_the_mixture() {
|
|
// BS-RoFormer masks are not constrained to sum to one, so this is a loose
|
|
// sanity property rather than an identity: the four stems together must
|
|
// reconstruct most of the input's energy. It catches a stem being silent
|
|
// or duplicated without needing the oracle's own output.
|
|
let root = oracle_root();
|
|
let taps = root.join("taps_cpu");
|
|
let ckpt = checkpoint();
|
|
if !ckpt.is_file() || !taps.join("09_recon.npy").is_file() {
|
|
eprintln!("SKIP: oracle tree absent");
|
|
return;
|
|
}
|
|
let (_, input) = read_npy(&taps.join("00_input.npy"));
|
|
let (_, recon) = read_npy(&taps.join("09_recon.npy"));
|
|
for ch in 0..AUDIO_CHANNELS {
|
|
let mut sum = vec![0.0f32; CHUNK_SAMPLES];
|
|
for stem in 0..NUM_STEMS {
|
|
let at = (stem * AUDIO_CHANNELS + ch) * CHUNK_SAMPLES;
|
|
for (s, v) in sum.iter_mut().zip(&recon[at..at + CHUNK_SAMPLES]) {
|
|
*s += v;
|
|
}
|
|
}
|
|
let mix = &input[ch * CHUNK_SAMPLES..(ch + 1) * CHUNK_SAMPLES];
|
|
let diff = Diff::of(&sum, mix);
|
|
eprintln!(" oracle stem-sum ch{ch}: snr {:.1} dB", diff.snr_db());
|
|
assert!(
|
|
diff.snr_db() > 10.0,
|
|
"the reference's own stems do not re-sum to the mixture ({:.1} dB) — \
|
|
the fixture is stale",
|
|
diff.snr_db()
|
|
);
|
|
}
|
|
}
|