Commit Graph

1920 Commits

Author SHA1 Message Date
jgrusewski
4f6b4540c5 fix: wire PopArt normalization + counterfactual double-negation bug
PopArt: normalize_rewards_popart_inplace was implemented but never
called from run_full_step. Without it, rank-normalized rewards [-1,+1]
spread across C51 atom range [-50,+50] — only 2% of atoms used,
near-zero C51 gradient. Now called before forward pass (Phase 0a).

Counterfactual: when do_flip=true AND cf_cycle==0 (directional mirror),
cf_reward = -reward double-negated (reward already flipped at line 1895).
Fix: cf_reward = do_flip ? reward : -reward. Affects ~1/6 of
counterfactual experiences that were teaching wrong directional signal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 17:56:42 +02:00
jgrusewski
ff62c7968a fix: gate conviction reward scaling by readiness — was killing gradient signal
The plan head's conviction output (sigmoid, [0,1]) scales rewards.
At init, conviction ≈ 0.1 (Xavier random weights through sigmoid),
which multiplied ALL rewards by 0.1 — killing the gradient signal.
The model couldn't learn meaningful Q-values, defaulted to uniform
random action selection → 1.7M trades on 4M bars → val_Sharpe -1000.

Fix: only apply conviction scaling when readiness ≥ 0.5 (plan head
mature). Before that, conviction defaults to 1.0 (identity). This
matches the existing readiness gate on position sizing (line 1416).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 17:30:52 +02:00
jgrusewski
ff7bbc7dd9 fix: remove cuStreamSynchronize — one-step lag is fine for atom_stats
The async pinned readback design intentionally reads previous epoch's
values. Epoch 1 shows zeros (lag), epoch 2+ shows real atom stats.
cuStreamSynchronize kills GPU pipeline — removed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 16:53:48 +02:00
jgrusewski
65604db516 fix: add cuStreamSynchronize before pinned atom_stats read
populate_q_out + q_stats_reduce launch async kernels that write to
pinned device-mapped memory. The CPU was reading immediately — before
the GPU finished writing. Added cuStreamSynchronize to ensure the
kernel results are visible before the pinned memory read.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 16:51:46 +02:00
jgrusewski
a0b52b7876 fix: revert atom_stats from graph-captured replay_forward_ungraphed
atomicAdd inside CUDA graph replay causes non-determinism and is
unnecessary. The only correct path: reduce_current_q_stats() calls
populate_q_out() outside the graph to compute atom_stats cleanly.
Graph-captured path stays NULL for both atom_stats and q_var.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 16:44:23 +02:00
jgrusewski
d596cbbc20 fix: call populate_q_out in reduce_current_q_stats for atom_stats
The graph-captured forward pass never calls compute_expected_q (it
works directly on logits for C51 loss). So atom_stats_buf was never
written during training. Now reduce_current_q_stats() calls
populate_q_out() first — runs compute_expected_q outside the graph
to populate atom_stats before q_stats_reduce reads them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 16:41:45 +02:00
jgrusewski
f359905fe6 fix: wire atom_stats in replay_forward_ungraphed — second NULL call site
The first fix only patched populate_q_out(). The graph-captured path
uses replay_forward_ungraphed() which had its own null_atom_stats=0.
This is the call site that actually runs during training.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 16:32:55 +02:00
jgrusewski
64d0eb8bfe fix: wire atom_stats_buf + q_var to populate_q_out — C51 atom utilization was always 0%
compute_expected_q was called with null_atom_stats=0 (NULL pointer),
so the kernel skipped atom entropy/utilization accumulation entirely.
atom_stats_buf existed but was never passed. This disabled:
- G4 adaptive gamma annealing (uses atom_utilization)
- Homeostatic regularizer atom_util observable
- ISV atom utilization signal

Now passes atom_stats_buf.raw_ptr() with a cuMemsetD32 zero before
each call (kernel uses atomicAdd). Also passes q_var_buf_trainer for
per-action Q-variance computation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 16:11:39 +02:00
jgrusewski
a54f9b8e40 perf: reward_rank_normalize O(N²) → bitonic sort O(N log²N) + fix warnings
reward_rank_normalize: 10.6s single call replaced with bitonic sort
pipeline — compute_abs_sharpe + bitonic_sort_step × O(log²N) passes +
scatter_rank. Target: <50ms for 4M elements.

Fix: bias_grad_reduce partials buffer undersized — max_out_dim now
includes state_dim and absolute floor of 512.

