Commit Graph

4015 Commits

Author SHA1 Message Date
jgrusewski
d2443c174d docs(generalization): add Task 31 — Temporal Causal Bottleneck (gem of gems)
2D information bottleneck that makes memorization structurally impossible.
All 14 market features compressed through [market_dim → 2] linear + tanh
before reaching the Q-network. With only 2 floats (~100 bits), the network
physically cannot encode which specific price sequence it's looking at —
it can only encode abstract market CONDITIONS.

Attacks root cause of IS/OOS gap at the architecture level. All other 28
techniques fight symptoms; this one eliminates the disease. Provides free
interpretability (plot 2D activations) and a built-in generalization
diagnostic (IS/OOS distribution overlap in bottleneck space).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 09:18:55 +02:00
jgrusewski
80bc8a2ee1 feat(generalization): #21 stochastic depth — CUDA Graph compatible layer dropout
Per-layer drop with 20% probability during training. Each hidden layer
(h_s1, h_s2, h_v) is independently scaled by 0 (dropped) or 1/(1-p)
(kept, expected-value correction). Only applied to online forward pass
on current states — target and Double-DQN passes use full network.

Implementation:
- New stochastic_depth_scale kernel in dqn_utility_kernels.cu
- Per-layer scale buffer [3] f32 — written by host before each graph
  replay (CUDA Graphs capture addresses, not contents)
- Scale kernel inserted in launch_cublas_forward after online Pass 1
- update_stochastic_depth_mask() generates random 0/keep scales per step
- At inference (experience collection), all layers active (no drop)

