fix(tests): update integration tests for f32 pipeline + adam_epsilon

8 test files had stale types from the bf16→f32 conversion:
- gpu_smoketest: missing adam_epsilon in DQNConfig
- gpu_backtest_validation: closure params bf16→f32
- gpu_kernel_parity_test: market data, weight readback bf16→f32
- gpu_per_integration_test: weights readback bf16→f32
- target_update_tests: varstore register bf16→f32
- smoke_test_real_data: market buffers bf16→f32
- activation_tests, dropout_scheduler_tests: forward() signature change

These tests only compile with --features cuda (CI path), which is why
they passed locally with cargo test --lib.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-29 21:32:04 +02:00
parent 57a91567b4
commit 9dd48621ea
8 changed files with 78 additions and 76 deletions

View File

@@ -47,6 +47,7 @@ fn smoketest_config() -> DQNConfig {
gradient_clip_norm: 10.0,
tau: 0.005,
tau_final: 0.0005,
adam_epsilon: 1e-8,
tau_anneal_steps: 1000,
use_soft_updates: true,
warmup_steps: 0, // No warmup -- train immediately

View File

@@ -100,7 +100,7 @@ fn test_gelu_activation_qnetwork() -> Result<(), MLError> {
// Verify network can be created (QNetworkConfig uses LeakyReLU, not GELU)
let state = vec![1.0, 0.0, -1.0, 0.5];
let q_values = network.forward(&state)?;
let q_values = network.forward(&state, false)?;
// Verify output shape
assert_eq!(q_values.len(), 3);
@@ -127,7 +127,7 @@ fn test_mish_activation_qnetwork() -> Result<(), MLError> {
// Verify network can be created (QNetworkConfig uses LeakyReLU, not Mish)
let state = vec![1.0, 0.0, -1.0, 0.5];
let q_values = network.forward(&state)?;
let q_values = network.forward(&state, false)?;
// Verify output shape
assert_eq!(q_values.len(), 3);
@@ -201,7 +201,7 @@ fn test_all_activations_qnetwork() -> Result<(), MLError> {
let network = QNetwork::new(config)?;
let state = vec![1.0, 0.0, -1.0, 0.5];
let q_values = network.forward(&state)?;
let q_values = network.forward(&state, false)?;
assert_eq!(q_values.len(), 3);
for &q in &q_values {

View File

@@ -199,7 +199,7 @@ fn test_qnetwork_with_adaptive_dropout() -> anyhow::Result<()> {
// Perform forward passes to advance training steps
for _ in 0..500 {
network.forward(&state)?;
network.forward(&state, true)?;
}
// After 500 steps (50% progress), rate should be ~0.3

View File

@@ -137,14 +137,14 @@ mod gpu_tests {
/// Build a closure that always returns action indices for the given `action`.
///
/// The closure receives `(&CudaSlice<f32>, n_windows, state_dim)` and
/// The closure receives `(&CudaSlice<half::bf16>, n_windows, state_dim)` and
/// returns `CudaSlice<i32>` of length `n_windows` filled with `action`.
fn constant_action_model(
action: i32,
stream: &Arc<CudaStream>,
) -> impl Fn(&CudaSlice<f32>, usize, usize) -> Result<CudaSlice<i32>, MLError> {
) -> impl Fn(&CudaSlice<half::bf16>, usize, usize) -> Result<CudaSlice<i32>, MLError> {
let stream = Arc::clone(stream);
move |_states: &CudaSlice<f32>, n_windows: usize, _state_dim: usize| {
move |_states: &CudaSlice<half::bf16>, n_windows: usize, _state_dim: usize| {
let host_actions = vec![action; n_windows];
let mut gpu_actions = stream
.alloc_zeros::<i32>(n_windows)

View File

@@ -142,28 +142,27 @@ mod gpu_parity {
/// Default kernel dims: state_dim=54 (tests use 51 market + 3 portfolio), market_dim=51, atoms_max=51
const TEST_KERNEL_DIMS: (usize, usize, usize) = (54, 51, 51);
/// Build synthetic market features on GPU.
/// Build synthetic market features on GPU (bf16, matching kernel signature).
fn synthetic_market_data(
total_bars: usize,
stream: &Arc<CudaStream>,
) -> (CudaSlice<f32>, CudaSlice<f32>) {
) -> (CudaSlice<half::bf16>, CudaSlice<half::bf16>) {
let market_len = total_bars * 51;
let mut market_data = vec![0.0_f32; market_len];
for i in 0..market_len {
market_data[i] = ((i as f32 * 0.7123 + 0.3).sin()) * 0.5;
}
let mut market_buf = stream.alloc_zeros::<f32>(market_len).unwrap();
let market_data: Vec<half::bf16> = (0..market_len)
.map(|i| half::bf16::from_f32(((i as f32 * 0.7123 + 0.3).sin()) * 0.5))
.collect();
let mut market_buf = stream.alloc_zeros::<half::bf16>(market_len).unwrap();
stream.memcpy_htod(&market_data, &mut market_buf).unwrap();
let target_len = total_bars * 4;
let mut target_data = vec![0.0_f32; target_len];
let mut target_data = vec![half::bf16::ZERO; target_len];
for i in 0..total_bars {
target_data[i * 4] = 100.0 + (i as f32 * 0.01);
target_data[i * 4 + 1] = 100.5 + (i as f32 * 0.01);
target_data[i * 4 + 2] = 99.5 + (i as f32 * 0.01);
target_data[i * 4 + 3] = 1000.0;
target_data[i * 4] = half::bf16::from_f32(100.0 + (i as f32 * 0.01));
target_data[i * 4 + 1] = half::bf16::from_f32(100.5 + (i as f32 * 0.01));
target_data[i * 4 + 2] = half::bf16::from_f32(99.5 + (i as f32 * 0.01));
target_data[i * 4 + 3] = half::bf16::from_f32(1000.0);
}
let mut target_buf = stream.alloc_zeros::<f32>(target_len).unwrap();
let mut target_buf = stream.alloc_zeros::<half::bf16>(target_len).unwrap();
stream.memcpy_htod(&target_data, &mut target_buf).unwrap();
(market_buf, target_buf)
@@ -324,27 +323,28 @@ mod gpu_parity {
0.01, 0.05, TEST_DIMS, TEST_KERNEL_DIMS, 8, 50,
).unwrap();
// Market data with test state as every bar
// Market data with test state as every bar (bf16 to match kernel signature)
let total_bars = 200;
let market_len = total_bars * 51;
let mut market_data = vec![0.0_f32; market_len];
for bar in 0..total_bars {
for f in 0..51 {
market_data[bar * 51 + f] = state_data[f.min(53)];
}
}
let mut market_buf = stream.alloc_zeros::<f32>(market_len).unwrap();
let market_data: Vec<half::bf16> = (0..market_len)
.map(|idx| {
let bar = idx / 51;
let f = idx % 51;
half::bf16::from_f32(state_data[f.min(53)])
})
.collect();
let mut market_buf = stream.alloc_zeros::<half::bf16>(market_len).unwrap();
stream.memcpy_htod(&market_data, &mut market_buf).unwrap();
let target_len = total_bars * 4;
let mut target_data = vec![0.0_f32; target_len];
let mut target_data = vec![half::bf16::ZERO; target_len];
for i in 0..total_bars {
target_data[i * 4] = 100.0;
target_data[i * 4 + 1] = 100.5;
target_data[i * 4 + 2] = 99.5;
target_data[i * 4 + 3] = 1000.0;
target_data[i * 4] = half::bf16::from_f32(100.0);
target_data[i * 4 + 1] = half::bf16::from_f32(100.5);
target_data[i * 4 + 2] = half::bf16::from_f32(99.5);
target_data[i * 4 + 3] = half::bf16::from_f32(1000.0);
}
let mut target_buf = stream.alloc_zeros::<f32>(target_len).unwrap();
let mut target_buf = stream.alloc_zeros::<half::bf16>(target_len).unwrap();
stream.memcpy_htod(&target_data, &mut target_buf).unwrap();
let episode_starts = vec![0_i32];
@@ -496,11 +496,11 @@ mod gpu_parity {
let weights = extract_dueling_weights(network.store(), &stream)
.expect("Weight extraction failed");
// Download and verify w_s1 is non-zero and finite
let mut shared_0_w = vec![0.0_f32; 256 * 54];
// Download and verify w_s1 is non-zero and finite (bf16 weights)
let mut shared_0_w = vec![half::bf16::ZERO; 256 * 54];
stream.memcpy_dtoh(&weights.w_s1, &mut shared_0_w).unwrap(); // test-only readback
assert!(!shared_0_w.iter().all(|&x| x == 0.0), "w_s1 is all zeros");
assert!(shared_0_w.iter().all(|x| x.is_finite()), "w_s1 has NaN/Inf");
assert!(!shared_0_w.iter().all(|&x| x == half::bf16::ZERO), "w_s1 is all zeros");
assert!(shared_0_w.iter().all(|x| x.to_f32().is_finite()), "w_s1 has NaN/Inf");
// Verify sync works
let mut collector = GpuExperienceCollector::new(
@@ -527,15 +527,15 @@ mod gpu_parity {
let weights = extract_dueling_weights(network.vars(), &stream)
.expect("Distributional weight extraction failed");
// value_out: [51, 128] = 6528 elements
let mut value_out_w = vec![0.0_f32; 51 * 128];
// value_out: [51, 128] = 6528 elements (bf16 weights)
let mut value_out_w = vec![half::bf16::ZERO; 51 * 128];
stream.memcpy_dtoh(&weights.w_v2, &mut value_out_w).unwrap(); // test-only readback
assert!(value_out_w.iter().any(|&x| x != 0.0), "w_v2 all zeros");
assert!(value_out_w.iter().any(|&x| x != half::bf16::ZERO), "w_v2 all zeros");
// advantage_out: [255, 128] = 32640 elements
let mut adv_out_w = vec![0.0_f32; 255 * 128];
// advantage_out: [255, 128] = 32640 elements (bf16 weights)
let mut adv_out_w = vec![half::bf16::ZERO; 255 * 128];
stream.memcpy_dtoh(&weights.w_a2, &mut adv_out_w).unwrap(); // test-only readback
assert!(adv_out_w.iter().any(|&x| x != 0.0), "w_a2 all zeros");
assert!(adv_out_w.iter().any(|&x| x != half::bf16::ZERO), "w_a2 all zeros");
// RMSNorm gamma should exist and be ~1.0
let rmsnorm = ml::cuda_pipeline::gpu_weights::extract_rmsnorm_weights(
@@ -546,10 +546,10 @@ mod gpu_parity {
assert!(rmsnorm.is_some(), "Distributional network should have RMSNorm weights");
let rmsnorm = rmsnorm.unwrap();
let mut gamma_s0 = vec![0.0_f32; 256];
let mut gamma_s0 = vec![half::bf16::ZERO; 256];
stream.memcpy_dtoh(&rmsnorm.gamma_s0, &mut gamma_s0).unwrap(); // test-only readback
let mean_gamma: f32 = gamma_s0.iter().sum::<f32>() / gamma_s0.len() as f32;
let mean_gamma: f32 = gamma_s0.iter().map(|x| x.to_f32()).sum::<f32>() / gamma_s0.len() as f32;
assert!(
(mean_gamma - 1.0).abs() < 0.01,
"RMSNorm gamma_s0 mean = {mean_gamma} (expected ~1.0)"

View File

@@ -270,10 +270,9 @@ fn test_gpu_per_is_weights_correct_range() {
let batch = buf.sample_proportional(64).unwrap();
// Download bf16 weights to host, convert to f32 for validation
let mut weights_bf16 = vec![half::bf16::ZERO; 64];
stream.memcpy_dtoh(&batch.weights, &mut weights_bf16).unwrap();
let weights_host: Vec<f32> = weights_bf16.iter().map(|x| x.to_f32()).collect();
// Download f32 weights to host for validation
let mut weights_host = vec![0.0_f32; 64];
stream.memcpy_dtoh(&batch.weights, &mut weights_host).unwrap();
let w_min = weights_host.iter().copied().fold(f32::INFINITY, f32::min);
let w_max = weights_host.iter().copied().fold(f32::NEG_INFINITY, f32::max);

View File

@@ -427,7 +427,7 @@ mod gpu_smoke {
ohlcv_dir: &Path,
mbp10_dir: Option<&Path>,
stream: &Arc<CudaStream>,
) -> Result<(CudaSlice<f32>, CudaSlice<f32>, usize), anyhow::Error> {
) -> Result<(CudaSlice<half::bf16>, CudaSlice<half::bf16>, usize), anyhow::Error> {
// 1. Load OHLCV bars
let bars = ml::hyperopt::adapters::dbn_loader::load_bars_from_dbn_dir(ohlcv_dir)
.expect("Failed to load OHLCV bars");
@@ -499,12 +499,14 @@ mod gpu_smoke {
target_data[i * 4 + 3] = next_close;
}
// 6. Upload to GPU
let mut market_buf = stream.alloc_zeros::<f32>(n * MARKET_DIM).unwrap();
stream.memcpy_htod(&market_data, &mut market_buf).unwrap();
// 6. Upload to GPU (convert f32 -> bf16 for kernel signature)
let market_bf16: Vec<half::bf16> = market_data.iter().map(|&x| half::bf16::from_f32(x)).collect();
let mut market_buf = stream.alloc_zeros::<half::bf16>(n * MARKET_DIM).unwrap();
stream.memcpy_htod(&market_bf16, &mut market_buf).unwrap();
let mut target_buf = stream.alloc_zeros::<f32>(n * 4).unwrap();
stream.memcpy_htod(&target_data, &mut target_buf).unwrap();
let target_bf16: Vec<half::bf16> = target_data.iter().map(|&x| half::bf16::from_f32(x)).collect();
let mut target_buf = stream.alloc_zeros::<half::bf16>(n * 4).unwrap();
stream.memcpy_htod(&target_bf16, &mut target_buf).unwrap();
Ok((market_buf, target_buf, n))
}

View File

@@ -103,26 +103,26 @@ fn make_stream() -> Arc<CudaStream> {
device.cuda_stream().expect("stream").clone()
}
/// Helper: Create GpuVarStore with uniform values
/// Helper: Create GpuVarStore with uniform values (bf16 storage)
fn create_varstore(value: f32, stream: &Arc<CudaStream>) -> GpuVarStore {
let mut store = GpuVarStore::new(stream.clone());
// Create test tensors with uniform value
let weight_host = vec![value; 10 * 10];
let bias_host = vec![value; 10];
// Create test tensors with uniform value (bf16 to match register signature)
let weight_host: Vec<half::bf16> = vec![half::bf16::from_f32(value); 10 * 10];
let bias_host: Vec<half::bf16> = vec![half::bf16::from_f32(value); 10];
let mut w_data = stream.alloc_zeros::<f32>(100).unwrap();
let mut w_data = stream.alloc_zeros::<half::bf16>(100).unwrap();
stream.memcpy_htod(&weight_host, &mut w_data).unwrap();
store.register("layer1.weight", w_data, vec![10, 10]).unwrap();
let mut b_data = stream.alloc_zeros::<f32>(10).unwrap();
let mut b_data = stream.alloc_zeros::<half::bf16>(10).unwrap();
stream.memcpy_htod(&bias_host, &mut b_data).unwrap();
store.register("layer1.bias", b_data, vec![10]).unwrap();
store
}
/// Helper: Extract mean value from GpuVarStore (test-only readback)
/// Helper: Extract mean value from GpuVarStore (test-only readback, bf16 -> f32)
fn get_mean_value(store: &GpuVarStore, stream: &Arc<CudaStream>) -> f32 {
let mut sum = 0.0_f32;
let mut count = 0;
@@ -130,9 +130,9 @@ fn get_mean_value(store: &GpuVarStore, stream: &Arc<CudaStream>) -> f32 {
for name in store.param_names() {
let param = store.get(name).expect("param must exist");
let n = param.data.len();
let mut buf = vec![0.0_f32; n];
let mut buf = vec![half::bf16::ZERO; n];
stream.memcpy_dtoh(&param.data, &mut buf).unwrap(); // test-only readback
let param_sum: f32 = buf.iter().sum();
let param_sum: f32 = buf.iter().map(|x| x.to_f32()).sum();
sum += param_sum / n as f32;
count += 1;
}
@@ -397,27 +397,27 @@ fn test_multiple_parameter_layers() {
let mut online = GpuVarStore::new(stream.clone());
let mut target = GpuVarStore::new(stream.clone());
// Add 3 layers
// Add 3 layers (bf16 storage)
for i in 1..=3 {
let w_host = vec![1.0_f32; 8 * 8];
let b_host = vec![1.0_f32; 8];
let w_host: Vec<half::bf16> = vec![half::bf16::from_f32(1.0); 8 * 8];
let b_host: Vec<half::bf16> = vec![half::bf16::from_f32(1.0); 8];
let tw_host = vec![0.0_f32; 8 * 8];
let tb_host = vec![0.0_f32; 8];
let tw_host: Vec<half::bf16> = vec![half::bf16::from_f32(0.0); 8 * 8];
let tb_host: Vec<half::bf16> = vec![half::bf16::from_f32(0.0); 8];
let mut w_data = stream.alloc_zeros::<f32>(64).unwrap();
let mut w_data = stream.alloc_zeros::<half::bf16>(64).unwrap();
stream.memcpy_htod(&w_host, &mut w_data).unwrap();
online.register(&format!("layer{i}.weight"), w_data, vec![8, 8]).unwrap();
let mut b_data = stream.alloc_zeros::<f32>(8).unwrap();
let mut b_data = stream.alloc_zeros::<half::bf16>(8).unwrap();
stream.memcpy_htod(&b_host, &mut b_data).unwrap();
online.register(&format!("layer{i}.bias"), b_data, vec![8]).unwrap();
let mut tw_data = stream.alloc_zeros::<f32>(64).unwrap();
let mut tw_data = stream.alloc_zeros::<half::bf16>(64).unwrap();
stream.memcpy_htod(&tw_host, &mut tw_data).unwrap();
target.register(&format!("layer{i}.weight"), tw_data, vec![8, 8]).unwrap();
let mut tb_data = stream.alloc_zeros::<f32>(8).unwrap();
let mut tb_data = stream.alloc_zeros::<half::bf16>(8).unwrap();
stream.memcpy_htod(&tb_host, &mut tb_data).unwrap();
target.register(&format!("layer{i}.bias"), tb_data, vec![8]).unwrap();
}
@@ -429,9 +429,9 @@ fn test_multiple_parameter_layers() {
for i in 1..=3 {
let param = target.get(&format!("layer{i}.weight")).expect("param must exist");
let n = param.data.len();
let mut buf = vec![0.0_f32; n];
let mut buf = vec![half::bf16::ZERO; n];
stream.memcpy_dtoh(&param.data, &mut buf).unwrap(); // test-only readback
let w_mean: f32 = buf.iter().sum::<f32>() / n as f32;
let w_mean: f32 = buf.iter().map(|x| x.to_f32()).sum::<f32>() / n as f32;
assert!(
(w_mean - 0.2).abs() < 1e-5,