Commit Graph

160 Commits

Author SHA1 Message Date
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
8b0122a5ac feat: has_ofi flag in fxcache header (explicit, no zero-detection)
Adds `has_ofi: bool` to `FxCacheData` and the fxcache binary header
(stored in `reserved[0]`). Propagates through load_fxcache, write_fxcache,
all callers (precompute_features, train_baseline_rl, hyperopt dqn adapter),
existing roundtrip tests, and adds two new has_ofi-specific roundtrip tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 09:48:38 +02:00
jgrusewski
fccfe1b0d6 feat: cache key includes symbol + data_source (prevents cross-instrument collisions)
Different instruments (ES.FUT vs NQ.FUT) and data source modes (ohlcv vs mbp10)
now produce distinct .fxcache keys, preventing silent overwrites and wrong-bar-type
loads at cache lookup time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 09:41:53 +02:00
jgrusewski
74c62eb5dc fix: fxcache key uses data_dir (not symbol_dir) to match precompute
The cache key in train_baseline_rl was computed from data_dir/symbol
(e.g. test_data/futures-baseline/ES.FUT) but precompute_features uses
data_dir (test_data/futures-baseline). Different paths → different keys
→ cache miss → OFI=false.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:26:01 +02:00
jgrusewski
b1fa34c5eb feat: --feature-cache-dir CLI arg for training binaries
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 22:59:08 +02:00
jgrusewski
047c01dcfa fix: precompute skips if cache exists + integrated into compile-and-train
- precompute_features early-exits if {cache_key}.fxcache already exists
  (cache key = SHA256 of filenames + sizes, only changes when data changes)
- compile-and-train copies binaries to PVC /data/bin/ after compile
- precompute step added to compile-and-train DAG (runs after compile,
  before training, parallel with GPU warmup)
- precompute template reads binary from PVC (no GitLab download)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:43:41 +02:00
jgrusewski
b7840fc978 fix: filter DBN files by --symbol in precompute (was mixing ES+NQ+ZN+6E)
collect_dbn_files_filtered() checks for symbol subdirectory first,
falls back to filename matching. Precompute now loads only the
target symbol's data instead of all 4 instruments mixed together.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 18:20:42 +02:00
jgrusewski
9f7c14978f feat: z-score normalization at precompute time + remove per-fold normalization
Normalize features once in precompute_features (single source of truth).
NormStats saved alongside .fxcache for inference denormalization.
Removed per-fold NormStats computation from train_baseline_rl, hyperopt
adapter, and smoketests — fxcache is pre-normalized, consumers use as-is.

NOTE: features still show raw price values (max=18000+) because
test_data/futures-baseline contains multiple symbols (ES, NQ, ZN, 6E)
mixed into one dataset. The feature extraction pipeline needs
investigation — log returns between different symbols produce garbage.
This is tracked as a separate task.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:48:26 +02:00
jgrusewski
a448e03e64 fix: 6 CUDA OOB bugs + remove 1228 lines of dead training path
Root cause of NaN at step 22: cuBLAS out-of-bounds reads in the GPU
experience collector and training forward pass, caused by buffer
dimension mismatches that produced garbage states → garbage Q-values →
NaN loss.

Bugs fixed:
1. GpuDqnTrainConfig::default() had bottleneck_dim=2, market_dim=42 —
   collector computed s1_input_dim=40 instead of state_dim=80, cuBLAS
   read 2x past W1 weight buffer (10KB OOB per SGEMM)
2. batch_states allocated with state_dim=80 but cuBLAS ldb=128 (CUTLASS
   K-tile alignment) — 48 bf16/f32 values read past end per row
3. Validation state tensor had 59 elements/row (42+3+8+6) instead of
   128 (state_dim_padded) — missing portfolio/MTF features + padding
4. CUTLASS K-tile overread on all hidden activation buffers (K=64 but
   tile=128) — added 128-element padding to 14 bf16 buffers
5. init_from_fxcache never set self.ofi_features — collector OFI buffer
   was 1-element placeholder, state_gather kernel read OOB
6. collect_gpu_experiences_slices missing *2 counterfactual multiplier —
   half of collected experiences never inserted into PER

