Files
foxhunt/ml/tests/cuda_fallback_validation.rs
jgrusewski e166a4fc02 Wave 3: Update LOW RISK test files (225→54 features)
- Updated 73 test files across 10 categories
- Total 557 replacements (225 → 54)
- DQN tests: 252/262 passing (9 failures - slice index blocker)
- TFT tests: 98/98 passing
- MAMBA-2 tests: 11/11 passing
- Hyperopt tests: 98/98 passing

Critical findings:
- Blocker: ml/src/trainers/dqn.rs:3444 hardcoded slice indices
- Architecture mismatch: extract_current_features() vs extract_current_features_v2()

Wave 3 Agent breakdown:
- Agent 1: DQN test files (12 files)
- Agent 2: PPO test files (2 files)
- Agent 3: TFT test files (6 files)
- Agent 4: MAMBA-2 test files (2 files)
- Agent 5: Feature extraction tests (3 files)
- Agent 6: Integration test files (9 files)
- Agent 7: Data loader test files (3 files)
- Agent 8: Hyperopt test files (1 file)
- Agent 9: Benchmark test files (9 files)
- Agent 10: Utility & misc test files (73 files)

Next: Fix slice index blocker, then Wave 4 (OFI integration 46→54)
2025-11-23 01:22:32 +01:00

516 lines
18 KiB
Rust

