Commit Graph

1930 Commits

Author SHA1 Message Date
jgrusewski
36d02b8f3c feat: Mamba2 d_h_history backward — gradient flows to OFI embed MLP
2 new cuBLAS GEMMs: d_h_history = W_A^T @ d_gate + W_B^T @ d_x.
Extracts last 10 dims as d_ofi_embed_mamba2, accumulated across K=8
timesteps. This enables the OFI embed MLP to learn from temporal
patterns discovered by the Mamba2 SSM.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 01:03:31 +02:00
jgrusewski
227d5cb5fb feat: Mamba2 history enrichment SH2→SH2+10 — temporal scan learns OFI momentum
h_history widened from [B, K, SH2] to [B, K, SH2+10]. OFI embed (10-dim)
appended per timestep. W_A/W_B projections grow to [SH2+10, STATE_D].
W_C stays [SH2, STATE_D] — operates on scan output, not history input.

The SSM temporal scan now sees order flow momentum, book aggression, and
bar duration alongside trunk features. Enables learning sequential
patterns in microstructure dynamics.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 00:56:00 +02:00
jgrusewski
6575eea8da feat: OFI embed MLP forward (18→10 cuBLAS) — learned order flow compression
Extracts [raw_ofi(8); delta_ofi(8); book_aggression(1); log_duration(1)]
= 18-dim from state vector, compresses to 10-dim via cuBLAS SGEMM +
bias+ReLU. Xavier init. Runs in training forward path before Mamba2
and attention, making the embedding available for temporal enrichment.

190 trainable params (18×10 + 10 bias). ~0.05ms per step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 00:49:18 +02:00
jgrusewski
9b61e46a71 feat: Hold action — backtest kernel, epsilon_greedy, action selector, defaults
Completes the Hold action implementation across remaining files:
- backtest_env_kernel.cu: Hold skips trade execution, fixes Flat
  encoding from dir=1→dir=3, fixes actual_dir re-encoding
- epsilon_greedy_kernel.cu: 5-action exposure→4-action direction
- gpu_action_selector.rs: count bonuses [f32;5]→[f32;4]
- gpu_iqn_head.rs: default branch_0_size 3→4
- dqn.rs: default num_actions 7→4
- All test assertions and doc comments updated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 00:40:56 +02:00
jgrusewski
b800591c22 feat: 4th direction Hold action — keep position unchanged, zero cost
Action space 81→108: direction(4) × magnitude(3) × order(3) × urgency(3).
Short(0), Hold(1), Long(2), Flat(3). Hold keeps current position with
zero transaction cost, giving the model a "do nothing" option.

Changes: trade_physics.cuh (Hold returns current_position), env_step
(Hold skips trade execution), action.rs (ExposureLevel::Hold variant),
config (branch_0_size 3→4), all match arms updated across 11 files.

Counterfactual mirror: Short↔Long, Hold↔Hold, Flat↔Flat.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 00:29:24 +02:00
jgrusewski
115623dbc4 fix: replace 11 memcpy_dtod_async with graph-safe copy_f32 kernel
memcpy_dtod_async is NOT captured by CUDA Graph — silently skipped
during replay. This caused:
- Attention reading stale trunk data (h_s2→scratch copy skipped)
- Attention output never written back (scratch→h_s2 copy skipped)
- Backward gradients using stale logits (d_logits staging skipped)
- Vaccine comparison using stale reference (prev_grad snapshot skipped)

Attention has been effectively a NO-OP since CUDA Graph was introduced.

Fix: new copy_f32 kernel in graph_utility_kernels.cu (same pattern as
existing mamba2_copy_enriched). 11 call sites replaced across
gpu_dqn_trainer.rs and fused_training.rs. graph_safe_copy_f32() and
graph_safe_copy_f32_on() helpers for single-stream and multi-stream.

Non-graph-captured DtoD copies (checkpoint, init, eval) left as-is.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 00:28:41 +02:00
jgrusewski
55d70b7cc8 feat: dense micro-reward + DSR fix + counterfactual sign fix
Dense micro-reward: OFI momentum × price confirmation (MBP-10 mid-price
mark-to-market) × adaptive cost tolerance (capital_ratio × Sharpe_ema)
+ book aggression + retrospective hold quality bonus. Replaces flat
-0.0001 holding cost. Scale: micro_reward_scale=0.1.

DSR Sharpe EMA: was hardcoded price_change_dsr=0.0 — adaptive cost
tolerance was permanently floored at 0.1. Now uses actual per-bar
returns from ps[PREV_CLOSE_SLOT].

Counterfactual: cf_cycle==1 (magnitude) and cf_cycle==2 (order) now
undo do_flip before computing CF reward, then re-apply. Previously
2/3 of counterfactual experiences had wrong sign when do_flip=true.

Rank normalization threshold: 0.001 → 1e-5 for dense micro-rewards.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 00:03:51 +02:00
jgrusewski
c2c1802e58 fix: walk-forward purge gap 100→2000 bars (~1 trading day)
100 bars = ~1 hour of imbalance bars. ES futures regimes persist 4-8
hours. Val_Sharpe was artificially inflated by regime autocorrelation
bleeding through the too-short purge gap. 2000 bars = ~1 full trading
day, ensuring validation data is from a different regime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 23:58:46 +02:00
jgrusewski
de8a139494 fix: wire OFI pipeline + PORTFOLIO_STRIDE 30→38 + fix concat_ofi indices
3 pre-existing bugs fixed:
- ofi_gpu uploaded but never passed to env_step — now wired via
  full_batch_states param (OFI at state[66..74) accessible in kernel)
- concat_ofi_features read indices 42/45 (portfolio features) instead
  of 66/69 (actual OFI location in 96-dim state vector)
- PORTFOLIO_STRIDE expanded 30→38 for prev-OFI delta storage (ps[30..37])

Added PREV_CLOSE_SLOT=6 and PREV_MID_SLOT=22 for MFT mark-to-market.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 23:56:55 +02:00
jgrusewski
063fd27166 feat: target_dim 4→6 + spec v5 with pearls (bar duration, book CoM, retrospective hold)
target_dim expansion: adds raw_open (OHLCV) and mid_price_open
(MBP-10 midpoint at bar formation) to fxcache targets. FXCACHE_VERSION
2→3 for auto-rebuild. Legacy v2 files handled with close-price fallback.

Spec v5 adds 3 pearls:
- Bar duration encoding in Mamba2 (continuous-time SSM awareness)
- Order book center of mass from all 10 MBP-10 levels (aggression signal)
- Retrospective hold quality bonus (teaches exit timing)

Plus: Hold action (4th direction), DSR Sharpe EMA fix, counterfactual
magnitude/order sign fix, MFT mid-price mark-to-market.

OFI embed MLP now 18→10 (was 16→8). Mamba2 width SH2+10 (was SH2+8).
Attention width D+10 (was D+8).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 23:47:04 +02:00
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