Files
foxhunt/docs/dqn-gpu-hot-path-audit.md
jgrusewski f77ebeb214 perf(htod): migrate gpu_iqn_head.rs to mapped-pinned + DtoD-only clone (5 sites)
- 4 stream.clone_htod sites in IQN constructor: cos_features precompute,
  online_taus + target_taus tile broadcast, init_iqn_xavier_weights
  upload — all rewritten to mapped_pinned::clone_to_device_f32_via_pinned.
- clone_cuda_slice helper rewritten to a single DtoD copy via the
  existing super::clone_cuda_slice_f32 helper. Previously did
  device→host→device, double-violating both no-DtoH and no-HtoD rules.
  Sole callsite is constructor seeding of target params from online.

Audit row appended (Fix 9) in docs/dqn-gpu-hot-path-audit.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 21:01:51 +02:00

16 KiB
Raw Blame History

DQN GPU Hot-Path Audit

Invariant 3 enforcement. Every cross-boundary call (DtoH, HtoD, memcpy between host and device) in the DQN code path is classified below. No hot-path memcpy DtoH/HtoD is allowed — zero tolerance. Pinned + device-mapped memory is the ONLY permitted mechanism for per-step host-visible data.

Hot path definition: every function called from fused_training.rs::run_full_step or its descendants that executes once per training step (up to 178 times per epoch at batch size 8192), including kernel invocations inside the captured CUDA graph (adam_grad_child, forward_child, aux_grad_child, aux_child, maintenance_child) and host-side orchestration between them.

Classification:

  • OK-pinned — uses the zero-copy pinned + device-mapped pattern (cuMemAllocHost_v2 + cuMemHostGetDevicePointer_v2); allowed on hot path.
  • MIGRATE — was a plain memcpy but has been converted to pinned in this commit.
  • COLD-PATH — acceptable placement (constructor init, checkpoint save/load, fold transition, epoch-boundary); annotated with reason.
