Commit Graph

3046 Commits

Author SHA1 Message Date
jgrusewski
43998a330a feat: two-phase hyperopt + backtest evaluator VRAM leak fix
Two-phase hyperopt splits 31D PSO search into sequential phases:
- Phase 1 (--phase fast, default): fix architecture to small network
  (hidden_dim=128, num_atoms=11), search learning dynamics (~15D).
- Phase 2 (--phase full): fix dynamics from Phase 1 JSON, search
  architecture (~5D). Halves dimensionality per phase → better convergence.
- Phase 1 output includes best_continuous_vector for Phase 2 consumption.

GpuBacktestEvaluator Drop impl: sync forked stream, destroy CUDA graph
and cuBLAS handles before CudaSlice buffers drop. Fixes 261MB/trial
VRAM leak on H100 hyperopt.

ml-core clippy fixes: hex literal, remove dead check_err drain,
unnecessary safety comment, unused OnceLock import.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 10:32:37 +01:00
jgrusewski
41eaa6522c perf: cap hyperopt hidden_dim=256, num_atoms=51 — 4x faster trials
Production defaults are sufficient for hyperopt exploration. Larger networks
can be tested in a separate phase with the best hyperparams found.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 10:02:01 +01:00
jgrusewski
e97c30b50a perf: cap hyperopt hidden_dim=256, num_atoms=51 — 4x faster trials
Production defaults are sufficient for hyperopt exploration. Larger networks
can be tested in a separate phase with the best hyperparams found.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 10:00:26 +01:00
jgrusewski
11855e958a feat: wire hyperopt search bounds from dqn-hyperopt.toml — zero hardcoded bounds
All 31 PSO search space bounds now loaded from config/training/dqn-hyperopt.toml
via HyperoptProfile::bound(). To change search ranges, edit the TOML — no code
changes needed.

Log-scale transforms (learning_rate, buffer_size, weight_decay, etc.) applied
in the adapter; TOML stores human-readable linear values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 09:40:48 +01:00
jgrusewski
3c8e177932 feat: HyperoptProfile with TOML search space bounds
Add SearchSpaceSection, PsoSection, HyperoptProfile structs to
training_profile.rs. All 31 PSO search bounds now configurable in
config/training/dqn-hyperopt.toml — no code changes needed to
adjust search ranges.

HyperoptProfile::bound("field", default) returns the TOML value
or falls back to the hardcoded default. Adapter wiring is next step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 09:33:55 +01:00
jgrusewski
720bccfb37 perf: chunked backtest evaluator — 7.8x fewer kernel launches
Batch 64 steps per cuBLAS forward call instead of 1 step at a time.
[n_windows × 64, state_dim] = [320, state_dim] per GEMM instead of
[5, state_dim]. Reduces kernel launches from 576K to 74K for 32K bars.

cuBLAS forward + expected_q + action_select batched across chunk.
env_step remains per-step sequential (stateful portfolio simulation).

Expected: backtest eval 107s → ~15s for large networks.
868 tests pass, 0 failed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 09:29:17 +01:00
jgrusewski
d1d406dd95 fix: cap hyperopt hidden_dim_base at 512 — backtest evaluator is O(bars × dim²)
1024-dim network causes 107s/epoch in backtest (32K bars × 5 windows).
With 512 max: ~25s/epoch. Training itself is only 0.7ms/step regardless.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 09:16:11 +01:00
jgrusewski
7be2efd3e0 perf: replace backtest evaluator warp-matvec with cuBLAS SGEMM
Last component using old shared-memory-tiling kernel. With hidden_dim=768,
shared memory exceeded 49KB → CUDA_ERROR_INVALID_VALUE.

Replace with CublasForward::forward_online() + compute_expected_q +
experience_action_select kernels. Same cuBLAS pipeline as training
and experience collection.

