use super::{l2_normalize_rows, OpError};
use crate::autograd::{self, Tensor};
const FD_EPS: f32 = 1e-3;
const TOL: f32 = 2e-2;
fn coeff(n: usize) -> Vec<f32> {
(0..n).map(|i| 0.37 + 0.13 * (i as f32)).collect()
}
fn scalar_loss(output: &Tensor, c: &[f32]) -> Tensor {
let ct = Tensor::new(c, output.shape());
output.mul(&ct).sum()
}
fn perturbed_loss<F>(
x_data: &[f32],
x_shape: &[usize],
flat_idx: usize,
delta: f32,
fwd: &F,
c: &[f32],
) -> f32
where
F: Fn(&Tensor) -> Tensor,
{
autograd::no_grad(|| {
let mut xd = x_data.to_vec();
xd[flat_idx] += delta;
let x = Tensor::new(&xd, x_shape);
let y = fwd(&x);
scalar_loss(&y, c).item()
})
}
fn assert_close(analytic: f32, numeric: f32, what: &str) {
let denom = analytic.abs().max(numeric.abs()).max(1.0);
let rel = (analytic - numeric).abs() / denom;
assert!(
rel < TOL,
"{what}: analytic grad {analytic} != finite-diff {numeric} (rel err {rel})"
);
}
fn gradcheck_input<F>(name: &str, x_data: &[f32], x_shape: &[usize], fwd: F) -> Vec<f32>
where
F: Fn(&Tensor) -> Tensor,
{
autograd::clear_graph();
let x = Tensor::new(x_data, x_shape).requires_grad();
let xid = x.id();
let y = fwd(&x);
let c = coeff(y.numel());
let loss = scalar_loss(&y, &c);
loss.backward();
let grad = autograd::get_grad(xid)
.unwrap_or_else(|| panic!("{name}: input received NO gradient — autograd graph severed"));
assert_eq!(grad.shape(), x_shape, "{name}: grad shape mismatch");
assert!(
grad.data().iter().all(|v| v.is_finite()),
"{name}: non-finite grad"
);
assert!(
grad.data().iter().any(|&v| v.abs() > 1e-9),
"{name}: all-zero grad"
);
for i in 0..x_data.len() {
let num = (perturbed_loss(x_data, x_shape, i, FD_EPS, &fwd, &c)
- perturbed_loss(x_data, x_shape, i, -FD_EPS, &fwd, &c))
/ (2.0 * FD_EPS);
assert_close(grad.data()[i], num, &format!("{name} dL/dx[{i}]"));
}
grad.data().to_vec()
}
#[test]
fn l2_normalize_rows_matches_hand_computed_unit_rows() {
let x = Tensor::new(&[3.0, 4.0, 0.6, 0.8], &[2, 2]);
let y = l2_normalize_rows(&x, 1e-12).expect("normalize must succeed");
assert_eq!(y.shape(), &[2, 2], "normalization is shape-preserving");
let want = [0.6f32, 0.8, 0.6, 0.8];
for (i, &w) in want.iter().enumerate() {
assert!(
(y.data()[i] - w).abs() < 1e-6,
"element {i}: got {}, want {w}",
y.data()[i]
);
}
}
#[test]
fn l2_normalize_rows_output_rows_have_unit_norm_above_the_clamp() {
let x: Vec<f32> = (0..12)
.map(|i| 0.41 + 0.27 * (i as f32) - 0.031 * ((i * i) as f32))
.collect();
let y = l2_normalize_rows(&Tensor::new(&x, &[3, 4]), 1e-12).expect("normalize must succeed");
for b in 0..3 {
let n: f32 = y.data()[b * 4..b * 4 + 4]
.iter()
.map(|v| v * v)
.sum::<f32>()
.sqrt();
assert!(
(n - 1.0).abs() < 1e-6,
"row {b} must have unit L2 norm, got {n}"
);
}
}
#[test]
fn l2_normalize_rows_below_the_clamp_divides_by_eps_not_by_the_norm() {
let x = Tensor::new(&[0.12, 0.16, 0.0, 0.0], &[1, 4]);
let y = l2_normalize_rows(&x, 0.5).expect("normalize must succeed");
let want = [0.24f32, 0.32, 0.0, 0.0]; for (i, &w) in want.iter().enumerate() {
assert!(
(y.data()[i] - w).abs() < 1e-6,
"element {i}: below the clamp the divisor is eps, got {} want {w}",
y.data()[i]
);
}
let n: f32 = y.data().iter().map(|v| v * v).sum::<f32>().sqrt();
assert!(
(n - 0.4).abs() < 1e-6,
"a clamped row is NOT renormalized to unit length; norm should be ||x||/eps = 0.4, got {n}"
);
}
#[test]
fn l2_normalize_rows_zero_row_yields_a_zero_row_not_nan() {
let x = Tensor::new(&[0.0, 0.0, 0.0, 3.0, 4.0, 0.0], &[2, 3]);
let y = l2_normalize_rows(&x, 1e-6).expect("normalize must succeed");
assert!(
y.data().iter().all(|v| v.is_finite()),
"an all-zero row must not produce NaN, got {:?}",
y.data()
);
for i in 0..3 {
assert_eq!(
y.data()[i],
0.0,
"zero row stays exactly zero at element {i}"
);
}
assert!((y.data()[3] - 0.6).abs() < 1e-6);
assert!((y.data()[4] - 0.8).abs() < 1e-6);
}
#[test]
fn l2_normalize_rows_stays_finite_at_extreme_underflow_below_the_clamp() {
autograd::clear_graph();
let x = Tensor::new(&[1e-20, 1e-20, 1e-20, 1e-20], &[1, 4]).requires_grad();
let xid = x.id();
let eps = 1e-6f32;
let y = l2_normalize_rows(&x, eps).expect("normalize must succeed");
assert!(
y.data().iter().all(|v| v.is_finite()),
"underflowing row must stay finite, got {:?}",
y.data()
);
for (i, &v) in y.data().iter().enumerate() {
assert!(
(v - 1e-14).abs() < 1e-18,
"element {i}: expected x/eps = 1e-14, got {v}"
);
}
let c = coeff(4);
scalar_loss(&y, &c).backward();
let g = autograd::get_grad(xid).expect("input must receive gradient");
for (i, &want) in c.iter().enumerate() {
let expect = want / eps;
let rel = (g.data()[i] - expect).abs() / expect.abs();
assert!(
rel < 1e-5,
"element {i}: clamped branch must give c/eps = {expect}, got {} \
(the projected form would give ~{:e} here)",
g.data()[i],
want / 2e-20
);
}
}
#[test]
fn l2_normalize_rows_rejects_wrong_rank() {
let rank1 = Tensor::new(&[1.0, 2.0, 3.0], &[3]);
assert_eq!(
l2_normalize_rows(&rank1, 1e-6).expect_err("x must be 2-D [B,H]"),
OpError::ShapeMismatch {
expected: vec![0, 0],
got: vec![3],
}
);
let rank3 = Tensor::new(&[1.0, 2.0, 3.0, 4.0], &[1, 2, 2]);
assert_eq!(
l2_normalize_rows(&rank3, 1e-6).expect_err("x must be 2-D [B,H]"),
OpError::ShapeMismatch {
expected: vec![0, 0],
got: vec![1, 2, 2],
}
);
}
#[test]
fn l2_normalize_rows_rejects_zero_dimensions() {
let zero_batch = Tensor::new(&[], &[0, 4]);
assert_eq!(
l2_normalize_rows(&zero_batch, 1e-6).expect_err("batch 0 must be rejected"),
OpError::ZeroDimension { which: "batch" }
);
let zero_hidden = Tensor::new(&[], &[3, 0]);
assert_eq!(
l2_normalize_rows(&zero_hidden, 1e-6).expect_err("hidden 0 must be rejected"),
OpError::ZeroDimension { which: "hidden" }
);
}
#[test]
fn l2_normalize_rows_rejects_non_positive_epsilon() {
let x = Tensor::new(&[3.0, 4.0], &[1, 2]);
for bad in [0.0f32, -1e-6, -1.0] {
let err = l2_normalize_rows(&x, bad)
.expect_err("a non-positive epsilon removes the divide-by-zero guard");
assert_eq!(err, OpError::invalid_epsilon(bad), "eps = {bad}");
assert_eq!(
err.epsilon(),
Some(bad),
"the offending value is recoverable"
);
}
}
#[test]
fn l2_normalize_rows_rejects_non_finite_epsilon() {
let x = Tensor::new(&[3.0, 4.0], &[1, 2]);
for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
let err =
l2_normalize_rows(&x, bad).expect_err("a non-finite epsilon is not a usable floor");
assert_eq!(err, OpError::invalid_epsilon(bad), "eps = {bad}");
}
}
#[test]
fn l2_normalize_rows_rejects_non_finite_input() {
let nan = Tensor::new(&[1.0, f32::NAN, 3.0, 4.0], &[2, 2]);
assert_eq!(
l2_normalize_rows(&nan, 1e-6).expect_err("NaN input must be rejected at the boundary"),
OpError::NonFiniteInput { position: 1 }
);
let inf = Tensor::new(&[1.0, 2.0, f32::INFINITY, 4.0], &[2, 2]);
assert_eq!(
l2_normalize_rows(&inf, 1e-6).expect_err("Inf input must be rejected at the boundary"),
OpError::NonFiniteInput { position: 2 }
);
}
#[test]
fn l2_normalize_rows_is_not_grad_connected_without_requires_grad() {
autograd::clear_graph();
let x = Tensor::new(&[3.0, 4.0], &[1, 2]);
let y = l2_normalize_rows(&x, 1e-6).expect("normalize must succeed");
assert!(
!y.requires_grad_enabled(),
"normalizing a frozen tensor must not fabricate a graph edge"
);
}
#[test]
fn l2_normalize_rows_records_the_named_backward_edge() {
autograd::clear_graph();
let x = Tensor::new(&[3.0, 4.0], &[1, 2]).requires_grad();
let y = l2_normalize_rows(&x, 1e-6).expect("normalize must succeed");
assert!(y.requires_grad_enabled(), "output must track gradient");
assert_eq!(
y.grad_fn().map(|f| f.name()),
Some("L2NormalizeRowsBackward"),
"the edge must be the named backward, not some borrowed neighbour"
);
}
#[test]
fn l2_normalize_rows_backward_matches_central_finite_differences_above_the_clamp() {
let x: Vec<f32> = (0..15)
.map(|i| 0.29 + 0.17 * (i as f32) - 0.021 * ((i * i) as f32))
.collect();
gradcheck_input("l2_normalize_rows above clamp", &x, &[3, 5], |t| {
l2_normalize_rows(t, 1e-8).expect("normalize must succeed")
});
}
#[test]
fn l2_normalize_rows_backward_is_orthogonal_to_the_row_above_the_clamp() {
autograd::clear_graph();
let xd = [0.7f32, -1.3, 0.45, 2.1];
let x = Tensor::new(&xd, &[1, 4]).requires_grad();
let xid = x.id();
let y = l2_normalize_rows(&x, 1e-8).expect("normalize must succeed");
scalar_loss(&y, &coeff(4)).backward();
let g = autograd::get_grad(xid).expect("input must receive gradient");
let dot: f32 = g.data().iter().zip(xd.iter()).map(|(a, b)| a * b).sum();
assert!(
dot.abs() < 1e-4,
"above the clamp the gradient must be orthogonal to the row, got <g,x> = {dot}"
);
}
#[test]
fn l2_normalize_rows_backward_below_epsilon_clamp_matches_central_finite_differences() {
let x: Vec<f32> = (0..8).map(|i| 0.03 + 0.019 * (i as f32)).collect();
let grad = gradcheck_input("l2_normalize_rows below clamp", &x, &[2, 4], |t| {
l2_normalize_rows(t, 0.5).expect("normalize must succeed")
});
let c = coeff(8);
for (i, &ci) in c.iter().enumerate() {
let want = ci / 0.5;
assert!(
(grad[i] - want).abs() < 1e-5,
"element {i}: clamped branch must give c/eps = {want}, got {}",
grad[i]
);
}
}
#[test]
fn l2_normalize_rows_backward_below_epsilon_clamp_is_identity_over_eps_not_the_projected_form() {
autograd::clear_graph();
let xd = [0.12f32, 0.16, 0.0];
let x = Tensor::new(&xd, &[1, 3]).requires_grad();
let xid = x.id();
let eps = 0.5f32;
let y = l2_normalize_rows(&x, eps).expect("normalize must succeed");
let c = coeff(3);
scalar_loss(&y, &c).backward();
let g = autograd::get_grad(xid).expect("input must receive gradient");
for (i, &ci) in c.iter().enumerate() {
let want = ci / eps;
assert!(
(g.data()[i] - want).abs() < 1e-5,
"element {i}: expected c/eps = {want}, got {}",
g.data()[i]
);
}
let n: f32 = xd.iter().map(|v| v * v).sum::<f32>().sqrt();
let yv: Vec<f32> = y.data().to_vec();
let dot: f32 = c.iter().zip(yv.iter()).map(|(a, b)| a * b).sum();
let mut differs = false;
for i in 0..3 {
let projected = (c[i] - yv[i] * dot) / n;
if (projected - g.data()[i]).abs() > 1e-3 {
differs = true;
}
}
assert!(
differs,
"the projected form and the clamped form must be DIFFERENT answers here, \
otherwise this test proves nothing about which branch was taken"
);
}
#[test]
fn l2_normalize_rows_backward_takes_each_row_branch_independently() {
let x = [0.6f32, 0.8, 0.0, 0.06, 0.08, 0.0];
let grad = gradcheck_input("l2_normalize_rows mixed branches", &x, &[2, 3], |t| {
l2_normalize_rows(t, 0.5).expect("normalize must succeed")
});
let c = coeff(6);
let dot0: f32 = grad[0] * x[0] + grad[1] * x[1] + grad[2] * x[2];
assert!(
dot0.abs() < 1e-4,
"row 0 is above the clamp; its gradient must be orthogonal to the row, got {dot0}"
);
for j in 0..3 {
let want = c[3 + j] / 0.5;
assert!(
(grad[3 + j] - want).abs() < 1e-5,
"row 1 element {j} is below the clamp; expected c/eps = {want}, got {}",
grad[3 + j]
);
}
}
#[test]
fn l2_normalize_rows_backward_switches_branch_at_the_documented_boundary() {
let eps = 0.25f32;
let c = coeff(3);
autograd::clear_graph();
let at = Tensor::new(&[eps, 0.0, 0.0], &[1, 3]).requires_grad();
let at_id = at.id();
let y_at = l2_normalize_rows(&at, eps).expect("normalize must succeed");
scalar_loss(&y_at, &c).backward();
let g_at = autograd::get_grad(at_id).expect("input must receive gradient");
assert!(
(g_at.data()[0] - c[0] / eps).abs() < 1e-5,
"n == eps is assigned to the CLAMPED branch; expected {}, got {}",
c[0] / eps,
g_at.data()[0]
);
autograd::clear_graph();
let above = Tensor::new(&[eps * 1.01, 0.0, 0.0], &[1, 3]).requires_grad();
let above_id = above.id();
let y_above = l2_normalize_rows(&above, eps).expect("normalize must succeed");
scalar_loss(&y_above, &c).backward();
let g_above = autograd::get_grad(above_id).expect("input must receive gradient");
assert!(
g_above.data()[0].abs() < 1e-4,
"just above the clamp the projected form annihilates the row direction; \
expected ~0, got {}",
g_above.data()[0]
);
assert!(
g_at.data().iter().all(|v| v.is_finite()) && g_above.data().iter().all(|v| v.is_finite()),
"neither side of the boundary may produce a non-finite gradient"
);
}