File:line Call signature Path class Annotation Status
gpu_dqn_trainer.rs:6586 malloc_hosttotal_loss_pinned OK-pinned Constructor: cuMemAllocHost+GetDevicePointer; GPU writes via dev ptr, CPU reads via readback_training_scalars() OK
gpu_dqn_trainer.rs:6598 malloc_hostmse_loss_pinned OK-pinned Constructor: same pattern as total_loss_pinned OK
gpu_dqn_trainer.rs:6611 malloc_hostq_divergence_pinned OK-pinned Constructor: same pattern OK
gpu_dqn_trainer.rs:6624 malloc_hostiqn_readiness_pinned OK-pinned Constructor: same pattern OK
gpu_dqn_trainer.rs:6662 malloc_hostgrad_norm_pinned OK-pinned Constructor: same pattern OK
gpu_dqn_trainer.rs:6707 malloc_host (plain, not DEVICEMAP) → readback scratch COLD-PATH Constructor: plain pinned for per-epoch flush_readback(); not written by GPU kernel directly OK
gpu_dqn_trainer.rs:6788 malloc_host → monitoring summary pinned OK-pinned Constructor: monitoring kernel writes via dev ptr each step OK
gpu_dqn_trainer.rs:6895 malloc_hosteval_v_range_pinned OK-pinned Constructor: GPU writes; host reads only at fold/epoch boundary via eval_v_range() OK
gpu_dqn_trainer.rs:6911 malloc_hostnan_flag_pinned OK-pinned Constructor: GPU writes on NaN; host reads only in fault path (read_nan_flags) OK
gpu_dqn_trainer.rs:6929 malloc_host → ISV isv_signals_pinned OK-pinned Constructor: GPU writes ISV bus each step; host reads via read_isv_health_and_regime() in compute_adaptive_budgets OK
gpu_dqn_trainer.rs:6949 malloc_hostlr_pinned OK-pinned Constructor: CPU writes LR; GPU reads via dev ptr OK
gpu_dqn_trainer.rs:6968 malloc_hostadaptive_clip_pinned OK-pinned Constructor: CPU writes clip; GPU reads via dev ptr OK
gpu_dqn_trainer.rs:6988 malloc_hostpopart_pinned OK-pinned Constructor: GPU writes PopArt stats; host reads at epoch boundary OK
gpu_dqn_trainer.rs:7099 malloc_hostq_stats_pinned OK-pinned Constructor: GPU writes Q stats; host reads via reduce_current_q_stats() at epoch boundary OK
gpu_dqn_trainer.rs:7127 malloc_hostq_readback_pinned OK-pinned Constructor: GPU writes per-step Q readback; host reads via last_readback_scalars() in training guard OK
gpu_dqn_trainer.rs:7206 cuMemAllocHost_v2var_ema pinned (ISV subsystem) OK-pinned Constructor: ISV variance EMA — GPU writes each step; host reads via ISV bus OK
gpu_dqn_trainer.rs:7243 cuMemAllocHost_v2regime_util pinned (ISV subsystem) OK-pinned Constructor: ISV regime utilization — GPU writes each step OK
gpu_dqn_trainer.rs:7289 malloc_hostpopart_var_pinned OK-pinned Constructor: GPU writes PopArt variance; host reads at epoch boundary OK
gpu_dqn_trainer.rs:7312 cuMemcpyHtoDAsync_v2 (constructor) COLD-PATH Constructor: one-shot HtoD to seed deterministic-mask buffer; never replayed OK
gpu_dqn_trainer.rs:7878 cuMemAllocHost_v2q_mean_scratch OK-pinned Constructor: GPU scratch for Q-mean reduction kernel OK
gpu_dqn_trainer.rs:7898 cuMemAllocHost_v2q_mean_ema OK-pinned Constructor: Q-mean EMA — GPU writes, host reads at epoch boundary OK
gpu_dqn_trainer.rs:7919 cuMemAllocHost_v2td_error_scratch OK-pinned Constructor: TD-error scratch for reduce kernel OK
gpu_dqn_trainer.rs:7939 cuMemAllocHost_v2q_var_scratch OK-pinned Constructor: Q-variance scratch for reduce kernel OK
gpu_dqn_trainer.rs:2300 stream.memcpy_htodatom_positions_buf COLD-PATH set_atom_positions(): called at fold boundary to reload C51 atom grid — not on hot path OK
gpu_dqn_trainer.rs:2606 stream.memcpy_dtoh → grad buf readback COLD-PATH per_branch_grad_norms(): per-epoch call only; drains grad buffer for logging OK
gpu_dqn_trainer.rs:2925 stream.memcpy_dtoh (online params) COLD-PATH per_branch_target_drift(): per-epoch drift measurement OK
gpu_dqn_trainer.rs:2928 stream.memcpy_dtoh (target params) COLD-PATH per_branch_target_drift(): per-epoch drift measurement OK
gpu_dqn_trainer.rs:2991 stream.memcpy_dtoh → weight inspection COLD-PATH per_branch_vsn_mean(): per-epoch VSN mean read OK
gpu_dqn_trainer.rs:3072 stream.memcpy_dtoh → Q-out inspection COLD-PATH compute_q_stats(): per-epoch Q-stats; not called per-step OK
gpu_dqn_trainer.rs:7541 stream.memcpy_htod → descriptor buffer COLD-PATH Constructor: one-shot HtoD for GEMM descriptor upload OK
gpu_dqn_trainer.rs:7680 stream.memcpy_htod → stochastic depth scale COLD-PATH Constructor: one-shot HtoD for stochastic depth kernel seed OK
gpu_dqn_trainer.rs:7686 stream.memcpy_htod → stochastic depth RNG COLD-PATH Constructor: one-shot HtoD for stochastic depth RNG OK
gpu_dqn_trainer.rs (removed) cuMemcpyDtoHAsync_v2 in run_causal_intervention_unconditional MIGRATED Fix 1: removed dead copy — result causal_mean_scratch was never consumed by any caller; result stays on GPU FIXED
gpu_iqn_head.rs:469 stream.clone_htodcos_features COLD-PATH Constructor: precompute cosine embedding table; one-shot OK
gpu_iqn_head.rs:477 stream.clone_htodonline_taus COLD-PATH Constructor: tau quantile tiling; one-shot OK
gpu_iqn_head.rs:480 stream.clone_htodtarget_taus COLD-PATH Constructor: tau quantile tiling; one-shot OK
gpu_iqn_head.rs:498 malloc_hosttotal_loss_pinned OK-pinned Constructor: GPU writes IQN loss; host reads via read_total_loss() OK
gpu_iqn_head.rs:534 malloc_hostt_pinned OK-pinned Constructor: Adam step counter — CPU increments, GPU reads via dev ptr OK
gpu_iqn_head.rs:548 malloc_hosttau_pinned OK-pinned Constructor (Fix 3): tau scalar — CPU writes, GPU reads via tau_dev_ptr; replaces CudaSlice<f32> + cuMemcpyHtoDAsync_v2 OK
gpu_iqn_head.rs:807 cuMemcpyDtoDAsync_v2 (DtoD) OK-pinned Device-to-device copy inside graph-captured target-H forward cache; no host crossing OK
gpu_iqn_head.rs:991 cuMemcpyDtoDAsync_v2 (DtoD) OK-pinned Device-to-device copy for cached target states; no host crossing OK
gpu_iqn_head.rs:1564 stream.clone_dtohtotal_loss COLD-PATH read_loss(): synchronous fallback for epoch-end logging only; read_total_loss() (pinned) is the hot-path equivalent OK
gpu_iqn_head.rs:1602 cuMemcpyDtoH_v2 (sync) in compute_iqr MIGRATED Fix 2A: compute_iqr() removed from submit_aux_ops; now called at epoch boundary via refresh_iqn_iqr() only; sync DtoH cannot be captured in CUDA graph — was silently a no-op on replayed steps FIXED
gpu_iqn_head.rs:1622 cuMemcpyHtoDAsync_v2 in compute_iqr MIGRATED Fix 2B: same relocation — async HtoD with stale IQR data was being replayed; now runs once at epoch boundary FIXED
gpu_iqn_head.rs (removed) cuMemcpyHtoDAsync_v2 in target_ema_update for tau_buf MIGRATED Fix 3: replaced CudaSlice<f32> + async copy with pinned+device-mapped page; CPU writes *tau_pinned = tau; kernel reads via tau_dev_ptr — zero PCIe overhead FIXED
gpu_iqn_head.rs:2058 stream.clone_htodparams_buf constructor COLD-PATH init_iqn_head_from_weights(): one-shot weight upload at construction/load; not on hot path OK
gpu_iqn_head.rs:2107 stream.clone_dtoh + stream.clone_htod in clone_cuda_slice COLD-PATH clone_cuda_slice(): called during checkpoint save/load only OK
fused_training.rs:476 stream.memcpy_htodsrc_gpu (PER source indices) COLD-PATH prepare_per_batch(): per-epoch PER batch preparation — not per-step OK
fused_training.rs:482 stream.memcpy_htodones_gpu COLD-PATH prepare_per_batch(): per-epoch importance weights init OK
fused_training.rs:2914 stream.clone_dtoh → state inspection COLD-PATH eval_state_debug(): debug helper, no hot-path caller OK
fused_training.rs:2928 stream.clone_dtoh → param inspection COLD-PATH eval_state_debug(): debug helper OK
fused_training.rs:2955 cuMemcpyDtoH_v2 (sync) COLD-PATH eval_v_range(): called only at fold boundary / epoch-0 init; explicit sync is documented and acceptable at boundary OK
training_loop.rs:220 collector.stream().memcpy_dtoh → PER scores COLD-PATH Pre-epoch PER priority init — runs once before the step loop starts OK
training_loop.rs:409 stream.memcpy_htodepisode_starts_buf COLD-PATH Per-epoch experience setup — before run_training_steps_slices begins OK
training_loop.rs:1003 clone_htod_f32 → targets device buffer COLD-PATH Data loading: DBN targets uploaded before step loop OK
training_loop.rs:1007 clone_htod_f32 → features device buffer COLD-PATH Data loading: feature matrix uploaded before step loop OK
tau_update_kernel.cu (Plan 1 Task 13) single-thread kernel: reads ISV[39,40,12]; writes ISV[42] COLD-PATH Per-epoch boundary call from training_loop.rs; 1-thread, 1-block; zero hot-path overhead OK
epsilon_update_kernel.cu (Plan 1 Task 14) single-thread kernel: reads ISV[39,40,12]; writes ISV[41] COLD-PATH Per-epoch boundary call from training_loop.rs; 1-thread, 1-block; zero hot-path overhead OK
gamma_update_kernel.cu (Plan 1 Task 10) single-thread kernel: reads ISV[12]; writes ISV[43] COLD-PATH Per-epoch boundary call from training_loop.rs; 1-thread, 1-block; ISV[43] read by fill_gamma_buf on hot path via read_isv_signal_at (pinned, zero-copy) OK
kelly_cap_update_kernel.cu (Plan 1 Task 11) single-thread kernel: reads ISV[12] + portfolio_states[n_envs, PS_STRIDE]; aggregates mean half-Kelly × health-safety; writes ISV[44] COLD-PATH Per-epoch boundary call from training_loop.rs; 1-thread, 1-block; portfolio_states dev ptr passed from GpuExperienceCollector without CPU roundtrip OK
atoms_update_kernel.cu (Plan 1 Task 9) 4-block kernel (one per branch): reads ISV[23..31) + spacing_raw params[52..56); writes atom_positions_buf[4 * num_atoms] via softmax + cumsum COLD-PATH Per-epoch boundary + SGD step (every 50 steps); grid=(4,1,1), block=(256,1,1); replaces CPU loop that launched adaptive_atom_positions 4× separately OK