Delete backtest_forward_kernel.cu (old warp-matvec kernel).
Fix hyperopt bounds test assertions for updated search ranges.
868 tests pass, 0 failed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 08:47:26 +01:00
jgrusewski
b759467259 fix: add pushgateway port 9091 to GPU test network policy
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 02:22:11 +01:00
jgrusewski
ae5ae056d6 fix: add gpu-test network policy label to all GPU workflow templates
5 templates were missing app.kubernetes.io/component=gpu-test label,
causing MinIO log archival to fail (port 9000 blocked by network policy).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 02:08:50 +01:00
jgrusewski
109f4e9414 perf: allow num_atoms up to 101 — H100 handles larger C51 distributions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 01:42:38 +01:00
jgrusewski
e464f89a04 perf: widen hidden_dim_base to 1024 — H100 has headroom at 0.7ms/step
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 01:41:44 +01:00
jgrusewski
6e40e86c09 perf: constrain hyperopt search space + increase H100 experience episodes
Hyperopt:
- num_atoms: 51-201 → 11-51 (GPU profile caps to hardware limit)
- hidden_dim_base: 256-1024 → 128-512 (1024+ is wasteful for 2-layer net)
- Prevents wildly oversized networks (num_atoms=200 caused 25s/epoch)

H100 experience:
- gpu_n_episodes: 256 → 2048 (8x larger cuBLAS batch saturates 132 SMs)
- gpu_timesteps_per_episode: 500 → 100 (fewer steps, more parallel episodes)
- Total experiences: 204K/epoch (was 128K) with better GPU utilization
- Expected: experience 357ms → ~100ms (SM utilization 5% → 40%+)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 01:40:16 +01:00
jgrusewski
897c063d7d fix: force use_branching=true in hyperopt — GPU pipeline requires branching
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 01:28:29 +01:00
jgrusewski
e3b8830a63 perf: GPU segment tree for O(log n) PER sampling — replaces O(n) prefix sum
Binary segment tree as flat CudaSlice<f32>[2*capacity_pow2]. Three kernels:
- seg_tree_update: batch priority update + propagate sums to root
- seg_tree_insert: insert raw priorities during replay buffer fill
- seg_tree_sample: parallel root-to-leaf traversal with Philox RNG

Replaces: prefix_sum (3 kernels), searchsorted, pow_alpha, per_gen_thresholds.
Deleted: prefix_sum_kernel.cu, searchsorted_kernel.cu, per_threshold_kernel.cu.

Sampling cost: O(n) growing with buffer fill → O(log n) constant.
Expected: H100 training 12→23ms/step (growing) → ~5ms/step (constant).
All 1,225 tests pass (359 ml-dqn + 866 ml).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 00:52:37 +01:00
jgrusewski
1ce22c7e9c docs: GPU segment tree spec + plan for O(log n) PER sampling
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 00:39:34 +01:00
jgrusewski
e8c0b83361 fix: prefix sum block_dim from device capability, not hardcoded
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 00:21:24 +01:00
jgrusewski
c0b43c9fad perf: fix PER prefix sum single-block fallback at 500K buffer
Root cause of H100 training time growth (12ms→23ms/step across epochs):
pfx_sum() fell back to single-block O(n) scan when num_blocks > 1024.

With block_dim=256 and buffer_size=500K: num_blocks = 1953 > 1024 → fallback.
Fix: increase block_dim to 1024 (capped at device max_threads_per_block).
Now: num_blocks = 500000/1024 = 489 < 1024 → multi-block 3-phase scan works.

Expected impact: PER sampling cost stays constant as buffer fills instead
of growing linearly. Training step time should be stable across epochs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 00:19:44 +01:00
jgrusewski
49444c19d8 fix: memcpy_dtoh buffer size mismatch in Q-value readback
q_out_buf is [config.batch_size, total_actions] but host readback buffers
were sized for [sample_size, total_actions]. When sample_size < batch_size,
cudarc's memcpy_dtoh assertion (dst.len >= src.len) panics with SIGABRT.

