#![cfg(any(feature = "gpu-wgpu", feature = "gpu-cuda"))]
use echidna::gpu::{GpuBackend, GpuTapeData};
use echidna::{record, BReverse};
use num_traits::Float;
#[cfg(feature = "gpu-wgpu")]
use echidna::gpu::WgpuContext;
#[cfg(feature = "gpu-cuda")]
use echidna::gpu::CudaContext;
#[cfg(feature = "gpu-wgpu")]
#[test]
fn m29_wgpu_atan_large_abs_a_stays_finite() {
let ctx = match WgpuContext::new() {
Some(c) => c,
None => return,
};
let (tape, _) = record(|v: &[BReverse<f64>]| v[0].atan(), &[1.0_f64]);
let gpu_data = GpuTapeData::from_tape_f64_lossy(&tape).unwrap();
let gpu_tape = ctx.upload_tape(&gpu_data);
let large = 1e20_f32;
let (_, g) = ctx.gradient_batch(&gpu_tape, &[large], 1).unwrap();
assert!(
g[0].is_finite(),
"atan derivative must be finite for |a|=1e20, got {}",
g[0]
);
}
#[cfg(feature = "gpu-wgpu")]
#[test]
fn m29_wgpu_atan_moderate_large_a_positive_normal_f32() {
let ctx = match WgpuContext::new() {
Some(c) => c,
None => return,
};
let (tape, _) = record(|v: &[BReverse<f64>]| v[0].atan(), &[1.0_f64]);
let gpu_data = GpuTapeData::from_tape_f64_lossy(&tape).unwrap();
let gpu_tape = ctx.upload_tape(&gpu_data);
let a = 3e9_f32;
let (_, g) = ctx.gradient_batch(&gpu_tape, &[a], 1).unwrap();
assert!(
g[0].is_finite() && g[0] > 0.0,
"atan'(3e9) should be ≈ 1.1e-19 (positive, normal f32), got {}",
g[0]
);
let expected = 1.0_f64 / (1.0 + 9e18);
let rel_err = ((g[0] as f64) - expected).abs() / expected;
assert!(
rel_err < 1e-3,
"atan'(3e9) = {:e}, expected ≈ {:e}",
g[0],
expected
);
}
#[cfg(feature = "gpu-cuda")]
#[test]
fn m29_cuda_atan_large_abs_a_finite_nonzero() {
let ctx = match CudaContext::new() {
Some(c) => c,
None => return,
};
let (tape, _) = record(|v: &[BReverse<f64>]| v[0].atan(), &[1.0_f64]);
let gpu_data = GpuTapeData::from_tape_f64_lossy(&tape).unwrap();
let gpu_tape = ctx.upload_tape(&gpu_data);
let large = 1e20_f32;
let (_, g) = ctx.gradient_batch(&gpu_tape, &[large], 1).unwrap();
assert!(
g[0].is_finite(),
"atan derivative must be finite for |a|=1e20, got {}",
g[0]
);
assert!(g[0] > 0.0);
}
#[cfg(feature = "gpu-wgpu")]
#[test]
fn l24_wgpu_div_small_denominator_db_finite() {
let ctx = match WgpuContext::new() {
Some(c) => c,
None => return,
};
let (tape, _) = record(|v: &[BReverse<f64>]| v[0] / v[1], &[1.0_f64, 1.0_f64]);
let gpu_data = GpuTapeData::from_tape_f64_lossy(&tape).unwrap();
let gpu_tape = ctx.upload_tape(&gpu_data);
let (_, g) = ctx
.gradient_batch(&gpu_tape, &[1e-10_f32, 1e-20_f32], 1)
.unwrap();
assert!(
g[1].is_finite(),
"db must be finite at small |b|, got {}",
g[1]
);
}
#[cfg(feature = "gpu-wgpu")]
#[test]
fn wgpu_hypot_inf_finite_returns_inf() {
let ctx = match WgpuContext::new() {
Some(c) => c,
None => return,
};
let (tape, _) = record(|v: &[BReverse<f64>]| v[0].hypot(v[1]), &[1.0_f64, 1.0_f64]);
let gpu_data = GpuTapeData::from_tape_f64_lossy(&tape).unwrap();
let gpu_tape = ctx.upload_tape(&gpu_data);
let result = ctx
.forward_batch(&gpu_tape, &[f32::INFINITY, 1.0_f32], 1)
.unwrap();
assert!(
result[0].is_infinite() && result[0] > 0.0,
"hypot(Inf, 1) must be +Inf, got {}",
result[0]
);
}
#[cfg(feature = "gpu-wgpu")]
#[test]
fn wgpu_hypot_inf_inf_returns_inf() {
let ctx = match WgpuContext::new() {
Some(c) => c,
None => return,
};
let (tape, _) = record(|v: &[BReverse<f64>]| v[0].hypot(v[1]), &[1.0_f64, 1.0_f64]);
let gpu_data = GpuTapeData::from_tape_f64_lossy(&tape).unwrap();
let gpu_tape = ctx.upload_tape(&gpu_data);
let result = ctx
.forward_batch(&gpu_tape, &[f32::INFINITY, f32::INFINITY], 1)
.unwrap();
assert!(
result[0].is_infinite() && result[0] > 0.0,
"hypot(Inf, Inf) must be +Inf (IEEE), got {}",
result[0]
);
}
#[cfg(feature = "gpu-wgpu")]
#[test]
fn wgpu_tangent_forward_hypot_large_magnitude_primal_finite() {
let ctx = match WgpuContext::new() {
Some(c) => c,
None => return,
};
let (tape, _) = record(|v: &[BReverse<f64>]| v[0].hypot(v[1]), &[1.0_f64, 1.0_f64]);
let gpu_data = GpuTapeData::from_tape_f64_lossy(&tape).unwrap();
let gpu_tape = ctx.upload_tape(&gpu_data);
let (r, g) = ctx
.gradient_batch(&gpu_tape, &[1e20_f32, 1e20_f32], 1)
.unwrap();
assert!(
r[0].is_finite(),
"tangent_forward hypot primal at (1e20, 1e20) must be finite (rescaled), got {}",
r[0]
);
assert!(g[0].is_finite() && g[1].is_finite());
let expected = 2.0_f32.sqrt() * 1e20_f32;
let rel = ((r[0] - expected) / expected).abs();
assert!(
rel < 1e-4,
"hypot(1e20, 1e20) = {}, expected ≈ {}",
r[0],
expected
);
}
#[cfg(feature = "gpu-cuda")]
#[test]
fn m24_cuda_upload_tape_empty_outputs_via_fallback() {
let ctx = match CudaContext::new() {
Some(c) => c,
None => return,
};
let (tape, _) = record(|v: &[BReverse<f64>]| v[0] * v[0] + v[0], &[2.0_f64]);
let mut gpu_data = GpuTapeData::from_tape_f64_lossy(&tape).unwrap();
gpu_data.output_indices.clear();
assert!(gpu_data.output_indices.is_empty());
let gpu_tape = ctx.upload_tape(&gpu_data);
let (r, _g) = ctx.gradient_batch(&gpu_tape, &[2.0_f32], 1).unwrap();
assert_eq!(r.len(), 1);
assert!((r[0] - 6.0).abs() < 1e-5, "f(2) = 6; got {}", r[0]);
}
#[cfg(feature = "gpu-wgpu")]
#[test]
fn m27_wgsl_expm1_small_a_precision_improves() {
let ctx = match WgpuContext::new() {
Some(c) => c,
None => return,
};
let (tape, _) = record(
|v: &[BReverse<f64>]| v[0].exp_m1(),
&[1.0_f64], );
let gpu_data = GpuTapeData::from_tape_f64_lossy(&tape).unwrap();
let gpu_tape = ctx.upload_tape(&gpu_data);
let (r, _g) = ctx.gradient_batch(&gpu_tape, &[1e-6_f32], 1).unwrap();
let expected = 1e-6_f64.exp_m1();
let actual = r[0] as f64;
let abs_err = (actual - expected).abs();
assert!(
abs_err < 1e-12,
"expm1(1e-6) expected ≈ {:e}, got {:e} (err {:e})",
expected,
actual,
abs_err
);
}
#[cfg(feature = "gpu-wgpu")]
#[test]
fn m27_wgsl_ln1p_small_a_precision_improves() {
let ctx = match WgpuContext::new() {
Some(c) => c,
None => return,
};
let (tape, _) = record(|v: &[BReverse<f64>]| v[0].ln_1p(), &[1.0_f64]);
let gpu_data = GpuTapeData::from_tape_f64_lossy(&tape).unwrap();
let gpu_tape = ctx.upload_tape(&gpu_data);
let (r, _g) = ctx.gradient_batch(&gpu_tape, &[1e-6_f32], 1).unwrap();
let expected = 1e-6_f64.ln_1p();
let actual = r[0] as f64;
let abs_err = (actual - expected).abs();
assert!(
abs_err < 1e-12,
"ln1p(1e-6) expected ≈ {:e}, got {:e} (err {:e})",
expected,
actual,
abs_err
);
}
#[cfg(feature = "gpu-cuda")]
#[test]
fn l23_cuda_powi_n1_at_zero_second_derivative_finite() {
let ctx = match CudaContext::new() {
Some(c) => c,
None => return,
};
let (tape, _) = record(|v: &[BReverse<f64>]| v[0].powi(1), &[0.0_f64]);
let gpu_data = GpuTapeData::from_tape_f64_lossy(&tape).unwrap();
let gpu_tape = ctx.upload_tape(&gpu_data);
let (hv_grad, hv) = ctx.hvp_batch(&gpu_tape, &[0.0_f32], &[1.0_f32], 1).unwrap();
assert_eq!(hv_grad.len(), 1);
assert_eq!(hv.len(), 1);
assert!(
hv[0].is_finite(),
"HVP at powi(x,1), x=0 must be finite, got {}",
hv[0]
);
assert_eq!(hv[0], 0.0, "Hessian of linear function is zero");
}
#[cfg(feature = "gpu-wgpu")]
#[test]
fn l23_wgpu_powi_n1_at_zero_second_derivative_finite() {
let ctx = match WgpuContext::new() {
Some(c) => c,
None => return,
};
let (tape, _) = record(|v: &[BReverse<f64>]| v[0].powi(1), &[0.0_f64]);
let gpu_data = GpuTapeData::from_tape_f64_lossy(&tape).unwrap();
let gpu_tape = ctx.upload_tape(&gpu_data);
let (_hv_grad, hv) = ctx.hvp_batch(&gpu_tape, &[0.0_f32], &[1.0_f32], 1).unwrap();
assert!(
hv[0].is_finite(),
"WGSL HVP at powi(x,1), x=0 must be finite, got {}",
hv[0]
);
assert_eq!(hv[0], 0.0, "Hessian of linear function is zero");
}