Commit Graph

3126 Commits

Author SHA1 Message Date
jgrusewski
867ff1aa7c feat: per_kernels.cu — decoupled lookback scan + binary search sampling
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:27:20 +02:00
jgrusewski
cf4294f1f6 fix: atomicAdd delta propagation for PER update (skip full tree rebuild)
The full tree rebuild (25 levels × 33M nodes) took too long on H100.
Only 8192 of 33M leaves change per step — rebuilding all nodes is
4000× wasted work.

Restore atomicAdd delta propagation in seg_tree_update_leaves:
O(B × log N) = 8192 × 25 = 200K atomics vs O(N) = 33M reads.
atomicExch for leaf writes (duplicate-index safe).
Keep seg_tree_rebuild_level for insert_batch (new slots need full rebuild).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:50:27 +02:00
jgrusewski
365a7a17b4 fix: pre-initialize cast kernels at buffer construction
cuModuleLoadData hangs on H100 CUDA 13 when called after CUDA graph
capture — the context state appears incompatible with module loading.
Pre-initialize the OnceLock during GpuReplayBuffer::new() when the
context is clean and no graphs have been captured.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:35:49 +02:00
jgrusewski
049d07a441 fix: use self.stream for cast kernel module load, ext_stream for launches
get_cast_kernels uses OnceLock + cuModuleLoadData which requires the
CUDA context to be bound to the current thread. When ext_stream (the
trainer's stream) was passed, its context wasn't bound — causing a hang
in cuModuleLoadData on H100.

Fix: always use self.stream (replay buffer's stream, with bound context)
for kernel compilation. Only use ext_stream for the actual kernel launches.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:26:30 +02:00
jgrusewski
5770ac5f62 diag: PER update eprintln + stream sync to identify H100 hang point
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:15:58 +02:00
jgrusewski
956adbe829 fix: two-phase seg_tree — parallel leaf writes + level-by-level rebuild
The atomicAdd delta propagation caused severe L2 cache contention on
H100: 8192 threads all atomicAdd to tree[1] (root) serialized through
a single cacheline, taking 10+ seconds per PER update.

Split into two phases:
1. seg_tree_update_leaves: write all leaves in parallel (zero contention)
2. seg_tree_rebuild_level: rebuild one tree level per kernel launch,
   bottom-up. 20 levels × ~2µs = ~40µs total. Each level is fully
   parallel — nodes at the same level are independent.

Also applies to seg_tree_insert (insert_batch path).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 12:57:50 +02:00
jgrusewski
907b5f3355 fix: cross-stream race in PER update + pre-allocate scratch buffers
The replay buffer used its own CUDA stream for seg_tree_update, but
the td_errors buffer was computed on the trainer's stream. Without
synchronization, the replay stream could read incomplete data — causing
a hang on H100 where streams execute truly concurrently.

Fix: update_priorities_gpu now accepts an optional ext_stream parameter.
The fused training path passes the trainer's main stream, ensuring all
GPU work (td_errors computation + seg_tree_update) runs on the same
stream with implicit ordering.

Also pre-allocate update_td_f32, update_batch_max, update_max_merge
scratch buffers at construction — eliminates 3 cuMemAlloc calls per
training step that could trigger implicit device synchronization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 12:36:15 +02:00
jgrusewski
22f30fe37f fix: atomicExch for seg_tree leaf writes prevents duplicate-index race
PER samples with replacement, so a batch can contain duplicate indices.
With non-atomic leaf writes, two threads processing the same index both
read the same old_pa, but only one write survives — internal nodes get
double the delta while the leaf changed once, permanently corrupting
the tree sum.

atomicExch returns the exact replaced value so each thread propagates
the correct delta regardless of concurrent duplicate writes.

Also fix readback_pinned layout doc: slot 9 is used for diversity loss.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 12:18:40 +02:00
jgrusewski
57c6fdcdd3 perf: CUDA graph for spectral norm — zero launch overhead
Capture the batched spectral norm kernel (10 matrices) + bf16→f32 sync
into a CUDA graph on step 1, replayed on steps 2+ with zero launch
overhead. Spectral norm uses fixed device pointers (params_bf16,
spec_u/v descriptors) that never reallocate, making it a perfect graph
capture candidate. Graph is invalidated on fold reset.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 12:06:03 +02:00
jgrusewski
a0d035f837 perf: eliminate unnecessary per-step memset_zeros
Remove 3 memset_zeros calls on buffers that are fully overwritten by
their consumer kernels (no atomicAdd accumulation):

- q_stats_buf (2 sites): q_stats_reduce is a single-thread kernel that
  directly assigns all 5 output floats — no accumulation pattern
- causal_mean_scratch (1 site): causal_mean_reduce writes out[0] via
  direct assignment in thread 0 — no atomicAdd

All remaining memsets are documented as REQUIRED with the specific
accumulation pattern (atomicAdd / beta=1.0 GEMM) that necessitates them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:58:10 +02:00
jgrusewski
d39f2b7558 perf: fuse single-block finalizer kernels
Audit all grid=(1,1,1) kernels called per training step:

- grad_norm_finalize: cannot fuse into grad_norm_kernel because the
  reduction uses multi-block atomicAdd — no block knows it holds the
  final sum without cooperative launch. 1-thread finalize is negligible.
- stochastic_depth_rng: standalone RNG writing 3 floats, not a finalizer
  after a reduction — nothing to fuse into.
- causal_reduce, causal_mean_reduce, q_stats: only called at
  intervention-interval or epoch-boundary, not per training step.

Document rationale inline so future reviewers don't re-investigate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:46:42 +02:00
jgrusewski
10857e2c10 perf: batched spectral norm — 10 launches → 1, delete old kernel
Replace 10 individual `spectral_norm_kernel` launches (each grid=(1,1,1))
with a single `spectral_norm_batched` launch (grid=(10,1,1)) that processes
all weight matrices in parallel across 10 blocks.

- Build descriptor buffer at construction (10 × 6 u64 entries with W/u/v
  pointers, out_dim, in_dim per matrix) — pointers are stable
- Delete `spectral_norm_kernel` from dqn_utility_kernels.cu (replaced by
  `spectral_norm_batched` which was already written)
- Remove spec_norm! macro and per-matrix launch loop

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:46:16 +02:00
jgrusewski
4d78f90cc0 perf: TF32 tensor cores for forward GEMMs
Switch all 3 forward-pass cublasGemmEx calls from CUBLAS_COMPUTE_32F to
CUBLAS_COMPUTE_32F_FAST_TF32, enabling H100 TF32 tensor cores for 2-3x
throughput. Backward pass remains at full F32 precision for gradient stability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:39:10 +02:00
jgrusewski
c30713c5a6 perf: weight views into params_buf — eliminate 25MB/step D2D copies
Refactor spectral normalization to operate directly on params_bf16 via
raw u64 offset pointers (computed by bf16_weight_ptrs_from_base) instead
of separate DuelingWeightSet/BranchingWeightSet CudaSlice allocations.

This eliminates 10 memcpy_dtod_async calls per training step (~25MB/step,
25GB/epoch at 1000 steps) that previously synced spectrally-normalized
weights from per-layer slices back to the flat params_bf16 buffer.

Key changes:
- apply_spectral_norm now takes &DuelingWeightSet (immutable) instead of
  &mut, only needed for first-call flatten into params_bf16
- Spectral norm kernels receive raw u64 pointers at GOFF_* offsets into
  params_bf16, writing in-place — no sync copies needed
- Spectral norm u/v vectors remain separate allocations (unchanged)
- unflatten_online_weights promoted to pub(crate) for test access
- Tests updated to call unflatten after spectral norm for readback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:31:01 +02:00
jgrusewski
d4269252c6 docs: H100 maximum performance spec and implementation plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:15:24 +02:00
jgrusewski
ae65740a3d fix: race-free segment tree propagation via atomicAdd deltas
seg_tree_update and seg_tree_insert used non-atomic
tree[node] = tree[2*node] + tree[2*node+1] with 8192 threads racing
on shared internal nodes. On H100 (132 SMs), all threads execute
simultaneously causing data races and hangs.

Replace with atomicAdd delta propagation: each thread computes
delta = new_leaf - old_leaf, then atomicAdd to every ancestor.
Commutative + associative = race-free. O(log n) per thread,
hardware-accelerated on SM 9.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:15:17 +02:00
jgrusewski
ef62ee14a0 perf: eliminate last sync points — non-blocking readback + async diversity loss
Remove all event.synchronize() and cuStreamSynchronize calls from the
training pipeline's readback paths. The readback scalars (loss, grad_norm,
Q-stats, diversity loss) are monitoring-only — the training guard reads
GPU buffers directly. Pinned-buffer async DtoH copies (12-20 bytes)
complete in <1 µs while thousands of GPU kernel launches elapse between
steps, so synchronization is unnecessary.

Changes:
- replay_adam_and_readback: remove per-step event.synchronize() fallback
- flush_readback: non-blocking pinned read (epoch end)
- flush_q_stats_readback: non-blocking pinned read (epoch end)
- readback_diversity_loss: async DtoH to pinned offset 9 (double-buffered)
- Remove readback_event/q_stats_event fields + per-step cuEventCreate

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:11:08 +02:00
jgrusewski
f804e2b04b perf: GPU-native causal sensitivity mean, zero sync in hot loop
Replace synchronous stream.synchronize() + memcpy_dtoh + CPU sum in
run_causal_intervention with a GPU reduction kernel (causal_mean_reduce)
and async DtoH to the pinned readback buffer at slot 8. The result is
double-buffered: the caller reads the previous iteration's value while
the current one transfers asynchronously.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 10:41:21 +02:00
jgrusewski
0c4a9a70bc perf: migrate Q-stats readback to consolidated pinned buffer
Extend readback_pinned from 3 to 16 floats and write Q-stats DtoH
into slots [3..8] instead of the non-pinned q_stats_pending field.
On H100 CUDA 13 driver, non-pinned host memory causes cuMemcpyDtoHAsync
to degrade to a synchronous copy, creating a latent hang risk.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 10:31:14 +02:00
jgrusewski
a40a374efa cleanup: remove dead GpuTrainResult, cast helpers, stale annotations
GpuTrainResult was replaced by FusedStepResult (CPU-only f32 scalars).
F32ToU32Caster, get_cast_kernels_f32_to_u32, and f32_slice_to_gpu_tensor_gpu
were only used by the old pre-fused training path. Also removes
#[allow(dead_code)] from gather_f32 which is actively used.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 10:23:39 +02:00
jgrusewski
f419d941c2 fix: route PER updates through seg_tree_update kernel
Replace the orphaned per_update_kernel call site in fused_training.rs
with agent.update_priorities_from_td() which routes through
GpuReplayBuffer::update_priorities_gpu_raw -> seg_tree_update.
This kernel correctly writes priorities AND propagates the segment tree
from leaf to root, fixing stale PER sampling.

Add update_priorities_from_td() on ReplayBufferType and DqnAgentConfig.
Remove priorities_f32_ptr() which exposed raw pointers for the deleted
kernel. Remove batch_max epoch-boundary flush since seg_tree_update
propagates max_priority internally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 10:13:18 +02:00
jgrusewski
07bcf4834d refactor: delete orphaned per_update_kernel and batch_max_buf
The per_update_priorities_kernel scatter-wrote to the priorities[] buffer
but never updated the PER segment tree, leaving sampling priorities stale.
Remove the orphaned CUDA kernel, its cubin include, struct field,
compilation, and the update_priorities_cuda_raw / batch_max_readback_and_reset
methods from GpuDqnTrainer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 10:13:02 +02:00
jgrusewski
05a9c597ee fix: eliminate HtoD sync-stall hang in fused training result
GpuTrainResult::from_fused_scalars() allocated GPU memory and performed
two synchronous cuMemcpyHtoD copies after every training step. On H100
CUDA 13 driver, synchronous HtoD blocks until ALL pending async GPU work
completes — same class of bug as the DtoH pinned memory fix.

The uploaded scalars were never used: the training guard already reads
loss/grad_norm directly from GPU-resident buffers via loss_gpu_buf() /
grad_norm_gpu_buf(). Replace GpuTrainResult with lightweight
FusedStepResult (CPU-only f32 scalars, zero GPU ops).

Also adds H100_HANG4 diagnostic eprintln between PER update,
bookkeeping, varmap sync, and graph_aux capture to isolate if any
secondary hang exists.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 01:41:43 +02:00
jgrusewski
d98ec8d343 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>
2026-04-06 01:16:12 +02:00
jgrusewski
f1009517bd fix: sync DtoH readback — non-pinned cuMemcpyDtoHAsync blocks on H100
cuMemcpyDtoHAsync_v2 with non-pinned host pointer degrades to
synchronous copy on CUDA 13 / H100 driver 580+, blocking until
ALL pending GPU work completes. With graph_forward + aux_ops + adam
queued, this caused a multi-minute hang.

Fix: explicit cuStreamSynchronize + synchronous cuMemcpyDtoH.
2026-04-06 01:09:28 +02:00
jgrusewski
f3c110dfbd diag: all adam_readback logs via eprintln 2026-04-06 01:00:51 +02:00
jgrusewski
1ba5ce93e7 diag: all H100_STEP logs via eprintln (unbuffered) 2026-04-06 00:52:19 +02:00
jgrusewski
735194c25d diag: guard + loop completion eprintln 2026-04-06 00:42:57 +02:00
jgrusewski
06172a8919 diag: eprintln per-step in training loop 2026-04-06 00:34:17 +02:00
jgrusewski
dde8adbd9c diag: per-line logging inside replay_adam_and_readback 2026-04-06 00:16:52 +02:00
jgrusewski
e2da8255e1 diag: H100_STEP tracing in run_full_step phases 2026-04-06 00:00:41 +02:00
jgrusewski
cedff67331 diag: replay_forward + PER insert progress logging for H100 hang 2026-04-05 23:47:02 +02:00
jgrusewski
c90ccc100e chore: gitignore hyperopt_results + remove from tracking 2026-04-05 23:33:11 +02:00
jgrusewski
ce665b2e7a diag: experience collection progress logging at 0/50/100%
Stream sync + log at timestep 0, 50%, and 100% to identify which
phase of experience collection hangs on H100 with 4096 episodes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 23:33:01 +02:00
jgrusewski
e08d304aa8 chore: remove dead code — raw_f32_ptr, ManuallyDrop, assert_finite_f32, load_smoke_data
Zero dead functions, zero unused imports, zero CPU fallback paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 23:15:38 +02:00
jgrusewski
5f16a86a4a chore: remove dead build_next_states_dtod + dtod_clone_bf16
Both were unused (bf16 path replaced by f32 path in #30).
build_next_states_dtod had the same CPU loop anti-pattern (795K memcpy
calls at 4096 episodes) that caused the H100 hang.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 23:10:33 +02:00
jgrusewski
e00203b9a0 fix: GPU kernel replaces 795K CPU memcpy calls in build_next_states
Root cause of H100 training hang: build_next_states_f32() used a CPU
for-loop launching individual memcpy_dtod per episode per fill timestep.
At gpu_n_episodes=4096 × 2 counterfactual × 97 fill steps = 795K CUDA
API calls that saturated the driver command queue.

Replaced with single build_next_states_kernel launch — all episodes
processed in parallel, zero CPU loops. One kernel, done.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 23:07:42 +02:00
jgrusewski
5ea1db7e37 fix: suppress unsafe_code warnings on GPU regime kernel launches
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 22:39:49 +02:00
jgrusewski
77bc8bda95 fix: GPU regime stratification active in walk-forward training
Moved init_from_fxcache BEFORE fold generation so GPU features buffer
exists when generate_folds_stratified_gpu is called. Features uploaded
as temporary f32 for regime classification (freed after fold gen).

Removes TODO — GPU prefix sum path is now the default when CUDA available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 22:31:49 +02:00
jgrusewski
69aca5ad75 feat: GPU-accelerated regime-stratified fold generation
generate_folds_stratified_gpu uses GPU prefix sums for O(1) range
queries during boundary adjustment. 1.1M bars: ~3ms vs ~1000ms CPU.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 22:19:27 +02:00
jgrusewski
f5021f38c3 feat: GPU regime classification + prefix sum kernels
regime_classify_kernel: parallel classification of 1.1M bars
regime_prefix_sum_kernel: cumulative counts for O(1) range queries
classify_regimes_gpu(): Rust wrapper that runs both and returns host prefix sums

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 22:10:20 +02:00
jgrusewski
c1e1cd47f7 fix: dual-stream CUDA graph capture eliminates H100 hang
Split graph_forward into graph_forward_main (Pass 1+2+loss+backward
on main stream) and graph_forward_ddqn (Pass 3 on double_dqn_stream).

Event synchronization happens in ungraphed replay_forward() code,
not inside captured functions. Each graph uses its own CublasForward
handle — no set_stream() during capture.

Fixes: H100 training hang with gpu_n_episodes=4096.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 22:04:29 +02:00
jgrusewski
f59c615687 refactor: split forward ops into per-stream functions
submit_forward_ops_main: Pass 1 + 2 + loss + grad + backward (main stream)
submit_forward_ops_ddqn: Pass 3 Double DQN (double_dqn_stream)

No set_stream() or event ops inside either function.
Graph capture will record each on its own stream.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 21:58:31 +02:00
jgrusewski
d5e323caf9 feat: separate CublasForward for double_dqn_stream
Each CUDA graph needs its own cuBLAS handle bound to its stream.
Eliminates set_stream() calls during graph capture that caused
the H100 training hang.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 21:54:11 +02:00
jgrusewski
f2b777a6ff fix: train_walk_forward uses regime-stratified fold generation
Was calling generate_folds() (non-stratified) instead of
generate_folds_stratified(). Walk-forward folds are now balanced
across trending/ranging/volatile regimes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 21:51:13 +02:00
jgrusewski
4956de7c74 docs: implementation plan for GPU regime + H100 hang fix
7 tasks: fold bug fix, dual cuBLAS, split forward ops, dual-graph
capture, GPU regime kernels, GPU stratification, H100 validation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 21:45:51 +02:00
jgrusewski
42c8d85bba docs: spec for GPU regime + H100 training hang fix
Three issues in one spec:
1. H100 hang: cuBLAS set_stream() inside CUDA graph capture → deadlock
2. Bug: train_walk_forward calls non-stratified generate_folds()
3. GPU regime: classification kernel + prefix sum for O(1) range queries

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 21:25:05 +02:00
jgrusewski
6e4778fad9 fix: update test expectations for production batch_size + OFI state_dim
- Profile tests: batch_size 1024→8192, mbp10_data_dir /mnt→/data
- Trainer tests: state_dim adapts to OFI config (45 without, 53 with)
- Add batch_size>0 validation in trainer constructor
- 891 unit tests pass, 0 failures

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 19:14:03 +02:00
jgrusewski
82983f356f config: explicit batch_size=8192 + max_bars=0 in production TOML
All training parameters now explicit in the TOML — no implicit defaults.
- batch_size=8192: maximizes H100 tensor core utilization
- max_bars=0: unlimited (train on full dataset)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 18:38:49 +02:00
jgrusewski
45e9855817 fix: production TOML data paths match Argo PVC mounts
- mbp10_data_dir: /mnt/training-data → /data/futures-baseline-mbp10
- Add missing trades_data_dir: /data/futures-baseline-trades
- Paths now match Argo workflow PVC mount at /data (CLI overrides still work)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 18:34:21 +02:00