//! CUDA Fallback Validation Tests (Agent 23 Test #15)
//!
//! Validates that all ML trainers correctly fall back to CPU when GPU is unavailable.
//! This ensures deployment flexibility across environments with and without GPU hardware.
//!
//! Test Coverage:
//! 1. Explicit CPU-only mode selection
//! 2. Device auto-selection (GPU if available, CPU fallback)
//! 3. Training capability verification on both CPU and GPU
//! 4. Device selection logging validation
//! 5. Feature flag compilation testing (cuda vs no cuda)
use candle_core::{DType, Device, Tensor};
use ml::dqn::{WorkingDQN, WorkingDQNConfig};
use ml::ppo::ppo::{PPOConfig, WorkingPPO};
use ml::trainers::mamba2::{Mamba2Hyperparameters, Mamba2Trainer};
/// Helper function to create test tensor batch
fn create_test_state_batch(
batch_size: usize,
state_dim: usize,
device: &Device,
) -> anyhow::Result<Tensor> {
Tensor::randn(0.0f32, 1.0, &[batch_size, state_dim], device)
.map_err(|e| anyhow::anyhow!("Failed to create test tensor: {}", e))
}
// ============================================================================
// Test 1: DQN CUDA Fallback
// ============================================================================
#[test]
fn test_dqn_explicit_cpu_mode() -> anyhow::Result<()> {
// Test 1: Verify DQN can be created and used (auto-selects device internally)
// Note: WorkingDQN uses Device::cuda_if_available internally, so we cannot force CPU mode
let config = WorkingDQNConfig {
state_dim: 54,
num_actions: 3,
hidden_dims: vec![64, 32],
learning_rate: 0.001,
gamma: 0.99,
epsilon_start: 1.0,
epsilon_end: 0.01,
epsilon_decay: 0.995,
replay_buffer_capacity: 1000,
batch_size: 32,
min_replay_size: 64,
target_update_freq: 100,
use_double_dqn: true,
use_huber_loss: true, // Huber loss default (more robust to outliers)
huber_delta: 1.0, // Standard Huber delta
};
let dqn = WorkingDQN::new(config.clone())?;
// Create test input (DQN will auto-select device)
let auto_device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
let state = create_test_state_batch(1, config.state_dim, &auto_device)?;
// Forward pass should work
let output = dqn.forward(&state)?;
// Verify output device (GPU if available, CPU fallback)
println!("DQN output device (auto-select): {:?}", output.device());
// Verify output shape
assert_eq!(
output.dims(),
&[1, config.num_actions],
"DQN output shape mismatch"
);
println!("✅ DQN auto-select mode: PASSED");
Ok(())
}
#[test]
fn test_dqn_auto_device_selection() -> anyhow::Result<()> {
// Test 2: Auto-select device (should work on both CPU and GPU)
let config = WorkingDQNConfig::emergency_safe_defaults();
let dqn = WorkingDQN::new(config.clone())?;
// Try auto device selection
let auto_device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
let state = create_test_state_batch(1, config.state_dim, &auto_device)?;
// Training should work on auto-selected device
let result = dqn.forward(&state);
assert!(result.is_ok(), "DQN should train on auto-selected device");
let output = result?;
println!("DQN auto-selected device: {:?}", output.device());
println!("DQN is using CUDA: {}", output.device().is_cuda());
println!("✅ DQN auto device selection: PASSED");
Ok(())
}
#[test]
#[cfg(not(feature = "cuda"))]
fn test_dqn_no_cuda_feature_fallback() -> anyhow::Result<()> {
// Test 3: When compiled without CUDA feature, should always use CPU
let config = WorkingDQNConfig::emergency_safe_defaults();
let dqn = WorkingDQN::new(config.clone())?;
let auto_device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
assert!(
auto_device.is_cpu(),
"Should fall back to CPU without CUDA feature"
);
let state = create_test_state_batch(1, config.state_dim, &Device::Cpu)?;
let output = dqn.forward(&state)?;
assert!(
output.device().is_cpu(),
"DQN should use CPU without CUDA feature"
);
println!("✅ DQN no-CUDA feature fallback: PASSED");
Ok(())
}
// ============================================================================
// Test 2: PPO CUDA Fallback
// ============================================================================
#[test]
fn test_ppo_explicit_cpu_mode() -> anyhow::Result<()> {
// Test 1: Verify PPO can be created and used on CPU explicitly
let config = PPOConfig {
state_dim: 54,
num_actions: 3,
policy_hidden_dims: vec![64, 32],
value_hidden_dims: vec![64, 32],
policy_learning_rate: 0.0001,
value_learning_rate: 0.0001,
clip_epsilon: 0.2,
value_loss_coeff: 0.5,
entropy_coeff: 0.01,
batch_size: 32,
mini_batch_size: 8,
num_epochs: 4,
max_grad_norm: 0.5,
..Default::default()
};
// Create PPO with explicit CPU device
let ppo = WorkingPPO::with_device(config.clone(), Device::Cpu)?;
// Create test input on CPU
let state = create_test_state_batch(1, config.state_dim, &Device::Cpu)?;
// Forward pass should work on CPU (use actor for action logits)
let action_logits = ppo.actor.forward(&state)?;
let value = ppo.critic.forward(&state)?;
println!(
"PPO action logits device (explicit CPU): {:?}",
action_logits.device()
);
println!("PPO value device (explicit CPU): {:?}", value.device());
assert!(
action_logits.device().is_cpu(),
"PPO should use CPU when requested"
);
assert!(value.device().is_cpu(), "PPO value should use CPU");
println!("✅ PPO explicit CPU mode: PASSED");
Ok(())
}
#[test]
fn test_ppo_auto_device_selection() -> anyhow::Result<()> {
// Test 2: Auto-select device (should work on both CPU and GPU)
let config = PPOConfig::default();
// Try auto device selection
let auto_device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
let ppo = WorkingPPO::with_device(config.clone(), auto_device.clone())?;
let state = create_test_state_batch(1, config.state_dim, &auto_device)?;
// Forward pass should work on auto-selected device
let action_logits = ppo.actor.forward(&state)?;
println!("PPO auto-selected device: {:?}", action_logits.device());
println!("PPO is using CUDA: {}", action_logits.device().is_cuda());
println!("✅ PPO auto device selection: PASSED");
Ok(())
}
#[test]
#[cfg(not(feature = "cuda"))]
fn test_ppo_no_cuda_feature_fallback() -> anyhow::Result<()> {
// Test 3: When compiled without CUDA feature, should always use CPU
let config = PPOConfig::default();
let auto_device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
assert!(
auto_device.is_cpu(),
"Should fall back to CPU without CUDA feature"
);
let ppo = WorkingPPO::with_device(config.clone(), Device::Cpu)?;
let state = create_test_state_batch(1, config.state_dim, &Device::Cpu)?;
let action_logits = ppo.actor.forward(&state)?;
assert!(
action_logits.device().is_cpu(),
"PPO should use CPU without CUDA feature"
);
println!("✅ PPO no-CUDA feature fallback: PASSED");
Ok(())
}
// ============================================================================
// Test 3: MAMBA-2 CUDA Fallback
// ============================================================================
#[test]
fn test_mamba2_auto_device_selection() -> anyhow::Result<()> {
// MAMBA-2 uses Device::cuda_if_available(0) internally with automatic fallback
let hyperparams = Mamba2Hyperparameters {
d_model: 256, // Minimum valid d_model
state_size: 16,
n_layers: 4, // Minimum valid n_layers
learning_rate: 0.001,
batch_size: 8,
epochs: 1, // Single epoch for validation
seq_len: 32,
dropout: 0.1,
grad_clip: 1.0,
weight_decay: 0.01,
warmup_steps: 10,
};
// MAMBA-2 trainer auto-selects device (GPU if available, CPU fallback)
let trainer = Mamba2Trainer::new(hyperparams.clone(), None)?;
// Verify trainer was created successfully
println!("MAMBA-2 trainer device: {:?}", trainer.device);
println!("MAMBA-2 is using CUDA: {}", trainer.device.is_cuda());
// MAMBA-2 should gracefully handle both CPU and GPU
assert!(
trainer.device.is_cpu() || trainer.device.is_cuda(),
"MAMBA-2 should use valid device"
);
println!("✅ MAMBA-2 auto device selection: PASSED");
Ok(())
}
#[test]
fn test_mamba2_cpu_fallback_logging() -> anyhow::Result<()> {
// Verify MAMBA-2 logs CPU fallback message when GPU is unavailable
// This test validates logging behavior (requires manual inspection of logs)
let hyperparams = Mamba2Hyperparameters {
d_model: 256, // Minimum valid d_model // Very small for CPU testing
state_size: 16, // Minimum valid state_size
n_layers: 4, // Minimum valid n_layers
learning_rate: 0.001,
batch_size: 4,
epochs: 1,
seq_len: 16,
dropout: 0.0,
grad_clip: 1.0,
weight_decay: 0.0,
warmup_steps: 0,
};
let trainer = Mamba2Trainer::new(hyperparams, None)?;
// Log device selection for manual verification
if trainer.device.is_cpu() {
println!("⚠️ MAMBA-2 using CPU (expected if GPU unavailable)");
} else {
println!("✅ MAMBA-2 using CUDA GPU");
}
println!("✅ MAMBA-2 CPU fallback logging: PASSED");
Ok(())
}
// ============================================================================
// Test 4: Cross-Trainer Device Consistency
// ============================================================================
#[test]
fn test_device_selection_consistency() -> anyhow::Result<()> {
// Verify all trainers use consistent device selection logic
let auto_device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
println!("=== Device Selection Consistency Test ===");
println!("Auto-selected device: {:?}", auto_device);
println!("Is CUDA: {}", auto_device.is_cuda());
println!("Is CPU: {}", auto_device.is_cpu());
// Test DQN
let dqn_config = WorkingDQNConfig::emergency_safe_defaults();
let dqn = WorkingDQN::new(dqn_config.clone())?;
let dqn_state = create_test_state_batch(1, dqn_config.state_dim, &auto_device)?;
let dqn_output = dqn.forward(&dqn_state)?;
println!("DQN device: {:?}", dqn_output.device());
// Test PPO
let ppo_config = PPOConfig::default();
let ppo = WorkingPPO::with_device(ppo_config.clone(), auto_device.clone())?;
let ppo_state = create_test_state_batch(1, ppo_config.state_dim, &auto_device)?;
let ppo_output = ppo.actor.forward(&ppo_state)?;
println!("PPO device: {:?}", ppo_output.device());
// Test MAMBA-2
let mamba_hyperparams = Mamba2Hyperparameters {
d_model: 256, // Minimum valid d_model
state_size: 16, // Minimum valid state_size
n_layers: 4, // Minimum valid n_layers
learning_rate: 0.001,
batch_size: 4,
epochs: 1,
seq_len: 16,
dropout: 0.0,
grad_clip: 1.0,
weight_decay: 0.0,
warmup_steps: 0,
};
let mamba_trainer = Mamba2Trainer::new(mamba_hyperparams, None)?;
println!("MAMBA-2 device: {:?}", mamba_trainer.device);
// All trainers should use consistent device selection
println!("✅ Device selection consistency: PASSED");
Ok(())
}
// ============================================================================
// Test 5: Device Allocation Verification
// ============================================================================
#[test]
fn test_device_tensor_allocation() -> anyhow::Result<()> {
// Verify tensors can be allocated on both CPU and GPU
let cpu_device = Device::Cpu;
let gpu_device_opt = Device::cuda_if_available(0).ok();
// Test CPU allocation
let cpu_tensor = Tensor::zeros(&[10, 54], DType::F32, &cpu_device)?;
assert!(cpu_tensor.device().is_cpu(), "CPU tensor allocation failed");
println!("✅ CPU tensor allocation: PASSED");
// Test GPU allocation (if available)
if let Some(gpu_device) = gpu_device_opt {
let gpu_tensor = Tensor::zeros(&[10, 54], DType::F32, &gpu_device)?;
assert!(
gpu_tensor.device().is_cuda(),
"GPU tensor allocation failed"
);
println!("✅ GPU tensor allocation: PASSED");
} else {
println!("⚠️ GPU not available, skipping GPU tensor allocation test");
}
Ok(())
}
// ============================================================================
// Test 6: Feature Flag Compilation
// ============================================================================
#[test]
#[cfg(feature = "cuda")]
fn test_cuda_feature_enabled() {
// Verify CUDA feature is enabled during compilation
println!("✅ CUDA feature flag: ENABLED");
// Verify GPU device can be accessed
match Device::cuda_if_available(0) {
Ok(device) => {
if device.is_cuda() {
println!("✅ CUDA device accessible: {:?}", device);
} else {
println!("⚠️ CUDA feature enabled but GPU not available");
}
},
Err(e) => {
println!("⚠️ CUDA feature enabled but device creation failed: {}", e);
},
}
}
#[test]
#[cfg(not(feature = "cuda"))]
fn test_cuda_feature_disabled() {
// Verify CUDA feature is disabled during compilation
println!("✅ CUDA feature flag: DISABLED");
// Verify only CPU device is available
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
assert!(
device.is_cpu(),
"Should only have CPU device without CUDA feature"
);
println!("✅ CPU-only mode verified");
}
// ============================================================================
// Test 7: Deployment Environment Simulation
// ============================================================================
#[test]
fn test_deployment_cpu_only_environment() -> anyhow::Result<()> {
// Simulate deployment environment without GPU
// This test validates the full stack works on CPU-only infrastructure
println!("=== Simulating CPU-Only Deployment Environment ===");
// Force CPU device
let cpu_device = Device::Cpu;
// Test 1: DQN on CPU
let dqn_config = WorkingDQNConfig {
state_dim: 54,
num_actions: 3,
hidden_dims: vec![32, 16], // Smaller for CPU
learning_rate: 0.001,
gamma: 0.99,
epsilon_start: 1.0,
epsilon_end: 0.01,
epsilon_decay: 0.995,
replay_buffer_capacity: 100,
batch_size: 8, // Small batch for CPU
min_replay_size: 16,
target_update_freq: 10,
use_double_dqn: true,
use_huber_loss: true, // Huber loss default (more robust to outliers)
huber_delta: 1.0, // Standard Huber delta
};
let dqn = WorkingDQN::new(dqn_config.clone())?;
let dqn_state = create_test_state_batch(1, dqn_config.state_dim, &cpu_device)?;
let _dqn_output = dqn.forward(&dqn_state)?;
println!("✅ DQN operational on CPU");
// Test 2: PPO on CPU
let ppo_config = PPOConfig {
state_dim: 54,
num_actions: 3,
policy_hidden_dims: vec![32, 16],
value_hidden_dims: vec![32, 16],
policy_learning_rate: 0.0001,
value_learning_rate: 0.0001,
clip_epsilon: 0.2,
value_loss_coeff: 0.5,
entropy_coeff: 0.01,
batch_size: 8,
mini_batch_size: 4,
num_epochs: 2,
max_grad_norm: 0.5,
..Default::default()
};
let ppo = WorkingPPO::with_device(ppo_config.clone(), cpu_device.clone())?;
let ppo_state = create_test_state_batch(1, ppo_config.state_dim, &cpu_device)?;
let _action_logits = ppo.actor.forward(&ppo_state)?;
let _value = ppo.critic.forward(&ppo_state)?;
println!("✅ PPO operational on CPU");
// Test 3: MAMBA-2 on CPU (auto-fallback)
let mamba_hyperparams = Mamba2Hyperparameters {
d_model: 256, // Minimum valid d_model
state_size: 16, // Minimum valid state_size
n_layers: 4, // Minimum valid n_layers
learning_rate: 0.001,
batch_size: 2,
epochs: 1,
seq_len: 8,
dropout: 0.0,
grad_clip: 1.0,
weight_decay: 0.0,
warmup_steps: 0,
};
let _mamba_trainer = Mamba2Trainer::new(mamba_hyperparams, None)?;
println!("✅ MAMBA-2 operational on CPU");
println!("✅ CPU-only deployment environment: VALIDATED");
Ok(())
}
#[test]
fn test_deployment_gpu_environment() -> anyhow::Result<()> {
// Simulate deployment environment with GPU (if available)
println!("=== Simulating GPU Deployment Environment ===");
match Device::cuda_if_available(0) {
Ok(gpu_device) if gpu_device.is_cuda() => {
println!("GPU available: {:?}", gpu_device);
// Test GPU-accelerated training
let dqn_config = WorkingDQNConfig::emergency_safe_defaults();
let dqn = WorkingDQN::new(dqn_config.clone())?;
let dqn_state = create_test_state_batch(1, dqn_config.state_dim, &gpu_device)?;
let dqn_output = dqn.forward(&dqn_state)?;
println!("DQN output device: {:?}", dqn_output.device());
println!("✅ GPU deployment environment: VALIDATED");
},
_ => {
println!("⚠️ GPU not available, skipping GPU deployment test");
println!(" This is expected in CPU-only environments");
},
}
Ok(())
}