perf(rl): pre-extract raw CUdeviceptr — zero device_ptr() in hot path

Cache all hot-path device pointers as raw u64 in HotPathPtrs struct at
trainer init. Replaces 9 device_ptr()/device_ptr_mut() calls (each
triggers cudarc event wait+record → cuStreamSynchronize) with direct
u64 reads from the cached struct.

Affected call sites:
- ISV staging DtoD (step_synthetic)
- Batched loss readback: pi/Q/V/FRD DtoD copies (step_synthetic)
- h_t→h_tp1 encoder DtoD copy (step_with_lobsim)
- trade_context/multires_output ptr pass to perception encoder

All pointers are stable (buffers pre-allocated at init, never
reallocated). Utility functions (read_slice_d, write_slice_f32_d, etc.)
retain device_ptr() as they operate on arbitrary caller-provided buffers
and each already does its own stream sync.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-26 10:46:43 +02:00
parent 41881f7724
commit 4a4e3171a7

View File

@@ -328,6 +328,26 @@ const RL_HINDSIGHT_FORWARD_CUBIN: &[u8] =
/// Phase E.3+ without touching the kernel.
const RL_LR_CONTROLLER_ALPHA: f32 = 0.4;
/// Pre-extracted raw CUdeviceptr values for hot-path buffers.
/// All pointers come from pre-allocated `CudaSlice` fields whose
/// device addresses are stable for the trainer's lifetime.
/// Reading these cached u64s avoids cudarc's `device_ptr()` /
/// `device_ptr_mut()` event-tracking path which internally does
/// `cuStreamWaitEvent` + `cuEventRecord` per call — responsible
/// for ~23 `cuStreamSynchronize` per step (89% wall time per nsys).
struct HotPathPtrs {
isv: u64,
ss_pi_loss: u64,
ss_q_loss: u64,
ss_v_loss_sum: u64,
ss_frd_loss_per_b_h: u64,
/// perception.h_t_d — stable (pre-allocated at PerceptionTrainer init).
h_t_encoder: u64,
h_tp1: u64,
trade_context: u64,
multires_output: u64,
}
/// Configuration for [`IntegratedTrainer`]. Wraps a `PerceptionTrainerConfig`
/// (the encoder + BCE + aux side) plus RL-specific overrides for the new
/// heads.
@@ -1038,6 +1058,9 @@ pub struct IntegratedTrainer {
/// loss-readback syncs per step_synthetic.
loss_staging_3: MappedF32Buffer,
/// Cached raw CUdeviceptr values — zero `device_ptr()` calls in hot path.
hot: HotPathPtrs,
// IQN replay-step scratch (dqn_replay_step only).
pub ss_iqn_tau_target_d: CudaSlice<f32>,
pub ss_iqn_target_q_d: CudaSlice<f32>,
@@ -2138,6 +2161,18 @@ impl IntegratedTrainer {
.new_event(None)
.map_err(|e| anyhow::anyhow!("replay_done_event: {e}"))?;
let hot = HotPathPtrs {
isv: isv_d.raw_ptr(),
ss_pi_loss: ss_pi_loss_d.raw_ptr(),
ss_q_loss: ss_q_loss_d.raw_ptr(),
ss_v_loss_sum: ss_v_loss_sum_d.raw_ptr(),
ss_frd_loss_per_b_h: ss_frd_loss_per_b_h_d.raw_ptr(),
h_t_encoder: perception.h_t_view().raw_ptr(),
h_tp1: h_tp1_d.raw_ptr(),
trade_context: trade_context_d.raw_ptr(),
multires_output: multires_output_d.raw_ptr(),
};
Ok(Self {
cfg,
perception,
@@ -2442,6 +2477,7 @@ impl IntegratedTrainer {
frd_loss_staging,
scalar_staging,
loss_staging_3,
hot,
}
.with_controllers_bootstrapped()?)
}
@@ -3612,9 +3648,8 @@ impl IntegratedTrainer {
// Queue DtoD for NEXT step's read — async, no sync needed here.
let nbytes = n * std::mem::size_of::<f32>();
unsafe {
let (src, _g) = self.isv_d.device_ptr(&self.stream);
cudarc::driver::result::memcpy_dtod_async(
self.isv_staging.dev_ptr, src, nbytes, self.stream.cu_stream(),
self.isv_staging.dev_ptr, self.hot.isv, nbytes, self.stream.cu_stream(),
)
.context("step_synthetic isv staging DtoD (async, for next step)")?;
}
@@ -4476,30 +4511,27 @@ impl IntegratedTrainer {
let f32_bytes = std::mem::size_of::<f32>();
unsafe {
// Slot 0: pi_loss
let (pi_src, _g0) = self.ss_pi_loss_d.device_ptr(&self.stream);
cudarc::driver::result::memcpy_dtod_async(
self.loss_staging_3.dev_ptr,
pi_src,
self.hot.ss_pi_loss,
f32_bytes,
self.stream.cu_stream(),
)
.context("batched loss DtoD: pi_loss")?;
// Slot 1: q_loss
let (q_src, _g1) = self.ss_q_loss_d.device_ptr(&self.stream);
cudarc::driver::result::memcpy_dtod_async(
self.loss_staging_3.dev_ptr + f32_bytes as u64,
q_src,
self.hot.ss_q_loss,
f32_bytes,
self.stream.cu_stream(),
)
.context("batched loss DtoD: q_loss")?;
// Slot 2: v_loss_sum
let (v_src, _g2) = self.ss_v_loss_sum_d.device_ptr(&self.stream);
cudarc::driver::result::memcpy_dtod_async(
self.loss_staging_3.dev_ptr + 2 * f32_bytes as u64,
v_src,
self.hot.ss_v_loss_sum,
f32_bytes,
self.stream.cu_stream(),
)
@@ -4510,10 +4542,9 @@ impl IntegratedTrainer {
let frd_n_h = crate::rl::common::FRD_N_HORIZONS;
let frd_n = b_size * frd_n_h;
unsafe {
let (frd_src, _g3) = self.ss_frd_loss_per_b_h_d.device_ptr(&self.stream);
cudarc::driver::result::memcpy_dtod_async(
self.frd_loss_staging.dev_ptr,
frd_src,
self.hot.ss_frd_loss_per_b_h,
frd_n * f32_bytes,
self.stream.cu_stream(),
)
@@ -5152,11 +5183,10 @@ impl IntegratedTrainer {
{
let nbytes = b_size * HIDDEN_DIM * std::mem::size_of::<f32>();
unsafe {
let s = self.stream.cu_stream();
let (src, _gs) = self.perception.h_t_view().device_ptr(&self.stream);
let (dst, _gd) = self.h_tp1_d.device_ptr_mut(&self.stream);
cudarc::driver::result::memcpy_dtod_async(dst, src, nbytes, s)
.context("step_with_lobsim: DtoD perception.h_t_d → self.h_tp1_d")?;
cudarc::driver::result::memcpy_dtod_async(
self.hot.h_tp1, self.hot.h_t_encoder, nbytes, self.stream.cu_stream(),
)
.context("step_with_lobsim: DtoD perception.h_t_d → self.h_tp1_d")?;
}
}
@@ -5165,10 +5195,8 @@ impl IntegratedTrainer {
// backward in step_synthetic, AND lands h_t at slot K-1 for
// the action-sampling forwards below.
{
let (tc_ptr, _g1) = self.trade_context_d.device_ptr(&self.stream);
let (mr_ptr, _g2) = self.multires_output_d.device_ptr(&self.stream);
self.perception.encoder_ctx_trade_ptr = tc_ptr;
self.perception.encoder_ctx_multires_ptr = mr_ptr;
self.perception.encoder_ctx_trade_ptr = self.hot.trade_context;
self.perception.encoder_ctx_multires_ptr = self.hot.multires_output;
}
let _ = self
.perception