Dead code removed (~1228 lines):
- train_with_data_full_loop (old Vec<(FeatureVector, Vec<f64>)> loop)
- init_gpu_data, init_gpu_raw_buffers, collect_gpu_experiences,
  run_training_steps (old helpers only called by above)
- train_with_preloaded_data, train_with_shared_data (old hyperopt APIs)
- train() and train_walk_forward() now convert to fixed-size arrays
  and route through init_from_fxcache + train_fold_from_slices

Other fixes:
- precompute_features defaults output-dir to sibling of data-dir
- workspace_root() + feature_cache_dir() helpers for reliable path
  resolution (works from any cwd, with/without CARGO_MANIFEST_DIR)
- Production smoketest uses fxcache (no slow DBN parsing)
- compute-sanitizer: 0 errors (was 2539)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:22:46 +02:00
jgrusewski
95fc94ae9c fix: precommit review — ensemble reuse, keep gpu_data across folds, eliminate Vec<f64>
Fix 1 (HIGH): Ensemble trainers were created + data uploaded inside the
fold loop, causing redundant GPU uploads per fold per ensemble member.
Moved creation + init_from_fxcache before the fold loop; inside the loop
we now reuse trainers via set_training_range + reset_for_fold.

Fix 2 (MEDIUM): reset_for_fold cleared gpu_data = None, forcing re-upload
on every fold even though the full dataset was already GPU-resident via
init_from_fxcache. Removed the clearing — data stays on GPU across folds.

Fix 3 (LOW): train_fold_from_slices allocated a Vec<f64> per bar via
t.to_vec(). Added train_with_data_full_loop_slices that accepts
&[([f64; 42], [f64; 4])] — both are Copy stack types, zero heap alloc
per element. Added matching collect_gpu_experiences_slices and
run_training_steps_slices helpers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:21:45 +02:00
jgrusewski
3278cec29b perf: PPO zero-copy fold loop — train_from_slices + delete train_ppo_fold
Add PpoTrainer::train_from_slices(&[[f64; 42]]) that converts to
Vec<Vec<f32>> internally (PPO train() API requires ownership). Replace
the PPO fold loop in train_baseline_rl.rs: eliminates the OHLCVBar
construction shim and calls train_from_slices directly on fxcache
feature slices. Delete the standalone train_ppo_fold function entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 22:54:00 +02:00
jgrusewski
8d5af977f7 feat: train_fold_from_slices — trainer accepts contiguous feature/target slices
Adds DQNTrainer::train_fold_from_slices(&[[f64;42]], &[[f64;4]]) so the
fold loop in train_baseline_rl passes raw fxcache slices directly, without
the caller constructing any Vec<(FeatureVector, Vec<f64>)>. Removes
features_to_trainer_format_fast helper (no longer needed) and updates
train_dqn_fold to call the new method.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 22:50:11 +02:00
jgrusewski
5c985df147 perf: zero-copy fold loop — fxcache to GPU once, index slices per fold
Rewrites the train_baseline_rl fold loop to eliminate per-fold waste:

- Data loading: fxcache -> fold index ranges from timestamps (no bar
  reconstruction). DBN fallback builds FxCacheData in-place.
- DQN trainer created ONCE before fold loop, fxcache uploaded to GPU
  ONCE via init_from_fxcache. Each fold uses set_training_range +
  set_val_data_from_slices + reset_for_fold instead of recreating.
- Tokio runtime created ONCE (not per fold).
- Hyperparams construction extracted to build_dqn_hyperparams().
- Deleted: prepare_fold_data, FoldData type, prefetch thread,
  DoubleBufferedLoader GPU staging, features_to_trainer_format (old).
- Added: generate_walk_forward_indices_from_timestamps (i64 ns
  timestamps, O(log n) partition_point, no OHLCVBar dependency).
- Added: features_to_trainer_format_fast (fxcache targets directly).
- PPO compatibility preserved: constructs minimal OHLCVBars from
  fxcache targets for train_ppo_fold (Task 6 will refactor).
- Ensemble mode preserved with per-member trainers for k>0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:46:30 +02:00
jgrusewski
f5b21406a8 fix: restore batch_size to GPU profiles — profile defaults, not auto-scaling
Auto-scaling returned 8192 on 4GB RTX 3050 (should be 64), causing
17s/epoch instead of 0.28s. The VRAM math didn't account for IQN (1.1GB),
attention, IQL, replay buffer (70% VRAM).