Cleanup: suppress 3 compiler warnings (unused vars, dead fields).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 15:10:11 +02:00
jgrusewski
c274c43f27 perf: bias_grad_reduce_f32 + curiosity_bias_grad — 2-phase shared-memory
bias_grad_reduce_f32_kernel: 28K calls × 0.24ms = 6.6s total (11.4% GPU).
Serial batch loop → 2-phase shared-memory block reduce + final reduce.

curiosity_bias_grad_reduce: 4 calls × 134ms = 539ms (0.9% GPU).
Same serial pattern → same 2-phase fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 14:54:33 +02:00
jgrusewski
a79b8761cf perf: cuMemsetD8Async → cuMemsetD32Async for 4x memset bandwidth
All gradient buffer clears now use cuMemsetD32Async (u32-wide writes)
instead of cuMemsetD8Async (byte-wide). 4x memory bandwidth utilization
for the ~33 memset calls per training step. Size params converted from
bytes to f32 element count (.num_bytes() → .len()).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 14:15:45 +02:00
jgrusewski
951dd97d71 perf: batch-parallel LN bwd + selectivity bwd + trade_plan cuBLAS + eliminate HtoD
attn_layer_norm_bwd: split into batch-parallel d_x + 2-phase d_gamma/d_beta
reduce (6.8ms → <0.5ms). selectivity_backward: decomposed into d_z kernel
+ 2-phase dW/db reduction (1.7ms → <0.1ms). trade_plan_forward: replaced
per-sample matmuls with 2 cuBLAS SGEMMs + elementwise activations (5ms →
<0.5ms). Epsilon buffer: GPU-side fill_f32 kernel eliminates per-step HtoD.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 14:08:00 +02:00
jgrusewski
1f36d2fd24 perf: q_denoise_backward cuBLAS pipeline + attn/IQL 2-phase bias grad
q_denoise_backward (63.5% GPU time, 169ms/call): decomposed into
cuBLAS forward replay + backward GEMMs. 8 cuBLAS GEMMs + elementwise
kernels replace 1800-thread serial loop. Target: <1ms/call.

attn_bias_grad_reduce + iql_bias_grad_reduce: converted from serial
batch loops to 2-phase shared-memory reduction (same pattern as IQN).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 13:50:32 +02:00
jgrusewski
2fb5d24cac cleanup: remove dead mamba2 kernels replaced by cuBLAS pipeline
mamba2_temporal_scan (forward) and mamba2_scan_backward are no longer
called — replaced by cuBLAS projection GEMMs + lightweight scan kernels.
Removes ~200 lines of dead CUDA code and 2 unused CUfunction handles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 13:23:20 +02:00
jgrusewski
64e6353a5d fix: IQN num_quantiles 64→32 in all binaries + production config
Default was hardcoded as 64 in train_baseline_rl.rs and evaluate_baseline.rs,
overriding the config default of 32. Added num_quantiles=32 to production
config so it's explicit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 13:06:58 +02:00
jgrusewski
9951b8b8cb perf: curiosity training — cuBLAS GEMMs (1085ms → <5ms)
Replace the serial curiosity_fwd_bwd_per_block kernel (CUR_TOTAL_PARAMS=11306
loop iterations × block-level shared-memory reduction per-iteration = 1085ms)
with a cuBLAS GEMM pipeline matching the existing curiosity inference path:

Forward: curiosity_prepare_input → GEMM1(W1) → bias_leaky_relu → GEMM2(W2) → mse_fwd_grad
Backward: gemm_dw(dW2) → bias_grad_reduce(db2) → gemm_dx(d_hidden) → leaky_relu_bwd → gemm_dw(dW1) → bias_grad_reduce(db1)

New CUDA kernels added to curiosity_training_kernel.cu:
  - curiosity_mse_fwd_grad: +b2 in-place, d_pred = 2/CUR_OUTPUT*(pred-target)
  - curiosity_leaky_relu_bwd: gates d_hidden by sign of post-activation hidden
  - curiosity_bias_grad_reduce: sum dy[N, D] over batch → grad_b[D]

