Files
rustytorch/crates/models/rtx-csm/examples/qmatmul_repro.rs
T
osobhandClaude Opus 4.7 15dd3575d4 Add rtx-csm: Rust-native port of Sesame CSM-1B with LoRA voice cloning
A new model crate at crates/models/rtx-csm implementing end-to-end
inference, quantization, and fine-tuning for Sesame's Conversational
Speech Model (CSM-1B). Built on candle 0.9 + Kyutai Mimi codec.

Key capabilities:
- Inference (FP F16 on Metal, F32 on CPU, BF16 on CUDA)
- Quantized inference (Q8_0 / Q4_K_M GGUF, ~3x speedup, ~50% memory)
- Streaming Mimi decode with proper StreamTensor state machine
- In-context voice cloning via SpeakerProfile
- Classifier-Free Guidance (Koel-TTS recipe)
- Long-form chunked generation with rolling context
- Audio post-processing (HPF + declick + EBU R128 LUFS)
- Text input normalization (brackets, times, unicode, length caps)
- Frame-level repetition guard (loop-escape)
- Top-k + top-p sampling
- LoRA fine-tuning end-to-end (training + inference, on FP and Q8 bases)
- In-process Whisper ASR via whisper-rs (under --features asr)
- Standalone TTS HTTP server (Axum)
- Bench harness with manifest export + per-prompt WER

Phases delivered: quantization, ASR/WER eval, LoRA voice cloning, HTTP
service. AudioSeal/WavLM/Unmute remain as documented future work.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-04-25 18:33:57 -07:00