Fix: allocate host buffer to match q_out.len(), then truncate to sample_size.
Affects: compute_epoch_q_diagnostics, compute_validation_loss, PER refresh.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:58:01 +01:00
jgrusewski
a75a98bd0d feat: TOML training profile system — config-driven hyperparameters
Training Profile Loader:
- 3-tier resolution: $FOXHUNT_TRAINING_PROFILE > filesystem > embedded defaults
- DqnTrainingProfile with 10 sections, all Option<T> for sparse profiles
- apply_to() applies only Some fields, preserving struct defaults
- 11 unit tests, all passing

TOML Profiles (config/training/):
- dqn-production.toml: full Rainbow DQN (40+ params)
- dqn-smoketest.toml: CI fast path (sparse, 8 overrides)
- dqn-hyperopt.toml: PSO search space ranges + fixed flags
- ppo-production.toml, ppo-smoketest.toml
- supervised-production.toml, supervised-smoketest.toml
- walk-forward.toml: window sizes

CLI Integration:
- train_baseline_rl: --training-profile (default: dqn-production)
- train_baseline_supervised: --training-profile (default: supervised-production)
- Merge priority: CLI args > TOML profile > GPU profile > struct defaults

Smoke Tests:
- smoke_params() now loads dqn-smoketest.toml instead of hardcoding
- Production features set as manual overrides (testing flags, not config)

Infrastructure:
- K8s job-template.yaml: TRAINING_PROFILE env var + --training-profile arg
- Delete old config/ml/training.toml (replaced, zero callers)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:35:41 +01:00
jgrusewski
baf8308931 docs: training profile configuration system — spec + plan
TOML-based per-model training profiles replacing hardcoded hyperparameters.
8 config files (DQN/PPO/supervised × production/smoketest + hyperopt + walk-forward).
3-tier loading: env var > filesystem > embedded defaults.
Full CLI coverage for all profile fields.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:22:58 +01:00
jgrusewski
8a2fcc968d cleanup: remove all debug eprintln, log_gpu_memory, unnecessary check_err drains
- Remove 6 eprintln!("[GPU-DEBUG]...") statements from gpu_dqn_trainer.rs,
  constructor.rs, and elementwise.rs
- Delete log_gpu_memory() function and all 18 callers in smoke test files
- Remove check_err() drains from GpuDqnTrainer::new() and
  GpuExperienceCollector::new() — root cause is fixed (per-context kernel cache)
- Keep check_err() after CUDA Graph capture (legitimate — drains event tracking errors)
- Fix broken import lines in smoke test files after sed cleanup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:10:27 +01:00
jgrusewski
da1cea1181 fix: sequential GPU test contamination — per-context ElementwiseKernels cache
Root cause: ElementwiseKernels was cached in a static OnceLock, compiled
once on the first test's CudaContext. When the second test created a new
CudaContext (fresh Arc), the cached CudaFunction handles were stale,
causing CUDA_ERROR_INVALID_VALUE on alloc_zeros (n=128, op=abs).

Fix: Replace OnceLock with a Mutex<HashMap<usize, Arc<ElementwiseKernels>>>
keyed by Arc<CudaContext> pointer address. Each distinct CudaContext gets
fresh kernel compilation. Old entries are evicted on context change.

Also: eliminate ALL GpuTensor from DQN training path (metrics.rs,
training_loop.rs). Replace with CPU computation for validation (cold path)
and raw CudaSlice for replay buffer insertion. Log deferred CUDA errors
instead of silently swallowing.

Result: ALL 6 sequential GPU smoke tests pass (was 1 failing).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 22:53:57 +01:00
jgrusewski
a143566549 fix: eliminate ALL GpuTensor from DQN training path
- Replace GpuTensor gradient accumulation with CPU-side BTreeMap<String, Vec<f32>>
- Replace GpuTensor validation loss computation with CPU Sharpe (cold path)
- Remove GpuTensor import from training_loop.rs and metrics.rs
- Delete dead cuda_slice_to_tensor conversion helpers
- Change val_features_gpu/val_closes_gpu/val_ofi_gpu from GpuTensor to Vec<f32>
- Replace PER priority refresh GpuTensor::from_vec with stream.clone_htod
- Log deferred CUDA errors instead of silently swallowing via check_err()
- Delete get_q_values() (dead, no callers)

