refactor(cuda): remove all #[cfg(not(feature = "cuda"))] dead CPU paths

Delete every CPU fallback block across 14 files (-286 lines):
- dqn.rs: CPU replay buffer, tensor construction, PER fallback, gradient paths
- hyperopt/adapters/ppo.rs: CPU curiosity modules, trajectory generation
- hyperopt/adapters/dqn.rs: CPU backtest fallback, sync no-op
- trainers/ppo.rs: CPU training error stub
- l2_cache.rs: CPU stub functions (gate callers behind cuda too)
- replay_buffer_type.rs: CPU is_gpu_prioritized fallback
- validation/harness.rs: CPU regime breakdown fallback
- build.rs: cpu_only_build cfg (never referenced)
- evaluate_baseline.rs: CPU gpu_handled fallback
- testing/integration/gpu: CPU test stubs

Zero #[cfg(not(feature = "cuda"))] remains in the codebase.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-16 16:26:41 +01:00
parent d95e205d4b
commit c5961cb766
14 changed files with 39 additions and 286 deletions

View File

@@ -149,18 +149,10 @@ pub fn set_persisting_l2_cache_size(num_bytes: usize) -> Result<usize, String> {
Ok(target)
}
/// CPU-only stub: always returns `Ok(0)`.
#[cfg(not(feature = "cuda"))]
pub fn set_persisting_l2_cache_size(_num_bytes: usize) -> Result<usize, String> {
Ok(0)
}
/// Reset the persisting L2 cache, releasing all persistent cache lines.
///
/// Call this during shutdown or when switching between models of very
/// different sizes to avoid stale persistence reservations.
///
/// No-op on non-CUDA builds or when L2 persistence is not supported.
#[cfg(feature = "cuda")]
pub fn reset_persisting_l2_cache() -> Result<(), String> {
use candle_core::cuda_backend::cudarc::driver::sys::{
@@ -178,11 +170,6 @@ pub fn reset_persisting_l2_cache() -> Result<(), String> {
Ok(())
}
/// CPU-only stub: always returns `Ok(())`.
#[cfg(not(feature = "cuda"))]
pub fn reset_persisting_l2_cache() -> Result<(), String> {
Ok(())
}
/// Convenience: configure L2 persistence for model weights if the GPU supports it.
///
@@ -198,6 +185,7 @@ pub fn reset_persisting_l2_cache() -> Result<(), String> {
/// tracing::info!("L2 pinning active: {} MB reserved", reserved / 1_048_576);
/// }
/// ```
#[cfg(feature = "cuda")]
pub fn pin_weights_in_l2(gpu_name: &str, weight_bytes: usize) -> Result<usize, String> {
if !supports_l2_persistence(gpu_name) {
tracing::debug!(
@@ -211,11 +199,12 @@ pub fn pin_weights_in_l2(gpu_name: &str, weight_bytes: usize) -> Result<usize, S
}
/// Convenience: unpin L2 cache (alias for [`reset_persisting_l2_cache`]).
#[cfg(feature = "cuda")]
pub fn unpin_l2() -> Result<(), String> {
reset_persisting_l2_cache()
}
#[cfg(test)]
#[cfg(all(test, feature = "cuda"))]
#[allow(clippy::let_underscore_must_use)]
mod tests {
use super::*;

View File

@@ -1367,32 +1367,19 @@ impl DQN {
// GPU PER: on CUDA, allocate GPU-resident ring buffer (hard error on failure).
// This activates the GpuBatch fast path in compute_loss_internal() and
// GPU TD error retention — eliminating 5 of 6 CPU↔GPU roundtrips per train step.
#[cfg(feature = "cuda")]
{
if device.is_cuda() {
// GPU PER is mandatory on CUDA.
super::replay_buffer_type::ReplayBufferType::try_gpu_with_halving(
config.replay_buffer_capacity,
config.state_dim,
config.per_alpha,
config.per_beta_start,
config.per_beta_max,
config.per_beta_annealing_steps,
config.per_max_memory_bytes,
&device,
)?
} else {
super::replay_buffer_type::ReplayBufferType::new_prioritized(
config.replay_buffer_capacity,
config.per_alpha,
config.per_beta_start,
config.per_beta_max,
config.per_beta_annealing_steps,
)?
}
}
#[cfg(not(feature = "cuda"))]
{
if device.is_cuda() {
// GPU PER is mandatory on CUDA.
super::replay_buffer_type::ReplayBufferType::try_gpu_with_halving(
config.replay_buffer_capacity,
config.state_dim,
config.per_alpha,
config.per_beta_start,
config.per_beta_max,
config.per_beta_annealing_steps,
config.per_max_memory_bytes,
&device,
)?
} else {
super::replay_buffer_type::ReplayBufferType::new_prioritized(
config.replay_buffer_capacity,
config.per_alpha,
@@ -2476,14 +2463,7 @@ impl DQN {
self.memory.sample(self.config.batch_size)?
};
#[cfg(feature = "cuda")]
let gpu_batch_opt = batch_sample.gpu_batch;
// CPU-side experience/weight Vecs: only needed for non-CUDA tensor construction.
// In the CUDA path, gpu_batch tensors replace these entirely.
#[cfg(not(feature = "cuda"))]
let experiences = batch_sample.experiences;
#[cfg(not(feature = "cuda"))]
let weights = batch_sample.weights;
let indices = batch_sample.indices;
// Initialize optimizer if not done
@@ -2590,62 +2570,9 @@ impl DQN {
}
// CUDA production: GpuBatch is mandatory — no CPU path.
#[cfg(feature = "cuda")]
return Err(MLError::TrainingError(
"GPU batch required — GPU PER is mandatory for DQN training.".to_owned(),
));
// Non-CUDA (tests only): build tensors from experience data via Tensor::new.
#[cfg(not(feature = "cuda"))]
{
let batch_size = experiences.len();
let state_dim = self.config.state_dim;
let (states, next_states, actions, rewards, dones) = experiences.iter().fold(
(
Vec::with_capacity(batch_size * state_dim),
Vec::with_capacity(batch_size * state_dim),
Vec::<u32>::with_capacity(batch_size),
Vec::<f32>::with_capacity(batch_size),
Vec::<f32>::with_capacity(batch_size),
),
|(mut s, mut ns, mut a, mut r, mut d), exp| {
s.extend_from_slice(&exp.state);
if exp.state.len() < state_dim {
s.resize(s.len() + state_dim - exp.state.len(), 0.0);
}
ns.extend_from_slice(&exp.next_state);
if exp.next_state.len() < state_dim {
ns.resize(ns.len() + state_dim - exp.next_state.len(), 0.0);
}
a.push(exp.action as u32);
r.push(exp.reward_f32());
d.push(if exp.done { 1.0_f32 } else { 0.0_f32 });
(s, ns, a, r, d)
},
);
let states_tensor = Tensor::new(states.as_slice(), device)
.and_then(|t| t.reshape((batch_size, state_dim)))
.and_then(|t| t.to_dtype(candle_core::DType::BF16))
.map_err(|e| MLError::TrainingError(format!("States tensor: {}", e)))?;
let next_states_tensor = Tensor::new(next_states.as_slice(), device)
.and_then(|t| t.reshape((batch_size, state_dim)))
.and_then(|t| t.to_dtype(candle_core::DType::BF16))
.map_err(|e| MLError::TrainingError(format!("Next states tensor: {}", e)))?;
let actions_tensor = Tensor::new(actions.as_slice(), device)
.map_err(|e| MLError::TrainingError(format!("Actions tensor: {}", e)))?;
let rewards_tensor = Tensor::new(rewards.as_slice(), device)
.map_err(|e| MLError::TrainingError(format!("Rewards tensor: {}", e)))?;
let dones_tensor = Tensor::new(dones.as_slice(), device)
.map_err(|e| MLError::TrainingError(format!("Dones tensor: {}", e)))?;
let weights_tensor = Tensor::new(weights.as_slice(), device)
.and_then(|t| t.to_dtype(dtype))
.map(|t| t.detach())
.map_err(|e| MLError::TrainingError(format!("Weights tensor: {}", e)))?;
break 'tensor_prep (batch_size, states_tensor, next_states_tensor, actions_tensor,
rewards_tensor, dones_tensor, Some(weights_tensor));
}
};
// Phase C+ branching loss path: independent advantage heads per action dimension.
@@ -2938,33 +2865,21 @@ impl DQN {
let loss_f32_tensor = loss_tensor.to_dtype(DType::F32)?;
// TD errors for PER priority updates
#[cfg(feature = "cuda")]
let is_gpu_per_br = self.memory.is_gpu_prioritized();
#[cfg(not(feature = "cuda"))]
let is_gpu_per_br = false;
#[allow(unused_variables)]
let (td_errors_vec, td_gpu, idx_gpu) = if self.config.use_per {
if is_gpu_per_br {
let td_tensor = td_errors_for_per.to_dtype(DType::F32)?;
#[cfg(feature = "cuda")]
{
let idx_tensor = gpu_batch_opt.as_ref()
.map(|gpu| gpu.indices.clone())
.ok_or_else(|| MLError::TrainingError(
"GPU PER indices required (branching path) — CPU fallback removed".to_owned()
))?;
(Vec::new(), Some(td_tensor), Some(idx_tensor))
}
#[cfg(not(feature = "cuda"))]
return Err(MLError::TrainingError("GPU PER requires cuda feature".to_owned()))
let idx_tensor = gpu_batch_opt.as_ref()
.map(|gpu| gpu.indices.clone())
.ok_or_else(|| MLError::TrainingError(
"GPU PER indices required (branching path) — CPU fallback removed".to_owned()
))?;
(Vec::new(), Some(td_tensor), Some(idx_tensor))
} else {
#[cfg(feature = "cuda")]
return Err(MLError::TrainingError(
"CPU PER fallback disabled — use ReplayBufferType::GpuPrioritized when cuda is enabled".to_owned()
));
#[cfg(not(feature = "cuda"))]
(td_errors_for_per.to_dtype(DType::F32)?.to_vec1()?, None::<Tensor>, None::<Tensor>)
}
} else {
(Vec::new(), None::<Tensor>, None::<Tensor>)
@@ -3409,34 +3324,22 @@ impl DQN {
// GPU SATURATION: Keep TD errors on GPU as Tensor — trainer uses
// update_priorities_gpu() directly, zero CPU transfer.
// CPU PER fallback is a hard error when cuda feature is enabled.
#[cfg(feature = "cuda")]
let is_gpu_per = self.memory.is_gpu_prioritized();
#[cfg(not(feature = "cuda"))]
let is_gpu_per = false;
#[allow(unused_variables)] // td_gpu/idx_gpu consumed in #[cfg(feature = "cuda")] struct fields
let (td_errors_vec, td_gpu, idx_gpu) = if self.config.use_per {
if is_gpu_per {
// GPU PER: keep TD errors on GPU, use GpuBatch indices directly
let td_tensor = diff.detach().to_dtype(DType::F32)?;
#[cfg(feature = "cuda")]
{
let idx_tensor = gpu_batch_opt.as_ref()
.map(|gpu| gpu.indices.clone())
.ok_or_else(|| MLError::TrainingError(
"GPU PER indices required (non-branching path) — CPU fallback removed".to_owned()
))?;
(Vec::new(), Some(td_tensor), Some(idx_tensor))
}
#[cfg(not(feature = "cuda"))]
return Err(MLError::TrainingError("GPU PER requires cuda feature".to_owned()))
let idx_tensor = gpu_batch_opt.as_ref()
.map(|gpu| gpu.indices.clone())
.ok_or_else(|| MLError::TrainingError(
"GPU PER indices required (non-branching path) — CPU fallback removed".to_owned()
))?;
(Vec::new(), Some(td_tensor), Some(idx_tensor))
} else {
#[cfg(feature = "cuda")]
return Err(MLError::TrainingError(
"CPU PER fallback disabled — use ReplayBufferType::GpuPrioritized when cuda is enabled".to_owned()
));
#[cfg(not(feature = "cuda"))]
(diff.detach().to_dtype(DType::F32)?.to_vec1()?, None::<Tensor>, None::<Tensor>)
}
} else {
(Vec::new(), None::<Tensor>, None::<Tensor>)
@@ -3547,25 +3450,16 @@ impl DQN {
self.training_steps += 1;
// Update priorities for PER (if using prioritized replay)
#[cfg(feature = "cuda")]
if let (Some(ref td_gpu), Some(ref idx_gpu)) =
(&result.td_errors_gpu, &result.indices_gpu)
{
if let (Some(ref td_gpu), Some(ref idx_gpu)) =
(&result.td_errors_gpu, &result.indices_gpu)
{
self.memory.update_priorities_gpu(idx_gpu, td_gpu)?;
} else if !result.indices.is_empty() {
self.memory
.update_priorities(&result.indices, &result.td_errors)?;
} else {
// No priorities to update (empty batch or non-PER path)
}
}
#[cfg(not(feature = "cuda"))]
if !result.indices.is_empty() {
self.memory.update_priorities_gpu(idx_gpu, td_gpu)?;
} else if !result.indices.is_empty() {
self.memory
.update_priorities(&result.indices, &result.td_errors)?;
} else {
// No priorities to update (empty batch or non-PER path)
}
// Step beta annealing for PER
self.memory.step();
@@ -3659,7 +3553,6 @@ impl DQN {
let result = self.compute_loss_internal(batch)?;
// Backward + clip WITHOUT optimizer step — zero GPU→CPU sync
#[cfg(feature = "cuda")]
let (grads, grad_norm_tensor) = if let Some(ref optimizer) = self.optimizer {
optimizer.backward_and_clip(&result.loss_tensor, self.gradient_clip_norm)?
} else {
@@ -3667,14 +3560,6 @@ impl DQN {
"Optimizer not initialized".to_owned(),
));
};
#[cfg(not(feature = "cuda"))]
let (grads, _grad_norm_tensor) = if let Some(ref optimizer) = self.optimizer {
optimizer.backward_and_clip(&result.loss_tensor, self.gradient_clip_norm)?
} else {
return Err(MLError::TrainingError(
"Optimizer not initialized".to_owned(),
));
};
// Squeeze grad_norm from [1] → rank-0 scalar tensor (cuda only — used by GPU accumulation path)
#[cfg(feature = "cuda")]

View File

@@ -493,10 +493,7 @@ impl ReplayBufferType {
/// Check if using GPU-resident prioritized replay.
/// Returns false when compiled without `cuda` feature (variant doesn't exist).
pub const fn is_gpu_prioritized(&self) -> bool {
#[cfg(feature = "cuda")]
{ matches!(self, Self::GpuPrioritized(_)) }
#[cfg(not(feature = "cuda"))]
{ false }
matches!(self, Self::GpuPrioritized(_))
}
/// Adaptive buffer sizing based on epsilon decay

View File

@@ -237,15 +237,6 @@ mod tests {
}
}
// Without the cuda feature, verify the source file is readable and intact.
#[cfg(not(feature = "cuda"))]
{
assert!(!src.is_empty(), "training_guard_kernel.cu should not be empty");
assert!(
src.contains("training_guard_check"),
"kernel source must contain training_guard_check"
);
}
}
// ════════════════════════════════════════════════════════════════════════

View File

@@ -1,20 +1,3 @@
//! Build script for ML crate - CUDA support conditional
//!
//! Enables CUDA when the 'cuda' feature is enabled, otherwise CPU-only
fn main() {
println!("cargo:rerun-if-changed=build.rs");
// Only set cpu_only_build when CUDA feature is NOT enabled
#[cfg(not(feature = "cuda"))]
{
println!("cargo:rustc-cfg=cpu_only_build");
println!("cargo:info=Building CPU-only ML crate");
}
#[cfg(feature = "cuda")]
{
println!("cargo:info=Building ML crate with CUDA support");
// CUDA-specific configuration can go here if needed
}
}

View File

@@ -1963,10 +1963,6 @@ fn main() -> Result<()> {
false // --no-gpu-eval was set; use CPU path
};
// When compiled without CUDA, gpu_handled is always false.
#[cfg(not(feature = "cuda"))]
let gpu_handled = false;
if !gpu_handled {
match evaluate_dqn_fold(
window.fold,
@@ -2198,9 +2194,6 @@ fn main() -> Result<()> {
false
};
#[cfg(not(feature = "cuda"))]
let gpu_handled = false;
if !gpu_handled {
warn!(
" [{}] Fold {} - GPU eval not available; use evaluate_supervised binary for CPU eval",

View File

@@ -3132,8 +3132,6 @@ impl HyperparameterOptimizable for DQNTrainer {
None
}
}
#[cfg(not(feature = "cuda"))]
{ None }
};
if let Some(metrics) = gpu_result {
@@ -3247,10 +3245,6 @@ impl HyperparameterOptimizable for DQNTrainer {
cuda_dev.synchronize()
.map_err(|e| MLError::TrainingError(format!("CUDA synchronize between trials FAILED — GPU in bad state: {e}")))?;
}
#[cfg(not(feature = "cuda"))]
{
// CPU-only: no GPU sync needed
}
info!("Resource cleanup complete");
// MEMORY LEAK FIX: Monitor memory usage after trial

View File

@@ -1007,21 +1007,6 @@ impl HyperparameterOptimizable for PPOTrainer {
#[cfg(feature = "cuda")]
self.ensure_gpu_data(&training_data, &ppo_agent)?;
// CPU path uses curiosity/reward shaping modules; GPU path handles these internally.
#[cfg(not(feature = "cuda"))]
let mut curiosity_module = (params.curiosity_weight > 0.001)
.then(|| {
info!("Curiosity-driven exploration enabled (weight={:.3})", params.curiosity_weight);
CuriosityModule::new(trial_device.clone(), 0.001, 2.0, 42, 128, 3)
})
.transpose()?;
#[cfg(not(feature = "cuda"))]
let curiosity_weight = params.curiosity_weight;
#[cfg(not(feature = "cuda"))]
let mut reward_shaper = Some(PPORewardShaper::default());
#[cfg(not(feature = "cuda"))]
let mut composite_reward = Some(CompositeReward::new());
// Create ExO-PPO trajectory replay buffer (M=4 rollouts, 4x sample efficiency)
let mut replay_buffer = TrajectoryReplayBuffer::new(4, params.clip_epsilon as f32);
info!("ExO-PPO trajectory replay enabled (M=4 rollouts)");
@@ -1036,18 +1021,8 @@ impl HyperparameterOptimizable for PPOTrainer {
for _batch_idx in 0..num_batches {
let episodes_this_batch = batch_episodes.min(num_train - _batch_idx * batch_episodes);
// GPU experience collection — no CPU fallback when CUDA enabled
#[cfg(feature = "cuda")]
// GPU experience collection
let mut trajectory_batch = self.gpu_collect_trajectories(episodes_this_batch)?;
#[cfg(not(feature = "cuda"))]
let mut trajectory_batch = self.generate_trajectories_from_data(
&ppo_agent, train_data, episodes_this_batch,
&mut curiosity_module, curiosity_weight,
&mut reward_shaper, &mut composite_reward,
gae_gamma, gae_lambda, &trial_device,
).map_err(|e| {
MLError::TrainingError(format!("Failed to generate trajectories: {}", e))
})?;
// Update PPO with on-policy trajectory batch
let (policy_loss, value_loss) = ppo_agent
@@ -1145,8 +1120,7 @@ impl HyperparameterOptimizable for PPOTrainer {
let avg_value_loss = total_value_loss / num_batches as f64;
let avg_reward = total_reward / num_batches as f64;
// GPU walk-forward backtest (CUDA only, graceful fallback to None)
#[cfg(feature = "cuda")]
// GPU walk-forward backtest
let (bt_sharpe, bt_trades) = if trial_device.is_cuda() {
match self.run_gpu_backtest(&ppo_agent, &trial_device, &params) {
Ok((s, t)) => {
@@ -1160,8 +1134,6 @@ impl HyperparameterOptimizable for PPOTrainer {
} else {
(None, None)
};
#[cfg(not(feature = "cuda"))]
let (bt_sharpe, bt_trades): (Option<f32>, Option<u32>) = (None, None);
let metrics = PPOMetrics {
policy_loss: avg_policy_loss,

View File

@@ -16,10 +16,7 @@ use candle_core::Device;
#[cfg(feature = "cuda")]
use common::metrics::training_metrics;
use tokio::sync::Mutex;
#[cfg(feature = "cuda")]
use tracing::{debug, info, warn};
#[cfg(not(feature = "cuda"))]
use tracing::{debug, info};
use crate::common::action::FactoredAction;
use crate::gpu::DeviceConfig;
@@ -459,17 +456,7 @@ impl PpoTrainer {
self.hyperparams.epochs
);
#[cfg(feature = "cuda")]
return self.train_gpu(market_data, progress_callback).await;
#[cfg(not(feature = "cuda"))]
{
drop(market_data);
drop(progress_callback);
Err(MLError::TrainingError(
"PPO training requires CUDA — CPU training path has been removed".to_owned(),
))
}
self.train_gpu(market_data, progress_callback).await
}
#[cfg(feature = "cuda")]

View File

@@ -237,9 +237,7 @@ impl ValidationHarness {
// 8. Per-regime breakdown (GPU when available, CPU fallback)
let per_regime_metrics = {
#[cfg(feature = "cuda")]
{
// Try GPU classification first; fall back to CPU on any error.
let cuda_device = candle_core::Device::cuda_if_available(0);
match cuda_device {
Ok(ref dev) if dev.is_cuda() => {
@@ -249,10 +247,6 @@ impl ValidationHarness {
_ => per_regime_breakdown(&all_returns, &all_features),
}
}
#[cfg(not(feature = "cuda"))]
{
per_regime_breakdown(&all_returns, &all_features)
}
};
// 9. Verdict

View File

@@ -97,10 +97,6 @@ mod tests {
}
}
#[cfg(not(feature = "cuda"))]
{
info!("⚠️ CUDA feature not enabled - skipping device properties test");
}
}
#[test]

View File

@@ -342,22 +342,6 @@ mod tests {
}
}
#[cfg(not(feature = "cuda"))]
#[test]
fn test_cuda_feature_disabled() {
init_gpu_test_env();
info!("⚠️ CUDA feature not enabled - testing graceful handling");
// Test that the system handles missing CUDA gracefully
assert!(!cuda_available(), "CUDA should not be available when feature is disabled");
// Test that we can still get a CPU device
let device = get_test_device();
assert!(!device.is_cuda(), "Should fallback to CPU device");
info!("✅ Graceful CUDA feature disabled handling verified");
}
#[test]
fn test_cuda_kernel_integration_framework() {
init_gpu_test_env();

View File

@@ -33,14 +33,7 @@ pub mod utils {
/// Check if CUDA is available for testing
pub fn cuda_available() -> bool {
#[cfg(feature = "cuda")]
{
cudarc::driver::CudaDevice::new(0).is_ok()
}
#[cfg(not(feature = "cuda"))]
{
false
}
cudarc::driver::CudaDevice::new(0).is_ok()
}
/// Get test device (CUDA if available, CPU otherwise)

View File

@@ -403,11 +403,6 @@ mod tests {
}
}
#[cfg(not(feature = "cuda"))]
{
info!("GPU monitoring fallback - CUDA feature not enabled");
}
info!("✅ Production GPU monitoring test completed");
}