fuel_tensor/test_utils.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Test helpers. The eager-`Tensor` helpers (`test_device!`, `assert_tensor_eq`,
3//! `to_vec{0,1,2,3}_round`) were removed in B6; what remains is host-slice and
4//! lazy infrastructure that the lazy_* port tests use.
5
6/// Oracle-gate comparison helper: assert two `f32` slices match within
7/// absolute tolerance `atol` OR relative tolerance `rtol`.
8///
9/// Used by the Phase 6a CI oracle gate — every anchor model's forward
10/// pass runs on both `realize_f32()` (fast) and `realize_f32()`
11/// (oracle), and the two outputs must agree within tolerance. Prints
12/// the first mismatching index plus max abs/rel deviations when the
13/// assertion fires so divergences are easy to localize.
14pub fn assert_allclose_f32(a: &[f32], b: &[f32], atol: f32, rtol: f32) {
15 assert_eq!(
16 a.len(),
17 b.len(),
18 "assert_allclose_f32: length mismatch {} vs {}",
19 a.len(),
20 b.len()
21 );
22 let mut max_abs = 0.0_f32;
23 let mut max_rel = 0.0_f32;
24 let mut first_bad: Option<usize> = None;
25 for (i, (&x, &y)) in a.iter().zip(b.iter()).enumerate() {
26 if !x.is_finite() || !y.is_finite() {
27 assert!(
28 x.is_finite() == y.is_finite() && x.is_nan() == y.is_nan(),
29 "assert_allclose_f32: finiteness mismatch at index {i}: {x} vs {y}"
30 );
31 continue;
32 }
33 let ad = (x - y).abs();
34 let rd = ad / x.abs().max(y.abs()).max(f32::MIN_POSITIVE);
35 if ad > max_abs {
36 max_abs = ad;
37 }
38 if rd > max_rel {
39 max_rel = rd;
40 }
41 if ad > atol && rd > rtol && first_bad.is_none() {
42 first_bad = Some(i);
43 }
44 }
45 if let Some(i) = first_bad {
46 panic!(
47 "assert_allclose_f32: first mismatch at index {i}: a={} b={} \
48 (diff abs={} rel={}); max abs={max_abs} max rel={max_rel} \
49 over {} elements (atol={atol} rtol={rtol})",
50 a[i],
51 b[i],
52 (a[i] - b[i]).abs(),
53 (a[i] - b[i]).abs() / a[i].abs().max(b[i].abs()).max(f32::MIN_POSITIVE),
54 a.len(),
55 );
56 }
57}
58
59#[cfg(feature = "cuda")]
60pub fn assert_cuda_matches_reference(t: &crate::lazy::Tensor, atol: f32, rtol: f32) {
61 let probe = fuel_hardware::probe::ProbeReport::probe_all();
62 let has_cuda = probe
63 .devices
64 .iter()
65 .any(|d| d.backend == fuel_ir::probe::BackendId::Cuda);
66 // GAP-243. NO SKIP PATH, BY DESIGN — and the reason is the same axis the
67 // per-family defaults in `fuel_test_support::hardware` are built on.
68 //
69 // That module makes CUDA absence fatal by default because every CUDA call
70 // site is an `#[ignore]`d test, so running one requires an explicit
71 // `-- --ignored`, and *that* is the declaration. This function has no
72 // `#[ignore]` to key on — but it does not need one: **naming it and passing
73 // it tensors IS the declaration**, and a stronger one than `--ignored`,
74 // which is a blanket flag that sweeps in tests nobody thought about. Nobody
75 // calls `assert_cuda_matches_reference` by accident.
76 //
77 // ⚠️ It CANNOT use `fuel_test_support::hardware::skip`, and a future reader
78 // will otherwise try to wire it up: `fuel-test-support` is a
79 // **dev-dependency**, while this is ordinary library code (`pub mod
80 // test_utils`, ungated). The mechanism is simply not in scope here, and
81 // bringing it into scope would mean promoting a test-only crate into the
82 // production dependency graph.
83 //
84 // Until 2026-08-27 this returned silently, so a caller asking for an
85 // assertion got a green having asserted nothing — with zero callers it was
86 // latent, which is the only reason it never lied to anyone.
87 assert!(
88 has_cuda,
89 "assert_cuda_matches_reference REQUIRES a live CUDA device and the probe \
90 found none.\n\n\
91 This helper has no skip path by design: a caller that invokes it has \
92 declared the requirement, so returning early would report success \
93 having compared nothing. If you want to tolerate a missing device, take \
94 that decision at the CALL SITE and do not call this."
95 );
96 let reference = t.realize_f32_reference();
97 let dev = fuel_cuda_backend::CudaDevice::new(0)
98 .expect("cuda device 0 available since probe found one");
99 let cuda = t.realize_f32_cuda(&dev);
100 assert_allclose_f32(&cuda, &reference, atol, rtol);
101}
102
103/// Element-wise absolute-tolerance comparison for two flat f32 slices.
104/// Panics with a descriptive message on the first cell exceeding
105/// `abs_tol`. Used by tests whose precision baseline drifts by a few
106/// ULPs across cuDNN algorithm choices (e.g. conv backward grads where
107/// baracuda's algorithm pick differs from a prior Fuel-internal cuDNN
108/// wrapper's choice; both outputs are equally IEEE-754-valid).
109pub fn assert_close_vec1(actual: &[f32], expected: &[f32], abs_tol: f32, label: &str) {
110 assert_eq!(
111 actual.len(),
112 expected.len(),
113 "{label}: len mismatch {} vs {}",
114 actual.len(),
115 expected.len(),
116 );
117 for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
118 let diff = (a - e).abs();
119 assert!(
120 diff <= abs_tol,
121 "{label}: idx {i} actual={a} expected={e} diff={diff} > {abs_tol}",
122 );
123 }
124}