Files
foxhunt/crates/ml-alpha/tests/backward_finite_diff.rs
jgrusewski deed15b34e feat(ml-alpha): cfc_step_backward emits grad_x for upstream chain
Adds grad_x[k] = sum_i d_pre[i] * W_in[i,k] computed by thread 0 of
the cfc_step_backward kernel (after the existing __syncthreads in
the shared-mem sd_pre relay). Required by the stacked Mamba2 -> CfC
design: Mamba2.backward_from_h_enriched needs grad on h_enriched,
which is the CfC's "x" input in the stacked topology.

For the existing CfC-only PerceptionTrainer (x = snap_features, no
upstream learnable layer), grad_x is computed but discarded into a
preallocated buffer.

backward_finite_diff tests still pass (4/4) — the new arg is the
14th positional kernel arg; existing callers updated. perception_
overfit smoke still passes (loss 0.5669 -> 0.0665 in 200 steps).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 22:59:36 +02:00

157 lines
5.8 KiB
Rust

//! Finite-difference validation of cfc_step_backward + multi_horizon_heads_backward.
//!
//! Per `feedback_no_cpu_test_fallbacks.md`: the kernel is the reference.
//! We perturb a single input, re-run the GPU forward, and compare the
//! resulting finite-difference gradient to the analytic backward.
use approx::assert_relative_eq;
use ml_alpha::cfc::step::{cfc_step_backward_gpu, cfc_step_gpu, CfcWeights};
use ml_alpha::heads::{
multi_horizon_heads_backward_gpu, multi_horizon_heads_gpu, HeadsWeights, HIDDEN_DIM, N_HORIZONS,
};
use ml_core::device::MlDevice;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
fn test_device() -> MlDevice {
MlDevice::cuda(0).expect("CUDA 0 required for ml-alpha tests")
}
fn rand_heads(seed: u64) -> HeadsWeights {
let mut r = ChaCha8Rng::seed_from_u64(seed);
HeadsWeights {
w: (0..N_HORIZONS * HIDDEN_DIM).map(|_| r.gen_range(-0.1..0.1)).collect(),
b: (0..N_HORIZONS).map(|_| r.gen_range(-0.01..0.01)).collect(),
}
}
fn rand_cfc(seed: u64, n_in: usize, n_hid: usize) -> CfcWeights {
let mut r = ChaCha8Rng::seed_from_u64(seed);
CfcWeights {
w_in: (0..n_hid * n_in).map(|_| r.gen_range(-0.1..0.1)).collect(),
w_rec: (0..n_hid * n_hid).map(|_| r.gen_range(-0.05..0.05)).collect(),
b: (0..n_hid).map(|_| r.gen_range(-0.01..0.01)).collect(),
tau: (0..n_hid).map(|_| r.gen_range(0.05..2.0)).collect(),
n_in,
n_hid,
}
}
#[test]
fn heads_backward_grad_h_matches_finite_diff() {
let dev = test_device();
let w = rand_heads(0x1111);
let mut r = ChaCha8Rng::seed_from_u64(0x2222);
let h: Vec<f32> = (0..HIDDEN_DIM).map(|_| r.gen_range(-1.0..1.0)).collect();
let probs = multi_horizon_heads_gpu(&dev, &w, &h).unwrap();
// Surrogate loss L = sum_k probs[k] -> dL/dp = 1 for all k.
let grad_probs = [1.0f32; N_HORIZONS];
let (_, _, grad_h_analytic) =
multi_horizon_heads_backward_gpu(&dev, &w, &probs, &h, &grad_probs).unwrap();
let eps = 1e-3_f32;
for &i in &[0usize, 32, 64, 96, 127] {
let mut h_up = h.clone();
h_up[i] += eps;
let mut h_dn = h.clone();
h_dn[i] -= eps;
let p_up = multi_horizon_heads_gpu(&dev, &w, &h_up).unwrap();
let p_dn = multi_horizon_heads_gpu(&dev, &w, &h_dn).unwrap();
let l_up: f32 = p_up.iter().sum();
let l_dn: f32 = p_dn.iter().sum();
let fd = (l_up - l_dn) / (2.0 * eps);
assert_relative_eq!(grad_h_analytic[i], fd, epsilon = 1e-3, max_relative = 1e-2);
}
}
#[test]
fn heads_backward_grad_b_matches_finite_diff() {
let dev = test_device();
let w = rand_heads(0x3333);
let mut r = ChaCha8Rng::seed_from_u64(0x4444);
let h: Vec<f32> = (0..HIDDEN_DIM).map(|_| r.gen_range(-1.0..1.0)).collect();
let probs = multi_horizon_heads_gpu(&dev, &w, &h).unwrap();
let grad_probs = [1.0f32; N_HORIZONS];
let (_, grad_b_analytic, _) =
multi_horizon_heads_backward_gpu(&dev, &w, &probs, &h, &grad_probs).unwrap();
let eps = 1e-3_f32;
for k in 0..N_HORIZONS {
let mut w_up = w.clone();
w_up.b[k] += eps;
let mut w_dn = w.clone();
w_dn.b[k] -= eps;
let p_up = multi_horizon_heads_gpu(&dev, &w_up, &h).unwrap();
let p_dn = multi_horizon_heads_gpu(&dev, &w_dn, &h).unwrap();
let l_up: f32 = p_up.iter().sum();
let l_dn: f32 = p_dn.iter().sum();
let fd = (l_up - l_dn) / (2.0 * eps);
assert_relative_eq!(grad_b_analytic[k], fd, epsilon = 1e-3, max_relative = 1e-2);
let _ = w; // suppress
let _ = w_up;
let _ = w_dn;
}
let _ = w;
}
#[test]
fn cfc_backward_grad_b_matches_finite_diff() {
let dev = test_device();
let n_in = 8;
let n_hid = 16;
let w = rand_cfc(0x5555, n_in, n_hid);
let mut r = ChaCha8Rng::seed_from_u64(0x6666);
let x: Vec<f32> = (0..n_in).map(|_| r.gen_range(-1.0..1.0)).collect();
let h_old: Vec<f32> = (0..n_hid).map(|_| r.gen_range(-1.0..1.0)).collect();
let dt_s = 0.02_f32;
// Surrogate loss L = sum_i h_new[i].
let grad_h_new = vec![1.0f32; n_hid];
let (_, _, grad_b_analytic, _, _) =
cfc_step_backward_gpu(&dev, &w, &x, &h_old, &grad_h_new, dt_s).unwrap();
let eps = 1e-3_f32;
for &i in &[0usize, 4, 8, 15] {
let mut w_up = w.clone();
w_up.b[i] += eps;
let mut w_dn = w.clone();
w_dn.b[i] -= eps;
let h_up = cfc_step_gpu(&dev, &w_up, &x, &h_old, dt_s).unwrap();
let h_dn = cfc_step_gpu(&dev, &w_dn, &x, &h_old, dt_s).unwrap();
let l_up: f32 = h_up.iter().sum();
let l_dn: f32 = h_dn.iter().sum();
let fd = (l_up - l_dn) / (2.0 * eps);
assert_relative_eq!(grad_b_analytic[i], fd, epsilon = 5e-3, max_relative = 5e-2);
}
}
#[test]
fn cfc_backward_grad_h_old_matches_finite_diff() {
let dev = test_device();
let n_in = 8;
let n_hid = 16;
let w = rand_cfc(0x7777, n_in, n_hid);
let mut r = ChaCha8Rng::seed_from_u64(0x8888);
let x: Vec<f32> = (0..n_in).map(|_| r.gen_range(-1.0..1.0)).collect();
let h_old: Vec<f32> = (0..n_hid).map(|_| r.gen_range(-1.0..1.0)).collect();
let dt_s = 0.02_f32;
let grad_h_new = vec![1.0f32; n_hid];
let (_, _, _, grad_h_old_analytic, _) =
cfc_step_backward_gpu(&dev, &w, &x, &h_old, &grad_h_new, dt_s).unwrap();
let eps = 1e-3_f32;
for &i in &[0usize, 4, 8, 15] {
let mut h_up = h_old.clone();
h_up[i] += eps;
let mut h_dn = h_old.clone();
h_dn[i] -= eps;
let h_new_up = cfc_step_gpu(&dev, &w, &x, &h_up, dt_s).unwrap();
let h_new_dn = cfc_step_gpu(&dev, &w, &x, &h_dn, dt_s).unwrap();
let l_up: f32 = h_new_up.iter().sum();
let l_dn: f32 = h_new_dn.iter().sum();
let fd = (l_up - l_dn) / (2.0 * eps);
assert_relative_eq!(grad_h_old_analytic[i], fd, epsilon = 5e-3, max_relative = 5e-2);
}
}