From 9dd48621eae3ab96f09a1fb5be0c45154f67635c Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Sun, 29 Mar 2026 21:32:04 +0200 Subject: [PATCH] fix(tests): update integration tests for f32 pipeline + adam_epsilon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- crates/ml-dqn/tests/gpu_smoketest.rs | 1 + crates/ml/tests/activation_tests.rs | 6 +- crates/ml/tests/dropout_scheduler_tests.rs | 2 +- crates/ml/tests/gpu_backtest_validation.rs | 6 +- crates/ml/tests/gpu_kernel_parity_test.rs | 78 ++++++++++----------- crates/ml/tests/gpu_per_integration_test.rs | 7 +- crates/ml/tests/smoke_test_real_data.rs | 14 ++-- crates/ml/tests/target_update_tests.rs | 40 +++++------ 8 files changed, 78 insertions(+), 76 deletions(-) diff --git a/crates/ml-dqn/tests/gpu_smoketest.rs b/crates/ml-dqn/tests/gpu_smoketest.rs index d11a4b8d2..c0fc6ad3e 100644 --- a/crates/ml-dqn/tests/gpu_smoketest.rs +++ b/crates/ml-dqn/tests/gpu_smoketest.rs @@ -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 diff --git a/crates/ml/tests/activation_tests.rs b/crates/ml/tests/activation_tests.rs index 47f8df543..6cdaa5cd2 100644 --- a/crates/ml/tests/activation_tests.rs +++ b/crates/ml/tests/activation_tests.rs @@ -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 { diff --git a/crates/ml/tests/dropout_scheduler_tests.rs b/crates/ml/tests/dropout_scheduler_tests.rs index f08558fb7..b0ef8907a 100644 --- a/crates/ml/tests/dropout_scheduler_tests.rs +++ b/crates/ml/tests/dropout_scheduler_tests.rs @@ -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 diff --git a/crates/ml/tests/gpu_backtest_validation.rs b/crates/ml/tests/gpu_backtest_validation.rs index 9e8320ec7..d7207d14b 100644 --- a/crates/ml/tests/gpu_backtest_validation.rs +++ b/crates/ml/tests/gpu_backtest_validation.rs @@ -137,14 +137,14 @@ mod gpu_tests { /// Build a closure that always returns action indices for the given `action`. /// - /// The closure receives `(&CudaSlice, n_windows, state_dim)` and + /// The closure receives `(&CudaSlice, n_windows, state_dim)` and /// returns `CudaSlice` of length `n_windows` filled with `action`. fn constant_action_model( action: i32, stream: &Arc, - ) -> impl Fn(&CudaSlice, usize, usize) -> Result, MLError> { + ) -> impl Fn(&CudaSlice, usize, usize) -> Result, MLError> { let stream = Arc::clone(stream); - move |_states: &CudaSlice, n_windows: usize, _state_dim: usize| { + move |_states: &CudaSlice, n_windows: usize, _state_dim: usize| { let host_actions = vec![action; n_windows]; let mut gpu_actions = stream .alloc_zeros::(n_windows) diff --git a/crates/ml/tests/gpu_kernel_parity_test.rs b/crates/ml/tests/gpu_kernel_parity_test.rs index 71a092671..8fe4a7ed8 100644 --- a/crates/ml/tests/gpu_kernel_parity_test.rs +++ b/crates/ml/tests/gpu_kernel_parity_test.rs @@ -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, - ) -> (CudaSlice, CudaSlice) { + ) -> (CudaSlice, CudaSlice) { 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::(market_len).unwrap(); + let market_data: Vec = (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::(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::(target_len).unwrap(); + let mut target_buf = stream.alloc_zeros::(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::(market_len).unwrap(); + let market_data: Vec = (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::(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::(target_len).unwrap(); + let mut target_buf = stream.alloc_zeros::(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::() / gamma_s0.len() as f32; + let mean_gamma: f32 = gamma_s0.iter().map(|x| x.to_f32()).sum::() / gamma_s0.len() as f32; assert!( (mean_gamma - 1.0).abs() < 0.01, "RMSNorm gamma_s0 mean = {mean_gamma} (expected ~1.0)" diff --git a/crates/ml/tests/gpu_per_integration_test.rs b/crates/ml/tests/gpu_per_integration_test.rs index 223cd3fbf..ef594e599 100644 --- a/crates/ml/tests/gpu_per_integration_test.rs +++ b/crates/ml/tests/gpu_per_integration_test.rs @@ -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 = 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); diff --git a/crates/ml/tests/smoke_test_real_data.rs b/crates/ml/tests/smoke_test_real_data.rs index 755fd5cd8..93a800d74 100644 --- a/crates/ml/tests/smoke_test_real_data.rs +++ b/crates/ml/tests/smoke_test_real_data.rs @@ -427,7 +427,7 @@ mod gpu_smoke { ohlcv_dir: &Path, mbp10_dir: Option<&Path>, stream: &Arc, - ) -> Result<(CudaSlice, CudaSlice, usize), anyhow::Error> { + ) -> Result<(CudaSlice, CudaSlice, 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::(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 = market_data.iter().map(|&x| half::bf16::from_f32(x)).collect(); + let mut market_buf = stream.alloc_zeros::(n * MARKET_DIM).unwrap(); + stream.memcpy_htod(&market_bf16, &mut market_buf).unwrap(); - let mut target_buf = stream.alloc_zeros::(n * 4).unwrap(); - stream.memcpy_htod(&target_data, &mut target_buf).unwrap(); + let target_bf16: Vec = target_data.iter().map(|&x| half::bf16::from_f32(x)).collect(); + let mut target_buf = stream.alloc_zeros::(n * 4).unwrap(); + stream.memcpy_htod(&target_bf16, &mut target_buf).unwrap(); Ok((market_buf, target_buf, n)) } diff --git a/crates/ml/tests/target_update_tests.rs b/crates/ml/tests/target_update_tests.rs index b65b98ff1..9bbf92145 100644 --- a/crates/ml/tests/target_update_tests.rs +++ b/crates/ml/tests/target_update_tests.rs @@ -103,26 +103,26 @@ fn make_stream() -> Arc { 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) -> 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 = vec![half::bf16::from_f32(value); 10 * 10]; + let bias_host: Vec = vec![half::bf16::from_f32(value); 10]; - let mut w_data = stream.alloc_zeros::(100).unwrap(); + let mut w_data = stream.alloc_zeros::(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::(10).unwrap(); + let mut b_data = stream.alloc_zeros::(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) -> f32 { let mut sum = 0.0_f32; let mut count = 0; @@ -130,9 +130,9 @@ fn get_mean_value(store: &GpuVarStore, stream: &Arc) -> 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(¶m.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 = vec![half::bf16::from_f32(1.0); 8 * 8]; + let b_host: Vec = 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 = vec![half::bf16::from_f32(0.0); 8 * 8]; + let tb_host: Vec = vec![half::bf16::from_f32(0.0); 8]; - let mut w_data = stream.alloc_zeros::(64).unwrap(); + let mut w_data = stream.alloc_zeros::(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::(8).unwrap(); + let mut b_data = stream.alloc_zeros::(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::(64).unwrap(); + let mut tw_data = stream.alloc_zeros::(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::(8).unwrap(); + let mut tb_data = stream.alloc_zeros::(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(¶m.data, &mut buf).unwrap(); // test-only readback - let w_mean: f32 = buf.iter().sum::() / n as f32; + let w_mean: f32 = buf.iter().map(|x| x.to_f32()).sum::() / n as f32; assert!( (w_mean - 0.2).abs() < 1e-5,