fix(dqn): IQN GPU PER weights, staged GPU buffer, CUDA default in all ML crates
Three fixes for GPU PER hot path: 1. IQN quantile loss used empty CPU weights Vec instead of GPU-resident weights_tensor_cached — caused CUDA_ERROR_ILLEGAL_ADDRESS from uninitialized GPU memory. Now uses cached GPU tensor matching C51 and standard DQN paths. 2. GpuPrioritized add()/add_batch() replaced with StagedGpuBuffer: add() stages on CPU (Vec::push, zero GPU ops), sample() batch-flushes staging→GPU in one DMA before sampling. Production path (insert_batch_tensors) bypasses staging entirely — GPU→GPU with zero CPU. 3. All 9 ML sub-crates default to cuda feature so `cargo test -p ml-dqn` exercises GPU code paths on CUDA workstations. CI service crates use default-features=false, unaffected. Test results: 350 passed (was 343+7 failed), 0 failed, 1 ignored. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@ categories.workspace = true
|
||||
description = "Shared ML types, traits, and infrastructure for Foxhunt"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["cuda"]
|
||||
cuda = ["candle-core/cuda", "candle-core/cudnn", "candle-nn/cuda", "candle-nn/cudnn"]
|
||||
s3-storage = ["aws-config", "aws-sdk-s3", "aws-types", "aws-credential-types", "urlencoding"]
|
||||
high-precision = ["rust_decimal/serde-float"]
|
||||
|
||||
@@ -14,7 +14,7 @@ categories.workspace = true
|
||||
description = "DQN reinforcement learning for Foxhunt trading"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["cuda"]
|
||||
cuda = ["candle-core/cuda", "candle-core/cudnn", "candle-nn/cuda", "candle-nn/cudnn"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -2493,11 +2493,16 @@ impl DQN {
|
||||
self.config.iqn_kappa,
|
||||
).map_err(|e| MLError::TrainingError(format!("Quantile Huber loss failed: {}", e)))?;
|
||||
|
||||
// Apply PER importance-sampling weights (detached — not learnable)
|
||||
let weights_tensor = Tensor::from_vec(weights.clone(), batch_size, &device)?
|
||||
.to_dtype(dtype)
|
||||
.map_err(|e| MLError::TrainingError(format!("Failed to cast weights tensor: {}", e)))?
|
||||
.detach();
|
||||
// IS weights: use cached GPU tensor or create from Vec
|
||||
let weights_tensor = if let Some(ref wt) = weights_tensor_cached {
|
||||
wt.clone()
|
||||
} else {
|
||||
Tensor::from_vec(weights.clone(), batch_size, device)
|
||||
.map_err(|e| MLError::TrainingError(format!("Failed to create weights tensor: {}", e)))?
|
||||
.to_dtype(dtype)
|
||||
.map_err(|e| MLError::TrainingError(format!("Failed to cast weights tensor: {}", e)))?
|
||||
.detach()
|
||||
};
|
||||
let weighted_loss = (per_sample_loss * weights_tensor)?.mean_all()?;
|
||||
weighted_loss
|
||||
|
||||
|
||||
@@ -50,6 +50,59 @@ pub struct GpuBatch {
|
||||
pub indices: candle_core::Tensor, // [batch_size] u32 on GPU (buffer indices)
|
||||
}
|
||||
|
||||
/// GPU buffer + CPU staging: `add()` stages on CPU (zero GPU ops),
|
||||
/// `sample()` batch-flushes staging → GPU in one DMA before sampling.
|
||||
#[cfg(feature = "cuda")]
|
||||
pub struct StagedGpuBuffer {
|
||||
pub gpu: crate::gpu_replay_buffer::GpuReplayBuffer,
|
||||
staging: Vec<Experience>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "cuda")]
|
||||
impl StagedGpuBuffer {
|
||||
fn flush(&mut self) -> Result<(), MLError> {
|
||||
if self.staging.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let n = self.staging.len();
|
||||
let sd = self.staging.first().map_or(0, |e| e.state.len());
|
||||
let device = self.gpu.device().clone();
|
||||
|
||||
let mut states_flat = Vec::with_capacity(n * sd);
|
||||
let mut next_flat = Vec::with_capacity(n * sd);
|
||||
let mut actions = Vec::with_capacity(n);
|
||||
let mut rewards = Vec::with_capacity(n);
|
||||
let mut dones = Vec::with_capacity(n);
|
||||
|
||||
for exp in self.staging.drain(..) {
|
||||
states_flat.extend_from_slice(&exp.state);
|
||||
if exp.state.len() < sd {
|
||||
states_flat.resize(states_flat.len() + sd - exp.state.len(), 0.0);
|
||||
}
|
||||
next_flat.extend_from_slice(&exp.next_state);
|
||||
if exp.next_state.len() < sd {
|
||||
next_flat.resize(next_flat.len() + sd - exp.next_state.len(), 0.0);
|
||||
}
|
||||
actions.push(exp.action as u32);
|
||||
rewards.push(exp.reward_f32());
|
||||
dones.push(if exp.done { 1.0_f32 } else { 0.0 });
|
||||
}
|
||||
|
||||
let states_t = candle_core::Tensor::from_vec(states_flat, (n, sd), &device)
|
||||
.map_err(|e| MLError::TrainingError(format!("GPU flush states: {e}")))?;
|
||||
let next_t = candle_core::Tensor::from_vec(next_flat, (n, sd), &device)
|
||||
.map_err(|e| MLError::TrainingError(format!("GPU flush next_states: {e}")))?;
|
||||
let actions_t = candle_core::Tensor::from_vec(actions, n, &device)
|
||||
.map_err(|e| MLError::TrainingError(format!("GPU flush actions: {e}")))?;
|
||||
let rewards_t = candle_core::Tensor::from_vec(rewards, n, &device)
|
||||
.map_err(|e| MLError::TrainingError(format!("GPU flush rewards: {e}")))?;
|
||||
let dones_t = candle_core::Tensor::from_vec(dones, n, &device)
|
||||
.map_err(|e| MLError::TrainingError(format!("GPU flush dones: {e}")))?;
|
||||
|
||||
self.gpu.insert_batch(&states_t, &next_t, &actions_t, &rewards_t, &dones_t)
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum wrapper for runtime buffer type switching.
|
||||
///
|
||||
/// Clone is cheap (Arc inner types) and enables sharing the buffer between
|
||||
@@ -60,9 +113,11 @@ pub enum ReplayBufferType {
|
||||
Uniform(Arc<Mutex<ExperienceReplayBuffer>>),
|
||||
/// Prioritized sampling based on TD errors (Rainbow DQN)
|
||||
Prioritized(Arc<PrioritizedReplayBuffer>),
|
||||
/// GPU-resident prioritized replay — all sampling on GPU (zero CPU round-trips)
|
||||
/// GPU-resident prioritized replay with CPU staging buffer.
|
||||
/// `add()` stages on CPU (zero GPU ops). `sample()` flushes staging → GPU
|
||||
/// in one batch DMA, then samples entirely on GPU.
|
||||
#[cfg(feature = "cuda")]
|
||||
GpuPrioritized(Arc<Mutex<crate::gpu_replay_buffer::GpuReplayBuffer>>),
|
||||
GpuPrioritized(Arc<Mutex<StagedGpuBuffer>>),
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ReplayBufferType {
|
||||
@@ -134,7 +189,10 @@ impl ReplayBufferType {
|
||||
};
|
||||
|
||||
let buffer = GpuReplayBuffer::new(config, device)?;
|
||||
Ok(Self::GpuPrioritized(Arc::new(Mutex::new(buffer))))
|
||||
Ok(Self::GpuPrioritized(Arc::new(Mutex::new(StagedGpuBuffer {
|
||||
gpu: buffer,
|
||||
staging: Vec::new(),
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Try GPU-resident PER, fall back to CPU PER on allocation failure.
|
||||
@@ -222,8 +280,9 @@ impl ReplayBufferType {
|
||||
}
|
||||
#[cfg(feature = "cuda")]
|
||||
Self::GpuPrioritized(buffer) => {
|
||||
let buf = buffer.lock();
|
||||
let gpu_batch = buf.sample_proportional(batch_size)?;
|
||||
let mut buf = buffer.lock();
|
||||
buf.flush()?;
|
||||
let gpu_batch = buf.gpu.sample_proportional(batch_size)?;
|
||||
Ok(BatchSample {
|
||||
experiences: vec![], // Empty — data is on GPU
|
||||
weights: vec![], // Empty — weights on GPU
|
||||
@@ -246,8 +305,8 @@ impl ReplayBufferType {
|
||||
buffer.push(experience)
|
||||
}
|
||||
#[cfg(feature = "cuda")]
|
||||
Self::GpuPrioritized(_) => {
|
||||
// No-op: GPU buffer uses insert_batch() from GPU experience collector
|
||||
Self::GpuPrioritized(buf) => {
|
||||
buf.lock().staging.push(experience);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -275,8 +334,8 @@ impl ReplayBufferType {
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(feature = "cuda")]
|
||||
Self::GpuPrioritized(_) => {
|
||||
// No-op: GPU buffer uses insert_batch() with tensors directly
|
||||
Self::GpuPrioritized(buf) => {
|
||||
buf.lock().staging.extend(experiences);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -311,7 +370,7 @@ impl ReplayBufferType {
|
||||
match self {
|
||||
Self::GpuPrioritized(buffer) => {
|
||||
let mut buf = buffer.lock();
|
||||
buf.update_priorities_gpu(indices, td_errors)
|
||||
buf.gpu.update_priorities_gpu(indices, td_errors)
|
||||
}
|
||||
Self::Uniform(_) | Self::Prioritized(_) => Ok(()), // No-op for non-GPU buffers
|
||||
}
|
||||
@@ -322,7 +381,7 @@ impl ReplayBufferType {
|
||||
match self {
|
||||
Self::Prioritized(buffer) => buffer.step(),
|
||||
#[cfg(feature = "cuda")]
|
||||
Self::GpuPrioritized(buffer) => buffer.lock().step(),
|
||||
Self::GpuPrioritized(buffer) => buffer.lock().gpu.step(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -353,7 +412,7 @@ impl ReplayBufferType {
|
||||
Self::Uniform(_) => 1.0,
|
||||
Self::Prioritized(buffer) => buffer.current_beta(),
|
||||
#[cfg(feature = "cuda")]
|
||||
Self::GpuPrioritized(buffer) => buffer.lock().current_beta(),
|
||||
Self::GpuPrioritized(buffer) => buffer.lock().gpu.current_beta(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,7 +422,10 @@ impl ReplayBufferType {
|
||||
Self::Uniform(buffer) => buffer.lock().len(),
|
||||
Self::Prioritized(buffer) => buffer.len(),
|
||||
#[cfg(feature = "cuda")]
|
||||
Self::GpuPrioritized(buffer) => buffer.lock().len(),
|
||||
Self::GpuPrioritized(buffer) => {
|
||||
let buf = buffer.lock();
|
||||
buf.gpu.len() + buf.staging.len()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,7 +446,9 @@ impl ReplayBufferType {
|
||||
}
|
||||
#[cfg(feature = "cuda")]
|
||||
Self::GpuPrioritized(buffer) => {
|
||||
buffer.lock().clear();
|
||||
let mut buf = buffer.lock();
|
||||
buf.gpu.clear();
|
||||
buf.staging.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -395,7 +459,10 @@ impl ReplayBufferType {
|
||||
Self::Uniform(buffer) => buffer.lock().len() >= batch_size,
|
||||
Self::Prioritized(buffer) => buffer.can_sample(batch_size),
|
||||
#[cfg(feature = "cuda")]
|
||||
Self::GpuPrioritized(buffer) => buffer.lock().can_sample(batch_size),
|
||||
Self::GpuPrioritized(buffer) => {
|
||||
let buf = buffer.lock();
|
||||
buf.gpu.len() + buf.staging.len() >= batch_size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,13 +562,13 @@ impl ReplayBufferType {
|
||||
Self::Uniform(buffer) => buffer.lock().capacity(),
|
||||
Self::Prioritized(buffer) => buffer.capacity(),
|
||||
#[cfg(feature = "cuda")]
|
||||
Self::GpuPrioritized(buffer) => buffer.lock().capacity(),
|
||||
Self::GpuPrioritized(buffer) => buffer.lock().gpu.capacity(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a locked reference to the inner GPU replay buffer (GpuPrioritized only).
|
||||
/// Get a locked reference to the staged GPU buffer (GpuPrioritized only).
|
||||
#[cfg(feature = "cuda")]
|
||||
pub fn as_gpu_buffer(&self) -> Option<parking_lot::MutexGuard<'_, crate::gpu_replay_buffer::GpuReplayBuffer>> {
|
||||
pub fn as_gpu_buffer(&self) -> Option<parking_lot::MutexGuard<'_, StagedGpuBuffer>> {
|
||||
match self {
|
||||
Self::GpuPrioritized(buffer) => Some(buffer.lock()),
|
||||
Self::Uniform(_) | Self::Prioritized(_) => None,
|
||||
|
||||
@@ -14,7 +14,7 @@ categories.workspace = true
|
||||
description = "ML ensemble coordination — voting, confidence, gating, hot-swap, inference pipeline"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["cuda"]
|
||||
cuda = ["candle-core/cuda", "candle-core/cudnn"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -14,7 +14,7 @@ categories.workspace = true
|
||||
description = "Model explainability (integrated gradients) for Foxhunt ML"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["cuda"]
|
||||
cuda = ["candle-core/cuda", "candle-core/cudnn", "candle-nn/cuda", "candle-nn/cudnn"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -14,7 +14,7 @@ categories.workspace = true
|
||||
description = "ML hyperparameter optimization — PSO, TPE, campaigns, sensitivity analysis"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["cuda"]
|
||||
cuda = ["candle-core/cuda", "candle-core/cudnn"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -14,7 +14,7 @@ categories.workspace = true
|
||||
description = "ML labeling algorithms for Foxhunt HFT training data"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["cuda"]
|
||||
cuda = ["candle-core/cuda", "candle-core/cudnn"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -14,7 +14,7 @@ categories.workspace = true
|
||||
description = "PPO reinforcement learning for Foxhunt trading"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["cuda"]
|
||||
cuda = ["candle-core/cuda", "candle-core/cudnn", "candle-nn/cuda", "candle-nn/cudnn"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -14,7 +14,7 @@ categories.workspace = true
|
||||
description = "Supervised models (TFT, Mamba, Liquid, TGGN, TLOB, KAN, xLSTM, Diffusion)"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["cuda"]
|
||||
cuda = ["candle-core/cuda", "candle-core/cudnn", "candle-nn/cuda", "candle-nn/cudnn"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -15,7 +15,7 @@ categories.workspace = true
|
||||
[features]
|
||||
# MINIMAL features for HFT inference only - ALL HEAVY ML REMOVED
|
||||
# CUDA opt-in: service crates build on CPU nodes without nvcc
|
||||
default = ["minimal-inference"]
|
||||
default = ["minimal-inference", "cuda"]
|
||||
|
||||
# PRODUCTION FEATURES - LIGHTWEIGHT ONLY
|
||||
minimal-inference = [] # Minimal inference with no optional deps
|
||||
|
||||
@@ -298,7 +298,7 @@ impl DQNAgentType {
|
||||
match self {
|
||||
Self::Standard(agent) => {
|
||||
if let Some(mut gpu_buf) = agent.memory.as_gpu_buffer() {
|
||||
gpu_buf.insert_batch(states, next_states, actions, rewards, dones)
|
||||
gpu_buf.gpu.insert_batch(states, next_states, actions, rewards, dones)
|
||||
} else {
|
||||
// CPU PER fallback: convert tensors back to experiences
|
||||
let batch_size = states.dim(0).map_err(|e| {
|
||||
|
||||
Reference in New Issue
Block a user