feat(ml): dynamic GPU batch sizing + tensor core alignment utility

- DQN: Scale UP batch_size on large GPUs (HardwareBudget::detect), raise static cap 4096→8192
- PPO: Scale UP batch_size from conservative 64 when GPU supports more
- Add align_to_tensor_cores() utility (round up to multiple of 8)
- Hidden dim_base rounding already aligned (nearest 256 = multiples of 8)
- Tests: 2422 pass, 0 failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-01 19:05:11 +01:00
parent 6157ea0f77
commit 71f2fb1ec3
5 changed files with 93 additions and 8 deletions

View File

@@ -20,6 +20,15 @@ pub mod gpu_ppo_collector;
/// Maximum bytes allowed for a single GPU upload (2 GB safety limit).
const MAX_UPLOAD_BYTES: usize = 2 * 1024 * 1024 * 1024;
/// Round dimension up to nearest multiple of 8 for tensor core alignment.
///
/// Tensor cores on Ampere (A100, L4, L40S) and Hopper (H100) require
/// matrix dimensions divisible by 8 (FP16/BF16) for optimal throughput.
/// This pads dimensions like state_dim=54 → 56 to enable tensor core paths.
pub fn align_to_tensor_cores(dim: usize) -> usize {
(dim + 7) & !7
}
/// Estimate VRAM usage in bytes for pre-uploaded f32 data.
pub fn estimate_vram_bytes(num_elements: usize) -> usize {
num_elements * std::mem::size_of::<f32>()
@@ -327,6 +336,16 @@ mod tests {
assert_eq!(state.dims(), &[1, 54]);
}
#[test]
fn test_tensor_core_alignment() {
assert_eq!(align_to_tensor_cores(54), 56);
assert_eq!(align_to_tensor_cores(64), 64); // already aligned
assert_eq!(align_to_tensor_cores(51), 56); // pad up
assert_eq!(align_to_tensor_cores(1), 8);
assert_eq!(align_to_tensor_cores(8), 8);
assert_eq!(align_to_tensor_cores(0), 0);
}
#[test]
fn test_ppo_gpu_data_upload_cpu() {
let market_data: Vec<Vec<f32>> = (0..200)

View File

@@ -240,7 +240,7 @@ impl HardwareBudget {
hi = mid - 1;
}
}
// Round down to nearest 256
// Round down to nearest 256 (all values are already multiples of 8 for tensor cores)
(lo / 256) * 256
}

View File