GpuCuriosityTrainer rewritten with CuriosityGemm (dedicated cuBLAS+cublasLt
handle) + intermediate buffers (input_buf, hidden_buf, pred_buf, d_hidden_buf).
Reuses forward kernels from curiosity_inference_kernel.cu. Keeps curiosity_adam_step.
Drops partial_grads buffer (max_blocks*11306 floats saved).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 12:54:18 +02:00
jgrusewski
bf87a8d0bb perf: kan_grad_reduce — batch-parallel 2-phase (8ms×208 → <0.1ms×208)
Replace single-phase one-thread-per-param serial loop (batch_size iterations
per thread) with kan_grad_reduce_p1 (block sums, grid=(ceil(B/256),total_params),
shared mem) + kan_grad_reduce_p2 (warp-shuffle final reduce, grid=(total_params)).
Allocate partials scratch [ceil(B/256)*total_params] for trunk + 4 branches.
Update CublasBackwardSet constructor signature and both call sites.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 12:49:03 +02:00
jgrusewski
b6f88a5525 perf: mamba2 forward+backward — cuBLAS projections + lightweight scan (1567ms → <6ms)
Replace catastrophic anti-pattern (one thread per weight, looping over all
8192 batch samples with inner SH2=256 matmul) with cuBLAS GEMM projections
+ lightweight scan kernels.

Forward: 2 cublasLtMatmul GEMMs (W_A, W_B projections) + scan kernel
  grid=(B, ceil(SH2/256)), block=256 — inner loop K=8, STATE_D=16.
Backward: reverse scan kernel grid=(B, ceil(STATE_D/32)), block=32 +
  3 cublasLtMatmul GEMMs (dW_A, dW_B, dW_C weight gradients).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 12:38:31 +02:00
jgrusewski
3f94802d8f perf: iqn_bias_grad_reduce — 2-phase warp-parallel (16ms×130 → <0.2ms×130)
Replace serial 3-thread kernel (each looping 524K iters) with 2-phase
block-parallel reduction: Phase 1 uses shared memory block reduce across
ceil(BQ/256) blocks per neuron; Phase 2 warp-reduces block partials.
Preallocated scratch buffer [ceil(BQ/256), max_out_dim] avoids runtime alloc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 12:33:35 +02:00
jgrusewski
276492be84 fix: epoch metrics from fused_ctx accumulators (every step, not guard-only)
Accumulate loss + grad_norm from pinned readback on EVERY step in
FusedTrainingCtx. Fixes smoke test failure where guard only ran
every 5th step (missing data when epoch has <5 steps).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 10:23:51 +02:00
jgrusewski
85dd26d481 fix: PER kernels read size from pinned device-mapped pointer at graph replay
Changed per_prefix_scan, per_sample, and is_weights_f32 kernels to accept
size as a const int* pointer (pinned device-mapped) instead of a baked int.
Graph replay now uses the CURRENT buffer size, not the capture-time value.

Host updates the pinned size on every insert_batch and clear.
per_sample also reads rng_step from pinned pointer (GPU-side increment).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 10:16:11 +02:00
jgrusewski
8a58f8410d fix: training guard host-side accumulator for epoch metrics
Guard's check_host uses host-side loss/grad_norm accumulation instead of
GPU kernel. Reads from pinned device-mapped readback (one-step lag).
Fixes 8 smoke test failures from stale zero values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 09:57:23 +02:00
jgrusewski
ac3e6e6488 infra: enhanced nsys profiling + cudaProfilerStart/Stop capture range
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 09:48:35 +02:00
jgrusewski
b0bcea6e93 perf: IQN trunk cuBLAS + host-side training guard + remove auto-replay-sizer
- Replace IQN trunk_forward_kernel with 2x cuBLAS GEMMs (tensor core path)
- Training guard: host-side pinned reads only, zero ungraphed GPU kernel
- Disable replay buffer VRAM auto-sizing (was inflating to 15.8M entries)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 09:31:29 +02:00
jgrusewski
68ef8eea3e fix: remove ungraphed vaccine batch sampling from training loop
Vaccine now uses prev_grad_buf inside maintenance_child (graphed).
The external sample() call was launching ~10 ungraphed PER kernels
on the training stream every 5th step — potential CUmodule corruption
and CPU-GPU serialization point.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 09:09:46 +02:00
jgrusewski
4d76d22d24 fix: Adam epsilon 1e-3 → 1e-8 (was bf16-safe, now f32 standard)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 01:43:18 +02:00
jgrusewski
2ae5ab3194 cleanup: remove all stale bf16/BF16 references from comments
Pure f32/TF32 pipeline — no bfloat16 anywhere.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 01:38:51 +02:00
jgrusewski
56c5458008 fix: null pointer crash in increment_step_counters + wire PER rng_step
Root cause: rng_step was passed as 0u64 (null) to increment_step_counters
kernel, causing atomicAdd on address 0 → CUDA_ERROR_ILLEGAL_ADDRESS →
cascading CUBLAS_STATUS_EXECUTION_FAILED on all subsequent operations.

