Commit Graph

3094 Commits

Author SHA1 Message Date
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
jgrusewski
57844e1cc5 diag: GPU-side NaN detection kernels + capture warmup investigation
Added dqn_nan_check_f32/bf16 kernels for zero-overhead GPU-side NaN
detection between graph stages. Key findings:

- NaN appears in f32 params at step 0 (before first training forward)
- States (input data) are always clean
- The graph capture warmup launches corrupt params via:
  C51 forward → NaN gradients → Adam warmup → NaN weights
- Removed Adam warmup launch, but NaN persists — the forward
  graph warmup itself may corrupt buffers through bf16 overflow
  in cuBLAS backward path
- Step 14/22 NaN in later runs is from accumulated corruption

Next: investigate whether cuBLAS backward with random Xavier weights
produces extreme gradients that overflow bf16 d_logits staging buffers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 00:39:34 +02:00
jgrusewski
b316a25262 fix: IQN native f32 arithmetic + gradient budget alpha-blend
Two fixes for training stability:

1. IQN kernel rewrite: all-bf16 → native f32 arithmetic.
   BF16 exp() overflows at logit > 11.1, and bf16 accumulation
   of 64 multiply-adds loses catastrophic precision. The IQN
   head does NOT use cuBLAS tensor cores, so bf16 provided zero
   performance benefit — only precision loss and NaN risk.

   Pattern matches C51/MSE kernels: bf16 I/O at DQN trunk
   boundary, f32 for all internal computation.

   - Rust: all CudaSlice<half::bf16> → CudaSlice<f32>
   - CUDA: all __nv_bfloat16 registers → float
   - Adam, grad_norm, EMA: native f32
   - IQN→PER boundary: f32→bf16 cast via cast_f32_to_td_errors()

2. Gradient budget alpha-blend: during MSE warmup (c51_alpha=0),
   the budget was clipping MSE gradients to 60% of max_grad_norm
   as if they were C51. Now blends: full budget at alpha=0,
   C51 fraction at alpha=1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 23:47:35 +02:00
jgrusewski
95e96aa976 docs: spec for Q-value explosion / NaN investigation
Documented all findings from the training step performance session:
- Shared memory race was real but not the NaN source (racecheck: 0 hazards)
- NaN is non-deterministic numerical instability, NOT a concurrency bug
- Suspected math bug in gradient budget allocation during MSE warmup
- Investigation plan: GPU NaN detection kernels, gradient audit, git bisect

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 23:00:16 +02:00
jgrusewski
657d7b49ec perf: dual graph forward (MSE-only) + NaN-safe scale kernel
Two optimizations:

1. Dual CUDA graph: graph_forward_mse skips C51 loss/grad/SAXPY blend
   during MSE warmup (c51_alpha=0). Eliminates 7 kernel invocations
   from the graph replay. replay_forward() routes by alpha automatically.

2. NaN-safe dqn_scale_f32_kernel: writes 0.0 directly when alpha==0
   instead of multiplying (IEEE 754: 0*NaN=NaN). Defense-in-depth for
   the C51 gradient buffer which can contain NaN from random logits.

Fold 3 epoch: 5.08s → 4.92s (under 5s target).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 20:58:54 +02:00
jgrusewski
137480c887 fix: shared memory race in block_reduce (shmem_reduce read/write hazard)
compute-sanitizer --tool racecheck found 4000+ hazards per kernel invocation
in both c51_loss_batched and mse_loss_batched. The race is between consecutive
block_reduce calls sharing the same shmem_reduce buffer:

1. reduce_max returns shmem_reduce[0] → next reduce_sum writes shmem_reduce[warp]
2. reduce_sum returns shmem_reduce[0] → next reduce_max writes shmem_reduce[warp]

Fixed by adding __syncthreads() after reading the return value in both
block_reduce_max_f and block_reduce_sum_f (read-then-sync pattern). Also
added explicit barriers between spectral decoupling reduces and subsequent
softmax/log_softmax calls.