Summary

Class Count
OK-pinned 31
COLD-PATH 20
MIGRATED (fixed in this commit) 4
Remaining MIGRATE (unfixed) 0

All MIGRATE sites resolved. Zero hot-path memcpy calls remain in the DQN training loop.

Fixes Applied (this commit)

Fix 1 — Remove dead async DtoH in run_causal_intervention_unconditional

gpu_dqn_trainer.rs: cuMemcpyDtoHAsync_v2 from causal_mean_scratch_ptr to readback_pinned[10] was removed. The only reader (run_causal_intervention, which has no callers) was never invoked, making this copy dead code. The result now stays on the GPU device buffer where the kernel leaves it.

Fix 2 — Relocate compute_iqr() from hot path to epoch boundary

fused_training.rs / training_loop.rs: iqn.compute_iqr() was being called inside submit_aux_ops, which is captured in the CUDA graph (aux_child). The sync cuMemcpyDtoH_v2 inside compute_iqr cannot be captured in a CUDA graph — it ran during capture only and was silently skipped on every replayed step. The subsequent cuMemcpyHtoDAsync_v2 (uploading IQR back) was therefore replaying stale data. Moved to refresh_iqn_iqr() called at epoch boundary in process_epoch_boundary.

Fix 3 — Convert tau_buf from device VRAM to pinned+device-mapped

