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]>
108 lines
3.9 KiB
Rust
108 lines
3.9 KiB
Rust
//! Diagnose where qmatmul diverges in our actual model: load layer-0 q_proj
|
|
//! from a real CSM GGUF, run through both Self::QTensor and Self::TensorF16
|
|
//! variants on the SAME input, compare.
|
|
|
|
use anyhow::Result;
|
|
use candle_core::quantized::{QMatMul, QTensor};
|
|
use candle_core::{DType, Device, Module, Tensor};
|
|
use candle_transformers::quantized_var_builder::VarBuilder as QVarBuilder;
|
|
use std::sync::Arc;
|
|
|
|
fn rms(a: &[f32], b: &[f32]) -> f32 {
|
|
let n = a.len() as f32;
|
|
let mut s = 0.0f32;
|
|
for (x, y) in a.iter().zip(b) {
|
|
s += (x - y).powi(2);
|
|
}
|
|
(s / n).sqrt()
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
let backend = std::env::args().nth(1).unwrap_or_else(|| "metal".into());
|
|
let gguf = std::env::args()
|
|
.nth(2)
|
|
.unwrap_or_else(|| "/tmp/csm-q8-v2.gguf".into());
|
|
let device = match backend.as_str() {
|
|
"cpu" => Device::Cpu,
|
|
_ => Device::new_metal(0)?,
|
|
};
|
|
println!("device: {device:?}");
|
|
println!("gguf: {gguf}");
|
|
|
|
// Build TWO QVarBuilders: one with DEQUANTIZE_ALL_F16, one without.
|
|
// Each call to vb.get returns an Arc<QTensor> regardless; what differs is
|
|
// how QMatMul::from_arc wraps it later.
|
|
let vb = QVarBuilder::from_gguf(&gguf, &device)?;
|
|
let key = "backbone.layers.0.attn.q_proj.weight";
|
|
let qt_arc = vb
|
|
.pp("backbone.layers.0.attn.q_proj")
|
|
.get_no_shape("weight")?;
|
|
println!(
|
|
"{key} shape={:?} dtype={:?}",
|
|
qt_arc.shape(),
|
|
qt_arc.dtype()
|
|
);
|
|
|
|
// Single QMatMul: candle's thread-locals initialize from env vars on first
|
|
// access in this process. Caller controls via shell env. The variant is
|
|
// private but we can introspect indirectly: TensorF16 path internally
|
|
// casts xs.to_dtype(F16); we can detect by feeding F32 xs and checking if
|
|
// the output is bit-identical to the manual F16-then-matmul path.
|
|
let qmm_qtensor = QMatMul::from_arc(qt_arc.clone())?;
|
|
let qmm_f16 = QMatMul::from_arc(qt_arc.clone())?;
|
|
println!(
|
|
"DEQUANTIZE_ALL_F16 env: {:?}",
|
|
std::env::var("CANDLE_DEQUANTIZE_ALL_F16")
|
|
);
|
|
|
|
// Run with a fixed input so both paths see the exact same xs.
|
|
let (n, k) = qt_arc.shape().dims2()?;
|
|
let m = 8usize;
|
|
let xs = Tensor::randn(0.0f32, 1.0, (m, k), &device)?;
|
|
println!("xs shape=({m}, {k}) weight shape=({n}, {k})");
|
|
|
|
// F32 input, output dtype tells us which variant we got.
|
|
let out_q = qmm_qtensor.forward(&xs)?;
|
|
let out_f = qmm_f16.forward(&xs)?;
|
|
// Manual F16 reference: dequantize to F16, transpose, matmul.
|
|
let deq_f16 = qt_arc.dequantize_f16(&device)?;
|
|
let manual_f16 = xs
|
|
.to_dtype(DType::F16)?
|
|
.matmul(&deq_f16.t()?)?
|
|
.to_dtype(DType::F32)?;
|
|
|
|
let v_q = out_q
|
|
.flatten_all()?
|
|
.to_dtype(DType::F32)?
|
|
.to_vec1::<f32>()?;
|
|
let v_f = out_f
|
|
.flatten_all()?
|
|
.to_dtype(DType::F32)?
|
|
.to_vec1::<f32>()?;
|
|
let v_m = manual_f16.flatten_all()?.to_vec1::<f32>()?;
|
|
let r = rms(&v_q, &v_f);
|
|
let r_qm = rms(&v_q, &v_m);
|
|
let r_fm = rms(&v_f, &v_m);
|
|
println!("manual F16 ref vs out_q: rms={r_qm:.6}");
|
|
println!("manual F16 ref vs out_f: rms={r_fm:.6}");
|
|
println!(
|
|
"QTensor variant out dtype={:?}, F16-deq variant out dtype={:?}",
|
|
out_q.dtype(),
|
|
out_f.dtype()
|
|
);
|
|
println!("L2 distance (rms) between QTensor and F16-dequant outputs: {r:.6}");
|
|
println!("first 8 values:");
|
|
println!(" QTensor: {:?}", &v_q[..8.min(v_q.len())]);
|
|
println!(" TensorF16: {:?}", &v_f[..8.min(v_f.len())]);
|
|
|
|
// Also dequantize directly and compare to QTensor path.
|
|
let deq_arc: Arc<QTensor> = qt_arc.clone();
|
|
let deq = deq_arc.dequantize(&device)?.to_dtype(DType::F32)?;
|
|
let out_deq = xs.matmul(&deq.t()?)?;
|
|
let v_d = out_deq.flatten_all()?.to_vec1::<f32>()?;
|
|
let rd = rms(&v_q, &v_d);
|
|
println!("L2 between QTensor and manual dequant matmul: {rd:.6}");
|
|
println!(" ManualDeq: {:?}", &v_d[..8.min(v_d.len())]);
|
|
Ok(())
|
|
}
|