Zero GpuTensor (ml-core elementwise kernels) in the DQN training pipeline.
All GPU operations use raw CudaSlice via cudarc or cuBLAS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 22:05:08 +01:00
jgrusewski
129417ce7a fix: bypass GpuTensor for replay buffer insertion — direct CudaSlice path
Replace CudaSlice→GpuTensor→CudaSlice roundtrip in experience batch
insertion with direct CudaSlice insertion into GpuReplayBuffer.

- experience_kernels.cu: output dones as float (0.0/1.0) instead of int
- GpuExperienceBatch.dones: CudaSlice<i32> → CudaSlice<f32>
- training_loop.rs: call gpu_buf.gpu.insert_batch() with raw CudaSlice
- Remove cuda_slice_to_tensor_f32 conversion (GpuTensor bridge)
- Remove log_gpu_memory helper (was a debug hack, not a fix)

Remaining: 1 sequential test still fails — GpuTensor operations in the
gradient accumulation path (lines 1015-1128) cache kernel handles on the
cudarc context. Needs systematic instrumented debugging to locate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 21:25:17 +01:00
jgrusewski
73b6513cff fix: sequential GPU test contamination — remove static OnceLock<MlDevice>
Root cause: `SMOKE_CUDA: OnceLock<MlDevice>` held a static Arc<CudaContext>
for the process lifetime. CudaSlice Drop from test N recorded errors on
this shared context's error_state, causing test N+1's bind_to_thread() to
fail with CUDA_ERROR_INVALID_VALUE.

Fix: create a fresh MlDevice per test (no static caching). Each test gets
its own CudaContext Arc with clean error_state.

Also: convert all GPU smoke tests from #[tokio::test] to synchronous #[test]
with explicit tokio::runtime::Builder::new_current_thread(). The runtime is
explicitly dropped between tests, ensuring all Arc<CudaContext> refs are freed.

Result: 5 of 6 sequential smoke tests now pass. The remaining 1 failure is
a real Candle GpuTensor bug: the replay buffer insertion path still uses
Candle's elementwise kernels, which cache CudaFunction handles that become
stale across test boundaries. Fix: eliminate Candle from replay buffer path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 21:06:40 +01:00
jgrusewski
3df8b2fc79 infra: resize test-data PVC to 50Gi for 3Q MBP-10 + trades data
3Q of MBP-10 order book data for ES.FUT is ~18GB compressed.
10Gi PVC was insufficient for the full OHLCV + MBP-10 + trades dataset.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:28:08 +01:00
jgrusewski
922fd5e93e ci: add DQN perf benchmark step + 3Q test data for CI
GPU test pipeline:
- Add perf-benchmark step after compile-and-test
- Runs DQN training on 3Q ES.FUT (OHLCV + MBP-10 + trades)
- Reports epoch time (ms), fails if > 500ms (H100 regression guard)
- batch_size=1024, 5 epochs × 100 steps, skips epoch 1 (init)

Populate test data job:
- Copy 3 quarters per symbol (was 1) for all data types
- OHLCV: ~6MB/symbol for walk-forward + perf benchmarks
- MBP-10: 3Q for OFI feature pipeline testing
- Trades: 3Q for trade-flow feature testing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:17:27 +01:00
jgrusewski
fafec932dd perf: eliminate all Candle forward() + fix sequential test Drop
- Remove ALL agent.forward() from DQN training hot paths
- Replace per-step Q-divergence check with cuBLAS compute_q_stats
- Remove dead test_training_rejects_missing_gpu_collector (tests dead code)
- Add Drop impls: FusedTrainingCtx syncs stream + drains errors before drop
- GpuDqnTrainer Drop: drain deferred CUDA errors via check_err()
- Sequential GPU test contamination: cudarc in-process context limitation,
  run ignored GPU tests via separate cargo invocations (CI already does this)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:06:09 +01:00
