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:
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}"))?;
|
||||
|
||||
Reference in New Issue
Block a user