refactor: remove gpu_n_episodes override — auto-scale from VRAM everywhere

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) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-04-02 14:21:45 +02:00
parent 30d60b07ad
commit 5546a45bc3
17 changed files with 37 additions and 78 deletions

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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()

View File

@@ -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 = {

View File

@@ -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)

View File

@@ -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

View File

@@ -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);

View File

@@ -258,7 +258,6 @@ pub struct RewardSection {
/// GPU experience collection parameters.
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ExperienceSection {
pub gpu_n_episodes: Option<usize>,
pub gpu_timesteps_per_episode: Option<usize>,
pub initial_capital: Option<f64>,
/// 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;
}

View File

@@ -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");

View File

@@ -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);

View File

@@ -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,

View File

@@ -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