use crate::call::Lowered;
use crate::run::{assemble_params, load_safetensors_f32, run_cpu, run_dynamic};
use anyhow::{Result, anyhow};
use serde::Serialize;
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone, Serialize)]
pub struct OutputParity {
pub index: usize,
pub cosine: f64,
pub max_abs_err: f64,
pub rel_err: f64,
pub numel: usize,
}
#[derive(Debug, Clone, Serialize)]
pub struct ParityReport {
pub passed: bool,
pub outputs: Vec<OutputParity>,
pub cosine_threshold: f64,
pub max_abs_threshold: f64,
}
pub fn compare_outputs(
got: &[Vec<f32>],
expected: &HashMap<String, (Vec<f32>, Vec<usize>)>,
) -> (bool, Vec<OutputParity>) {
let cosine_threshold = 0.999;
let max_abs_threshold = 1e-2;
let mut outputs = Vec::new();
let mut passed = true;
for (i, out) in got.iter().enumerate() {
let Some((exp, _)) = expected.get(&format!("out::{i}")) else {
continue;
};
if exp.len() != out.len() {
passed = false;
outputs.push(OutputParity {
index: i,
cosine: 0.0,
max_abs_err: f64::INFINITY,
rel_err: f64::INFINITY,
numel: out.len(),
});
continue;
}
let cos = cosine(out, exp);
let mut max_abs = 0.0f64;
let mut denom = 0.0f64;
for (&g, &e) in out.iter().zip(exp) {
max_abs = max_abs.max((g as f64 - e as f64).abs());
denom = denom.max((e as f64).abs());
}
let rel = if denom > 0.0 {
max_abs / denom
} else {
max_abs
};
if cos < cosine_threshold || max_abs > max_abs_threshold {
passed = false;
}
outputs.push(OutputParity {
index: i,
cosine: cos,
max_abs_err: max_abs,
rel_err: rel,
numel: out.len(),
});
}
(passed, outputs)
}
#[cfg(feature = "runtime")]
pub fn verify_bundle(bundle_dir: &Path, reference: &Path) -> Result<ParityReport> {
let meta: crate::BundleMeta =
serde_json::from_str(&std::fs::read_to_string(bundle_dir.join("meta.json"))?)?;
let refm = load_safetensors_f32(reference)?;
let inputs: Vec<(String, Vec<f32>)> = meta
.inputs
.iter()
.map(|i| {
let key = format!("in::{}", i.name);
let (data, _) = refm
.get(&key)
.ok_or_else(|| anyhow!("reference input {key:?} missing"))?;
Ok((i.name.clone(), data.clone()))
})
.collect::<Result<Vec<_>>>()?;
let got = crate::run_bundle(bundle_dir, &inputs)?;
let (passed, outputs) = compare_outputs(&got, &refm);
Ok(ParityReport {
passed,
outputs,
cosine_threshold: 0.999,
max_abs_threshold: 1e-2,
})
}
fn cosine(a: &[f32], b: &[f32]) -> f64 {
let mut dot = 0.0f64;
let mut na = 0.0f64;
let mut nb = 0.0f64;
for (&x, &y) in a.iter().zip(b) {
dot += x as f64 * y as f64;
na += (x as f64).powi(2);
nb += (y as f64).powi(2);
}
if na == 0.0 && nb == 0.0 {
return 1.0;
}
if na == 0.0 || nb == 0.0 {
return 0.0;
}
dot / (na.sqrt() * nb.sqrt())
}
#[cfg(feature = "runtime")]
pub fn verify(
dir: &Path,
lo: &Lowered,
hir: rlx_ir::HirModule,
device: rlx_runtime::Device,
) -> Result<ParityReport> {
let weights_path = dir.join("weights.safetensors");
let weights = load_safetensors_f32(&weights_path)?;
let params = assemble_params(lo, &weights)?;
let ref_path = dir.join("reference.safetensors");
let reference = load_safetensors_f32(&ref_path)
.map_err(|e| anyhow!("reference.safetensors required for --verify: {e}"))?;
let inputs: Vec<(String, Vec<f32>)> = lo
.inputs
.iter()
.map(|i| {
let key = format!("in::{}", i.name);
let (data, _) = reference
.get(&key)
.ok_or_else(|| anyhow!("reference input {key:?} missing"))?;
Ok((i.name.clone(), data.clone()))
})
.collect::<Result<Vec<_>>>()?;
let got = if lo.inputs.iter().any(|i| i.is_dynamic()) {
let mut pairs: Vec<(u32, usize)> = Vec::new();
for i in lo.inputs.iter().filter(|i| i.is_dynamic()) {
let (_, shape) = reference
.get(&format!("in::{}", i.name))
.ok_or_else(|| anyhow!("reference input for {:?} missing", i.name))?;
for (ax, dd) in i.dyn_dims.iter().enumerate() {
if let (Some(sym), Some(&sz)) = (dd, shape.get(ax)) {
pairs.push((*sym, sz));
}
}
}
run_dynamic(
hir,
¶ms,
&inputs,
device,
&rlx_ir::DimBinding::from_pairs(&pairs),
)?
} else {
run_cpu(hir, ¶ms, &[], &inputs, device)?
};
let cosine_threshold = 0.999;
let max_abs_threshold = 1e-2;
let mut outputs = Vec::new();
let mut passed = true;
for (i, out) in got.iter().enumerate() {
let key = format!("out::{i}");
let (expected, _) = reference
.get(&key)
.ok_or_else(|| anyhow!("reference output {key:?} missing"))?;
if expected.len() != out.len() {
passed = false;
outputs.push(OutputParity {
index: i,
cosine: 0.0,
max_abs_err: f64::INFINITY,
rel_err: f64::INFINITY,
numel: out.len(),
});
continue;
}
let cos = cosine(out, expected);
let mut max_abs = 0.0f64;
let mut denom = 0.0f64;
for (&g, &e) in out.iter().zip(expected) {
max_abs = max_abs.max((g as f64 - e as f64).abs());
denom = denom.max((e as f64).abs());
}
let rel = if denom > 0.0 {
max_abs / denom
} else {
max_abs
};
if cos < cosine_threshold || max_abs > max_abs_threshold {
passed = false;
}
outputs.push(OutputParity {
index: i,
cosine: cos,
max_abs_err: max_abs,
rel_err: rel,
numel: out.len(),
});
}
Ok(ParityReport {
passed,
outputs,
cosine_threshold,
max_abs_threshold,
})
}