fix: pinned host memory for async DtoH readback

cuMemcpyDtoHAsync with non-pinned host pointers blocks the CPU on
CUDA 13 / H100 driver 580+ until ALL pending GPU work completes.
With graph_forward + aux_ops + adam queued, this caused multi-minute
hangs between training steps.

Fix: allocate 12 bytes of pinned (page-locked) host memory via
cuMemHostAlloc(DEVICEMAP) for the 3 scalar readbacks (loss, mse_loss,
grad_norm). Pinned memory enables true async DtoH — CPU returns
immediately, GPU copies when it reaches that point in the stream.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-04-06 01:16:12 +02:00
parent f1009517bd
commit d98ec8d343

View File

@@ -636,12 +636,10 @@ pub struct GpuDqnTrainer {
/// CUDA event for async scalar readback (replaces per-step stream.synchronize)
readback_event: Option<CudaEvent>,
/// Host-side buffer for deferred loss readback from previous step (blended MSE+C51)
pending_loss: f32,
/// Host-side buffer for deferred MSE loss readback from previous step
pending_mse_loss: f32,
/// Host-side buffer for deferred grad_norm readback from previous step (sum-of-squares)
pending_grad_norm: f32,
/// Pinned host buffer [3 × f32] for async DtoH scalar readback.
/// Layout: [0]=loss, [1]=mse_loss, [2]=grad_norm_sq.
/// Pinned memory enables true async cuMemcpyDtoHAsync without CPU blocking.
readback_pinned: *mut f32,
/// Whether there's an in-flight readback to collect
readback_pending: bool,
@@ -918,6 +916,10 @@ impl Drop for GpuDqnTrainer {
self.graph_forward_mse = None;
self.graph_forward_ddqn = None;
self.graph_adam = None;
// Free pinned host memory
if !self.readback_pinned.is_null() {
let _ = unsafe { cudarc::driver::result::free_host(self.readback_pinned.cast()) };
}
}
}
@@ -2804,9 +2806,14 @@ impl GpuDqnTrainer {
scalars_readback_buf,
scalars_readback_host,
readback_event: None,
pending_loss: 0.0,
pending_mse_loss: 0.0,
pending_grad_norm: 0.0,
readback_pinned: {
let flags = cudarc::driver::sys::CU_MEMHOSTALLOC_DEVICEMAP;
unsafe {
cudarc::driver::result::malloc_host(3 * std::mem::size_of::<f32>(), flags)
.map_err(|e| MLError::ModelError(format!("pinned readback alloc: {e}")))?
as *mut f32
}
},
readback_pending: false,
bf16_states_buf,
bf16_next_states_buf,
@@ -3004,11 +3011,14 @@ impl GpuDqnTrainer {
}
}
self.readback_pending = false;
// Blend MSE + C51 loss: guard against 0.0 * NaN = NaN (IEEE 754)
let blended = Self::blend_loss(self.c51_alpha, self.pending_mse_loss, self.pending_loss);
// Read from pinned buffer: [0]=loss, [1]=mse_loss, [2]=grad_norm_sq
let (loss, mse_loss, grad_norm_sq) = unsafe {
(*self.readback_pinned, *self.readback_pinned.add(1), *self.readback_pinned.add(2))
};
let blended = Self::blend_loss(self.c51_alpha, mse_loss, loss);
FusedTrainScalars {
total_loss: blended,
grad_norm: self.pending_grad_norm.sqrt(),
grad_norm: grad_norm_sq.sqrt(),
}
} else {
// First step — no previous data
@@ -3022,26 +3032,28 @@ impl GpuDqnTrainer {
self.replay_adam()?;
if adam_step < 3 { eprintln!("adam_readback: step {adam_step} — async readback start"); }
// 3. Sync stream then DtoH readback (synchronous — non-pinned host memory)
// cuMemcpyDtoHAsync with non-pinned host pointers degrades to sync on
// CUDA 13+ / H100 driver 580+, blocking for ALL pending GPU work.
// Explicit sync + sync copy is faster and predictable.
unsafe { cudarc::driver::sys::cuStreamSynchronize(self.stream.cu_stream()); }
// 3. Async DtoH into pinned host buffer (truly async — no CPU blocking)
unsafe {
cudarc::driver::sys::cuMemcpyDtoH_v2(
(&mut self.pending_loss as *mut f32).cast(),
// [0] = loss
cudarc::driver::sys::cuMemcpyDtoHAsync_v2(
self.readback_pinned.cast(),
self.total_loss_buf.raw_ptr(),
std::mem::size_of::<f32>(),
self.stream.cu_stream(),
);
cudarc::driver::sys::cuMemcpyDtoH_v2(
(&mut self.pending_mse_loss as *mut f32).cast(),
// [1] = mse_loss
cudarc::driver::sys::cuMemcpyDtoHAsync_v2(
self.readback_pinned.add(1).cast(),
self.mse_loss_buf.raw_ptr(),
std::mem::size_of::<f32>(),
self.stream.cu_stream(),
);
cudarc::driver::sys::cuMemcpyDtoH_v2(
(&mut self.pending_grad_norm as *mut f32).cast(),
// [2] = grad_norm_sq
cudarc::driver::sys::cuMemcpyDtoHAsync_v2(
self.readback_pinned.add(2).cast(),
self.grad_norm_f32_buf.raw_ptr(),
std::mem::size_of::<f32>(),
self.stream.cu_stream(),
);
}
@@ -3075,10 +3087,13 @@ impl GpuDqnTrainer {
}
}
self.readback_pending = false;
let blended = Self::blend_loss(self.c51_alpha, self.pending_mse_loss, self.pending_loss);
let (loss, mse_loss, grad_norm_sq) = unsafe {
(*self.readback_pinned, *self.readback_pinned.add(1), *self.readback_pinned.add(2))
};
let blended = Self::blend_loss(self.c51_alpha, mse_loss, loss);
Ok(FusedTrainScalars {
total_loss: blended,
grad_norm: self.pending_grad_norm.sqrt(),
grad_norm: grad_norm_sq.sqrt(),
})
}