jgrusewski
d7cdd778c8 perf: eliminate ALL Candle forward() from DQN training pipeline
Replace every agent.forward() (Candle dispatch chain) in the training
hot path with cuBLAS forward via fused_ctx.compute_q_values/compute_q_stats.

Removed:
- Per-step Q-divergence Candle forward in train_step_single_batch
- Per-step Q-divergence Candle forward in train_step_with_accumulation
- collect_qvalue_statistics (dead Candle path, zero callers)
- select_actions_batch_gpu (dead, GPU collector uses experience_action_select kernel)
- Candle forward in compute_epoch_q_diagnostics → cuBLAS
- Candle forward in compute_validation_loss → cuBLAS
- Candle forward in refresh_stale_per_priorities → cuBLAS

Zero Candle involvement in the DQN training pipeline.
All Q-value computation uses cuBLAS SGEMM + GPU reduction kernels.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 17:29:49 +01:00
jgrusewski
0c0873d075 perf: replace Candle Q-value estimation with cuBLAS + GPU reduction
Replace agent.forward() (Candle dispatch chain) with cuBLAS SGEMM forward +
compute_expected_q kernel + q_stats_reduce kernel. Zero Candle involvement in
the DQN training path. Only 20 bytes (5 scalars) read from GPU at epoch end.

Validation phase: 27ms → 0ms on RTX 3050.
Total epoch: 78ms → 50ms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 16:58:28 +01:00
jgrusewski
5738edaa0a perf: keep GPU training data resident across epochs — init 73ms → 0ms
Remove per-epoch GPU data freeing that forced re-upload every epoch.
Training data within a walk-forward window is immutable — uploading once
and keeping it GPU-resident eliminates the init phase entirely.

Epoch time: 153ms → 78ms on RTX 3050 (epochs 2+).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 16:29:32 +01:00
jgrusewski
c2d116dfdf perf: cuBLAS SGEMM pipeline + dead code elimination — 37s → 86ms/epoch (430x)
Phase 2: Replace 1-warp/sample fused kernels with cuBLAS SGEMM batched forward/backward.
- batched_forward.rs: cuBLAS SGEMM forward (10 GEMM + bias/ReLU per pass)
- batched_backward.rs: cuBLAS SGEMM backward (chain rule via GEMM, no atomicAdd)
- c51_loss_kernel.cu: standalone C51 distributional loss (256 threads, 2KB shmem)
- c51_grad_kernel: dL/d_logits with dueling routing for cuBLAS backward
- BF16 alignment fix: pad offsets to even for short2 vectorized loads
- Training step: 10.7ms → 0.7ms (15x) on RTX 3050

Phase 3: Unified cuBLAS Q-forward + dead code elimination (-4,400 lines net).
- Rewrite experience collector: timestep loop + cuBLAS replaces monolithic 3,272-line kernel
- Delete dqn_training_kernel.cu (1,385 lines) — replaced by dqn_utility_kernels.cu (118 lines)
- Delete dqn_experience_kernel.cu (3,272 lines) — replaced by experience_kernels.cu (656 lines)
- Remove BF16 warp-matvec helpers from common_device_functions.cuh (-159 lines)
- Remove dead methods/fields from GpuDqnTrainer (-500 lines)
- Experience collection: 348ms → 12ms (29x) on RTX 3050
- No fallback paths — cuBLAS is the only Q-forward implementation
- All 1,514 tests pass, GPU smoke test verified with real data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:33:00 +01:00
jgrusewski
e1b8b46255 perf: fuse 20 BF16 conversion launches into 1 flat-buffer conversion
Replace 40 per-tensor f32_to_bf16_kernel launches per EMA step (20 online
+ 20 target) with 2 single-launch conversions over flat contiguous buffers.

