Per-horizon P(up) at h ∈ {30, 100, 300, 1000, 6000} snapshots forward.
Single-block 5-thread kernel; each thread is its own 128-dim dot
product + sigmoid. No atomicAdd.
Tests (5/5 pass on sm_86) assert invariants only:
- sigmoid output ∈ [0, 1] for all heads
- zero weights + zero bias → 0.5 exactly
- bias = +20 → saturates near 1
- bias = -20 → saturates near 0
- per-head independence (mixed-bias configuration)
Addendum updated to explicitly state no-CPU-mirror discipline per
feedback_no_cpu_test_fallbacks.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
88 lines
2.9 KiB
Rust
88 lines
2.9 KiB
Rust
//! multi_horizon_heads GPU invariants.
|
|
//!
|
|
//! Per `feedback_no_cpu_test_fallbacks.md`: validation via analytically-
|
|
//! known synthetic inputs + property assertions.
|
|
|
|
use approx::assert_relative_eq;
|
|
use ml_alpha::heads::{multi_horizon_heads_gpu, HeadsWeights, HIDDEN_DIM, N_HORIZONS};
|
|
use ml_core::device::MlDevice;
|
|
|
|
fn test_device() -> MlDevice {
|
|
MlDevice::cuda(0).expect("CUDA 0 required for ml-alpha tests")
|
|
}
|
|
|
|
#[test]
|
|
fn probs_are_in_unit_interval() {
|
|
let dev = test_device();
|
|
let w = HeadsWeights {
|
|
w: vec![0.1; N_HORIZONS * HIDDEN_DIM],
|
|
b: vec![0.0; N_HORIZONS],
|
|
};
|
|
let h: Vec<f32> = (0..HIDDEN_DIM).map(|i| 0.01 * i as f32).collect();
|
|
let probs = multi_horizon_heads_gpu(&dev, &w, &h).expect("gpu");
|
|
for k in 0..N_HORIZONS {
|
|
assert!((0.0..=1.0).contains(&probs[k]), "head {k} prob {} not in [0,1]", probs[k]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn zero_weights_and_bias_give_half() {
|
|
// logit = 0 -> sigmoid(0) = 0.5
|
|
let dev = test_device();
|
|
let w = HeadsWeights {
|
|
w: vec![0.0; N_HORIZONS * HIDDEN_DIM],
|
|
b: vec![0.0; N_HORIZONS],
|
|
};
|
|
let h: Vec<f32> = (0..HIDDEN_DIM).map(|i| i as f32).collect();
|
|
let probs = multi_horizon_heads_gpu(&dev, &w, &h).expect("gpu");
|
|
for k in 0..N_HORIZONS {
|
|
assert_relative_eq!(probs[k], 0.5_f32, epsilon = 1e-6);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn large_positive_bias_saturates_high() {
|
|
let dev = test_device();
|
|
let w = HeadsWeights {
|
|
w: vec![0.0; N_HORIZONS * HIDDEN_DIM],
|
|
b: vec![20.0; N_HORIZONS],
|
|
};
|
|
let h = vec![0.0; HIDDEN_DIM];
|
|
let probs = multi_horizon_heads_gpu(&dev, &w, &h).expect("gpu");
|
|
for k in 0..N_HORIZONS {
|
|
assert!(probs[k] > 0.999, "head {k} should saturate near 1 with bias=20, got {}", probs[k]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn large_negative_bias_saturates_low() {
|
|
let dev = test_device();
|
|
let w = HeadsWeights {
|
|
w: vec![0.0; N_HORIZONS * HIDDEN_DIM],
|
|
b: vec![-20.0; N_HORIZONS],
|
|
};
|
|
let h = vec![0.0; HIDDEN_DIM];
|
|
let probs = multi_horizon_heads_gpu(&dev, &w, &h).expect("gpu");
|
|
for k in 0..N_HORIZONS {
|
|
assert!(probs[k] < 0.001, "head {k} should saturate near 0 with bias=-20, got {}", probs[k]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn per_head_independence() {
|
|
// Set bias[0] = 5, bias[4] = -5, others = 0 with zero weights.
|
|
// sigmoid(5) ≈ 0.993, sigmoid(0) = 0.5, sigmoid(-5) ≈ 0.0067.
|
|
let dev = test_device();
|
|
let w = HeadsWeights {
|
|
w: vec![0.0; N_HORIZONS * HIDDEN_DIM],
|
|
b: vec![5.0, 0.0, 0.0, 0.0, -5.0],
|
|
};
|
|
let h = vec![0.0; HIDDEN_DIM];
|
|
let probs = multi_horizon_heads_gpu(&dev, &w, &h).expect("gpu");
|
|
assert!(probs[0] > 0.99 && probs[0] < 1.0);
|
|
assert_relative_eq!(probs[1], 0.5, epsilon = 1e-6);
|
|
assert_relative_eq!(probs[2], 0.5, epsilon = 1e-6);
|
|
assert_relative_eq!(probs[3], 0.5, epsilon = 1e-6);
|
|
assert!(probs[4] > 0.0 && probs[4] < 0.01);
|
|
}
|