gpu_iqn_head.rs: tau_buf: CudaSlice<f32> + tau_host: f32 + cuMemcpyHtoDAsync_v2 replaced by tau_pinned: *mut f32 + tau_dev_ptr: u64 allocated via cuMemAllocHost_v2(DEVICEMAP) + cuMemHostGetDevicePointer_v2. CPU writes *tau_pinned = tau directly; the EMA kernel reads via tau_dev_ptr with no PCIe copy. Drop impl updated to free tau_pinned.

Fix 4 — Add mapped-pinned staging helpers in mapped_pinned.rs

mapped_pinned.rs: added clone_to_device_{f32,i32}_via_pinned and upload_{f32,i32}_via_pinned helpers that allocate a temporary MappedXxxBuffer (cuMemHostAlloc DEVICEMAP), write the payload via host_ptr, then async DtoD into a target CudaSlice<T>. Used to migrate cold-path constructor uploads off explicit cudaMemcpy HtoD per feedback_no_htod_htoh_only_mapped_pinned.md while preserving struct field types (e.g. portfolio_buf, weight buffers consumed by Adam) where the kernel mutates the buffer every step and device-resident memory is correct.

Fix 5 — Migrate cuda_pipeline/mod.rs callers to mapped-pinned

cuda_pipeline/mod.rs: 7 in-file callers of clone_htod_f32 in DqnGpuData::upload_slices, DqnGpuData::upload_ofi, DqnGpuData::build_batch_states (portfolio), DqnGpuData::htod_copy_into, and PpoGpuData::upload rewritten to use mapped_pinned::clone_to_device_f32_via_pinned. The htod_f32 / clone_htod_f32 helper bodies at lines 129-145 remain in place because other crates are still in mid-migration; they will be deleted once all consumers migrate.

Fix 6 — Migrate gpu_walk_forward.rs to mapped-pinned (3 sites)

gpu_walk_forward.rs: 3 clone_htod_f32 calls in the walk-forward GPU data upload (features, targets, OFI) rewritten to mapped_pinned::clone_to_device_f32_via_pinned.

Fix 7 — Migrate PPO trainer + hyperopt adapters to mapped-pinned (6 sites)

trainers/ppo.rs (2 sites: features, targets in set_raw_market_data), hyperopt/adapters/ppo.rs (3 sites: features, targets in upload path; action_indices in backtest forward closure), hyperopt/adapters/mamba2.rs (1 site: gpu_to_stream). All clone_htod_f32 and bare stream.clone_htod calls rewritten to mapped_pinned::clone_to_device_{f32,i32}_via_pinned.

Fix 8 — Migrate weight-upload sites in attention/tlob/iql/weights (4 sites)

gpu_weights.rs (1 site: RmsNormWeightSet::ones constructor), gpu_attention.rs (1 site: attention params constructor), gpu_tlob.rs (1 site: TLOB params constructor — mod tests blocks intentionally untouched per scope), gpu_iql_trainer.rs (2 sites: per_sample_support seed, IQL params init). All htod_f32 calls rewritten to mapped_pinned::upload_f32_via_pinned; the gpu_weights.rs ones-init uses clone_to_device_f32_via_pinned since the destination is freshly allocated.

Fix 9 — Migrate gpu_iqn_head.rs constructors + DtoD-only target clone (5 sites)

gpu_iqn_head.rs: 4 stream.clone_htod calls (cos_features precompute, online_taus + target_taus tile broadcast, init_iqn_xavier_weights upload) rewritten to mapped_pinned::clone_to_device_f32_via_pinned. Additionally the helper clone_cuda_slice was replaced — it previously did device→host (clone_dtoh) → host→device (clone_htod) round-trip, double-violating both the no-DtoH and no-HtoD rules. Now uses clone_cuda_slice_f32 (single async DtoD), the only callsite being the target-network seed at constructor time (target_params = clone_cuda_slice(&online_params)).