Profile-tested values:
- RTX 3050: 64 (4GB, minimal)
- A100: 2048 (40-80GB)
- H100: 8192 (80GB, production)
- Default: 256 (conservative)

Auto-scaling remains as fallback for batch_size=0 (unknown GPU).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:26:24 +02:00
jgrusewski
f11c263700 chore: remove per-step debug eprints, keep per-epoch only
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:26:03 +02:00
jgrusewski
1f43d8c3a6 debug: add fxcache key debug + fix PREFETCH_K hang
- Print fxcache lookup key to diagnose cache miss on H100
- PREFETCH_K=16 (was usize::MAX causing 8M PER samples in one shot)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:06:36 +02:00
jgrusewski
b7735f2e9e debug: add eprintln traces for H100 hang — fxcache, experience collection, training phases 2026-04-02 16:58:16 +02:00
jgrusewski
9aad6ff60e refactor: remove max_training_steps_per_epoch — always train full dataset
Epoch duration self-balances: bigger GPU → bigger auto-scaled batch →
fewer steps per epoch. The manual cap created 7 different values
(0, 8, 64, 100, 200, 300, 2000) across configs/tests/examples, making
behavior inconsistent between environments.

Removed from: DQNHyperparameters, training profiles (smoketest,
localdev, production), CLI args, Argo templates, hyperopt adapter,
all test overrides, supervised example.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:41:16 +02:00
jgrusewski
d353af98e9 chore: clean up dead batch_size override code in train_baseline_rl
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:34:24 +02:00
jgrusewski
1321e11fb2 refactor: remove batch_size CLI override from train_baseline_rl
All VRAM-derived parameters (batch_size, gpu_n_episodes, buffer_size)
are auto-scaled — CLI overrides bypass this and cause inconsistent
behavior between local testing and production.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:33:00 +02:00
jgrusewski
5546a45bc3 refactor: remove gpu_n_episodes override — auto-scale from VRAM everywhere
gpu_n_episodes was manually overridden in GPU profiles, training configs,
test files, and hyperopt — all set to 0 or small fixed values that
bypassed the auto-scaling logic, causing a div-by-zero crash in
train_baseline_rl.