Fixes:
- Replace all atomicAdd with plain writes (single-thread kernel)
- Add null guards for optional pointers (iqn_t, attn_t, rng_step)
- Allocate pinned device-mapped rng_step in GpuReplayBuffer
- Wire rng_step_dev_ptr from replay buffer to FusedTrainingCtx
- Remove stale atomicAdd comment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 01:36:37 +02:00
jgrusewski
1d3a533f99 fix: capture PER sampling inside parent graph — zero ungraphed PER kernels per step
Move sample_proportional (prefix scan, binary search, gather x6, IS weights)
from training_loop.rs into run_full_step, captured as per_sample child graph
node. Steps 1+ replay all 13 children via single cuGraphLaunch with no
ungraphed PER kernel dispatches.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 01:14:53 +02:00
jgrusewski
ead6d195fa feat: true single-graph — ONE cuGraphLaunch per step, zero ungraphed launches
Composes 11 child graphs into single parent via cuGraphAddChildGraphNode.
Counter increments (GPU-side), IQL modulate, PER priority update all
captured as child graph nodes. PhaseEvents removed — single parent launch
makes per-child event profiling irrelevant.

Step 0 runs ungraphed + captures all 11 children + composes parent.
Steps 1+ replay via single cuGraphLaunch on the training stream.
After graph launch: only pinned scalar reads (nanoseconds, no GPU ops).

Child graph order:
  counters -> spectral -> forward -> ddqn -> aux -> post_aux ->
  adam_grad -> adam -> maintenance -> iql_modulate -> per_priority

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 00:53:46 +02:00
jgrusewski
b2135d01e0 feat: direct-to-trainer gather — eliminates upload_batch_gpu + 6 DtoD copies per step
PER gather now writes directly to GpuDqnTrainer's padded buffers via
gather_f32_rows_padded, gather_f32_scalar, and gather_i32_scalar kernels
compiled into the replay buffer cubin. set_trainer_buffers() wires stable
device pointers at init; sample_proportional uses them when available,
falling back to intermediate buffers otherwise. memset_zeros calls in
the sampling hot path converted to raw cuMemsetD8Async.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 00:34:10 +02:00
jgrusewski
5b197151db feat: graph_utility_kernels.cu — gather_padded + increment_step_counters for true single-graph
Adds 4 GPU-native kernels replacing host-side operations on the training
hot path: gather_f32_rows_padded (row gather + 128-byte zero-pad),
gather_f32_scalar, gather_i32_scalar, and increment_step_counters (atomic
counter bumps + cosine-annealed tau — zero CPU sync per step).
Wired into build.rs (nvcc cubin compile) and gpu_dqn_trainer.rs
(GRAPH_UTILITY_CUBIN include_bytes!).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 00:22:45 +02:00
jgrusewski
26edbb6d8f fix: isolate shrink_perturb and scale_adam_momentum from graphed CUmodule
shrink_and_perturb() is called at epoch boundaries after child graphs are
captured. shrink_perturb_kernel lives in the same CUmodule as scale_f32,
saxpy_f32, spectral_norm, adam_update, and other graphed CUfunctions.
On Hopper (sm_90), launching any CUfunction from a graphed CUmodule
ungraphed corrupts the child graph kernel state → 3100ms replay.

scale_adam_momentum() is called at cosine LR warm restarts (also after
graph capture). scale_f32_kernel is captured in forward_child — same
CUmodule violation.

Fix: add shrink_perturb_ungraphed and scale_f32_ungraphed loaded from
the existing ungraphed_module (separate CUmodule instance of the same
DQN_UTILITY_CUBIN). Both ungraphed callers now use isolated handles.
CUmodule count: 5 → 5 (ungraphed_module already existed, reused).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 00:16:29 +02:00
jgrusewski
e8d382fc2f fix: isolate ungraphed pad_states + stochastic_depth_rng into separate CUmodule
ROOT CAUSE: pad_states_kernel and stochastic_depth_rng_kernel were loaded from
the SAME CUmodule as graph-captured kernels (adam_update, grad_norm, saxpy, etc.)
but launched UNGRAPHED every step. On Hopper, launching ANY CUfunction from a
CUmodule that also has graph-captured CUfunctions corrupts the graph's kernel
state — causing 3100ms replay instead of ~30ms.

