Commit Graph

4015 Commits

Author SHA1 Message Date
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
jgrusewski
6e03a5346e fix: hyperopt preload respects max_bars + deduplicate profile loading
- Preloaded fxcache truncated to max_bars from training profile
  (1.1M → 5000 bars for smoketest, ~40x faster per trial)
- Consolidated 3 redundant DqnTrainingProfile::load() calls into 2
  (VRAM gate reuses base_hp in train_with_params)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 18:19:15 +02:00
jgrusewski
1abdbb6388 test: hyperopt backtest metrics output in smoketest
Sharpe=-1.27, 269 trades, 22% win rate — expected for 3 random epochs.
Full PSO → train → GPU backtest → Sharpe objective pipeline validated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 18:13:17 +02:00
jgrusewski
95db8ef13d fix: VRAM gate uses training profile + backtest metrics working
- Load TOML profile BEFORE VRAM budget gate so hidden_dim/batch_size
  reflect the actual profile (smoketest=64 vs production=256)
- Fixes backtest_metrics=None: VRAM gate was pruning all trials using
  conservative() defaults (hidden=256, batch=1024) on RTX 3050
- Add test_hyperopt_trial_produces_backtest_metrics regression test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 18:04:51 +02:00
jgrusewski
77e08e6624 feat: configurable training profile for hyperopt + hyperopt smoketest
- Add training_profile field to DQNTrainer (default: "dqn-production")
- Smoketest uses "dqn-smoketest" profile for RTX 3050 compatible sizes
- DQNTrainer fields (data_source, mbp10_data_dir, trades_data_dir) are
  source of truth — TOML profile provides training params only
- Fix relative path resolution in preload_data for mbp10/trades dirs
- Add preload_data() call in hyperopt test (fxcache shared via Arc)
- Buffer size floor of 1024 in hyperopt param application

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:27:40 +02:00
jgrusewski
5f5ebd32bc fix: fxcache key match with MBP-10 + trades data paths
- Add mbp10_data_dir and trades_data_dir to smoketest TOML profile
- Fix data_loading path resolution: resolve relative paths against
  workspace root via CARGO_MANIFEST_DIR (CWD is crates/ml during tests)
- Add buffer_size floor of 1024 in hyperopt adapter (GPU PER minimum)
- Regenerated fxcache with full OFI features (MBP-10 + trades)

All tests now consistently hit fxcache with OFI=true.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:35:26 +02:00
jgrusewski
b7ea7c0760 chore: remove runtime NaN tracing stream syncs
Removes run_nan_tracing (3 stream syncs per step on first 20 steps)
and unused grad_buf_ptr/params_buf_ptr accessors. Root causes are
fixed — runtime detection no longer needed. isfinite guards in CUDA
kernels remain as silent defense-in-depth.

17/17 smoke tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 15:33:36 +02:00
jgrusewski
a5eeeaa477 fix: smoketest max_bars 60K→5K + counterfactual test threshold
- Reduce max_bars from 60000 to 5000 (smoketests ran 16min, now ~1min)
- Fix test_counterfactual_experiences_in_buffer: compute threshold from
  actual config instead of hardcoded 4000, enlarge buffer to avoid wrap

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:51:21 +02:00
jgrusewski
24bae7d101 fix: test_data_dir returns base dir for fxcache key match
test_data_dir() was returning the symbol-specific path
(test_data/futures-baseline/ES.FUT) causing fxcache key mismatch —
the cache was created with the base dir. Now returns the parent
directory so trainer.train(data_dir, "ES.FUT", ...) can discover
the fxcache via matching cache key.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 13:49:01 +02:00
jgrusewski
c9191e7c58 chore: remove unused NaN diagnostic accessor methods
Removes cql_grad_scratch_ptr, iqn_trunk_m_ptr, save_h_s1_ptr,
stream_cu — added during NaN investigation, no longer called.

Retains run_nan_tracing (lightweight GPU-side checks on first 20 steps)
and all isfinite guards as production safety nets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 13:40:12 +02:00
jgrusewski
7b6e676952 fix: HER f32/bf16 type mismatch + stride bug — final NaN root cause
Root cause #8: HER in-place relabel kernel read f32 PER states as
`const __nv_bfloat16*`, reinterpreting raw f32 bytes as garbage bf16
values (up to ~1e38). These were written into padded states_buf at HER
rows, corrupting the data read by the IQN trunk backward GEMM.

Root cause #9: HER dst_stride used unpadded state_dim (48) instead of
pad128(state_dim) (128), scattering goal columns into padding region.

Both fixes verified: 200/200 runs with zero NaN (was 4-15% before).
Removed diagnostic tracing code from investigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 13:31:12 +02:00
jgrusewski
fc3f6f9df7 fix: 7 NaN root causes in GPU training pipeline + regression test
Root causes found and fixed:
- f32/bf16 type mismatch in pad_states_kernel (PER stores f32, kernel read bf16)
- States buffer stride mismatch in backward_full (lda=48 on pad128=128 buffer)
- States buffer stride mismatch in apply_iqn_trunk_gradient (same bug)
- IQN trunk forward stride mismatch in custom matvec kernel
- IQN grad_norm sqrt-per-block bug (sqrtf of partials → wrong norm + NaN)
- Uninitialized state vector slots in experience_state_gather (stale GPU memory)
- CUDA graph warmup launches producing extreme gradients from Xavier weights

Defense-in-depth guards:
- f32_to_bf16_kernel: NaN→0, clamp ±500
- Adam isfinite guards in DQN, IQN, attention, curiosity kernels
- IQN backward isfinite on d_h_s2 atomicAdd
- clipped_saxpy isfinite on auxiliary gradient input

Adds test_no_nan_after_graph_capture regression test (2s, fxcache-based).

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