- Add bf16_params_buf and bf16_target_params_buf (flat CudaSlice<u16>) that
  mirror the GOFF_* layout of the F32 params_buf/target_params_buf
- Precompute bf16_goff_byte_offsets[20] at construction for zero-cost pointer
  arithmetic into flat BF16 buffers during kernel launches
- sync_online_bf16: single f32_to_bf16_kernel(params_buf, bf16_params_buf, N)
- sync_target_bf16: single f32_to_bf16_kernel(target_params_buf, bf16_target_params_buf, N)
- Forward kernels pass raw u64 device pointers at GOFF offsets instead of
  individual CudaSlice<u16> references — zero additional allocation
- Remove DuelingWeightSetBf16/BranchingWeightSetBf16 dependency from trainer
- Add flat target_params_buf for fused single-kernel EMA update

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 12:00:13 +01:00
jgrusewski
fa191983e6 perf: multi-block prefix sum for PER — utilize all 132 H100 SMs
Replace single-block prefix sum (1 SM, max 1024 threads) with a 3-phase
multi-block parallel scan that distributes work across all available SMs:

Phase 1: Per-block Hillis-Steele scan (256 elements/block, all SMs active)
Phase 2: Scan block_sums in a single block (num_blocks < 1024)
Phase 3: Propagate block offsets to make results globally correct

For N=100K: 391 blocks x 256 threads vs old 1 block x 1024 threads.
Fast path preserved for N <= 256 (single-block kernel, lower overhead).
Fallback to chunked single-block for N > 256K (num_blocks > max_threads).
block_sums buffer pre-allocated in GpuReplayBuffer::new() — zero
cuMemAlloc in the hot path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 11:57:06 +01:00
jgrusewski
e4b7d2ffb0 feat: GPU TOML profile system — remove ALL hardcoded VRAM if/else chains
Created config/gpu/{default,rtx3050,h100,a100}.toml with all GPU-specific
parameters: batch_size, num_atoms, buffer_size, hidden_dim_base,
replay_buffer_vram_fraction, gpu_n_episodes, gpu_timesteps_per_episode,
cuda_stack_bytes.

GpuProfile::load() auto-detects GPU by device name, falls back to
embedded defaults (include_str!). Override via FOXHUNT_GPU_PROFILE env.

Removed dead code:
- detect_vram_mb(), vram_scaled_hidden_dims(), vram_scaled_base_dim(),
  resolve_hidden_dim_base() + 18 tests for these functions

All callers updated: train_baseline_rl, DQNTrainer constructor,
PPO trainer, smoke tests, pipeline tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 11:39:40 +01:00
jgrusewski
d4c9c9c34e docs: Phase 2 plan — cuBLAS batched GEMM + kernel fusion (10.7ms → <1ms/step)
Based on kernel deep dive: 1.56% occupancy on H100 from 1-warp/sample
architecture, 46KB stack spill, 47 kernel launches per step.

7 tasks: cuBLAS forward, batched backward, fuse BF16/EMA, multi-block
prefix sum, C51 loss kernel, H100 profiling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 11:23:09 +01:00
jgrusewski
8a68bdf21d feat: GPU TOML profile system — replace hardcoded VRAM if/else chains
Replace scattered VRAM-based if/else chains with a declarative TOML profile
system. GPU profiles (rtx3050, a100, h100, default) are selected by device
name and embedded at compile time via include_str! for zero-filesystem
fallback in CI/containers, with filesystem and env var overrides.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 11:21:55 +01:00
jgrusewski
f97f4b23e6 fix: wire max_training_steps_per_epoch to DQN + per-step profiling
- DQN CLI --max-steps-per-epoch was only wired to PPO (bug: 1394 steps ran instead of 10)
- Added per-substep timing: sample/fused/guard breakdown per epoch
- Phase 1a results: PER sampling now 0.1ms/step (was ~100ms with CPU roundtrips)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 11:08:38 +01:00
jgrusewski
2c39d100a0 perf(ml-dqn): eliminate CPU roundtrips in PER sample_proportional
Eliminate the 3 CPU roundtrips per sample_proportional() call (called
300x/epoch), which was the #1 performance bottleneck:

