use crate::{Device, Session};
use rlx_ir::Graph;
#[derive(Clone, Debug)]
pub struct BackendDivergence {
pub device: Device,
pub max_abs: f32,
pub max_rel: f32,
pub worst_output: usize,
pub suspect: bool,
}
fn divergence_stats(oracle: &[Vec<f32>], got: &[Vec<f32>]) -> (f32, f32, usize) {
let (mut max_abs, mut max_rel, mut worst) = (0.0f32, 0.0f32, 0usize);
for (i, (o, g)) in oracle.iter().zip(got).enumerate() {
for (a, b) in o.iter().zip(g) {
let abs = (a - b).abs();
let rel = abs / (a.abs() + 1e-6);
if abs > max_abs {
max_abs = abs;
}
if rel > max_rel {
max_rel = rel;
worst = i;
}
}
}
(max_abs, max_rel, worst)
}
pub fn backend_divergence(
graph: &Graph,
params: &[(&str, &[f32])],
inputs: &[(&str, &[f32])],
tol_rel: f32,
) -> Vec<BackendDivergence> {
let run_on = |dev: Device| -> Vec<Vec<f32>> {
let mut s = Session::new(dev).compile(graph.clone());
for (n, v) in params {
s.set_param(n, v);
}
s.run(inputs)
};
let oracle = run_on(Device::Cpu);
crate::available_devices()
.into_iter()
.filter(|&d| d != Device::Cpu && !matches!(d, Device::Ane))
.map(|dev| {
let (max_abs, max_rel, worst_output) = divergence_stats(&oracle, &run_on(dev));
BackendDivergence {
device: dev,
max_abs,
max_rel,
worst_output,
suspect: max_rel > tol_rel,
}
})
.collect()
}
pub fn report_backend_divergence(
label: &str,
graph: &Graph,
params: &[(&str, &[f32])],
inputs: &[(&str, &[f32])],
tol_rel: f32,
) -> bool {
let report = backend_divergence(graph, params, inputs, tol_rel);
if report.is_empty() {
eprintln!("[{label}] backend divergence: CPU-only node — nothing to compare");
return false;
}
let mut any = false;
for d in &report {
any |= d.suspect;
eprintln!(
"[{label}] {:>6} vs cpu: max_abs={:.3e} max_rel={:.3e} (output #{}) — {}",
crate::device_label(d.device),
d.max_abs,
d.max_rel,
d.worst_output,
if d.suspect {
"SUSPECT (relative error too large for round-off — likely a kernel bug)"
} else {
"round-off (explainable)"
},
);
}
any
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn divergence_stats_flags_worst_output() {
let oracle = vec![vec![1.0f32, 2.0], vec![2.0f32, 4.0]];
let got = vec![vec![1.0f32, 2.0], vec![2.5f32, 4.0]];
let (abs, rel, worst) = divergence_stats(&oracle, &got);
assert!((abs - 0.5).abs() < 1e-6, "abs={abs}");
assert!((rel - 0.25).abs() < 1e-3, "rel={rel}"); assert_eq!(worst, 1, "worst output should be #1");
let (a0, r0, _) = divergence_stats(&oracle, &oracle);
assert_eq!((a0, r0), (0.0, 0.0));
}
#[test]
fn backend_divergence_cpu_only_is_empty() {
use rlx_ir::{DType, Graph, Shape};
let mut g = Graph::new("d");
let x = g.input("x", Shape::new(&[2, 2], DType::F32));
let w = g.param("w", Shape::new(&[2, 2], DType::F32));
let mm = g.matmul(x, w, Shape::new(&[2, 2], DType::F32));
g.set_outputs(vec![mm]);
let report = backend_divergence(
&g,
&[("w", &[1.0, 0.0, 0.0, 1.0])],
&[("x", &[1.0, 2.0, 3.0, 4.0])],
2e-3,
);
assert!(report.iter().all(|d| d.device != Device::Cpu));
}
}