Files
rustytorch/crates/models/rtx-csm/examples/qmatmul_repro.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

201 lines
7.0 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(())
}