Now: single auto-scaling path via optimal_n_episodes() from VRAM/SM
count. No manual override field. Cap at 16384 (consistent with
AutoBatchSizer's 8192 cap pattern). Floor at 32 for small GPUs.

Removed gpu_n_episodes from:
- DQNHyperparameters, PpoHyperparameters structs
- All 4 GPU profiles (rtx3050, h100, a100, default)
- Training profiles (smoketest, localdev)
- ExperienceProfile struct + serde
- Hyperopt adapter
- All test overrides

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:21:45 +02:00
jgrusewski
5b06484da7 fix: 11 H100 training bugs — Sharpe per-trade, batch autosizing, PER/HER capacity, Q-clip
Sharpe calculation:
- Use per-trade returns (sum_returns/sum_sq_returns) instead of per-bar
  step_returns. Per-bar Sharpe collapsed variance → bogus 19.75 with PF=0.03.
- Annualize by sqrt(trades_per_year) not sqrt(bars_per_year).

Batch sizing:
- Cap auto-computed batch_size at 8192 (VRAM ceiling of 2M is OOM limit, not
  optimal RL batch).
- Add VRAM floor: batch_size < ceiling/4096 gets scaled UP (128 → 512 on H100).
- Only let hyperopt override batch_size when explicitly non-zero — preserve
  profile's batch_size=0 auto-compute sentinel.

Replay buffer:
- Divide per_max_memory_bytes by 3.0 for regime heads (PER budget was 3x too
  large, causing OOM cascade 74M → 37M → 18M → 9M → 4.6M).
- HER buffer uses original_buffer_size (pre-autosizer), not inflated 74M.

Q-value clipping:
- Wire hyperparams.q_clip_min/max to DQNConfig (was hardcoded ±500, production
  TOML has ±50). Prevents Q-value overestimation ratio of 94.6x at epoch 2.

Training stability:
- Anti-LR warmup: skip first 5 epochs (early Sharpe unreliable from random
  policy). Prevents bogus 3x LR boost at epoch 2.
- min_replay_size from profile (1000), not hyperopt batch_size (128).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 00:54:12 +02:00
jgrusewski
f9609baff6 fix: train-baseline-rl compiles in-cluster, requires fxcache
- DAG: compile (ci-compile-cpu) + gpu-warmup → train (H100)
- Compiles from source targeting compute cap 90
- fxcache REQUIRED — fails if no .fxcache found
- Error chains printed with {:#} for full cause visibility
- Uses cargo-target-pvc for compile cache + sccache

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:37:23 +02:00
jgrusewski
a4ba8bddaa feat: fxcache stores per-bar timestamps for walk-forward windowing
Each record now starts with an i64 timestamp (nanoseconds since epoch)
before the feature/target/OFI data. v1 records grow from 432 to 440
bytes, v2 from 112 to 120 bytes. The timestamp is always i64 even in
bf16 mode. train_baseline_rl reconstructs bars with real timestamps
instead of placeholders so the walk-forward windower can split by month.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:29:41 +02:00
jgrusewski
0cd3d58a2b feat: train_baseline_rl loads from fxcache, skipping DBN extraction
Auto-discovers fxcache via env var or sibling directory. Falls back
to DBN loading if no cache found. Reconstructs aligned bars from
cached targets for walk-forward windowing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:23:41 +02:00
jgrusewski
6893d75800 fix: unify cache key — precompute and loader use same function
Precompute binary now uses calculate_dbn_cache_key_full from the
library instead of its own compute_cache_key. Canonicalize all
paths in the key function so relative/absolute produce the same
key. Walk up parent dirs for sibling feature-cache discovery.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 02:13:49 +02:00
jgrusewski
51f686e723 feat: mbp10_data_dir and trades_data_dir are unconditional (no Option)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 01:24:30 +02:00
jgrusewski
08d6b1a32d refactor: precompute_features runs pure CPU — no DQNTrainer needed
Extract standalone functions from DQNTrainer:
- extract_features_from_bars(): 42-dim feature extraction
- extract_ohlcv_bars_from_dbn(): DBN to OHLCVBar conversion
- collect_dbn_files_recursive(): file discovery

DQNTrainer delegates to these, ensuring consistency.
Precompute binary no longer initializes CUDA — runs on any
CPU node (ci-compile-cpu at EUR 0.85/hr vs GPU at EUR 12.60/hr).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 00:52:36 +02:00
jgrusewski
b14e9cbd6b fix: precompute_features uses full GPU path, not CPU
DQNTrainer constructor requires CUDA for regime-conditional heads.
Use DQNTrainer::new() (GPU) instead of new_with_device(Cpu).
Set buffer_size=100_000 to satisfy PER minimum threshold.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 00:17:37 +02:00
jgrusewski
0c8620fd81 feat: precompute_features binary — DBN to .fxcache pipeline
CLI binary that reads OHLCV + MBP-10 + trades DBN data via DQNTrainer,
runs the full feature extraction pipeline, and writes a .fxcache file
for zero-overhead GPU loading during training. Uses CPU device to avoid
CUDA dependency for data-only workloads.

Also promotes load_training_data() and ofi_features to pub visibility
so the example binary can access them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 23:56:30 +02:00
jgrusewski
3d63f73c08 fix: add retry with backoff to Databento download_quarter
3 retries with exponential backoff (2s, 4s, 8s) for transient
connection failures (IncompleteBody/UnexpectedEof). Partial files
are deleted before retry so skip-existing logic works correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 22:57:43 +02:00
jgrusewski
05fc1783e1 feat: configurable --min-hold-bars for A/B testing (3 vs 5)
Add --min-hold-bars CLI arg to train_baseline_rl and hyperopt_baseline_rl.
Wire through Argo workflow as parameter. Default 5 (TOML), override via
CLI for quick A/B experiments without config changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 11:12:13 +02:00
jgrusewski
261cb3bac2 feat: wire --initial-capital into train_baseline_rl + Argo workflow
train_baseline_rl now accepts --initial-capital (default $35K) matching
hyperopt. Argo compile-and-train passes the workflow parameter to the
train-best step. Both hyperopt and training now use consistent capital.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 09:58:51 +02:00
jgrusewski
56373f0941 feat: DQN gems — spectral decoupling, manifold mixup, regime replay, family hyperopt
Phase 1: Remove 5 DQNConfig boolean flags (use_soft_updates, use_iqn,
enable_q_value_clipping, use_cvar_action_selection, use_count_bonus).
All features now unconditionally active — no dead toggle branches.

Phase 2: Add spectral decoupling (L2 on Q-value logits, Pezeshki 2021)
and manifold mixup (Beta-sampled distribution interpolation with atomic
barrier sync) directly in C51 CUDA loss kernel. Zero CPU involvement.

Phase 3: Tag Experience transitions with market regime (ADX/CUSUM GPU
classifier kernel). Add regime-biased PER sampling via rejection with
IS weight correction. Decay factor controls cross-regime bleeding.

Phase 4: 6 family intensity scalars for hyperopt (adversarial,
regularization, augmentation, loss shaping, ensemble, causal). Scales
34 generalization params through 6 PSO dimensions instead of 34.
Search space: 24D → 30D (families additive, individual params kept).

20 files changed, +563/-69 lines. Full workspace compiles clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 00:39:23 +02:00
jgrusewski
9c17b2708d fix(examples): evaluate_baseline bf16 closure types for GpuBacktestEvaluator
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:53:17 +02:00
jgrusewski
be8f3310d5 feat: gradient stability + 9-agent audit bug fixes
Per-component gradient clipping:
- 2 new CUDA kernels (dqn_clipped_saxpy, dqn_clip_grad)
- CQL gradient isolated into separate scratch buffer
- Budget allocation: C51=70%, CQL=15%, IQN=10%, Ens=5%
- Dynamic budget — inactive components' share goes to C51

Spectral norm extended to all 10 weight matrices:
- Was trunk-only (W_s1, W_s2), now covers all heads
- 16 u/v power iteration buffers, batched GOFF sync-back
- Uniform sigma_max, end-to-end Lipschitz bounded

Bug fixes from 9-agent audit:
- Backtest episode reset: all 8 fields (was 5), max_equity updated before floor check
- gradient_clip_norm unified: 10.0 everywhere (was 10.0 vs 1.0)
- entropy_coefficient: Option<f64> → f64, single default 0.001
- Close price: unwrap_or(1.0) → direct indexing (no silent wrong rewards)
- step_count: only increments during training (was incrementing on inference)
- Quantile loss: single CUDA context (was 3×), silent fallbacks removed
- MaybeNoisyLinear: single-variant enum removed → direct NoisyLinear
- Budget fractions: exported as pub(crate) constants, referenced not hardcoded

Monitoring:
- Per-component gradient Prometheus gauges (C51 raw, combined)
- Diagnostic logging every 1000 steps

Tests: 7 new gradient budget tests, 1 gradient bounds smoke test
All 488 tests pass (359 ml-dqn + 129 ml)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:52:44 +01:00
jgrusewski
69415f1a97 fix: remove use_noisy reference in train_baseline_rl example
Noisy nets are now always on (mandatory feature since config unification).
Epsilon start always uses the noisy-nets-aware default (0.05).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:11:17 +01:00
jgrusewski
8777288880 feat: v_range computed from reward_scale + gamma — config flows HP → DQNConfig → GpuConfig
Task 4: Add `reward_scale` field (default 10.0) to DQNHyperparameters with
`computed_v_min()`/`computed_v_max()` methods. Formula:
v_range = (reward_scale / (1 - gamma) * 1.2).clamp(20, 300).
conservative() now computes v_min/v_max = +-240 (was hardcoded +-50).
Hyperopt adapter uses same formula instead of hardcoded max_abs_reward.

Task 5: Verified DQNConfig receives v_min/v_max from DQNHyperparameters
in constructor.rs (lines 285-286). Chain intact.

Task 6: Verified GpuDqnTrainConfig receives v_min/v_max from DQNConfig
in fused_training.rs (lines 169-170). Chain intact.

All Default impls updated: DQNConfig, GpuDqnTrainConfig,
ExperienceCollectorConfig, DqnBacktestConfig — zero hardcoded v_min/v_max.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 10:16:26 +01:00
jgrusewski
04d8802c94 refactor: remove 8 always-on use_ booleans — features are mandatory
Remove use_double_dqn, use_dueling, use_per, use_branching,
use_distributional, use_noisy_nets, use_huber_loss, and use_cql
from DQNConfig, DQNHyperparameters, and DqnParams structs.

These features are always enabled (Rainbow DQN standard). The boolean
flags were dead code — every constructor set them to true, and the
only code paths that set them to false were in tests that disabled
features for simplicity. With the fields removed, the features are
unconditionally active, eliminating ~490 lines of dead configuration.

Key changes:
- Struct field declarations removed from 3 core config structs
- Conditional branches (if use_X { ... } else { ... }) simplified:
  dueling/branching/PER network creation is now unconditional
- Checkpoint metadata hardcodes "true" for backward compatibility
- Hyperopt search space index 11 (use_branching) fixed at 1.0
- TOML/YAML config files cleaned of removed fields
- Tests that toggled these flags updated or rewritten

45 files changed, -487 net lines. Zero new test failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 09:45:54 +01:00
jgrusewski
d1b183c6dc feat: add Phase Risk (P4) to four-phase hyperopt — 8D risk parameter search
Add HyperoptPhase::Risk variant that fixes dynamics + architecture from
Phase 2 best params and searches only risk parameters (8D):
kelly_fractional, dd_threshold, loss_aversion, time_decay_rate,
q_gap_threshold, w_dsr, w_pnl, w_dd.

CLI: --phase risk (alongside fast, full, reward)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:51:49 +01:00
jgrusewski
727ed0d43b refactor: remove use_qr_dqn — IQN always enabled via iqn_lambda
use_qr_dqn was a hacky toggle that gated the IQN dual-head behind
a num_atoms threshold. IQN is now always enabled when iqn_lambda > 0
(default 0.25). C51 remains the main loss; IQN is the auxiliary head
for CVaR risk quantification.

Removed use_qr_dqn from: DQNParams, DQNHyperparameters, fused_training,
hyperopt adapter, all tests, all examples.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:46:41 +01:00
jgrusewski
eabc7ae36b feat: Phase 3 reward tuning — fix dynamics+architecture, search 7 reward dims
Three-phase hyperopt pipeline:
  Phase 1 (fast): fix architecture + reward, search dynamics (~17D)
  Phase 2 (full): fix dynamics, search architecture (~5D)
  Phase 3 (reward): fix dynamics + architecture, search reward weights (7D)

Phase 3 is the fastest (~10s/trial) since only experience collection
changes. CLI: --phase reward --hyperopt-params phase2_results.json

Argo template runs all 3 phases sequentially. Phase 2 writes to
_phase2_results.json, Phase 3 writes final _hyperopt_results.json.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 21:45:00 +01:00
jgrusewski
39c636a0cb fix: update evaluate_baseline for new backtest evaluator API
evaluate_dqn/evaluate_dqn_graphed now take (weights, branching_weights,
DqnBacktestConfig) instead of (weights, network_dims). This was the
compile error breaking all H100 CI runs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 15:16:11 +01:00
jgrusewski
286839b499 refactor: remove dead --phase single code path
Two phases only: fast (default) and full. No legacy 31D single-phase
search — it's a dead path that was never the right choice.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 10:37:31 +01:00
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
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
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
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
91e351b07a fix: resolve TODO stub + RegimeConditional fused training support
- GpuTrainResult::gpu_tensor_to_cuda_slice: implement via CudaSlice::clone()
  (was TODO returning hard error, blocking training guard loss readback)
- DQNAgentType::primary_dqn_mut(): new method returning &mut DQN for both
  Standard and RegimeConditional variants
- FusedTrainingCtx: replace as_standard_mut() with primary_dqn_mut() at
  all 3 call sites (EMA target update, IQN EMA, VarStore sync)
- fused_post_step/fused_post_step_no_ema/fused_post_step_gpu_bookkeeping:
  delegate to primary_dqn_mut() instead of returning error for RegimeConditional
- train_baseline_rl: add error chain logging for fold failures

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