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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
- 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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>