perf: indirect pointer upload + graph ALL remaining ops

Batch upload:
- Added indirect pad_states + indirect copy kernels to cubin
- Batch source pointers uploaded as 8 x u64 via async HtoD to batch_ptr_buf
- Kernels read source addresses from device indirection buffer
- upload_batch_gpu replaced with upload_batch_ptrs → graph-captured indirect kernels
- 8 ungraphed DtoD/kernel launches → 0 (captured in graph_forward)

HER relabel (Random strategy):
- Captured as graph_her (random_donors + inplace_relabel)
- Uses stable addresses: donor_indices (pre-allocated), batch_ptr_buf[1] (indirect)
- 2 ungraphed launches → 1 graph replay

PER priority update:
- Captured in graph_adam (after regime_scale)
- Kernel changed to read indices/priorities from batch_ptr_buf[6..8] (indirect)
- PER pointers uploaded via upload_per_ptrs before graph_adam replay
- 1 ungraphed launch → 0 (captured in graph_adam)

New CUDA kernels:
- pad_states_indirect_kernel: reads src ptr from device buffer
- indirect_copy_f32_kernel: f32 copy with indirect src
- indirect_copy_i32_kernel: i32 copy with indirect src
- per_update_priorities_kernel: changed to indirect indices/priorities

Per-step operation count:
  9 graph replays (forward, adam, ema, attention, iql, iqn, her)
  + 1 async HtoD (8 batch pointers, 64 bytes)
  + 1 async HtoD (tau, 4 bytes)
  + 1 async HtoD (adam_step, 4 bytes)
  + 1 async HtoD (iqn tau, 4 bytes)
  + 1 async HtoD (per ptrs, 16 bytes)
  = 9 graph replays + 5 async HtoD (92 bytes total)

Zero ungraphed kernel launches in the per-step hot path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-04-02 10:28:18 +02:00
parent 13dd2e77bf
commit 937bbabb84
4 changed files with 294 additions and 55 deletions

View File