1. Replace memcpy_dtoh(cs_total) with DtoD copy to pre-allocated
   total_sum_buf — total_sum never leaves GPU
2. Replace CPU rand() + memcpy_htod(thresholds) with GPU-resident
   Philox PRNG kernel — thresholds generated directly on device
3. Replace memcpy_htod([0.0], max_weight) with memset_zeros —
   async GPU memset, zero host staging
4. is_weights_f32 kernel now reads total_sum from GPU pointer
   instead of scalar argument

Pre-allocate 13 PER sampling buffers on the GpuReplayBuffer struct
(thresholds, indices, gathered data, weights, max_weight, total_sum_buf,
rng_step counter). All intermediate computation uses these pre-allocated
buffers. Output GpuBatchSlices are DtoD-cloned for ownership transfer.

Add max_batch_size field to GpuReplayBufferConfig (defaults to 1024).
Delete the cs_total() method entirely.

Net result: zero memcpy_dtoh, zero memcpy_htod, zero CPU synchronization
points in the PER sampling hot path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 10:38:33 +01:00
jgrusewski
6fc90c0bdb refactor(ml-dqn): is_weights_f32 kernel reads total_sum from GPU pointer
Change the is_weights_f32 CUDA kernel signature from taking total_sum as
a scalar f32 argument (which requires a CPU readback) to reading it from
a const float* GPU-resident pointer. This eliminates the last memcpy_dtoh
in the PER sampling hot path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 10:37:52 +01:00
jgrusewski
1eca11ef77 feat(ml-dqn): add GPU-resident Philox PRNG kernel for PER threshold generation
Eliminates the CPU roundtrip of memcpy_dtoh(total_sum) -> CPU rand() ->
memcpy_htod(thresholds) by generating all random thresholds directly on
GPU using Philox 4x32-10 counter-based PRNG (Salmon et al., SC 2011).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 10:37:26 +01:00
jgrusewski
8b44a8c9c5 docs: Phase 1 implementation plan — eliminate PER CPU roundtrips
7 tasks: Philox RNG kernel, pre-allocated buffers, GPU-native sampling,
adam_step cleanup, profiling validation, to_host deprecation.

Target: 37s → 12-15s/epoch on H100.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 10:26:11 +01:00
jgrusewski
6e49d4a2be docs: H100 epoch optimization spec (37s → <5s) + phase profiling
Spec: 3-phase plan to reduce DQN training epoch from 37s to <5s on H100.
- Phase 1: eliminate 300 CPU roundtrips in PER sampling (GPU Philox RNG)
- Phase 2: increase kernel occupancy (batch_size 512+, cuBLAS profiling)
- Phase 3: pipeline overlap (dual-stream experience/training)

Profiling instrumentation added to training loop (init/experience/training/validation breakdown).

RTX 3050 baseline: training=97.2%, experience=2.3% — PER sampling
with CPU RNG + DtoH sync is the primary bottleneck.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 10:23:17 +01:00
jgrusewski
775b5597a2 perf: add per-phase epoch profiling instrumentation
Measures init, experience collection, training steps, and validation
phases separately. Logs breakdown at end of each epoch:
  "Epoch N/M phase breakdown: init=Xms experience=Xms training=Xms validation=Xms total=Xms"

RTX 3050 baseline: training=97.2%, experience=2.3%, validation=0.3%

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 10:05:53 +01:00
jgrusewski
c20d810067 fix: supervised GPU smoke test dimension mismatches
- Liquid: remove seq_len from input (forward_loss uses 2D [batch, input_size])
- Mamba2: single sample input matching d_model=32
- xLSTM: remove seq_len from input (same as Liquid)
- Diffusion: create checkpoint dir for models using dir-based saves