174 lines
6.7 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.
//! Minimal repro: does candle's `xs.qmatmul(qt)` produce the same result as
//! `xs.matmul(qt.dequantize().t())` for various Q8_0 / Q4_K weights?
//!
//! If they diverge on a fresh random matrix → confirmed candle bug.
//! If they match on random but diverge for our checkpoint → numerical edge case.
//!
//! Run on Metal AND CPU separately:
//! cargo run -p rtx-csm --release --features metal --example qmatmul_repro -- metal
//! cargo run -p rtx-csm --release --features metal --example qmatmul_repro -- cpu
use anyhow::Result;
use candle_core::quantized::{GgmlDType, QMatMul, QTensor};
use candle_core::{DType, Device, Module, Tensor};
fn rms(a: &[f32], b: &[f32]) -> f32 {
let n = a.len() as f32;
let mut sum = 0.0f32;
for (x, y) in a.iter().zip(b) {
sum += (x - y).powi(2);
}
(sum / n).sqrt()
}
fn max_abs_diff(a: &[f32], b: &[f32]) -> f32 {
a.iter()
.zip(b)
.map(|(x, y)| (x - y).abs())
.fold(0.0f32, f32::max)
}
fn run_case(
name: &str,
weight: &Tensor,
xs: &Tensor,
dtype: GgmlDType,
device: &Device,
) -> Result<()> {
println!("\n=== {name} (qtype={dtype:?}) ===");
println!(
"weight shape={:?} dtype={:?}, xs shape={:?} dtype={:?}",
weight.shape(),
weight.dtype(),
xs.shape(),
xs.dtype()
);
// Quantize twice (QTensor isn't Clone): one for qmm path, one for deq.
unsafe {
std::env::remove_var("CANDLE_DEQUANTIZE_ALL");
std::env::remove_var("CANDLE_DEQUANTIZE_ALL_F16");
}
let qt_a = QTensor::quantize(weight, dtype)?;
let qt_b = QTensor::quantize(weight, dtype)?;
println!("qt shape={:?} dtype={:?}", qt_a.shape(), qt_a.dtype());
// Path A: QMatMul forward via raw QTensor variant — the suspect kernel.
let qmm = QMatMul::from_qtensor(qt_a)?;
let path_a = qmm.forward(xs)?;
// Path B: dequantize manually, then xs @ deq.t() — the known-good path.
let deq = qt_b.dequantize(device)?.to_dtype(xs.dtype())?;
let path_b = xs.matmul(&deq.t()?)?;
// Path C: matmul against the original (unquantized) weight — ground truth
// mod the quantization error itself. Distance from C tells us how much
// each path errs vs the float baseline.
let path_c = xs.matmul(&weight.t()?)?;
let a_vals = path_a.flatten_all()?.to_dtype(DType::F32)?.to_vec1::<f32>()?;
let b_vals = path_b.flatten_all()?.to_dtype(DType::F32)?.to_vec1::<f32>()?;
let c_vals = path_c.flatten_all()?.to_dtype(DType::F32)?.to_vec1::<f32>()?;
let a_vs_b_rms = rms(&a_vals, &b_vals);
let a_vs_b_max = max_abs_diff(&a_vals, &b_vals);
let b_vs_c_rms = rms(&b_vals, &c_vals); // pure quant error
let a_vs_c_rms = rms(&a_vals, &c_vals); // qmatmul error vs float baseline
println!("path A (xs.qmatmul) vs path B (xs @ deq.t): rms={a_vs_b_rms:.6} max={a_vs_b_max:.6}");
println!("path B (dequant) vs path C (float baseline): rms={b_vs_c_rms:.6} (pure quant noise)");
println!("path A (qmatmul) vs path C (float baseline): rms={a_vs_c_rms:.6}");
if a_vs_b_rms > 10.0 * b_vs_c_rms {
println!("⚠ path A diverges from path B by >10× the pure-quant noise — qmatmul kernel BUG suspected");
} else if a_vs_b_rms < 1e-4 {
println!("✓ path A and path B agree within rounding — qmatmul kernel CORRECT");
}
println!("first 8 values:");
println!(" A: {:?}", &a_vals[..8.min(a_vals.len())]);
println!(" B: {:?}", &b_vals[..8.min(b_vals.len())]);
println!(" C: {:?}", &c_vals[..8.min(c_vals.len())]);
Ok(())
}
fn main() -> Result<()> {
let backend = std::env::args().nth(1).unwrap_or_else(|| "cpu".into());
let device = match backend.as_str() {
"metal" => Device::new_metal(0)?,
_ => Device::Cpu,
};
println!("device: {device:?}");
// 2D inputs (M, K) so both matmul ranks line up; QMatMul handles batched
// inputs internally.
let weight_small = Tensor::randn(0.0f32, 0.02, (32, 32), &device)?;
let xs_small = Tensor::randn(0.0f32, 1.0, (4, 32), &device)?;
run_case("small 32x32", &weight_small, &xs_small, GgmlDType::Q8_0, &device)?;
let weight_attn = Tensor::randn(0.0f32, 0.02, (2048, 2048), &device)?;
let xs_attn = Tensor::randn(0.0f32, 1.0, (8, 2048), &device)?;
run_case("attn 2048x2048", &weight_attn, &xs_attn, GgmlDType::Q8_0, &device)?;
let weight_kv = Tensor::randn(0.0f32, 0.02, (512, 2048), &device)?;
let xs_kv = Tensor::randn(0.0f32, 1.0, (8, 2048), &device)?;
run_case("kv 512x2048", &weight_kv, &xs_kv, GgmlDType::Q8_0, &device)?;
run_case(
"attn 2048x2048 (Q4_K)",
&weight_attn,
&xs_attn,
GgmlDType::Q4K,
&device,
)?;
// GGUF round-trip integrity test: write a real CSM weight to GGUF, read
// it back, see if the values match.
println!("\n=== GGUF round-trip integrity ===");
use candle_core::quantized::gguf_file;
let tmp = std::env::temp_dir().join("csm_qrt_test.gguf");
let qt_orig = QTensor::quantize(&weight_attn, GgmlDType::Q8_0)?;
let deq_orig = qt_orig.dequantize(&device)?.to_dtype(DType::F32)?;
{
let f = std::fs::File::create(&tmp)?;
let mut w = std::io::BufWriter::new(f);
let metadata: Vec<(&str, &gguf_file::Value)> = vec![];
let tensors: Vec<(&str, &QTensor)> = vec![("test.weight", &qt_orig)];
gguf_file::write(&mut w, &metadata, &tensors)?;
}
println!("wrote {} bytes", std::fs::metadata(&tmp)?.len());
let mut f = std::fs::File::open(&tmp)?;
let ct = gguf_file::Content::read(&mut f)?;
let qt_round = ct.tensor(&mut f, "test.weight", &device)?;
println!(
"round-trip qt shape={:?} dtype={:?}",
qt_round.shape(),
qt_round.dtype()
);
let deq_round = qt_round.dequantize(&device)?.to_dtype(DType::F32)?;
let v_orig = deq_orig.flatten_all()?.to_vec1::<f32>()?;
let v_round = deq_round.flatten_all()?.to_vec1::<f32>()?;
let r = rms(&v_orig, &v_round);
let m = max_abs_diff(&v_orig, &v_round);
println!("dequant(orig) vs dequant(round-trip): rms={r:.8} max={m:.8}");
if r > 1e-6 {
println!("⚠ GGUF round-trip CORRUPTS data");
} else {
println!("✓ GGUF round-trip preserves data");
}
let _ = std::fs::remove_file(&tmp);
// Now test qmatmul on the round-tripped tensor.
let qmm_round = QMatMul::from_qtensor(qt_round)?;
let out_round = qmm_round.forward(&xs_attn)?;
let qt_orig_b = QTensor::quantize(&weight_attn, GgmlDType::Q8_0)?;
let qmm_orig = QMatMul::from_qtensor(qt_orig_b)?;
let out_orig = qmm_orig.forward(&xs_attn)?;
let v_o = out_orig.flatten_all()?.to_vec1::<f32>()?;
let v_r = out_round.flatten_all()?.to_vec1::<f32>()?;
let r2 = rms(&v_o, &v_r);
println!("qmatmul(orig) vs qmatmul(round-trip): rms={r2:.8}");
Ok(())
}