Fix: load from a separate ungraphed_module. Zero CUmodule contamination between
graphed and ungraphed execution contexts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 00:02:23 +02:00
jgrusewski
19b6b5af47 fix: complete CUfunction isolation — zero cross-child sharing + raw memset + pinned memory
Three changes to eliminate the 3100ms replay regression on Hopper:

1. CUfunction isolation: saxpy_f32_kernel was shared across 3 child graphs
   (forward, aux, adam_grad). Added saxpy_f32_adam_grad and saxpy_f32_aux from
   separate CUmodules. Also isolated grad_norm_standalone for post_aux_child
   (was shared with forward_child's d_logits clipping path).

2. Raw cuMemsetD8Async: replaced all cudarc memset_zeros in graph-captured
   functions (submit_forward_ops_main, apply_cql_gradient,
   run_causal_intervention_unconditional) with raw cuMemsetD8Async which is
   properly captured by CUDA Graph. 6 call sites fixed.

3. DtoD memcpy audit: all memcpy_dtod_async calls verified — large buffer
   copies (grad snapshot 2.6MB, multi-horizon blend) are correct for DtoD;
   no scalar copies found that should use pinned memory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 23:24:52 +02:00
jgrusewski
79fce72afb fix: CUfunction isolation for post_aux child + raw memset in IQN/IQL/Attention
ROOT CAUSE of 3100ms adam_child: adam_update_kernel and grad_norm_finalize_kernel
were shared between post_aux_child and adam_update/forward children. On Hopper,
CUfunction sharing across child graphs corrupts kernel state.

Fix: load utility cubin from separate CUmodule for post_aux child, giving it
isolated adam_update_post_aux, grad_norm_finalize_post_aux, and scale_f32_post_aux
handles. Zero cross-child CUfunction sharing for these kernels.

Also: convert memset_zeros to raw cuMemsetD8Async in IQN (3 calls), IQL (1),
Attention (1) — eliminates cudarc device_ptr_mut() overhead during graph capture.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 23:11:15 +02:00
jgrusewski
c7185d2483 perf: convert all CudaSlice kernel args to raw u64 — eliminate cudarc overhead in graph
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 23:01:51 +02:00
jgrusewski
4b6d9d2e82 perf: fused RELU_BIAS epilogue for target, collector, and ensemble forward paths
Adds try-fused-fallback-to-separate pattern to forward_target_raw,
forward_online_f32, and forward_value_head — matching the existing
pattern in forward_online_raw. Eliminates ~10-14 separate bias+ReLU
kernel launches by fusing them into the preceding cuBLAS GEMM epilogue.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 22:40:46 +02:00
jgrusewski
76cfc0d185 perf: IQN quantiles 64→32 — saves 4.3GB VRAM, halves IQN GEMMs
Academic literature shows diminishing returns past 32 quantiles.
64 quantiles allocated 8.6GB for tiled intermediates (B×Q×hidden).
32 quantiles: 4.3GB (half), ~13 GEMMs halved in aux_child.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 22:28:23 +02:00
jgrusewski
1481ba2699 feat: unified single-graph — 7+ children, zero ungraphed kernel launches
Add post_aux_child (selectivity + denoise + risk_sgd + multi-horizon) and
maintenance_child (causal intervention + gradient vaccine) to the CUDA graph
pipeline. All kernel launches now run inside child graphs; only kernel-free
ops (pinned readbacks, host counters, IQL modulate_td_errors on its own
CUfunction) remain outside the graph.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 22:08:54 +02:00
jgrusewski
2334d1d6ce feat: unconditional submit_post_aux_ops + submit_maintenance_ops + prev_grad_buf
Add child-graph-capturable submit methods that run post-aux and
maintenance ops without conditional guards (graphs_captured, step %
interval). This enables moving all kernel launches into CUDA child
graphs, eliminating ungraphed ops that cause CUfunction corruption on
Hopper during 3100ms adam replays.

- prev_grad_buf: snapshot of grad_buf after Adam for next-step vaccine
- submit_post_aux_ops: selectivity, denoise, risk SGD, multi-horizon
- submit_maintenance_ops: causal intervention + gradient vaccine
- run_causal_intervention_unconditional: no step/graph guards
- apply_gradient_vaccine_from_prev: uses prev_grad_buf instead of
  separate forward+backward pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 21:52:51 +02:00
jgrusewski
075ed8ba4f fix: disable selectivity+denoise outside graph — CUfunction shared with graphed children
step_denoise_adam uses adam_update_kernel + grad_norm_finalize_kernel UNGRAPHED
after child graph replays. Same CUfunctions are captured in adam_child and
forward_child. On Hopper, launching a captured CUfunction ungraphed corrupts
graph kernel state → 3100ms adam_child replay on next step.

Fix: skip selectivity+denoise ungraphed training. These are auxiliary optimizers
with non-fatal error handling. To re-enable: load separate CUfunction instances
for ungraphed paths or move into child graphs.

Expected: adam drops from 3100ms to ~30ms. Epoch from ~690s to ~60s.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 21:00:05 +02:00
jgrusewski
e28add3768 fix: adam_child 3100ms — grad_norm_finalize_kernel shared between forward+adam children
ROOT CAUSE: grad_norm_finalize_kernel (CUfunction) was captured in BOTH
forward_child (d_logits clipping at lines 7572/7821) AND adam_child
(compute_grad_norm_for_adam via launch_grad_norm_finalize at line 6346).
Sharing a CUfunction between two captured graphs corrupts kernel state
on Hopper (sm_90), causing the second graph to replay at 3100ms instead
of <1ms. This is the SAME class of bug as the earlier grad_norm_standalone
fix — we fixed one shared CUfunction but missed the finalize kernel.

FIX: Load grad_norm_finalize from a SEPARATE CUmodule (new cubin load)
for the adam path. Each CUfunction instance is now captured in exactly
one child graph. Also split adam into adam_grad + adam_update children
for per-operation timing visibility.

Expected: adam drops from 3100ms to ~30ms. Total step ~350ms. Epoch ~62s.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 20:09:19 +02:00
jgrusewski
7b0280a187 debug: split adam into adam_grad + adam_update to isolate 3.1s bottleneck
adam_child = 3113ms even after CUfunction fix. Split into:
- adam_grad: mamba2_backward + step_mamba2_adam + pruning + grad_norm
- adam_update: Adam kernel + unflatten + ISV

If adam_grad is fast and adam_update is slow, the bottleneck is Adam/unflatten.
If adam_grad is slow, grad_norm_kernel has an issue.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 19:13:32 +02:00
jgrusewski
32151ba8bc fix: backward branches reuse forward workspace — saves 128MB (OOM fix)
The backward branch parallelism allocated 4 × 32MB = 128MB for per-branch
cuBLAS workspaces, pushing H100 VRAM over 80GB → OOM. Forward and backward
are in the same child graph (sequential) — workspaces never conflict.

Now backward receives forward's branch_workspace_ptrs as a parameter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 16:57:34 +02:00
jgrusewski
80d871ae1d feat: IQN prepare_buffers extraction for parallel stream dispatch
Separates buffer prep (decode_actions, DtoD copies) from training pipeline
so prep runs on main stream and execute_training_pipeline runs on iqn_stream.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 16:37:54 +02:00
jgrusewski
108a0993c7 perf: parallel backward branches using fork-join on branch_streams
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 16:35:27 +02:00
jgrusewski
9fb330da7e fix: adam_child 3120ms — CUfunction conflict between forward_child and adam_child
grad_norm_standalone was captured in BOTH forward_child (d_logits clipping)
and adam_child (gradient norm). Same CUfunction in two captured graphs
corrupts kernel state on Hopper — the GPU replayed a corrupted node for
3120ms per step (88% of total step time).

Fix: adam_child uses grad_norm_kernel (the regular handle, not captured
elsewhere) instead of grad_norm_standalone. Each CUfunction is now captured
in exactly one child graph.

Expected: adam_child drops from 3120ms to ~30ms. Total step from ~3550ms
to ~460ms. Epoch from ~700s to ~80s.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 16:34:12 +02:00
jgrusewski
16f5147356 feat: IQN/Attention accept workspace+stream override for multi-stream
Add optional override_stream and override_workspace parameters to
execute_training_pipeline (GpuIqnHead), and forward/backward/adam_step
(GpuAttention). All existing callers pass None, None — behaviour is
unchanged. The parallel path can now dispatch these ops to dedicated
CUDA streams with separate cuBLAS workspaces.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 16:20:31 +02:00
jgrusewski
dc9f25df63 feat: allocate aux streams, workspaces, events for Phase 2 multi-stream
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 16:15:38 +02:00