#![warn(missing_docs)]
#![warn(clippy::all)]
pub mod adapters;
mod backend;
mod config;
mod numeric;
mod report;
mod sweep;
pub use backend::Backend;
pub use config::Config;
pub use numeric::{numerical_gradient, realized_step, realized_step_with, Precision};
pub use report::{ComponentRecord, InvalidReason, Report, Stage, UncheckedReason, Verdict};
pub use sweep::{shape_sweep, SweepReport};
pub fn gradcheck<B, F>(name: &str, data: &[f64], shape: &[usize], f: F, cfg: &Config) -> Report
where
B: Backend,
F: Fn(B::Tensor) -> B::Tensor,
{
let n: usize = shape.iter().product();
assert_eq!(
data.len(),
n,
"gradcheck: data length {} does not match shape {:?} (= {} elements)",
data.len(),
shape,
n
);
let x = B::from_slice(data, shape);
let analytic = B::analytic_grad(&f, &x);
assert_eq!(
analytic.len(),
n,
"gradcheck: backend returned a gradient of length {} for an input of {} elements",
analytic.len(),
n
);
let numeric = numerical_gradient::<B, _>(&f, data, shape, cfg.step, cfg.precision);
build_report::<B>(name, shape, analytic, numeric, cfg)
}
pub fn gradcheck_corrupted<B, F>(
name: &str,
data: &[f64],
shape: &[usize],
f: F,
cfg: &Config,
corrupt: f64,
) -> Report
where
B: Backend,
F: Fn(B::Tensor) -> B::Tensor,
{
let n: usize = shape.iter().product();
let x = B::from_slice(data, shape);
let analytic: Vec<f64> = B::analytic_grad(&f, &x)
.into_iter()
.map(|v| v * corrupt)
.collect();
let numeric = numerical_gradient::<B, _>(&f, data, shape, cfg.step, cfg.precision);
let _ = n;
build_report::<B>(name, shape, analytic, numeric, cfg)
}
pub fn compare(
name: &str,
backend: &'static str,
shape: &[usize],
analytic: Vec<f64>,
numeric: Vec<f64>,
cfg: &Config,
) -> Report {
struct Labelled;
thread_local! {
static LABEL: core::cell::Cell<&'static str> = const { core::cell::Cell::new("compare") };
}
impl Backend for Labelled {
type Tensor = ();
fn name() -> &'static str {
LABEL.with(|l| l.get())
}
fn from_slice(_: &[f64], _: &[usize]) -> Self::Tensor {}
fn to_vec(_: &Self::Tensor) -> Vec<f64> {
Vec::new()
}
fn forward_sum(_: &dyn Fn(Self::Tensor) -> Self::Tensor, _: &Self::Tensor) -> f64 {
0.0
}
fn analytic_grad(_: &dyn Fn(Self::Tensor) -> Self::Tensor, _: &Self::Tensor) -> Vec<f64> {
Vec::new()
}
}
LABEL.with(|l| l.set(backend));
build_report::<Labelled>(name, shape, analytic, numeric, cfg)
}
pub(crate) fn adjudicate(a: f64, n: f64, cfg: &Config) -> ComponentRecord {
let signed_zero_discrepancy =
a == 0.0 && n == 0.0 && a.is_sign_negative() != n.is_sign_negative();
if !a.is_finite() || !n.is_finite() {
let stage = if a.is_finite() {
Stage::Probe
} else {
Stage::Analytic
};
return ComponentRecord {
a,
n,
diff: f64::NAN,
tol_used: f64::NAN,
outcome: Verdict::Invalid {
stage,
reason: InvalidReason::NonFinite,
},
signed_zero_discrepancy,
};
}
let m = a.abs().max(n.abs());
let diff = (a - n).abs();
let reject_bound = cfg.abs_tol + cfg.rel_tol * m;
let certify_bound = cfg.rel_tol * m;
let outcome = if diff > reject_bound {
Verdict::Mismatch
} else if m > cfg.floor && diff <= certify_bound {
Verdict::Pass
} else {
Verdict::Unchecked(UncheckedReason::AmbiguousAtFloor)
};
ComponentRecord {
a,
n,
diff,
tol_used: reject_bound,
outcome,
signed_zero_discrepancy,
}
}
fn build_report<B: Backend>(
name: &str,
shape: &[usize],
analytic: Vec<f64>,
numeric: Vec<f64>,
cfg: &Config,
) -> Report {
let mk = |verdict: Verdict, components: Vec<ComponentRecord>| Report {
name: name.to_string(),
backend: B::name(),
shape: shape.to_vec(),
analytic: analytic.clone(),
numeric: numeric.clone(),
components,
worst_index: 0,
worst_rel_error: 0.0,
verdict,
nondeterministic: false,
};
if analytic.len() != numeric.len() || analytic.is_empty() {
return mk(Verdict::StructuralMismatch, Vec::new());
}
if !cfg.is_finite() {
return mk(
Verdict::Invalid {
stage: Stage::Comparator,
reason: InvalidReason::NonFinite,
},
Vec::new(),
);
}
let components: Vec<ComponentRecord> = analytic
.iter()
.zip(numeric.iter())
.map(|(&a, &n)| adjudicate(a, n, cfg))
.collect();
let verdict = components
.iter()
.fold(Verdict::Pass, |acc, c| acc.combine(c.outcome.clone()));
let verdict = match verdict {
Verdict::Unchecked(_) => {
let (checked, total) = (
components
.iter()
.filter(|c| matches!(c.outcome, Verdict::Pass | Verdict::Mismatch))
.count() as u32,
components.len() as u32,
);
if checked == 0 {
Verdict::Unchecked(UncheckedReason::AmbiguousAtFloor)
} else {
Verdict::Unchecked(UncheckedReason::Partial { checked, total })
}
}
v => v,
};
let (worst_index, worst_rel_error) = components
.iter()
.enumerate()
.filter(|(_, c)| matches!(c.outcome, Verdict::Pass | Verdict::Mismatch))
.fold((0usize, 0.0f64), |(bi, bv), (i, c)| {
let m = c.a.abs().max(c.n.abs()).max(f64::MIN_POSITIVE);
let rel = c.diff / m;
if rel > bv {
(i, rel)
} else {
(bi, bv)
}
});
Report {
name: name.to_string(),
backend: B::name(),
shape: shape.to_vec(),
analytic,
numeric,
components,
worst_index,
worst_rel_error,
verdict,
nondeterministic: false,
}
}
pub fn assert_detects_wrong_gradient<B>()
where
B: Backend,
{
let data = [0.7_f64, -1.3, 2.1, -0.4, 1.9, -3.2];
let shape = [2usize, 3];
let cfg = Config::default();
let corrupted =
gradcheck_corrupted::<B, _>("negative-control(x1.5)", &data, &shape, |t| t, &cfg, 1.5);
assert!(
!corrupted.passed(),
"NEGATIVE CONTROL FAILED: gradcheck reported a pass for a gradient scaled by 1.5. \
The harness cannot detect a wrong gradient on backend '{}', so every other result \
it produces is meaningless. Report: {corrupted}",
B::name()
);
let clean = gradcheck::<B, _>("negative-control(x1.0)", &data, &shape, |t| t, &cfg);
assert!(
clean.passed(),
"NEGATIVE CONTROL FAILED IN THE OTHER DIRECTION: gradcheck reported a mismatch for a \
correct identity gradient on backend '{}'. Tolerances or the adapter are wrong. \
Report: {clean}",
B::name()
);
}