Files
rustytorch/crates/models/rtx-csm/examples/ort_conflict_probe.rs
T
osobhandClaude Opus 4.7 a5cedfb46a rtx-csm: emotional_speech_guide — CREMA-D vs RAVDESS firdhokk verdict
8-gen bench (4 emotions × 2 corpora) at seed=42 against firdhokk
Whisper-LV3:

  target    RAVDESS              CREMA-D
  happy     happy (0.999) ✓      happy (0.999) ✓
  angry     neutral (0.92)       sad (0.99)
  fearful   happy (0.998)        fearful (0.984) ✓
  sad       angry (0.99)         fearful (0.99)

CREMA-D 2/4 vs RAVDESS 1/4. Larger / more naturalistic corpus
produces more class-pure fearful direction. Neither corpus solves
angry or sad — recipe shifts into 'vague expressivity' rather than
class-specific corners.

Practical: prefer CREMA-D when available; A/B both per emotion if
class precision matters.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-04-30 00:01:02 -07:00

143 lines
5.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Phase 8.1.2 — runtime-conflict gate.
//!
//! whisper-rs (Phase 7.6) revealed that linking ggml/whisper.cpp into
//! the same binary as candle/CSM causes a 2-3× regression in CSM Metal
//! inference even when whisper isn't actively used. This binary checks
//! whether `voice_activity_detector` (Silero V5 via the `ort` ONNX
//! runtime) has the same problem before we wire VAD gating into
//! converse_server.
//!
//! Test pattern: time N CSM Q8 backbone+depth-decoder forwards, load
//! ort + Silero VAD, run a few VAD predictions, then time N more CSM
//! forwards. If the ratio T_after / T_before > 1.05 we treat ort as a
//! regression risk and defer Silero-VAD gating.
//!
//! Usage:
//! ```bash
//! cargo run -p rtx-csm --release --features metal,vad --example ort_conflict_probe
//! ```
use anyhow::{Context, Result};
use rtx_csm::{GenerateOptions, Generator, stt::Stt};
use std::time::Instant;
const N_FORWARDS: usize = 10;
fn time_csm_forwards(generator: &mut Generator, label: &str) -> Result<f64> {
// Each "forward" here is one short generate() call — exercises the
// backbone + depth decoder + Mimi decode path. The same path that
// shows the whisper-rs regression.
let opts = GenerateOptions {
max_audio_ms: 200,
seed: 0,
..GenerateOptions::default()
};
// Warm-up (not counted) so kernel compilation doesn't pollute the
// before/after comparison.
generator.generate("Hi.", 0, &[], opts.clone())?;
let mut total_ms = 0.0;
for i in 0..N_FORWARDS {
let t = Instant::now();
generator.generate("Hi.", 0, &[], opts.clone())?;
let ms = t.elapsed().as_secs_f64() * 1000.0;
total_ms += ms;
eprintln!(" {label} forward {}: {ms:.1} ms", i + 1);
}
Ok(total_ms / N_FORWARDS as f64)
}
#[cfg(feature = "vad")]
fn run_ort_a_few_times() -> Result<()> {
use voice_activity_detector::VoiceActivityDetector;
let mut vad = VoiceActivityDetector::builder()
.sample_rate(16_000_i64)
.chunk_size(512_usize)
.build()
.context("VoiceActivityDetector::builder")?;
// Five 30 ms predictions. Enough to fully load the runtime + run
// the model so any global state it touches is initialized.
for _ in 0..5 {
let chunk = vec![0.0f32; 512];
let _ = vad.predict(chunk);
}
Ok(())
}
#[cfg(not(feature = "vad"))]
fn run_ort_a_few_times() -> Result<()> {
anyhow::bail!(
"this binary requires `--features vad` to load the ort runtime; \
build with `--features metal,vad`."
);
}
fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::WARN)
.init();
let device = if candle_core::utils::metal_is_available() {
candle_core::Device::new_metal(0)?
} else {
candle_core::Device::Cpu
};
eprintln!("device: {device:?}");
// Load Q8 GGUF (canonical production config).
let q8_path = std::path::Path::new("/tmp/csm_q8.gguf");
if !q8_path.exists() {
anyhow::bail!(
"expected /tmp/csm_q8.gguf — produce it with \
`cargo run --example quantize --release --features metal -- --policy q8`"
);
}
eprintln!("loading CSM-1B Q8 from {}", q8_path.display());
let mut generator =
Generator::load_csm_1b_quantized(q8_path, &device, /* enable_cfg */ false)?;
eprintln!("model loaded");
// Critical: also load Kyutai STT (which links sentencepiece-sys).
// Phase 8.1.3 discovered ort + sentencepiece have a hard protobuf
// version conflict at process startup (sentencepiece bundles 3.14,
// ort bundles 3.21). The minimal probe above didn't catch it
// because it skipped Kyutai. Load STT here so the probe matches the
// real converse_server linkage. If the binary even launches past
// this point, sentencepiece + ort co-exist (i.e. version conflict
// resolved in some future bump).
eprintln!("loading Kyutai STT (verifies ort/sentencepiece linkage)...");
let _stt = Stt::load_default(&device).context("load Kyutai STT")?;
eprintln!("Kyutai STT loaded — sentencepiece/ort coexist OK");
// Phase 1: time CSM forwards before ort is loaded.
let before_ms = time_csm_forwards(&mut generator, "before")?;
eprintln!();
eprintln!("loading ort + Silero VAD...");
run_ort_a_few_times()?;
eprintln!("ort loaded; running CSM forwards again...");
eprintln!();
// Phase 2: time CSM forwards after ort is loaded + has run.
let after_ms = time_csm_forwards(&mut generator, "after")?;
let ratio = after_ms / before_ms;
let pct = (ratio - 1.0) * 100.0;
println!();
println!("=== ort conflict probe ===");
println!("CSM forwards (n={N_FORWARDS} each):");
println!(" before ort load: mean = {before_ms:.1} ms");
println!(" after ort load: mean = {after_ms:.1} ms");
println!(" ratio: {ratio:.3} ({pct:+.1}%)");
println!();
if ratio > 1.05 {
println!("*** REGRESSION DETECTED ***");
println!("ort load slowed CSM forwards by {pct:.1}% — same risk class as whisper-rs.");
println!("Defer Silero-VAD gating; pursue VAD only via sidecar process.");
std::process::exit(2);
} else {
println!("PASS: ort coexists with candle Metal at {pct:+.1}% delta (within ±5%).");
println!("Safe to proceed with Phase 8.1.3 (Silero-VAD gating).");
}
Ok(())
}