Forces every layer to produce useful features independently — prevents
deep compositional memorization where removal of any single layer
would collapse the output. First RL trading application of stochastic
depth (proven in Vision Transformers, novel in DQN).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 09:17:25 +02:00
jgrusewski
c83ceeda54 feat(generalization): #11 time-reversal + #17 counterfactual regret (CUDA)
Time-reversal (#11): 20% of episodes (index % 5 == 0) played backwards
in state_gather kernel. Reversed bar indexing forces the model to rely
on instantaneous features rather than temporal trajectory patterns.
If it can't trade backwards data, it memorized sequence artifacts.

Counterfactual regret (#17): at trade completion, compute PnL for all
9 exposure levels and blend reward with regret (taken - best_possible).
Default 30% regret / 70% raw PnL. Normalizes rewards across regimes —
a bad trade in a bad market has low regret, a bad trade in a good
market has high regret. From game theory (CFR).

Both fully in CUDA env_step/state_gather kernels. No CPU paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 09:05:44 +02:00
jgrusewski
3c0fbcc5e3 feat(generalization): #10 mirror universe + #19 position entropy (CUDA kernels)
Mirror universe (#10): alternate odd/even epochs with negated return
features + inverted exposure actions in CUDA experience kernels.
Doubles effective data diversity without extra market data. The model
must learn direction-invariant structure — if it only works on normal
data but fails on mirrored, it learned directional bias.

Position entropy (#19): GPU-resident position visit histogram [N, 9]
incremented at each timestep in env_step kernel. At episode end,
computes H(histogram) / log(9) and adds scaled bonus to reward.
Forces the model to explore all 9 exposure levels rather than
degenerating to "always flat" or "always long" strategies.

Both fully in CUDA — zero CPU-side computation. Wired through
ExperienceCollectorConfig → kernel args → TOML [generalization].

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 08:58:50 +02:00
jgrusewski
7a7197cbfd feat(generalization): 7 gems & pearls — CUDA kernels + config + TOML wiring
Phase 1 generalization techniques to close IS/OOS Sharpe gap (+0.57→-1.30).
All techniques fully wired from DQNHyperparameters → TOML [generalization]
section → ExperienceCollectorConfig → CUDA kernel arguments.

New techniques implemented:
- #13 Vol normalization: divide return features by realized vol (state_gather)
- #18 Asymmetric DD loss: extra penalty on Q-overestimation in drawdown
      (mse_loss_batched + c51_loss_batched)
- #22 Feature noise: N(0, scale) per feature via LCG RNG (state_gather)
- #23 Causal feature masking: random 30% feature subset zeroed per epoch
      (state_gather, mask uploaded from Rust)
- #24 Anti-intuitive LR: 3x LR when Sharpe good, 0.3x when bad (Rust-only)
- #25 Trade clustering: CV(inter-trade intervals) penalty via ps[3:6]
      (env_step, uses reserved portfolio state slots)
- #27 Ensemble disagreement: Q_target -= weight * ensemble_std
      (mse_loss + c51_loss, buffer allocated for future ensemble wiring)

Also includes 30-task plan doc with all 28+ techniques across 3 phases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 08:51:05 +02:00
jgrusewski
37cb034eed feat(generalization): periodic shrink-and-perturb every 20 epochs
Kills memorized weights by blending 85% current + 15% Xavier random.
Surviving weights encode robust features. Memorized artifacts die.
Configurable: shrink_perturb_interval, alpha, sigma.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 23:43:33 +02:00
jgrusewski
ae99567362 feat(generalization): CQL alpha 0.1→1.0, gradient budget 15%→25%
CQL was effectively disabled (0.1 alpha × 0.15 budget = 1.5% of gradient).
Now: alpha=1.0 × 0.25 budget = 25% of gradient enforces conservatism.
C51 reduced from 70% to 60% to accommodate.

CQL penalizes Q-values for actions not in the data — directly prevents
the model from being "confident but wrong" on OOS state-action pairs.

Hyperopt search range updated: [0.0, 1.0] → [0.5, 5.0].

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 23:40:30 +02:00
jgrusewski
c3ea0e57d1 feat(generalization): domain randomization — per-epoch jitter on sim params
Prevents memorization of fixed simulation parameters that exist only
in training. Each epoch randomizes:
- tx_cost_multiplier: U[0.5, 2.5] (was fixed 1.0)
- spread: U[0.5x, 3.0x] base spread
- fill_ioc_prob: U[0.65, 0.95] (was fixed 0.85)
- fill_limit_min/max: randomized ranges
- spread_cost: U[0.3, 0.8] fraction
- Episode starts: ±25% stride jitter (was deterministic)
- Episode length: ±25% base (was fixed)

Controlled by enable_domain_randomization flag (default: true).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 23:38:05 +02:00
jgrusewski
3e29e1e4a8 docs: 8 novel generalization techniques plan (domain rand + CQL + adversarial + curiosity Q-penalty + counterfactual + regime-adversarial + feature filtering)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 23:32:12 +02:00
jgrusewski
85d9b4c03e docs: generalization gap action plan (domain randomization + CQL + multi-fold)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 23:24:10 +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
58e09a52ac fix(ci): train-dqn uses compile-and-train (no pre-built binary dependency)
Was referencing training-pipeline which needs binary-tag for pre-built
binaries from GitLab packages. Now references compile-and-train which
compiles from source then trains. No stale binary risk.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:44:08 +02:00
jgrusewski
0914ad0315 feat(ensemble): GPU-native ensemble reduce kernel, remove CPU fallbacks
- New ensemble_reduce_kernel.cu: fused sigmoid→mean→weighted-sum in
  one kernel, single block, thread-per-model. Pure f32, no bf16.
- build.rs: nvcc compilation without --use_fast_math
- aggregate_logits_gpu: loads cubin, uploads raw f32 buffers, launches
  kernel, reads back single scalar. No GpuTensor/ActivationKernels.
- Removed aggregate_logits_cpu (dead code, GPU-only system)
- ml-explainability: removed unreachable dead code after stub return,
  prefixed unused vars. Zero warnings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:35:37 +02:00
jgrusewski
bbfb39f066 fix(infra): add DNS + GitLab + pushgateway + registry to training netpol
argo-training-workflow was missing DNS egress (port 53), causing
MinIO hostname resolution to timeout. Also missing GitLab SSH (2222),
GitLab API (8181), Pushgateway (9091), and Registry (5000).

Now matches the gpu-test-workflow netpol which works correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:33:28 +02:00
jgrusewski
f29aadffb0 fix(tests): stale assertions for dd_threshold, epochs, reward_scale, sigma
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:09:38 +02:00
jgrusewski
44248f11c7 fix(tests): update 22D→24D assertions + production epochs 100→200
6 test assertions hardcoded 22D search space, now 24D (added w_dd,
dd_threshold). Production profile test expected epochs=100, now 200.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:56:55 +02:00
jgrusewski
769e3d3ad9 ci: disable auto GPU tests on push (manual only, H100 is expensive)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:53:01 +02:00
jgrusewski
45473a45f0 fix(tests): action_space fallback 5→9, gpu_smoketest adam_epsilon
- metrics.rs: fallback action_space was hardcoded 5 (old non-branching).
  Changed to 9 (exposure levels). Branching is always active.
- smoke_test_real_data: assertion updated to accept >=9 action space
  (81 with factored counts, 9 for short runs with empty factored counts)
- gpu_smoketest: added missing adam_epsilon field

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:47:14 +02:00
jgrusewski
9dd48621ea fix(tests): update integration tests for f32 pipeline + adam_epsilon
8 test files had stale types from the bf16→f32 conversion:
- gpu_smoketest: missing adam_epsilon in DQNConfig
- gpu_backtest_validation: closure params bf16→f32
- gpu_kernel_parity_test: market data, weight readback bf16→f32
- gpu_per_integration_test: weights readback bf16→f32
- target_update_tests: varstore register bf16→f32
- smoke_test_real_data: market buffers bf16→f32
- activation_tests, dropout_scheduler_tests: forward() signature change

These tests only compile with --features cuda (CI path), which is why
they passed locally with cargo test --lib.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:32:04 +02:00
jgrusewski
57a91567b4 fix(ci): PTX cache invalidation includes build.rs + all kernel dirs
The script only hashed .cu/.cuh in crates/ml/. Missed:
- build.rs changes (removing --use_fast_math → stale cubins cached)
- crates/ml-dqn/src/*.cu (replay buffer, seg tree kernels)
- crates/ml-core/src/cuda_autograd/*.cu
- crates/ml-ppo/src/cuda_nn/*.cu

Now hashes ALL kernel sources + ALL build.rs files. Any change to
nvcc flags or kernel source invalidates the entire PTX cache.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:01:03 +02:00
jgrusewski
3d37a3527f fix(ci): update Argo DQN training workflow for f32 pipeline
- train-dqn: hyperopt-trials 30→50, hyperopt-epochs 8→20,
  train-epochs 80→200 (matches dqn-production.toml, 24D search space)
- training-workflow: fixed --max-steps-per-epoch bug (was passing
  train-epochs as max-steps, limiting training to 80 steps/epoch
  instead of 80 epochs). Changed to --epochs.
- Added FOXHUNT_TRAINING_PROFILE env var pointing to
  dqn-production.toml so the training binary uses the correct config
  (adam_epsilon=1e-8, cosine LR, drawdown penalty, etc.)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:56:53 +02:00
jgrusewski
501e301f88 feat(hyperopt): add w_dd and dd_threshold to 24D search space
Previously fixed at w_dd=1.0, dd_threshold=0.02. Now PSO can search:
- w_dd: [0.0, 5.0] — drawdown penalty weight (0=disabled, 5=aggressive)
- dd_threshold: [0.01, 0.15] — drawdown % before penalty starts

Search space expanded from 22D to 24D. Backward compatible: vectors
shorter than 24 elements use defaults via bounds check.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:35:29 +02:00
jgrusewski
acbc3c896e feat(training): continuous drawdown penalty — smooth ramp from dd_threshold to floor
The model had Omega>1 (profitable trade selection) but MaxDD 41-50%
(catastrophic drawdown timing), causing negative total returns despite
winning trades. Root cause: zero reward gradient between 0% and 25% DD.
The only drawdown consequence was the hard capital floor at 25% which
terminates the episode with reward=-10.

Fix: compute_drawdown_penalty() in trade_physics.cuh — smooth linear
ramp from 0 at dd_threshold (2%) to -5.0 at the capital floor (25%).
Applied every step, not just at trade exit, so the model learns to
reduce position size DURING drawdowns.

- Added compute_drawdown() and compute_drawdown_penalty() to trade_physics.cuh
- Wired dd_threshold and w_dd from config through to CUDA kernel
- Added to all 3 TOML profiles (smoketest, localdev, production)

Early results: MaxDD dropped from 88.9% → 36.6% by epoch 3.
Q-values went negative in drawdown states — the model is learning.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:23:51 +02:00
jgrusewski
7bfceda441 fix(metrics): unify return basis — total_return from same series as Omega
total_return_pct used simple pnl/initial_capital (arithmetic).
Omega/Sharpe/Sortino used pnl/current_equity (compounding per-trade).
These diverge after drawdown: Omega>1 but negative total_return was
mathematically impossible yet appeared in every run.

Fix: total_return_pct = ∏(1+r_i)-1 using the same per-trade returns
that feed Omega. Now Omega>1 ⟺ positive total_return. Always.

Single-pass equity curve + returns computation. No separate loops.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:03:48 +02:00
jgrusewski
ed7d070a29 fix(metrics): per-trade returns relative to current equity, not initial capital
Returns were computed as pnl/initial_capital for ALL trades. After a
49% drawdown (equity=51K), a $500 loss was recorded as 500/100K=0.5%
instead of 500/51K=0.98%. This deflated losses after drawdown,
inflating Omega (5.5x) while total return was -9.5% — contradictory.

Fix: track running equity and compute each trade's return relative to
equity AT TIME OF TRADE. Now Omega, Sharpe, Sortino all reflect the
actual impact of each trade on the current portfolio.

This was the 'sneaky bug' causing Omega>5 with negative returns.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:54:48 +02:00
jgrusewski
6060937a78 fix(hyperopt): 2GB/trial memory leak — reuse CUDA context, fork stream
Root cause: each trial created a fresh MlDevice (new CudaStream) but
the primary CudaContext was shared. cudaFreeAsync returns memory to
the allocating STREAM's pool, not the context's global heap. New
trial's new stream couldn't reclaim the old stream's freed blocks.

Evidence: Trial 0 leaked 2063MB, Trial 2 leaked 3212MB. Available
RAM dropped 10.4GB→5.2GB over 3 trials. Later trials ran on a
memory-starved system, explaining systematic performance degradation.

Fix: fork a new stream from the shared device instead of creating
a fresh MlDevice. Forked streams share the same context and async
memory pool — free_async blocks are immediately available to the
next trial's allocations.

Also fixed: TRIAL_SUMMARY best_epoch now shows actual best, not
epochs_completed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:39:10 +02:00
jgrusewski
b59e2f7847 fix(hyperopt): TRIAL_SUMMARY best_epoch was epochs_completed, not actual best
best_epoch always showed 20 (total epochs) instead of the epoch where
the best val_loss checkpoint was saved. Misleading — looked like the
model never peaked. Now correctly reports actual_best_epoch from the
trainer's checkpoint tracker.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:29:42 +02:00
jgrusewski
8a42af0bc6 refactor: LHS is the correct algorithm for small budgets, not a fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:18:47 +02:00
jgrusewski
3e1f3fd0cc fix(hyperopt): LHS fallback when budget < 2× swarm size
With 10 trials and n_initial=5, PSO got 5 remaining evals for a
20-particle swarm. Only 5 of 20 particles were evaluated before the
budget observer killed the run — 75% of the swarm had no cost value.
PSO can't compute a proper gbest from partial data.

Fix: when remaining_trials < n_particles × 2, skip PSO entirely and
use additional LHS samples instead. LHS gives better space coverage
than an incomplete swarm iteration.

For 10 trials: 5 initial LHS + 5 additional LHS = 10 independent
space-filling samples. Much better than 5 LHS + 5 broken PSO particles.

PSO still runs when budget is sufficient (≥40 remaining for 20 particles).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:12:47 +02:00
jgrusewski
ad4e22e87d fix(hyperopt): PSO premature convergence — tune inertia/cognitive/social
Argmin defaults: inertia=0.72, cognitive=1.19, social=1.19
Problem: social >> inertia causes particles to collapse toward the
global best immediately. With only 1 LHS trial as the initial best,
the entire swarm clusters around that point and can't explore.
Every PSO trial was worse than the random LHS trial.

Fix: inertia=0.9 (high momentum, maintains exploration),
cognitive=1.5 (strong personal best memory),
social=0.8 (weak global pull, prevents premature convergence).

Applied to both sequential and parallel optimizer paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:05:16 +02:00
jgrusewski
ba97a6e819 fix(metrics): replace hardcoded √252 annualization with √N
Sharpe/Sortino were annualized with √252 (daily trading assumption).
For intraday strategies with hundreds of trades per eval window, this
inflated magnitudes ~10x, causing Sharpe=-1.4 while Omega=10.9 on
the same return series — mathematically contradictory.

Fix: scale by √N where N = actual number of returns in the series.
This gives the Sharpe of the evaluation window, not a synthetic annual.

- evaluation/metrics.rs: Sharpe, Sortino, Calmar all fixed
- trainer/metrics.rs: val_loss Sharpe (compute_validation_loss)
- ppo.rs: epoch Sharpe proxy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:42:43 +02:00
jgrusewski
1b118809a3 docs: fix outdated objective function weights in hyperopt docstring
The docstring claimed 25% HFT activity score. The actual code uses 5%
(-0.05 coefficient). The real objective is 60% risk-adjusted composite
(Sharpe+Sortino+Calmar+Omega). Quality over quantity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:33:21 +02:00
jgrusewski
99cf25fd20 config: production min_epochs_before_stopping 100→80
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:23:53 +02:00
jgrusewski
ae704a7b73 config: production epochs 100→200, min_epochs_before_stopping 50→100
H100 has the compute budget — 200 epochs with cosine decay gives the
model full room to learn through C51 transition and converge.
Early stopping can't fire before epoch 100.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:22:21 +02:00
jgrusewski
7be192a737 fix(config): align H100 production config with f32 pipeline
- adam_epsilon: added 1e-8 (was missing → fell back to old 1e-3)
- lr_decay_type=2: cosine decay 1e-4→1e-5 over 100 epochs
- huber_delta: 1.0 (was missing → fell back to 100.0)
- gradient_clip_norm: 1.0 (was 10.0, too loose for mixed-precision)
- reward_scale: 1.0 (was 10.0, amplifies bf16 rounding)
- spectral_norm_sigma_max: 1.5 (was 3.0)
- q_clip: ±50 (was ±200)
- min_epochs_before_stopping: 50 (was 10, too early)
- curiosity_weight: 0.1 (was missing)
- Removed duplicate gradient_clip_norm in [advanced] vs [training]
- Removed [branching] section (sizes come from code defaults)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:20:45 +02:00
jgrusewski
63b0cb5f46 revert: remove cosine decay from smoketest (3 epochs too short)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:19:32 +02:00
jgrusewski
3ed9d5a300 config: cosine LR decay for smoketest too (1e-4→1e-5 over 3 epochs)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:18:47 +02:00
jgrusewski
332b3eb120 feat(training): cosine LR decay in TOML profile, min_epochs_before_stopping=80
- Added lr_decay_type (0=constant, 1=linear, 2=cosine) and lr_min to
  the TOML training profile system. Wired through apply_to() with
  total_steps derived from epochs.

- dqn-localdev.toml: lr_decay_type=2 (cosine 1e-4→1e-5 over 200 epochs),
  min_epochs_before_stopping=80 (was 50, gives model more room to recover
  after C51 transition).

- Smoketest config unchanged (lr_decay_type not set → default Constant).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:16:47 +02:00
jgrusewski
f6caf61252 fix(training): 4 metric quality fixes — buffer, episodes, hyperopt, Q-stats
1. Localdev config: gpu_n_episodes 32→64, gpu_timesteps 100→200
   (12,800 exp/epoch, 4x more → stable Sharpe estimation)

2. Localdev config: buffer_size 10K→50K, min_replay_size 100→500
   (fills over ~4 epochs instead of <1, reduces overfitting)

3. Hyperopt: added CampaignConfig::dqn_localdev() (10 trials × 20 epochs),
   updated test_local_hyperopt to use it with env var overrides
   (FOXHUNT_HYPEROPT_TRIALS, FOXHUNT_HYPEROPT_EPOCHS)

4. Q-value range: was [X,X] every epoch because:
   - train_step.rs Q-stats code was DEAD (training loop uses
     fused.run_full_step() directly, not self.train_step())
   - Only sampled 10 experiences for Q-stats (tiny batch → tight range)
   Fix: reduce_current_q_stats() reads the training batch's q_out_buf
   directly (64 samples, no extra forward pass), wired into the training
   loop's inner step. q_stats_kernel.cu converted to f32 arithmetic.
   Now shows real variation: [-5.25, 5.03] instead of [2.80, 2.80]

Removed dead Q-estimation code from train_step.rs (was never reached
in the fused GPU training path).

11/11 smoke tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:06:09 +02:00
jgrusewski
e9e2e87a64 fix(hyperopt): max_steps_per_epoch by GPU tier, adam_epsilon 1e-3→1e-8
- Hyperopt adapter now sets max_training_steps_per_epoch:
  RTX 3050 (≤40GB) = 200 steps, H100 (≥40GB) = 2000 steps.
  Without this, each trial trained the full dataset (2917 steps/epoch)
  making hyperopt 11x slower than necessary on local GPU.

- adam_epsilon default 1e-3→1e-8 everywhere (conservative(), DQNConfig).
  The old 1e-3 was a BF16 workaround (bf16(1e-8)=0 → div-by-zero).
  Adam is now f32, so standard 1e-8 is correct.

- Early stopping enabled in dqn-localdev.toml (patience=20).

Hyperopt: 2 trials × 5 epochs in 132s (was ~20min). Zero NaN.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:22:16 +02:00
jgrusewski
948ffa7c67 fix(monitoring): f32 epoch accumulators, per-branch diversity, remove dead kernels
- acc_buf bf16→f32: epoch gradient/loss accumulators were bf16, causing
  avg_grad to be quantized to 0.895 every epoch. Now f32, shows real
  variation (0.776→0.783→0.788).

- EPOCH_DIAG warn!→info!: diagnostic logging, not a warning condition.

- Removed legacy training_guard_check + training_guard_accumulate kernels
  (dead code, replaced by fused training_guard_check_and_accumulate).

- Added dqn-localdev.toml: 200-epoch RTX 3050 profile for extended runs.

200-epoch run completed successfully:
  Best Sharpe: +6.43 at epoch 107
  Final: Sharpe=+2.05, PF=1.35, Return=+1052%, 0 NaN

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:49:38 +02:00
jgrusewski
b39790acff fix(monitoring): real per-branch action diversity from GPU, not deterministic OrderRouter
The action diversity metric was always 9/45 because:
1. GPU monitoring only tracked exposure (9 levels), discarding order+urgency
2. Training loop re-derived order+urgency via deterministic route_action()
3. Action space was hardcoded as 45 (should be 9×3×3=81)

Fix:
- Expanded monitoring_reduce kernel to decode and count all 3 branches
  (exposure[9], order[3], urgency[3]) from factored action indices
- Expanded summary buffer 16→24 bf16 for the extra counts
- MonitoringSummary now has order_counts[3] + urgency_counts[3]
- Training loop feeds raw GPU counts into monitor (no OrderRouter)
- Diversity = active_exp × active_ord × active_urg / (b0 × b1 × b2)
- Branch sizes from agent.branch_sizes(), not hardcoded
- Entropy computed over exposure distribution with correct normalization

Before: "Action diversity=9/45 (20.0%)" — every epoch, every run
After:  "Action diversity=18/81 (22.2%) — exposure=3/9 order=3/3 urgency=2/3"
        varies per epoch, reflects actual model behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:34:36 +02:00
jgrusewski
68804a1a51 fix(cuda): remove --use_fast_math, eliminate cross-stream NaN race, f32 rewards/dones
Three root causes of sporadic NaN during training:

1. --use_fast_math (nvcc) breaks IEEE 754 NaN semantics: fmaxf(NaN,x)
   returns NaN instead of x, isnan()/isinf() compile to false.
   Replaced with --ftz=true --fmad=true --prec-div=true --prec-sqrt=true
   across all 4 build.rs (ml, ml-dqn, ml-ppo, ml-core).

2. Cross-stream race: replay buffer wrote batch data on the device's
   original stream while the trainer read it on a forked stream.
   Fixed by passing the forked stream to the DQN agent via agent_device,
   so all GPU components share a single CUDA stream (zero sync overhead).

3. Rewards/dones stored as bf16 in replay buffer caused done=0xFFFF NaN.
   Converted entire rewards/dones pipeline to f32: experience collector,
   replay buffer storage, nstep kernel, loss/grad kernels.

Also:
- Removed fast_isnan/fast_isinf/fast_isfinite wrappers — standard
  isnan/isinf/isfinite work correctly without --use_fast_math
- Updated dqn-smoketest.toml: lr=1e-4, epsilon=1e-8 (f32 Adam values)
- Removed debug printfs from gather kernels
- Added curiosity_weight to training profile system
- Cleaned up smoke_params() inline overrides

11/11 smoke tests pass, 5/5 stress runs of 50-epoch test pass,
359/359 ml-dqn + 895/895 ml unit tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:18:37 +02:00
jgrusewski
e4bca3fad1 debug(bf16): DONE_NAN raw=0xFFFF — all bits set in done flags
Added DONE_NAN printf: raw bf16 bits are 0xFFFF (all 1s = bf16 NaN) for
intermittent done corruption. This is NOT from normal bf16 writes (0/1).

Key findings:
- Experience kernel writes valid done flags (debug printf confirmed)
- compute-sanitizer initcheck: 0 uninitialized reads
- Gather bounds check: no OOB indices detected
- The 0xFFFF pattern (all bits set) suggests memset(-1) or uninitialized memory
  from a PREVIOUS allocation that was freed and reallocated

Next investigation: check if cudarc's alloc_zeros properly zeroes the dones
buffer, or if the GPU memory allocator returns memory from a previous freed
allocation that had 0xFFFF values (e.g., from a debug sentinel).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:28:35 +02:00
jgrusewski
b3632afe0a debug(bf16): gather bounds check + NaN source traced to done=nan in replay buffer
Added bounds checking to gather_bf16 and gather_u32 kernels (capacity param).
Prevents OOB reads from corrupt PER segment tree indices.

Debug printf in MSE loss kernel confirms:
- done=nan (bf16 replay buffer dones contain NaN bits)
- is_weight sometimes negative (reduce_max_f32 atomicMax bug with neg floats)
- avg_mse=nan cascading from done=nan through Bellman target

The experience kernel writes valid done flags (0/1 as bf16). compute-sanitizer
initcheck: 0 errors (no uninitialized reads). racecheck: pending.
The NaN enters between experience write and loss kernel read — possibly from
the segment tree sampling a slot with corrupt priority (NaN priority → NaN
tree sum → traversal lands on wrong leaf → reads wrong data).

Next: convert dones + rewards to f32 throughout (same pattern as IS-weights).
This eliminates ALL remaining bf16 data paths in the training pipeline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:17:04 +02:00
jgrusewski
541463e48a fix(bf16): f32 IS-weights (permanent) + debug NaN printf
Re-applied f32 IS-weights permanently. The f32_to_bf16_cast kernel had a
manual RNE rounding overflow bug producing NaN for edge-case values.
Fixed cast to use __float2bfloat16 intrinsic, but NaN persists from
a DIFFERENT source: done=nan in replay buffer (not IS-weight).

Debug printf in MSE loss kernel shows:
- done=nan (bf16 replay buffer corruption)
- is_weight negative (should be [0,1] — reduce_max atomicMax bug with negative floats)
- avg_mse=nan cascading from done=nan through Bellman target

Root cause: replay buffer done flag corruption — likely a buffer overwrite
from an adjacent allocation. Need compute-sanitizer to find the OOB write.

895/895 unit + 359/359 ml-dqn tests pass.
Smoke tests: intermittent (NaN from corrupted done flags in replay buffer).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 12:53:05 +02:00
jgrusewski
875f263ec9 feat(bf16): f32 backward dX scratch + bf16 staging — eliminates dX truncation
Backward pass dX computation now uses f32 scratch buffers with bf16
staging for the backward chain. Pattern: f32 GemmEx → relu_mask (f32) →
f32→bf16 cast → next layer reads bf16 dY.

Changes:
- 6 bw_d_h_* scratch buffers: CudaSlice<half::bf16> → CudaSlice<f32>
- New bw_dy_bf16_staging: shared bf16 buffer for layer transitions
- backward_fc_layer dX: gemmex_bf16 → gemmex_bf16_acc_f32 (f32 output)
- launch_dx_only: gemmex_bf16 → gemmex_bf16_acc_f32 (f32 with beta)
- relu_mask_kernel: reads/writes f32 (no bf16 clamp needed)
- f32_to_bf16_cast_kernel: ±500 clamp at type boundary (in backward_kernels.cu)
- cast_dx_to_staging: f32 scratch → bf16 staging per layer
- IQN/ensemble backward: bf16→f32 cast for dX input, f32→bf16 for dY output
- bw_d_h_s2_as_bf16(): attention backward receives bf16 via staging

Hyperparameters updated for f32 Adam:
- learning_rate: 1e-5 → 1e-4 (updates must exceed bf16 shadow step ~1e-3)
- adam_epsilon: 1e-3 → 1e-8 (standard Adam, bf16 workaround no longer needed)
- grad_norm NaN skip kept as defense-in-depth (source still under investigation)

895/895 unit + 359/359 ml-dqn tests pass.
7-11/11 smoke tests (intermittent NaN from unknown source — NOT backward dX).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 12:17:04 +02:00
jgrusewski
05a184168f fix(bf16): restore relu_mask BW_SAFE_MAX ±500 clamp (NOT a NaN guard)
The guard cleanup agent incorrectly removed the backward dX overflow clamp
from relu_mask_kernel. This clamp is a LEGITIMATE bf16 overflow prevention
at the type boundary — identical to the forward-pass bias kernel clamping.

The dX GemmEx writes bf16 output. When the f32 accumulated sum exceeds
bf16 max (~65504), it writes Inf. The relu_mask ±500 clamp converts Inf
to a finite value, preventing cascade through the backward chain.

Also reverted the outside-graph params cast (the in-graph capture is correct).

Remaining issue: grad_norm drops to 0 in epoch 2+ with f32 master weights.
The model learns in epoch 1 (grad_norm=0.004) but stagnates in epoch 2+.
Investigating CUDA graph replay of f32→bf16 params cast.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:20:56 +02:00
jgrusewski
9a81d1a931 feat(bf16): f32 master weights + Adam moments (NVIDIA AMP pattern)
Implements the standard mixed-precision training pattern:
- f32 master weights (params_buf, target_params_buf) for Adam precision
- f32 Adam moments (m_buf, v_buf) — eliminates bf16 truncation
- bf16 shadow copies (params_bf16, target_params_bf16) for GemmEx tensor cores
- f32→bf16 cast after Adam step + after EMA target update
- Adam kernel: native f32 arithmetic, no bf16 epsilon/beta workarounds
- EMA kernel: native f32 blending

NaN guard cleanup (4 guards removed):
- grad_norm_kernel: fast_isnan removed (f32 gradients can't NaN from type conversion)
- adam_update_kernel: fast_isnan early-return removed (f32 throughout)
- relu_mask_kernel: BW_SAFE_MAX ±500 clamp removed (no bf16 dX overflow)
- experience_kernels: target_position guard removed (f32 arithmetic)

Kept: 2 legitimate data guards (reward/portfolio_return from market data)

895/895 unit + 359/359 ml-dqn tests pass.
Smoke tests: 7-11/11 (intermittent NaN on long runs — debugging graph capture
of f32→bf16 params cast). Short-running tests all pass consistently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:09:38 +02:00
jgrusewski
09f5f9fb25 feat(bf16): f32 d_logits buffers — native atomicAdd, zero NaN from gradients
d_value_logits, d_adv_logits (+ MSE/CQL scratch): bf16 → f32
- Native atomicAdd(float*) replaces atomicAddBF16 CAS loop
- Eliminates bf16 accumulation overflow in gradient kernels
- Gradient value clamping ±100 removed (unnecessary with f32)
- NaN guards removed from loss kernels

Architecture:
- f32 d_logits for gradient accumulation (atomicAdd-safe)
- bf16 staging buffers (d_value_logits_bf16, d_adv_logits_bf16)
  cast via f32_to_bf16_kernel before backward dW GemmEx
- dqn_saxpy_f32_kernel for gradient blending (MSE+C51 alpha)
- CQL backward uses bf16 staging after f32→bf16 cast

Remaining intermittent NaN (~1/2000 steps on long runs):
- Source: bf16 params_buf weight precision loss → forward pass
- Fix: f32 master weights (next commit)

895/895 unit + 359/359 ml-dqn tests pass.
9-11/11 smoke tests (intermittent NaN on 50-epoch runs).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 10:47:55 +02:00