This race caused non-deterministic NaN in loss values. The effect was masked
by GPU printf statements (which acted as implicit memory barriers) and by
CUDA Graph replay timing differences.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 20:44:42 +02:00
jgrusewski
ca7201a1ec docs: spec for training step performance + Q-value explosion investigation
Two remaining issues: (1) ~400ms/step instead of ~1-7ms on RTX 3050,
(2) Q-value explosion when C51 kicks in at epoch 2.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 17:30:18 +02:00
jgrusewski
287b37c937 fix: remove cuStreamSynchronize from training hot path (ensemble diversity readback)
The per-step GPU sync for 4-byte diversity_loss monitoring readback
was blocking the CPU on every training step, destroying the async
pipeline. Each step took ~350ms instead of ~1ms.

Fix: accumulate diversity_loss on GPU, defer readback to epoch boundary
via readback_diversity_loss(). The sync now happens ONCE per epoch
instead of per step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 17:19:12 +02:00
jgrusewski
2df09f5fbf feat: max_bars config for fast smoketests (truncates dataset after loading)
Smoketest sets max_bars=60000 (~1 quarter). Applied in load_training_data()
on both fxcache and DBN paths. 0 = unlimited (production).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 16:42:05 +02:00
jgrusewski
6565221b9f fix: remove hardcoded MIN_GPU_CAPACITY=1024, use batch_size as floor
The GPU PER segment tree works with any power-of-2 capacity. The
arbitrary 1024 floor prevented small buffer sizes needed for fast
smoketests. Now the minimum is batch_size (can't sample more than
buffer holds).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 15:32:59 +02:00
jgrusewski
be8a5c3331 fix: smoketest buffer_size=1024 (MIN_GPU_CAPACITY floor)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 15:30:45 +02:00
jgrusewski
4aceea1699 fix: profile is single source of truth, q_clip wired as guard, fewer walk-forward folds
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 15:25:02 +02:00
jgrusewski
3b6c9b5e4d fix: reset Adam optimizer state between walk-forward folds (prevents Q-value explosion)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 13:07:26 +02:00
jgrusewski
7354351a89 fix: separate MSE/C51 loss accumulators (prevents NaN during warmup)
Both MSE and C51 loss kernels were writing to the same total_loss_buf
via atomicAdd. During warmup (alpha=0, pure MSE), C51 still runs and
produces NaN from random logits, contaminating the shared buffer even
though gradients are correctly blended. Now MSE writes to mse_loss_buf,
C51 writes to total_loss_buf, and readback blends with IEEE 754 NaN
guards (0.0 * NaN = NaN avoided via epsilon threshold).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:57:43 +02:00
jgrusewski
36d4c39f2d docs: spec + plan for C51 loss accumulator fix (separate MSE/C51 buffers)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:49:13 +02:00
jgrusewski
08ff59bdec perf: tune smoketest profile for fast validation (buffer=256, episodes=4)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 11:28:47 +02:00
jgrusewski
2e06d86e43 fix: replace aliased SAXPY with in-place scale kernel (fixes __restrict__ NaN bug)
The C51+MSE gradient blending passed the same pointer as both y (output)
and x (input) to dqn_saxpy_f32_kernel. That kernel has __restrict__ on
both parameters, promising the compiler they don't alias. When they DO
alias, NVCC's no-alias optimizations produce undefined behavior (NaN
from register corruption).

Added dqn_scale_f32_kernel (y[i] *= alpha) — single-pointer variant
with no __restrict__, no aliasing possible. The two self-aliased SAXPY
calls are replaced with scale+SAXPY pairs using distinct pointers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:18:29 +02:00
jgrusewski
938d75fdac docs: implementation plan for CUDA kernel safety fix
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:12:47 +02:00
jgrusewski
37d78e02c8 docs: spec for CUDA kernel safety fix (__restrict__ aliasing NaN bug)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:07:47 +02:00
jgrusewski
05ba6296cf fix: smoketest epochs=3 (was 10, too long for validation)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 10:24:33 +02:00
jgrusewski
9c081ac7ce refactor: CLI binaries use shared fxcache::discover_and_load()
Replace 40-line inline fxcache discovery block in train_baseline_rl.rs
with a single call to ml::fxcache::discover_and_load(). precompute_features.rs
already uses correct symbol/data_source args — no changes needed there.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 10:05:26 +02:00
jgrusewski
d2d80b7392 refactor: all train() and load_training_data() callers pass explicit symbol
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 10:02:37 +02:00