fix(cuda): enable GPU experience collection for regime-conditional DQN

The GPU experience collector only supported DQNAgentType::Standard, but
enable_regime_qnetwork defaults to true, creating RegimeConditional
agents. This silently fell back to CPU with a debug! message invisible
at INFO log level.

- Add primary_head() accessor to RegimeConditionalDQN (returns trending head)
- Extract weights from primary head for both collector init and weight sync
- Upgrade debug! to warn! for missing GPU collection prerequisites
- Add warn! to PPO for silent GPU skip when raw market data not uploaded

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-02-28 17:01:01 +01:00
parent 5a27bde9dd
commit 40febb0061
3 changed files with 36 additions and 5 deletions

View File

@@ -235,6 +235,14 @@ impl RegimeConditionalDQN {
})
}
/// Get a reference to the primary (trending) DQN head.
///
/// Used by GPU experience collector for weight extraction — the trending head
/// is the most common regime and provides representative network weights.
pub fn primary_head(&self) -> &DQN {
&self.trending_head
}
/// Forward pass through regime-specific Q-network head
///
/// # Arguments

View File

@@ -1412,9 +1412,16 @@ impl DQNTrainer {
use crate::cuda_pipeline::gpu_experience_collector::GpuExperienceCollector;
let stream = cuda_dev.cuda_stream();
// Read-lock agent to check for dueling networks
// Read-lock agent to extract dueling network weights for GPU collector.
// Works with both Standard and RegimeConditional agents — for regime-conditional,
// we use the primary (trending) head's weights since the GPU kernel runs a single
// Q-network for fast experience collection.
let agent = self.agent.read().await;
let init_result = if let DQNAgentType::Standard(ref dqn) = &*agent {
let dqn_ref: Option<&crate::dqn::DQN> = match &*agent {
DQNAgentType::Standard(ref dqn) => Some(dqn),
DQNAgentType::RegimeConditional(ref regime_dqn) => Some(regime_dqn.primary_head()),
};
let init_result = if let Some(dqn) = dqn_ref {
match (
dqn.dueling_q_network.as_ref(),
dqn.dueling_target_network.as_ref(),
@@ -1432,12 +1439,12 @@ impl DQNTrainer {
))
}
_ => {
debug!("GPU experience collector skipped: requires dueling networks + curiosity module");
warn!("GPU experience collector skipped: requires dueling networks + curiosity module (have dueling_q={}, dueling_target={}, curiosity={})",
dqn.dueling_q_network.is_some(), dqn.dueling_target_network.is_some(), self.curiosity_module.is_some());
None
}
}
} else {
debug!("GPU experience collector skipped: regime-conditional DQN not supported");
None
};
drop(agent);
@@ -2254,7 +2261,11 @@ impl DQNTrainer {
#[cfg(feature = "cuda")]
if let Some(ref mut collector) = self.gpu_experience_collector {
let agent = self.agent.read().await;
if let DQNAgentType::Standard(ref dqn) = &*agent {
let dqn_ref: Option<&crate::dqn::DQN> = match &*agent {
DQNAgentType::Standard(ref dqn) => Some(dqn),
DQNAgentType::RegimeConditional(ref regime_dqn) => Some(regime_dqn.primary_head()),
};
if let Some(dqn) = dqn_ref {
if let Some(ref online) = dqn.dueling_q_network {
if let Err(e) = collector.sync_online_weights(online.vars()) {
warn!("GPU online weight sync failed: {}", e);

View File

@@ -502,6 +502,18 @@ impl PpoTrainer {
}
}
} else {
// Log why GPU collection is not available so it doesn't silently fall back to CPU
#[cfg(feature = "cuda")]
{
if gpu_ppo_collector.is_none() {
warn!("PPO GPU experience collection skipped: collector not initialized");
} else if self.features_raw_cuda.is_none() || self.targets_raw_cuda.is_none() {
warn!("PPO GPU experience collection skipped: raw market data not uploaded (call set_raw_market_data() before train())");
} else {
// All conditions met but pattern match still failed — shouldn't happen
warn!("PPO GPU experience collection skipped: unexpected state");
}
}
None
};