All 8 supervised GPU smoke tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 09:14:53 +01:00
jgrusewski
65891816ee fix: resolve all CI test failures — TFT, DQN pipeline, benchmarks
DQN pipeline tests:
- Split dqn-pipeline into per-test cargo invocations in CI (CUDA Graph
  capture corrupts async memory pool between sequential tests)
- Drop impl for GpuDqnTrainer: sync stream + destroy graph before buffers
- check_err drains in constructor and after graph capture

TFT fixes:
- forward_loss: reshape output [batch,horizon,quantiles] → [batch,quantiles]
  to match target shape (fixes DimensionMismatch {expected:3, actual:3})
- smoke test: accept step=0 for models without backward support
- benchmark: remove hardcoded batch_size≤4 assertion (H100 can be larger)

Cleanup:
- Remove debug eprintln from elementwise.rs
- GPU-native cat bounds check

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 09:09:51 +01:00
jgrusewski
b53f2330b8 fix: CUDA Graph + cudarc event compat — check_err after capture + constructor
CUDA Graph capture disables event tracking, causing cudarc to store
errors via record_err() during device_ptr() calls. These errors block
all subsequent bind_to_thread() calls (which calls check_err first).

Fixes:
- check_err() immediately after re-enabling events post-capture
  (clears stale errors on the stream's context)
- check_err() in DQNTrainer constructor (clears errors from previous
  trainer instances sharing the same primary CUDA context)
- check_err() at training start (covers multi-test sequential execution)
- GPU-native cat bounds check fix (dst_start + n <= output.len())
- Removed should_use_cuda_graph — always use CUDA Graph
- Removed hacky clear_cuda_context test helper

CUDA Graph + raw post-graph ops work on all GPUs.
4/5 pipeline tests pass locally (5th is flaky due to CUDA primary
context error retention between sequential tests in same process).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 00:41:14 +01:00
jgrusewski
ff0549972d fix: clear stale cudarc errors after CUDA Graph capture + raw post-graph ops
Root cause: graph capture disables event tracking, but cudarc's
record_err() stores errors from cuStreamWaitEvent on disabled events.
bind_to_thread() calls check_err() and returns the stored error,
blocking ALL subsequent cudarc API calls (launch, sync, memcpy).

Fix:
- check_err() before EMA kernel launch to consume stale errors
- Raw cuStreamSynchronize + cuMemcpyDtoH for post-graph readback
  (bypasses bind_to_thread entirely)
- Raw device pointers for EMA kernel args (bypasses device_ptr event wait)
- GPU-native cat bounds check (dst_start + n <= output.len())
- CUDA Graph always enabled (removed should_use_cuda_graph function)

4/5 DQN pipeline tests pass with CUDA Graph on RTX 3050.
5th (epsilon_greedy) passes individually, flaky in sequence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 00:12:49 +01:00
jgrusewski
4a06e99ee5 fix: bypass stale cudarc events for all post-graph operations
After CUDA Graph capture (which disables/re-enables event tracking),
CudaSlices modified during capture have stale write events. Any cudarc
API call (memcpy_dtoh, synchronize, launch_builder.arg) on these slices
fails with CUDA_ERROR_INVALID_VALUE because cuStreamWaitEvent gets an
invalid event handle.

Fix: use raw CUDA driver calls for ALL post-graph operations:
- cuStreamSynchronize instead of cudarc synchronize() (avoids bind_to_thread)
- cuMemcpyDtoH_v2 via raw_device_ptr for scalar readbacks (2 × 4 bytes)
- raw_device_ptr for EMA kernel args (bypasses device_ptr event wait)

Event tracking remains enabled globally — only the 3 post-graph call sites
use raw driver calls. All other cudarc operations (cat/stack, alloc, etc.)
work normally with event tracking.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 23:25:58 +01:00