@@ -455,6 +455,61 @@ extern "C" __global__ void pad_states_kernel(
dst[idx] = (j < sd) ? src[b * sd + j] : bf16_zero();
}
/* ══════════════════════════════════════════════════════════════════════
* INDIRECT PAD STATES KERNEL (graph-capture compatible)
*
* Same as pad_states_kernel but reads the source pointer from a device
* buffer. This allows the CUDA graph to be replayed with different batch
* data each step — only the pointer in src_ptr_buf changes (via async HtoD).
*
* Launch config: grid=(ceil(batch * padded_sd / 256), 1, 1), block=(256, 1, 1).
* ══════════════════════════════════════════════════════════════════════ */
extern "C" __global__ void pad_states_indirect_kernel(
__nv_bfloat16* __restrict__ dst,
const unsigned long long* __restrict__ src_ptr_buf, /* [1] device ptr to source */
int batch,
int sd,
int padded_sd
) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx >= batch * padded_sd) return;
const __nv_bfloat16* src = (const __nv_bfloat16*)src_ptr_buf[0];
int b = idx / padded_sd;
int j = idx % padded_sd;
dst[idx] = (j < sd) ? src[b * sd + j] : bf16_zero();
}
/* ══════════════════════════════════════════════════════════════════════
* INDIRECT MEMCPY KERNEL (graph-capture compatible)
*
* Copies N elements from a source address stored in a device buffer
* to a fixed destination. Replaces async DtoD copies that can't be
* graphed because the source pointer changes per step.
*
* Launch config: grid=(ceil(n/256), 1, 1), block=(256, 1, 1).
* ══════════════════════════════════════════════════════════════════════ */
extern "C" __global__ void indirect_copy_f32_kernel(
float* __restrict__ dst,
const unsigned long long* __restrict__ src_ptr_buf, /* [1] device ptr */
int n
) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i >= n) return;
const float* src = (const float*)src_ptr_buf[0];
dst[i] = src[i];
}
extern "C" __global__ void indirect_copy_i32_kernel(
int* __restrict__ dst,
const unsigned long long* __restrict__ src_ptr_buf, /* [1] device ptr */
int n
) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i >= n) return;
const int* src = (const int*)src_ptr_buf[0];
dst[i] = src[i];
}
/* ══════════════════════════════════════════════════════════════════════
* BOTTLENECK TANH + CONCAT KERNEL (#31)
*
@@ -1102,7 +1157,7 @@ extern "C" __global__ void her_inplace_relabel(
__nv_bfloat16* __restrict__ dst_states, /* [batch_size, dst_stride] padded */
__nv_bfloat16* __restrict__ dst_next_states, /* [batch_size, dst_stride] padded */
float* __restrict__ dst_rewards, /* [batch_size] f32 */
const __nv_bfloat16* __restrict__ src_next_states, /* [batch_size, src_stride] unpadded from GpuBatch */
const unsigned long long* __restrict__ src_next_ptr_buf, /* [1] ptr to src next_states */
const int* __restrict__ donor_indices, /* [her_batch_size] i32 */
int offset, /* normal_count: first HER row in dst */
int goal_dim, /* number of goal columns to replace */
@@ -1114,6 +1169,7 @@ extern "C" __global__ void her_inplace_relabel(
int lane = threadIdx.x; /* warp lane 0-31 */
int donor = donor_indices[i];
int dst_row = offset + i;
const __nv_bfloat16* src_next_states = (const __nv_bfloat16*)src_next_ptr_buf[0];
/* Replace goal columns (first goal_dim elements) with donor's achieved goal */
for (int d = lane; d < goal_dim; d += 32) {

View File

@@ -180,6 +180,9 @@ pub struct GpuDqnTrainConfig {
pub c51_warmup_epochs: usize,
/// CQL regularization strength (0.0 = disabled, 0.1 = mild, 1.0 = full offline-RL).
pub cql_alpha: f32,
pub per_alpha: f32,
pub per_epsilon: f32,
pub per_capacity: usize,
/// Curiosity Q-penalty lambda: scales gamma by 1/(1 + lambda * curiosity_error).
/// 0.0 = disabled. Typical range 0.5-5.0. Higher = more aggressive penalization
/// of Q-values in novel states (prevents overconfident extrapolation OOS).
@@ -255,6 +258,9 @@ impl Default for GpuDqnTrainConfig {
entropy_coefficient: 0.001, // Must be ≤ reward magnitude (~0.001). Old 0.01 was 10x reward → entropy dominated learning.
c51_warmup_epochs: 5,
cql_alpha: 0.1,
per_alpha: 0.6,
per_epsilon: 1e-6,
per_capacity: 500_000,
curiosity_q_penalty_lambda: 0.0,
asymmetric_dd_weight: 0.0,
ensemble_disagreement_penalty: 0.0,
@@ -650,6 +656,10 @@ pub struct GpuDqnTrainer {
pruning_compute_kernel: CudaFunction,
/// HER in-place goal relabel kernel (writes directly into padded staging buffers).
pub(crate) her_inplace_kernel: CudaFunction,
pad_states_indirect_kernel: CudaFunction,
indirect_copy_f32_kernel: CudaFunction,
indirect_copy_i32_kernel: CudaFunction,
batch_ptr_buf: CudaSlice<u64>,
/// #20 Pruning epoch (epoch at which to compute the mask).
pruning_epoch: usize,
/// #20 Pruning fraction (0.7 = prune 70% of smallest weights).
@@ -1481,7 +1491,7 @@ impl GpuDqnTrainer {
/// `state_dim`: unpadded state dimension
pub fn launch_her_inplace_relabel(
&self,
src_next_states_ptr: u64,
_src_next_states_ptr: u64, // unused — kernel reads from batch_ptr_buf[1]
donor_indices_ptr: u64,
normal_count: usize,
her_batch_size: usize,
@@ -1511,7 +1521,7 @@ impl GpuDqnTrainer {
.arg(&states_ptr)
.arg(&next_states_ptr)
.arg(&rewards_ptr)
.arg(&src_next_states_ptr)
.arg(&(self.batch_ptr_buf.raw_ptr() + 8))
.arg(&donor_indices_ptr)
.arg(&offset_i32)
.arg(&goal_dim_i32)
@@ -2156,7 +2166,7 @@ impl GpuDqnTrainer {
// per array. Stack is set once in DQNTrainer::new() (64KB for all kernels).
// ── Compile 4 utility kernels (grad_norm, adam_update, BF16 converters) ─
let (grad_norm_kernel, grad_norm_finalize_kernel, adam_update_kernel, f32_to_bf16_kernel, bf16_to_f32_kernel, saxpy_kernel, zero_kernel, regime_scale_kernel, shrink_perturb, _relu_mask_in_module, spectral_norm_kernel, clipped_saxpy_kernel, clip_grad_kernel, pad_states_kernel, saxpy_f32_kernel, stochastic_depth_kernel, stochastic_depth_rng_kernel, bn_tanh_backward_kernel, bn_bias_grad_kernel, bn_tanh_concat_kernel_fn, vaccine_dot_kernel, vaccine_project_kernel, causal_intervene_kernel_fn, causal_reduce_kernel_fn, pruning_mask_kernel, pruning_compute_kernel, her_inplace_kernel) =
let (grad_norm_kernel, grad_norm_finalize_kernel, adam_update_kernel, f32_to_bf16_kernel, bf16_to_f32_kernel, saxpy_kernel, zero_kernel, regime_scale_kernel, shrink_perturb, _relu_mask_in_module, spectral_norm_kernel, clipped_saxpy_kernel, clip_grad_kernel, pad_states_kernel, saxpy_f32_kernel, stochastic_depth_kernel, stochastic_depth_rng_kernel, bn_tanh_backward_kernel, bn_bias_grad_kernel, bn_tanh_concat_kernel_fn, vaccine_dot_kernel, vaccine_project_kernel, causal_intervene_kernel_fn, causal_reduce_kernel_fn, pruning_mask_kernel, pruning_compute_kernel, her_inplace_kernel, pad_states_indirect_kernel, indirect_copy_f32_kernel, indirect_copy_i32_kernel) =
compile_training_kernels(&stream, &config)?;
// Separate grad_norm instance for non-graph launches (clip_grad_buf_inplace).
@@ -2632,6 +2642,8 @@ impl GpuDqnTrainer {
.map_err(|e| MLError::ModelError(format!("alloc bn_d_hidden: {e}")))?;
let tau_buf = stream.alloc_zeros::<f32>(1)
.map_err(|e| MLError::ModelError(format!("tau_buf alloc: {e}")))?;
let batch_ptr_buf = stream.alloc_zeros::<u64>(8)
.map_err(|e| MLError::ModelError(format!("batch_ptr_buf alloc: {e}")))?;
Ok(Self {
config,
stream,
@@ -2779,6 +2791,10 @@ impl GpuDqnTrainer {
pruning_mask_kernel,
pruning_compute_kernel,
her_inplace_kernel,
pad_states_indirect_kernel,
indirect_copy_f32_kernel,
indirect_copy_i32_kernel,
batch_ptr_buf,
pruning_epoch: prune_ep,
pruning_fraction: prune_frac,
causal_intervene_kernel: causal_intervene_kernel_fn,
@@ -2848,8 +2864,9 @@ impl GpuDqnTrainer {
self.params_initialized = true;
}
// ── GPU-direct upload (DtoD, no CPU staging) ────────────────
self.upload_batch_gpu(gpu_batch)?;
// ── Upload batch pointers to GPU indirection buffer (async HtoD)
// The indirect upload kernels in graph_forward read from batch_ptr_buf.
self.upload_batch_ptrs_6(gpu_batch);
// Upload + capture + replay forward ONLY.
// Adam is deferred to replay_adam_and_readback() so the caller can
@@ -3491,13 +3508,13 @@ impl GpuDqnTrainer {
/// * `epsilon` — PER epsilon floor
pub fn update_priorities_cuda(
&mut self,
indices: &CudaSlice<u32>,
priorities: &CudaSlice<half::bf16>,
_indices: &CudaSlice<u32>, // unused — kernel reads from batch_ptr_buf[6]
_priorities_data: &CudaSlice<half::bf16>, // unused — kernel reads from batch_ptr_buf[7]
alpha: f32,
epsilon: f32,
) -> Result<(), MLError> {
let b = self.config.batch_size as i32;
let capacity = priorities.len() as i32;
let capacity = _priorities_data.len() as i32;
let blocks = ((self.config.batch_size + 255) / 256) as u32;
let launch_cfg = LaunchConfig {
grid_dim: (blocks, 1, 1),
@@ -3509,8 +3526,8 @@ impl GpuDqnTrainer {
self.stream
.launch_builder(&self.per_update_kernel)
.arg(&self.td_errors_buf)
.arg(indices)
.arg(priorities)
.arg(&(self.batch_ptr_buf.raw_ptr() + 6 * 8))
.arg(&(self.batch_ptr_buf.raw_ptr() + 7 * 8))
.arg(&self.batch_max_buf)
.arg(&alpha)
.arg(&epsilon)
@@ -3898,6 +3915,7 @@ impl GpuDqnTrainer {
return Err(MLError::ModelError(format!("CUDA graph_forward begin_capture: {e}")));
}
self.submit_indirect_upload_ops()?;
self.submit_spectral_norm_ops(online_d, online_b)?;
let submit_fwd_result = self.submit_forward_ops();
@@ -4012,6 +4030,124 @@ impl GpuDqnTrainer {
/// Submit spectral norm ops for graph capture.
/// Normalizes all weight matrices and syncs back to params_buf.
/// Must run BEFORE submit_forward_ops so cuBLAS reads normalized weights.
/// Submit batch upload ops using indirect kernels (graph-capture compatible).
fn submit_indirect_upload_ops(&mut self) -> Result<(), MLError> {
let b = self.config.batch_size;
let sd = self.config.state_dim;
let total = b * sd;
let blocks = ((total + 255) / 256) as u32;
let cfg = LaunchConfig { grid_dim: (blocks, 1, 1), block_dim: (256, 1, 1), shared_mem_bytes: 0 };
let bp = self.batch_ptr_buf.raw_ptr();
let ptr_sz = std::mem::size_of::<u64>() as u64;
let b_i32 = b as i32;
let sd_i32 = sd as i32;
// States
unsafe {
self.stream.launch_builder(&self.pad_states_indirect_kernel)
.arg(&self.states_buf.raw_ptr()).arg(&bp)
.arg(&b_i32).arg(&sd_i32).arg(&sd_i32)
.launch(cfg).map_err(|e| MLError::ModelError(format!("indirect pad states: {e}")))?;
}
// Next_states
let bp1 = bp + ptr_sz;
unsafe {
self.stream.launch_builder(&self.pad_states_indirect_kernel)
.arg(&self.next_states_buf.raw_ptr()).arg(&bp1)
.arg(&b_i32).arg(&sd_i32).arg(&sd_i32)
.launch(cfg).map_err(|e| MLError::ModelError(format!("indirect pad next: {e}")))?;
}
let blocks_b = ((b + 255) / 256) as u32;
let cfg_b = LaunchConfig { grid_dim: (blocks_b, 1, 1), block_dim: (256, 1, 1), shared_mem_bytes: 0 };
// Rewards
unsafe {
self.stream.launch_builder(&self.indirect_copy_f32_kernel)
.arg(&self.rewards_buf.raw_ptr()).arg(&(bp + 2*ptr_sz)).arg(&b_i32)
.launch(cfg_b).map_err(|e| MLError::ModelError(format!("indirect rewards: {e}")))?;
}
// Dones
unsafe {
self.stream.launch_builder(&self.indirect_copy_f32_kernel)
.arg(&self.dones_buf.raw_ptr()).arg(&(bp + 3*ptr_sz)).arg(&b_i32)
.launch(cfg_b).map_err(|e| MLError::ModelError(format!("indirect dones: {e}")))?;
}
// IS-weights
unsafe {
self.stream.launch_builder(&self.indirect_copy_f32_kernel)
.arg(&self.is_weights_buf.raw_ptr()).arg(&(bp + 4*ptr_sz)).arg(&b_i32)
.launch(cfg_b).map_err(|e| MLError::ModelError(format!("indirect weights: {e}")))?;
}
// Actions
unsafe {
self.stream.launch_builder(&self.indirect_copy_i32_kernel)
.arg(&self.actions_buf.raw_ptr()).arg(&(bp + 5*ptr_sz)).arg(&b_i32)
.launch(cfg_b).map_err(|e| MLError::ModelError(format!("indirect actions: {e}")))?;
}
Ok(())
}
/// Upload batch source pointers to GPU indirection buffer (async HtoD).
/// Upload all batch + PER pointers to GPU indirection buffer (async HtoD).
/// Layout: [0]=states, [1]=next_states, [2]=rewards, [3]=dones,
/// [4]=weights, [5]=actions, [6]=indices, [7]=priorities
/// Upload 6 batch source pointers (states through actions).
pub fn upload_batch_ptrs_6(&self, gpu_batch: &crate::dqn::replay_buffer_type::GpuBatch) {
let ptrs: [u64; 6] = [
gpu_batch.states.data().raw_ptr(),
gpu_batch.next_states.data().raw_ptr(),
gpu_batch.rewards.raw_ptr(),
gpu_batch.dones.raw_ptr(),
gpu_batch.weights.raw_ptr(),
gpu_batch.actions.raw_ptr(),
];
unsafe {
cudarc::driver::sys::cuMemcpyHtoDAsync_v2(
self.batch_ptr_buf.raw_ptr(),
ptrs.as_ptr().cast(),
6 * std::mem::size_of::<u64>(),
self.stream.cu_stream(),
);
}
}
/// Upload PER pointers (indices + priorities) to slots [6] and [7].
pub fn upload_per_ptrs(&self, indices_ptr: u64, priorities_ptr: u64) {
let ptrs: [u64; 2] = [indices_ptr, priorities_ptr];
unsafe {
cudarc::driver::sys::cuMemcpyHtoDAsync_v2(
self.batch_ptr_buf.raw_ptr() + 6 * std::mem::size_of::<u64>() as u64,
ptrs.as_ptr().cast(),
2 * std::mem::size_of::<u64>(),
self.stream.cu_stream(),
);
}
}
pub fn upload_batch_ptrs_8(
&self,
gpu_batch: &crate::dqn::replay_buffer_type::GpuBatch,
indices_ptr: u64,
priorities_ptr: u64,
) {
let ptrs: [u64; 8] = [
gpu_batch.states.data().raw_ptr(),
gpu_batch.next_states.data().raw_ptr(),
gpu_batch.rewards.raw_ptr(),
gpu_batch.dones.raw_ptr(),
gpu_batch.weights.raw_ptr(),
gpu_batch.actions.raw_ptr(),
indices_ptr,
priorities_ptr,
];
unsafe {
cudarc::driver::sys::cuMemcpyHtoDAsync_v2(
self.batch_ptr_buf.raw_ptr(),
ptrs.as_ptr().cast(),
8 * std::mem::size_of::<u64>(),
self.stream.cu_stream(),
);
}
}
fn submit_spectral_norm_ops(
&mut self,
online_dueling: &mut DuelingWeightSet,
@@ -4351,6 +4487,30 @@ impl GpuDqnTrainer {
// ── 8. Regime-adaptive PER scaling (element-wise, all pre-allocated) ─
self.regime_scale_td_errors()?;
// ── 9. PER priority update (indirect pointers from batch_ptr_buf[6..8]) ─
{
let b = self.config.batch_size as i32;
let blocks = ((self.config.batch_size + 255) / 256) as u32;
let alpha = self.config.per_alpha;
let epsilon = self.config.per_epsilon;
let capacity = self.config.per_capacity as i32;
let bp = self.batch_ptr_buf.raw_ptr();
let ptr_sz = std::mem::size_of::<u64>() as u64;
unsafe {
self.stream.launch_builder(&self.per_update_kernel)
.arg(&self.td_errors_buf)
.arg(&(bp + 6 * ptr_sz)) // indices ptr
.arg(&(bp + 7 * ptr_sz)) // priorities ptr
.arg(&self.batch_max_buf)
.arg(&alpha)
.arg(&epsilon)
.arg(&b)
.arg(&capacity)
.launch(LaunchConfig { grid_dim: (blocks, 1, 1), block_dim: (256, 1, 1), shared_mem_bytes: 0 })
.map_err(|e| MLError::ModelError(format!("PER update capture: {e}")))?;
}
}
Ok(())
}
@@ -5709,7 +5869,7 @@ impl GpuDqnTrainer {
fn compile_training_kernels(
stream: &Arc<CudaStream>,
config: &GpuDqnTrainConfig,
) -> Result<(CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction), MLError> {
) -> Result<(CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction, CudaFunction), MLError> {
info!(
state_dim = config.state_dim,
total_params = compute_total_params(config),
@@ -5776,9 +5936,15 @@ fn compile_training_kernels(
let her_inplace = module.load_function("her_inplace_relabel")
.map_err(|e| MLError::ModelError(format!("her_inplace_relabel load: {e}")))?;
let pad_states_indirect = module.load_function("pad_states_indirect_kernel")
.map_err(|e| MLError::ModelError(format!("pad_states_indirect load: {e}")))?;
let indirect_copy_f32 = module.load_function("indirect_copy_f32_kernel")
.map_err(|e| MLError::ModelError(format!("indirect_copy_f32 load: {e}")))?;
let indirect_copy_i32 = module.load_function("indirect_copy_i32_kernel")
.map_err(|e| MLError::ModelError(format!("indirect_copy_i32 load: {e}")))?;
info!("GpuDqnTrainer: 27 utility kernels loaded from precompiled cubin");
Ok((grad_norm, grad_norm_finalize, adam_update, f32_to_bf16, bf16_to_f32, saxpy, zero, regime_scale, shrink_perturb, _relu_mask_from_module, spectral_norm, clipped_saxpy, clip_grad, pad_states, saxpy_f32, stochastic_depth, stochastic_depth_rng, bn_tanh_bw, bn_bias_grad, bn_tanh_concat, vaccine_dot, vaccine_project, causal_intervene, causal_reduce, pruning_mask_fn, pruning_compute_fn, her_inplace))
info!("GpuDqnTrainer: 30 utility kernels loaded from precompiled cubin");
Ok((grad_norm, grad_norm_finalize, adam_update, f32_to_bf16, bf16_to_f32, saxpy, zero, regime_scale, shrink_perturb, _relu_mask_from_module, spectral_norm, clipped_saxpy, clip_grad, pad_states, saxpy_f32, stochastic_depth, stochastic_depth_rng, bn_tanh_bw, bn_bias_grad, bn_tanh_concat, vaccine_dot, vaccine_project, causal_intervene, causal_reduce, pruning_mask_fn, pruning_compute_fn, her_inplace, pad_states_indirect, indirect_copy_f32, indirect_copy_i32))
}
/// Load the standalone Polyak EMA kernel from precompiled cubin.

View File

@@ -10,8 +10,8 @@
extern "C" __global__
void per_update_priorities_kernel(
const __nv_bfloat16* __restrict__ td_errors,
const unsigned int* __restrict__ indices,
__nv_bfloat16* __restrict__ priorities,
const unsigned long long* __restrict__ indices_ptr_buf, /* [1] ptr to indices */
const unsigned long long* __restrict__ priorities_ptr_buf, /* [1] ptr to priorities */
__nv_bfloat16* __restrict__ batch_max,
float alpha,
float epsilon,
@@ -21,6 +21,9 @@ void per_update_priorities_kernel(
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i >= batch_size) return;
const unsigned int* indices = (const unsigned int*)indices_ptr_buf[0];
__nv_bfloat16* priorities = (__nv_bfloat16*)priorities_ptr_buf[0];
__nv_bfloat16 td_raw = td_errors[i];
__nv_bfloat16 td_abs = bf16_fabs(td_raw);
__nv_bfloat16 alpha_bf = bf16(alpha);

View File

@@ -111,6 +111,7 @@ pub(crate) struct FusedTrainingCtx {
graph_iqn: Option<crate::cuda_pipeline::gpu_dqn_trainer::SendSyncGraph>,
/// CUDA Graph for EMA target update + bf16 cast + unflatten.
graph_ema: Option<crate::cuda_pipeline::gpu_dqn_trainer::SendSyncGraph>,
graph_her: Option<crate::cuda_pipeline::gpu_dqn_trainer::SendSyncGraph>,
/// Ensemble extra heads (heads 1..K-1). Head 0 lives inside the CUDA Graph.
/// Each element is an independent (DuelingWeightSet, BranchingWeightSet) pair
/// that shares the same trunk but has perturbed value/advantage weights.
@@ -224,6 +225,9 @@ impl FusedTrainingCtx {
entropy_coefficient: dqn.config.entropy_coefficient as f32,
c51_warmup_epochs: hyperparams.c51_warmup_epochs,
cql_alpha: hyperparams.cql_alpha as f32,
per_alpha: hyperparams.per_alpha as f32,
per_epsilon: 1e-6,
per_capacity: hyperparams.buffer_size,
curiosity_q_penalty_lambda: hyperparams.curiosity_q_penalty_lambda as f32,
asymmetric_dd_weight: hyperparams.asymmetric_dd_weight as f32,
ensemble_disagreement_penalty: hyperparams.ensemble_disagreement_penalty as f32,
@@ -513,6 +517,7 @@ impl FusedTrainingCtx {
graph_iql: None,
graph_iqn: None,
graph_ema: None,
graph_her: None,
ensemble_extra_heads,
ensemble_diversity_weight: hyperparams.ensemble_diversity_weight as f32,
ensemble_aggregate_kernel,
@@ -590,10 +595,7 @@ impl FusedTrainingCtx {
&self.target_dueling, &self.target_branching,
).map_err(|e| anyhow::anyhow!("Fused train_step_gpu (forward only): {e}"))?;
// HER in-place relabeling: modify trainer's states_buf/rewards_buf directly.
// Zero intermediate GpuBatch, zero GPU allocs — just DtoD copies + gather kernel.
// Runs AFTER upload so the normal portion is already in the staging buffers.
// Only the HER portion (last her_batch_size rows) gets overwritten.
// HER relabeling via CUDA graph (Random) or ungraphed (Future/Final).
if let Some(ref mut her) = self.gpu_her {
use crate::cuda_pipeline::gpu_her::HerGpuStrategy;
let her_batch_size = her.config.her_batch_size();
@@ -602,13 +604,44 @@ impl FusedTrainingCtx {
let goal_dim = her.config.goal_dim.min(state_dim);
let normal_count = batch_size.saturating_sub(her_batch_size);
// Compute donor indices (GPU-native)
match her.config.strategy {
HerGpuStrategy::Random => {
her.generate_random_donors_gpu(batch_size)
.map_err(|e| anyhow::anyhow!("HER random donors GPU: {e}"))?;
if self.graph_her.is_none() {
// First step: run ungraphed
her.generate_random_donors_gpu(batch_size)
.map_err(|e| anyhow::anyhow!("HER random (first): {e}"))?;
let donor_ptr = her.donor_indices.raw_ptr();
self.trainer.launch_her_inplace_relabel(
0, donor_ptr, normal_count, her_batch_size, goal_dim, state_dim,
).map_err(|e| anyhow::anyhow!("HER relabel (first): {e}"))?;
// Capture
self.stream.synchronize()
.map_err(|e| anyhow::anyhow!("sync before her capture: {e}"))?;
unsafe { self.stream.context().disable_event_tracking(); }
if let Ok(()) = self.stream.begin_capture(
cudarc::driver::sys::CUstreamCaptureMode::CU_STREAM_CAPTURE_MODE_THREAD_LOCAL,
) {
let _ = her.generate_random_donors_gpu(batch_size);
let donor_ptr = her.donor_indices.raw_ptr();
let _ = self.trainer.launch_her_inplace_relabel(
0, donor_ptr, normal_count, her_batch_size, goal_dim, state_dim,
);
if let Ok(Some(graph)) = self.stream.end_capture(
cudarc::driver::sys::CUgraphInstantiate_flags::CUDA_GRAPH_INSTANTIATE_FLAG_AUTO_FREE_ON_LAUNCH,
) {
use crate::cuda_pipeline::gpu_dqn_trainer::SendSyncGraph;
self.graph_her = Some(SendSyncGraph(graph));
tracing::info!("Captured HER CUDA graph (random donors + relabel)");
}
}
unsafe { self.stream.context().enable_event_tracking(); }
let _ = self.stream.context().check_err();
} else if let Some(ref graph) = self.graph_her {
graph.0.launch().map_err(|e| anyhow::anyhow!("HER graph replay: {e}"))?;
}
}
HerGpuStrategy::Future | HerGpuStrategy::Final => {
// Future/Final use episode_ids from gpu_batch (changes per step)
let episode_ids = gpu_batch.episode_ids.as_ref().ok_or_else(|| {
anyhow::anyhow!("HER {:?} requires episode_ids", her.config.strategy)
})?;
@@ -618,18 +651,12 @@ impl FusedTrainingCtx {
episode_ids, source_indices,
1, episode_ids.len(), her_batch_size,
).map_err(|e| anyhow::anyhow!("HER donor selection: {e}"))?;
let donor_ptr = her.donor_indices.raw_ptr();
self.trainer.launch_her_inplace_relabel(
0, donor_ptr, normal_count, her_batch_size, goal_dim, state_dim,
).map_err(|e| anyhow::anyhow!("HER relabel: {e}"))?;
}
}
// In-place relabel via GPU kernel: overwrites goal columns + rewards
// directly in the trainer's padded staging buffers. Zero allocation.
// Works for any goal_dim (1, 2, ..., state_dim).
let src_next_ptr = gpu_batch.next_states.data().raw_ptr();
let donor_ptr = her.donor_indices.raw_ptr();
self.trainer.launch_her_inplace_relabel(
src_next_ptr, donor_ptr,
normal_count, her_batch_size, goal_dim, state_dim,
).map_err(|e| anyhow::anyhow!("HER in-place relabel kernel: {e}"))?;
}
// C51 gradient clip now captured in graph_adam — no per-step call needed.
@@ -887,31 +914,18 @@ impl FusedTrainingCtx {
}
self.pending_vaccine_batch = None;
// ── Step 5f: Replay graph_adam — Adam sees budget-allocated gradient ──
// grad_buf contains: C51 (≤70%) + CQL (≤15%) + IQN (≤10%) + ensemble (≤5%).
// Budgets sum to ≤100% of max_grad_norm → Adam safety clip should never fire.
// Attention has its own optimizer and does NOT contribute to grad_buf.
// Pruning mask now captured in graph_adam — no per-step call needed.
// Upload PER pointers BEFORE graph_adam replay (PER update is captured in graph)
if let Some(priorities_tensor) = agent.priorities_tensor()? {
self.trainer.upload_per_ptrs(
gpu_batch.indices.raw_ptr(),
priorities_tensor.data().raw_ptr(),
);
}
// Replay graph_adam (CQL + clip + pruning + Adam + regime_scale + PER update)
let fused_result = self.trainer.replay_adam_and_readback()
.map_err(|e| { eprintln!("!!! ADAM REPLAY FAILED: {e}"); anyhow::anyhow!("graph_adam replay: {e}") })?;
// Regime PER scaling now captured in graph_adam — no per-step call needed.
// ── Step 6: GPU-native PER priority update ─────────────────────
// td_errors stay on GPU (td_errors_buf). Single CUDA kernel scatter-writes
// new priorities + atomicMax for batch max. Zero DtoH readback.
if let (Some(priorities_tensor), Some((alpha, epsilon))) =
(agent.priorities_tensor()?, agent.per_alpha_epsilon())
{
// GpuBatch::indices is now native CudaSlice<u32> — pass directly.
self.trainer.update_priorities_cuda(
&gpu_batch.indices,
priorities_tensor.data(),
alpha,
epsilon,
).map_err(|e| anyhow::anyhow!("GPU PER priority update: {e}"))?;
}
// training_steps++ and PER beta annealing (no CPU priority update)
agent.fused_post_step_gpu_bookkeeping()
.map_err(|e| anyhow::anyhow!("Fused GPU bookkeeping: {e}"))?;