From 5546a45bc353e4488db063fde72c925be94483ea Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Thu, 2 Apr 2026 14:21:45 +0200 Subject: [PATCH] =?UTF-8?q?refactor:=20remove=20gpu=5Fn=5Fepisodes=20overr?= =?UTF-8?q?ide=20=E2=80=94=20auto-scale=20from=20VRAM=20everywhere?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gpu_n_episodes was manually overridden in GPU profiles, training configs, test files, and hyperopt — all set to 0 or small fixed values that bypassed the auto-scaling logic, causing a div-by-zero crash in train_baseline_rl. Now: single auto-scaling path via optimal_n_episodes() from VRAM/SM count. No manual override field. Cap at 16384 (consistent with AutoBatchSizer's 8192 cap pattern). Floor at 32 for small GPUs. Removed gpu_n_episodes from: - DQNHyperparameters, PpoHyperparameters structs - All 4 GPU profiles (rtx3050, h100, a100, default) - Training profiles (smoketest, localdev) - ExperienceProfile struct + serde - Hyperopt adapter - All test overrides Co-Authored-By: Claude Opus 4.6 (1M context) --- config/gpu/a100.toml | 1 - config/gpu/default.toml | 1 - config/gpu/h100.toml | 4 -- config/gpu/rtx3050.toml | 1 - config/training/dqn-localdev.toml | 3 +- config/training/dqn-smoketest.toml | 3 +- crates/ml-core/src/gpu/profile.rs | 6 --- crates/ml/examples/train_baseline_rl.rs | 3 +- crates/ml/src/hyperopt/adapters/dqn.rs | 2 - crates/ml/src/trainers/dqn/config.rs | 4 -- .../src/trainers/dqn/trainer/training_loop.rs | 51 ++++++++----------- crates/ml/src/trainers/ppo.rs | 15 ++++-- crates/ml/src/training_profile.rs | 4 -- .../dqn_early_stopping_termination_test.rs | 3 -- crates/ml/tests/dqn_training_pipeline_test.rs | 12 +---- .../tests/ppo_recurrent_performance_tests.rs | 1 - crates/ml/tests/smoke_test_real_data.rs | 1 - 17 files changed, 37 insertions(+), 78 deletions(-) diff --git a/config/gpu/a100.toml b/config/gpu/a100.toml index c82ba24be..1cd527d16 100644 --- a/config/gpu/a100.toml +++ b/config/gpu/a100.toml @@ -7,7 +7,6 @@ hidden_dim_base = 256 replay_buffer_vram_fraction = 0.70 [experience] -gpu_n_episodes = 0 # 0 = auto from VRAM gpu_timesteps_per_episode = 500 [cuda] diff --git a/config/gpu/default.toml b/config/gpu/default.toml index 49eeda987..cac4d8e09 100644 --- a/config/gpu/default.toml +++ b/config/gpu/default.toml @@ -7,7 +7,6 @@ hidden_dim_base = 256 replay_buffer_vram_fraction = 0.50 [experience] -gpu_n_episodes = 0 # 0 = auto from VRAM gpu_timesteps_per_episode = 200 [cuda] diff --git a/config/gpu/h100.toml b/config/gpu/h100.toml index 110dd7fc8..b83339af3 100644 --- a/config/gpu/h100.toml +++ b/config/gpu/h100.toml @@ -7,10 +7,6 @@ hidden_dim_base = 256 replay_buffer_vram_fraction = 0.70 [experience] -# 2048 episodes: cuBLAS SGEMM batch=2048 saturates 132 SMs. -# At 256 episodes, the tiny 256×48 GEMMs leave >90% of SMs idle. -# 2048×100 = 204K experiences/epoch (vs 256×500 = 128K). -gpu_n_episodes = 0 # 0 = auto from VRAM gpu_timesteps_per_episode = 100 [cuda] diff --git a/config/gpu/rtx3050.toml b/config/gpu/rtx3050.toml index 62cff96a3..88d88c663 100644 --- a/config/gpu/rtx3050.toml +++ b/config/gpu/rtx3050.toml @@ -7,7 +7,6 @@ hidden_dim_base = 256 replay_buffer_vram_fraction = 0.40 [experience] -gpu_n_episodes = 0 # 0 = auto from VRAM gpu_timesteps_per_episode = 100 [cuda] diff --git a/config/training/dqn-localdev.toml b/config/training/dqn-localdev.toml index 2d2ae12e4..cc369d147 100644 --- a/config/training/dqn-localdev.toml +++ b/config/training/dqn-localdev.toml @@ -1,6 +1,6 @@ # DQN Local Dev Profile — extended training on RTX 3050 (4GB VRAM) # Larger buffer + more episodes than smoketest for meaningful convergence. -# gpu_n_episodes=64 × gpu_timesteps=200 = 12,800 experiences/epoch +# gpu_n_episodes=auto × gpu_timesteps=200 (auto-scaled from VRAM) # buffer_size=50K → fills over ~4 epochs, reducing memorization [training] @@ -45,7 +45,6 @@ patience = 20 min_epochs_before_stopping = 80 [experience] -gpu_n_episodes = 64 gpu_timesteps_per_episode = 200 min_hold_bars = 3 diff --git a/config/training/dqn-smoketest.toml b/config/training/dqn-smoketest.toml index a8a5291d0..e5f317cdf 100644 --- a/config/training/dqn-smoketest.toml +++ b/config/training/dqn-smoketest.toml @@ -1,5 +1,5 @@ # DQN Smoke Test Profile — f32 master weights + bf16 tensor core shadows -# gpu_n_episodes=32 × gpu_timesteps=100 = 3200 experiences/epoch +# gpu_n_episodes=auto × gpu_timesteps=100 (auto-scaled from VRAM) # batch_size=64 fits in 4GB RTX 3050 # # Mixed-precision (NVIDIA AMP pattern): @@ -49,7 +49,6 @@ patience = 5 min_epochs_before_stopping = 5 [experience] -gpu_n_episodes = 32 gpu_timesteps_per_episode = 100 min_hold_bars = 3 diff --git a/crates/ml-core/src/gpu/profile.rs b/crates/ml-core/src/gpu/profile.rs index 147e73696..c83b17a14 100644 --- a/crates/ml-core/src/gpu/profile.rs +++ b/crates/ml-core/src/gpu/profile.rs @@ -46,7 +46,6 @@ pub struct TrainingProfile { /// GPU experience collection parameters. #[derive(Debug, Clone, Deserialize)] pub struct ExperienceProfile { - pub gpu_n_episodes: usize, pub gpu_timesteps_per_episode: usize, } @@ -203,7 +202,6 @@ impl GpuProfile { replay_buffer_vram_fraction: 0.50, }, experience: ExperienceProfile { - gpu_n_episodes: 64, gpu_timesteps_per_episode: 200, }, cuda: CudaProfile { @@ -276,7 +274,6 @@ mod tests { assert_eq!(profile.training.batch_size, 64); assert_eq!(profile.training.num_atoms, 11); assert_eq!(profile.training.buffer_size, 5_000); - assert_eq!(profile.experience.gpu_n_episodes, 16); assert_eq!(profile.experience.gpu_timesteps_per_episode, 100); assert_eq!(profile.cuda.cuda_stack_bytes, 16384); } @@ -287,7 +284,6 @@ mod tests { assert_eq!(profile.training.batch_size, 1024); assert_eq!(profile.training.num_atoms, 51); assert_eq!(profile.training.buffer_size, 500_000); - assert_eq!(profile.experience.gpu_n_episodes, 2048); assert_eq!(profile.experience.gpu_timesteps_per_episode, 100); assert_eq!(profile.cuda.cuda_stack_bytes, 65536); } @@ -307,7 +303,6 @@ mod tests { assert_eq!(profile.training.batch_size, 256); assert_eq!(profile.training.num_atoms, 21); assert_eq!(profile.training.buffer_size, 50_000); - assert_eq!(profile.experience.gpu_n_episodes, 64); assert_eq!(profile.experience.gpu_timesteps_per_episode, 200); assert_eq!(profile.cuda.cuda_stack_bytes, 32768); } @@ -341,7 +336,6 @@ mod tests { assert_eq!(profile.training.batch_size, 256); assert_eq!(profile.training.num_atoms, 21); assert_eq!(profile.training.buffer_size, 50_000); - assert_eq!(profile.experience.gpu_n_episodes, 64); assert_eq!(profile.experience.gpu_timesteps_per_episode, 200); assert_eq!(profile.cuda.cuda_stack_bytes, 32768); } diff --git a/crates/ml/examples/train_baseline_rl.rs b/crates/ml/examples/train_baseline_rl.rs index c6d7fed94..87f090ea2 100644 --- a/crates/ml/examples/train_baseline_rl.rs +++ b/crates/ml/examples/train_baseline_rl.rs @@ -562,8 +562,7 @@ fn train_dqn_fold( // Small GPUs (RTX 3050 profile: buffer_size=5000 < 100K threshold) bypass // AutoReplaySizer entirely, so VRAM fraction is irrelevant for them. replay_buffer_vram_fraction: gpu_profile.training.replay_buffer_vram_fraction, - // GPU experience collection: profile-driven defaults replace VRAM if/else chains - gpu_n_episodes: gpu_profile.experience.gpu_n_episodes, + // GPU experience collection: n_episodes auto-scales from VRAM gpu_timesteps_per_episode: gpu_profile.experience.gpu_timesteps_per_episode, max_training_steps_per_epoch: args.max_steps_per_epoch, ..DQNHyperparameters::default() diff --git a/crates/ml/src/hyperopt/adapters/dqn.rs b/crates/ml/src/hyperopt/adapters/dqn.rs index ab506ac35..dfc6fffcd 100644 --- a/crates/ml/src/hyperopt/adapters/dqn.rs +++ b/crates/ml/src/hyperopt/adapters/dqn.rs @@ -2042,8 +2042,6 @@ impl HyperparameterOptimizable for DQNTrainer { hyperparams.mbp10_data_dir = self.mbp10_data_dir.clone().unwrap_or_default(); hyperparams.trades_data_dir = self.trades_data_dir.clone().unwrap_or_default(); - // GPU tier-dependent settings - hyperparams.gpu_n_episodes = if budget.gpu_memory_mb >= 40960 { 512 } else { 128 }; hyperparams.max_training_steps_per_epoch = if budget.gpu_memory_mb >= 40960 { 2000 } else { 200 }; // AutoReplaySizer VRAM fraction hyperparams.replay_buffer_vram_fraction = { diff --git a/crates/ml/src/trainers/dqn/config.rs b/crates/ml/src/trainers/dqn/config.rs index a0920523c..274e95cc6 100644 --- a/crates/ml/src/trainers/dqn/config.rs +++ b/crates/ml/src/trainers/dqn/config.rs @@ -1172,9 +1172,6 @@ pub struct DQNHyperparameters { /// Enable the NVRTC-compiled GPU experience collection kernel. /// When false, experience collection uses the batched CPU path while /// the training step (forward/backward/optimizer) still runs on CUDA. - /// Number of parallel episodes per GPU kernel launch (default: 128, scaled dynamically) - /// Higher values improve GPU utilization on larger GPUs (e.g. H100: 8192) - pub gpu_n_episodes: usize, /// Timesteps per episode in GPU kernel (default: 500, max: 1000) /// Higher values collect more experience per launch at the cost of VRAM pub gpu_timesteps_per_episode: usize, @@ -1681,7 +1678,6 @@ impl DQNHyperparameters { spectral_decoupling_lambda: 0.01, // Default: mild logit magnitude penalty (Pezeshki 2021) // Phase 3: GPU experience collection - gpu_n_episodes: 256, // Floor: optimal_n_episodes() scales up dynamically (H100→8192) gpu_timesteps_per_episode: 500, // Default: 500 timesteps per episode avg_spread: 0.0001, // Default: 1bp (ES/NQ futures) max_training_steps_per_epoch: 0, // Default: unlimited (full dataset training) diff --git a/crates/ml/src/trainers/dqn/trainer/training_loop.rs b/crates/ml/src/trainers/dqn/trainer/training_loop.rs index 7b0828ed7..d25f03c3f 100644 --- a/crates/ml/src/trainers/dqn/trainer/training_loop.rs +++ b/crates/ml/src/trainers/dqn/trainer/training_loop.rs @@ -894,20 +894,18 @@ impl DQNTrainer { } /// Compute allocation episode count for GPU buffers. - /// Auto-scales when configured >= 128 (production), respects smaller test overrides. + /// Always auto-scales from VRAM — no manual override. fn compute_alloc_episodes(&self, state_dim: usize) -> usize { use ml_core::memory_optimization::detect_gpu_hardware; - let configured = self.hyperparams.gpu_n_episodes; - if configured >= 128 { - match detect_gpu_hardware() { - Ok(hw) => configured.max(hw.optimal_n_episodes( + match detect_gpu_hardware() { + Ok(hw) => { + let optimal = hw.optimal_n_episodes( state_dim, self.hyperparams.gpu_timesteps_per_episode, - )).min(0x8000), - Err(_) => configured, + ); + optimal.max(32).min(16384) } - } else { - configured + Err(_) => 256, // safe fallback } } @@ -962,32 +960,25 @@ impl DQNTrainer { let raw_sd = if !self.hyperparams.mbp10_data_dir.is_empty() { 53 } else { 45 }; let aligned_sd = (raw_sd + 7) & !7; - // Cache n_episodes on first epoch + // Cache n_episodes on first epoch — always auto-scaled from VRAM. let n_episodes = if let Some(cached) = self.cached_n_episodes { cached } else { use ml_core::memory_optimization::detect_gpu_hardware; - let configured = self.hyperparams.gpu_n_episodes; - let computed = if configured >= 128 { - match detect_gpu_hardware() { - Ok(hw) => { - let optimal = hw.optimal_n_episodes( - aligned_sd, - self.hyperparams.gpu_timesteps_per_episode, - ); - let chosen = configured.max(optimal).min(4096); - if chosen != configured { - info!( - "GPU auto-scaled n_episodes: {} -> {} (SMs={}, VRAM={:.0}MB)", - configured, chosen, hw.sm_count, hw.free_memory_mb - ); - } - chosen as i32 - } - Err(_) => configured as i32, + let computed = match detect_gpu_hardware() { + Ok(hw) => { + let optimal = hw.optimal_n_episodes( + aligned_sd, + self.hyperparams.gpu_timesteps_per_episode, + ); + let chosen = optimal.max(32).min(16384) as i32; + info!( + "GPU auto-scaled n_episodes: {} (SMs={}, VRAM={:.0}MB)", + chosen, hw.sm_count, hw.free_memory_mb + ); + chosen } - } else { - configured as i32 + Err(_) => 256_i32, // safe fallback }; self.cached_n_episodes = Some(computed); computed diff --git a/crates/ml/src/trainers/ppo.rs b/crates/ml/src/trainers/ppo.rs index b5810040a..694652389 100644 --- a/crates/ml/src/trainers/ppo.rs +++ b/crates/ml/src/trainers/ppo.rs @@ -65,8 +65,6 @@ pub struct PpoHyperparameters { pub max_grad_norm_lstm: f64, // Phase 3: GPU experience collection kernel configuration - /// Number of parallel episodes per GPU kernel launch (default: 128, scaled dynamically) - pub gpu_n_episodes: usize, /// Timesteps per episode in GPU kernel (default: 500, max: 1000) pub gpu_timesteps_per_episode: usize, /// Initial capital for portfolio simulation (default: 1_000_000.0) @@ -125,7 +123,6 @@ impl PpoHyperparameters { max_grad_norm_lstm: 0.5, // Tighter clipping for LSTM vs MLP (10.0) // Phase 3: GPU experience collection - gpu_n_episodes: 128, // Default: 128 (good for 4-8GB VRAM GPUs) gpu_timesteps_per_episode: 500, // Default: 500 timesteps per episode initial_capital: 1_000_000.0, // Default: $1M avg_spread: 0.0001, // Default: 1bp (ES/NQ futures) @@ -532,8 +529,18 @@ impl PpoTrainer { ) { use crate::cuda_pipeline::gpu_ppo_collector::PpoCollectorConfig; - let n_episodes = self.hyperparams.gpu_n_episodes as i32; let timesteps = self.hyperparams.gpu_timesteps_per_episode.min(1000) as i32; + // Auto-scale n_episodes from VRAM + let n_episodes = { + use ml_core::memory_optimization::detect_gpu_hardware; + match detect_gpu_hardware() { + Ok(hw) => { + let optimal = hw.optimal_n_episodes(80, self.hyperparams.gpu_timesteps_per_episode); + optimal.max(32).min(16384) as i32 + } + Err(_) => 128_i32, + } + }; let total_bars = self.raw_data_num_bars as i32; let usable_bars = (total_bars - timesteps).max(1); let stride = (usable_bars / n_episodes).max(1); diff --git a/crates/ml/src/training_profile.rs b/crates/ml/src/training_profile.rs index 546167d76..3056dcdea 100644 --- a/crates/ml/src/training_profile.rs +++ b/crates/ml/src/training_profile.rs @@ -258,7 +258,6 @@ pub struct RewardSection { /// GPU experience collection parameters. #[derive(Debug, Clone, Deserialize, Default)] pub struct ExperienceSection { - pub gpu_n_episodes: Option, pub gpu_timesteps_per_episode: Option, pub initial_capital: Option, /// Maps to `DQNHyperparameters::transaction_cost_multiplier`. @@ -937,9 +936,6 @@ impl DqnTrainingProfile { // TOML: tx_cost_multiplier → hp: transaction_cost_multiplier // TOML: initial_capital → hp: initial_capital (f32 in hp, f64 in TOML) if let Some(ref ex) = self.experience { - if let Some(v) = ex.gpu_n_episodes { - hp.gpu_n_episodes = v; - } if let Some(v) = ex.gpu_timesteps_per_episode { hp.gpu_timesteps_per_episode = v; } diff --git a/crates/ml/tests/dqn_early_stopping_termination_test.rs b/crates/ml/tests/dqn_early_stopping_termination_test.rs index bbbaa557b..71a676aa1 100644 --- a/crates/ml/tests/dqn_early_stopping_termination_test.rs +++ b/crates/ml/tests/dqn_early_stopping_termination_test.rs @@ -155,7 +155,6 @@ async fn test_early_stopping_terminates_with_error() { hyperparams.max_training_steps_per_epoch = 300; hyperparams.replay_buffer_vram_fraction = 0.0; - hyperparams.gpu_n_episodes = 16; hyperparams.gpu_timesteps_per_episode = 50; let mut trainer = DQNTrainer::new(hyperparams.clone()) .expect("Failed to create DQN trainer"); @@ -228,7 +227,6 @@ async fn test_gradient_collapse_propagates_error() { hyperparams.max_training_steps_per_epoch = 300; hyperparams.replay_buffer_vram_fraction = 0.0; - hyperparams.gpu_n_episodes = 16; hyperparams.gpu_timesteps_per_episode = 50; let mut trainer = DQNTrainer::new(hyperparams.clone()) .expect("Failed to create DQN trainer"); @@ -289,7 +287,6 @@ async fn test_healthy_training_completes_successfully() { hyperparams.max_training_steps_per_epoch = 300; // Fast epochs: ~3s vs ~370s hyperparams.replay_buffer_vram_fraction = 0.0; // Disable AutoReplaySizer for test determinism - hyperparams.gpu_n_episodes = 16; hyperparams.gpu_timesteps_per_episode = 50; let mut trainer = DQNTrainer::new(hyperparams.clone()) .expect("Failed to create DQN trainer"); diff --git a/crates/ml/tests/dqn_training_pipeline_test.rs b/crates/ml/tests/dqn_training_pipeline_test.rs index b7307fbc4..ae700eab9 100644 --- a/crates/ml/tests/dqn_training_pipeline_test.rs +++ b/crates/ml/tests/dqn_training_pipeline_test.rs @@ -107,13 +107,11 @@ fn scale_for_gpu(hp: &mut DQNHyperparameters) { let profile = GpuProfile::load(); hp.buffer_size = hp.buffer_size.min(profile.training.buffer_size); hp.batch_size = hp.batch_size.min(profile.training.batch_size); - hp.gpu_n_episodes = hp.gpu_n_episodes.min(profile.experience.gpu_n_episodes); hp.gpu_timesteps_per_episode = hp.gpu_timesteps_per_episode.min(profile.experience.gpu_timesteps_per_episode); hp.num_atoms = hp.num_atoms.min(profile.training.num_atoms); info!( - "GPU profile: buffer={}, batch={}, episodes={}, timesteps={}, atoms={}", - hp.buffer_size, hp.batch_size, hp.gpu_n_episodes, - hp.gpu_timesteps_per_episode, hp.num_atoms, + "GPU profile: buffer={}, batch={}, timesteps={}, atoms={}", + hp.buffer_size, hp.batch_size, hp.gpu_timesteps_per_episode, hp.num_atoms, ); } @@ -196,7 +194,6 @@ async fn test_dqn_trains_on_es_fut() -> Result<()> { // ======================================================================== info!("ACT: Running DQN training..."); - hyperparams.gpu_n_episodes = 16; hyperparams.gpu_timesteps_per_episode = 50; hyperparams.max_training_steps_per_epoch = 64; scale_for_gpu(&mut hyperparams); @@ -314,7 +311,6 @@ async fn test_dqn_loss_decreases() -> Result<()> { hyperparams.learning_rate = 0.001; hyperparams.early_stopping_enabled = false; - hyperparams.gpu_n_episodes = 16; hyperparams.gpu_timesteps_per_episode = 50; hyperparams.max_training_steps_per_epoch = 64; scale_for_gpu(&mut hyperparams); @@ -389,7 +385,6 @@ async fn test_dqn_checkpoint_save_load() -> Result<()> { hyperparams.batch_size = 64; hyperparams.checkpoint_frequency = 2; - hyperparams.gpu_n_episodes = 16; hyperparams.gpu_timesteps_per_episode = 50; hyperparams.max_training_steps_per_epoch = 64; scale_for_gpu(&mut hyperparams); @@ -459,7 +454,6 @@ async fn test_dqn_q_value_predictions() -> Result<()> { hyperparams.epochs = 2; hyperparams.batch_size = 32; - hyperparams.gpu_n_episodes = 16; hyperparams.gpu_timesteps_per_episode = 50; hyperparams.max_training_steps_per_epoch = 64; scale_for_gpu(&mut hyperparams); @@ -521,7 +515,6 @@ async fn test_dqn_epsilon_greedy() -> Result<()> { hyperparams.batch_size = 64; hyperparams.max_training_steps_per_epoch = 64; - hyperparams.gpu_n_episodes = 16; hyperparams.gpu_timesteps_per_episode = 50; hyperparams.min_replay_size = 50; hyperparams.warmup_steps = 0; @@ -603,7 +596,6 @@ async fn test_dqn_full_production_training() -> Result<()> { hyperparams.checkpoint_frequency = 10; hyperparams.early_stopping_enabled = true; - hyperparams.gpu_n_episodes = 16; hyperparams.gpu_timesteps_per_episode = 50; hyperparams.max_training_steps_per_epoch = 64; scale_for_gpu(&mut hyperparams); diff --git a/crates/ml/tests/ppo_recurrent_performance_tests.rs b/crates/ml/tests/ppo_recurrent_performance_tests.rs index 6bd8efc92..6614b0099 100644 --- a/crates/ml/tests/ppo_recurrent_performance_tests.rs +++ b/crates/ml/tests/ppo_recurrent_performance_tests.rs @@ -171,7 +171,6 @@ fn create_test_hyperparams(_use_lstm: bool, sequence_length: usize) -> PpoHyperp circuit_breaker_threshold: 5, sequence_length, max_grad_norm_lstm: 0.5, - gpu_n_episodes: 16, gpu_timesteps_per_episode: 50, initial_capital: 1_000_000.0, avg_spread: 0.0001, diff --git a/crates/ml/tests/smoke_test_real_data.rs b/crates/ml/tests/smoke_test_real_data.rs index 02561eb43..ff81b0b70 100644 --- a/crates/ml/tests/smoke_test_real_data.rs +++ b/crates/ml/tests/smoke_test_real_data.rs @@ -801,7 +801,6 @@ async fn smoke_e2e_dqn_training_loop() { hyperparams.min_replay_size = 50; // CI smoke: 16 episodes x 50 timesteps = 800 experiences/epoch. // Exercises full fused CUDA kernel (branching+C51+NoisyNets+DSR). - hyperparams.gpu_n_episodes = 2; hyperparams.gpu_timesteps_per_episode = 10; // CI: cap training steps to avoid full 204K-bar dataset sweep (6375->64 steps). // 64 steps x batch_size 32 = 2048 gradient updates — sufficient to validate