@@ -343,7 +343,7 @@ impl DQNAgentType {
pub struct DQNHyperparameters {
/// Learning rate (typically 1e-4 to 1e-3)
pub learning_rate: f64,
/// Batch size (must be ≤230 for RTX 3050 Ti 4GB)
/// Batch size (dynamically capped by AutoBatchSizer based on GPU VRAM)
pub batch_size: usize,
/// Discount factor (typically 0.95-0.99)
pub gamma: f64,

View File

@@ -260,9 +260,9 @@ impl DQNTrainer {
));
}
// VRAM safety cap: only clamp batch_size if it would OOM.
// The optimizer (PSO/hyperopt) controls batch_size — we just enforce the ceiling.
const STATIC_MAX_BATCH_SIZE: usize = 4096;
// Dynamic batch sizing: scale UP for larger GPUs, cap DOWN for smaller ones.
// Uses HardwareBudget for consistent sizing across DQN/PPO.
const STATIC_MAX_BATCH_SIZE: usize = 8192;
let max_safe_batch = match AutoBatchSizer::new() {
Ok(sizer) => {
let config = BatchSizeConfig {
@@ -286,6 +286,20 @@ impl DQNTrainer {
}
};
// Scale UP when running with conservative defaults on large GPUs
let budget = crate::hyperopt::HardwareBudget::detect();
let gpu_optimal = budget
.max_batch_size(50.0, 0.0005, 64.0, 8192.0)
.unwrap_or(hyperparams.batch_size as f64) as usize;
if hyperparams.batch_size < gpu_optimal && hyperparams.batch_size <= 128 {
info!(
"DQN batch_size scaled UP: {} → {} (GPU: {})",
hyperparams.batch_size, gpu_optimal.min(max_safe_batch), budget.gpu_name
);
hyperparams.batch_size = gpu_optimal.min(max_safe_batch);
}
// Cap DOWN if still exceeding VRAM ceiling
if hyperparams.batch_size > max_safe_batch {
info!(
"Batch size {} exceeds GPU VRAM ceiling ({}), clamping",
@@ -4110,4 +4124,26 @@ mod tests {
reward_unclamped
);
}
#[test]
fn test_dynamic_batch_size_l4() {
// L4 has 24GB VRAM — HardwareBudget should allow batch_size >> 230
let budget = crate::hyperopt::HardwareBudget {
gpu_memory_mb: 24_000,
gpu_name: "NVIDIA L4".to_string(),
};
let batch = budget.max_batch_size(50.0, 0.0005, 64.0, 8192.0);
assert!(batch.unwrap_or(0.0) > 230.0, "L4 should support DQN batch > 230, got {:?}", batch);
}
#[test]
fn test_dynamic_batch_size_h100() {
// H100 has 80GB VRAM — should hit the 8192 ceiling
let budget = crate::hyperopt::HardwareBudget {
gpu_memory_mb: 81_920,
gpu_name: "NVIDIA H100".to_string(),
};
let batch = budget.max_batch_size(50.0, 0.0005, 64.0, 8192.0);
assert!((batch.unwrap_or(0.0) - 8192.0).abs() < 1.0, "H100 should hit 8192 ceiling, got {:?}", batch);
}
}

View File

@@ -258,7 +258,7 @@ impl PpoTrainer {
});
}
// Dynamic GPU validation: detect hardware and auto-shrink batch size if needed
// Dynamic GPU validation: scale UP for large GPUs, cap DOWN for small ones
let (device, effective_batch_size) = if use_gpu {
let caps = cached_capabilities();
let max_batch = resolve_batch_size(
@@ -267,12 +267,31 @@ impl PpoTrainer {
hyperparams.batch_size,
);
let device = DeviceConfig::Auto.resolve().unwrap_or(Device::Cpu);
// Scale UP when running with conservative defaults on large GPUs
let effective = if device.is_cuda() && hyperparams.batch_size <= 64 {
let budget = crate::hyperopt::HardwareBudget::detect();
let gpu_optimal = budget
.max_batch_size(80.0, 0.0004, 64.0, 4096.0)
.unwrap_or(max_batch as f64) as usize;
let scaled = gpu_optimal.min(max_batch);
if scaled > hyperparams.batch_size {
info!(
"PPO batch_size scaled UP: {} → {} (GPU: {})",
hyperparams.batch_size, scaled, caps.device_name
);
}
scaled
} else {
max_batch
};
if device.is_cuda() {
info!("PPO using GPU: {} (batch_size: {})", caps.device_name, max_batch);
info!("PPO using GPU: {} (batch_size: {})", caps.device_name, effective);
} else {
warn!("GPU requested but unavailable, falling back to CPU");
}
(device, max_batch)
(device, effective)
} else {
(Device::Cpu, hyperparams.batch_size)
};
@@ -1519,4 +1538,15 @@ mod tests {
error_msg
);
}
#[test]
fn test_ppo_dynamic_batch_size_l4() {
// L4 has 24GB VRAM — HardwareBudget should allow PPO batch > 64
let budget = crate::hyperopt::HardwareBudget {
gpu_memory_mb: 24_000,
gpu_name: "NVIDIA L4".to_string(),
};
let batch = budget.max_batch_size(80.0, 0.0004, 64.0, 4096.0);
assert!(batch.unwrap_or(0.0) > 64.0, "L4 should support PPO batch > 64, got {:?}", batch);
}
}