Commit Graph

221 Commits

Author SHA1 Message Date
jgrusewski
93c77b91b7 refactor(reward): delete 8 behavioral shaping terms in one sweep
Mass deletion of the "v7 gem" reward terms identified in the Phase 1
inventory as category errors — each one rewarded an outcome-adjacent
behavior instead of encoding the underlying physics, and each
empirically hurt validation metrics more than it helped training
stability.

Deleted from experience_kernels.cu and all plumbing (Rust configs,
launch args, hyperopt logs, TOML entries):

  * order_credit_weight        - reward redundant with compute_tx_cost
                                 (order_type_idx already differentiates
                                 fills by order type)
  * risk_efficiency_weight     - reward double-counted drawdown penalty
                                 asymmetrically (only on winners)
  * urgency_credit_weight      - reward was vol-normalized unrealized P&L,
                                 pure rename of core return
  * commitment_lambda          - triple-counted churn + tx_cost
  * w_dsr                      - kernel wrote DSR EMA but no longer added
                                 to reward (dead); removed the EMA
                                 bookkeeping too
  * dsr_eta                    - kernel arg for the deleted DSR EMA
  * position_entropy_weight    - rewarded action-bucket diversity
                                 regardless of outcome; histogram buffer
                                 + zero-init removed too
  * exit_timing_weight         - already inactive (used raw_next future
                                 price, comment-deleted earlier)
  * ofi_reward_weight          - dead plumbing; OFI already passed as
                                 feature through state[OFI_START..]
  * opportunity_cost_scale     - penalized flat when Q-gap wide;
                                 redundant with Q-values themselves

Kernel arg count: experience_env_step_batch shrank from ~55 to ~45 args.
Rust-side config surface reduced correspondingly.

Results on E1 smoke test (20-epoch):
  BEFORE any Phase 2 work:
    Val Sharpe -120 to -150, MaxDD 10-15%, Sharpe_raw -0.39
  AFTER reward_noise + Kelly (both envs) + urgency + this sweep:
    Val Sharpe      -17 to -22       (7× better)
    Val MaxDD       0.27%            (40× better)
    Val Sharpe_raw  ~-0.09           (4× better)
    Training Sharpe_raw  ~0          (stabilized from ±20 swings)
    Final q_gap     0.1712           (highest yet, collapse mechanism fine)

The extreme train-Sharpe swings (+17 one epoch, -13 next) were not
learning dynamics — they were shaping-term noise. Core reward (P&L +
drawdown + churn + holding + tx_cost + Kelly physics cap) gives training
metrics that actually reflect what the model does.

Inventory doc (docs/superpowers/specs/2026-04-21-phase1-reward-inventory.md)
extended with a "better-form taxonomy" section: every deleted gem has
a correct layer it belongs to (physics, feature, diagnostic, gradient-
level regularization — not reward). Kelly cap and Q-target smoothing
are already relocated; others are scheduled per the taxonomy's P1/P2/P3
priority list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 01:40:36 +02:00
jgrusewski
71ae90768d refactor(reward): Kelly sizing from behavioral reward to health-coupled physics cap
Phase 2 second relocation: the kelly_sizing_weight reward penalty
(experience_kernels.cu:1727-1746 — penalized deviation from Kelly-optimal
sizing) is deleted. Kelly is now a physics constraint in trade_physics.cuh:
the environment refuses to let the agent over-lever, not the reward
scoring the agent for matching a formula.

New helper in trade_physics.cuh (shared device function, reusable by
the forthcoming unified env kernel):

  kelly_position_cap(win_count, loss_count, sum_wins, sum_losses,
                     max_position, safety_multiplier)

Applied in experience_kernels.cu between margin cap and execute_trade,
with health-coupled safety multiplier:

  safety = 0.5 + 0.5 × health
  - health=1 (healthy): full Kelly — trust the learned policy
  - health=0 (collapsing): half Kelly — constrain when decisions less
    reliable

Cold-start warmup (critical — otherwise balanced priors yield kelly_f=0
until real trades accumulate, starving Q-learning):

  maturity = min(1.0, total_trades / 10)
  effective_kelly = maturity × kelly_f + (1 - maturity) × 0.5

Early on (0 trades): cap dominated by 50% floor.
As real trades accumulate (10+): pure data-driven Kelly.

Validation env (backtest_env_kernel.cu) does NOT yet get the Kelly cap —
that requires extending its portfolio state or adding a separate
kelly_stats buffer, which naturally belongs in the Phase 3 unified env
kernel refactor. The current asymmetry is a KNOWN temporary — training
is constrained, validation is not — and will be resolved when both
kernels share the same env_step() device function.

Also completes removal of kelly_sizing_weight from all plumbing:
- experience_kernels.cu: kernel arg deleted
- gpu_experience_collector.rs: launch arg, config field, default
- training_loop.rs: hyperparam propagation
- config.rs: field, default, intensity clamp (with tombstone)
- hyperopt/adapters/dqn.rs: log reference
- config/training/*.toml (6 entries across 4 files): orphan configs
  (none were wired to a profile parser field)

Verification:
- cargo check -p ml --lib clean
- E1 smoke test passes: final epoch q_gap=0.1109, health=0.51 (warmup
  floor of 0.5 gives early exploration enough room; floor of 0.25
  was too tight and failed at q_gap=0.0496)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 01:07:07 +02:00
jgrusewski
4bbf6180d1 refactor(reward): relocate reward_noise_scale to health-coupled Q-target smoothing
Phase 2 kick-off from the env-unification design. First relocation: the
reward_noise_scale field that perturbed training rewards is deleted, and
its regularization effect moves to the correct layer — Q-target label
smoothing in c51_loss_kernel.cu — now health-coupled rather than fixed.

Before:
- reward += pseudo_noise × max(|reward| × 0.05, 0.01)   (in env reward path)
- Q-target label smoothing = fixed LABEL_SMOOTHING_EPS = 0.01

After:
- Reward untouched by noise. Core reward = actual outcome + aligned penalties.
- Q-target label smoothing eps_eff = 0.02 × (1 − health) read from ISV[12]
  - health=1 (healthy): eps_eff=0, sharp targets preserved
  - health=0.5: eps_eff=0.01, matches old fixed behavior at mid-health
  - health=0 (collapsing): eps_eff=0.02, maximum regularization prevents
    overcommitment to the collapsed distribution

Why health-coupled:
Same insight as the distillation SAXPY fix — every fixed kernel scalar is
a temporal-coupling candidate when we have the ISV pinned buffer available.
Regularization strength should scale INVERSELY with network health: it's
most needed exactly when things are falling apart.

Files touched:
- c51_loss_kernel.cu: LABEL_SMOOTHING_EPS const replaced with
  LABEL_SMOOTHING_BASE + in-kernel health read from isv_signals[12]
- experience_kernels.cu: deleted reward noise block + kernel arg
- gpu_experience_collector.rs: dropped launch .arg + config field + default
- training_loop.rs: dropped hyperparam propagation
- config.rs: deleted field + intensity clamp + default (with tombstone)
- hyperopt/adapters/dqn.rs: dropped log reference
- config/training/*.toml (4 files): dropped orphan reward_noise_scale
  entries (none were being parsed — the profile parser had no field)

Verification:
- `cargo check -p ml --lib` clean
- E1 smoke test passes: final q_gap=0.1190, health=0.52 (health-coupled
  smoothing at ~mid-health matches old fixed behavior, collapse-prevention
  mechanism intact)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 00:43:24 +02:00
jgrusewski
904185004c feat: L40S GPU profile + auto-derive cuda-compute-cap from GPU pool
argo-train.sh now auto-selects cuda-compute-cap based on --gpu-pool:
  - ci-training-h100* → sm_90 (Hopper)
  - ci-training-l40s  → sm_89 (Ada Lovelace)

Added config/gpu/l40s.toml:
  - batch_size=4096 (between H100's 8192 and A100's 2048)
  - buffer_size=300K (scaled for 48GB VRAM)
  - gpu_timesteps_per_episode=2000 (bandwidth-limited)
  - gpu_n_episodes=2048 (scaled from H100's 4096)

GPU profile loader maps "L40S" → "l40s" (was "a100" fallback).

Also fixed pre-existing test drift: num_atoms=52 in h100.toml/a100.toml
was 51 in test expectations (padding alignment for C51 kernels).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 16:59:06 +02:00
jgrusewski
caa01070c8 feat: C51 atom warm-start + robust PopArt (median/IQR) from bitonic sort
Atom warm-start: bitonic sort rewards → quantile positions → write to
atom_positions_buf as initialization. Existing SGD optimizer refines.
Atoms start where reward mass actually is instead of uniform [-50,+50].

Robust PopArt: median/IQR normalization from sorted rewards replaces
Welford mean/var. More robust for bimodal distribution (many ±0.1
micro-rewards + few ±5.0 trade exits). Conditional: popart_robust=true.

Both reuse the same bitonic sort (~14ms per epoch, amortized).
gather_quantiles kernel extracts positions. extract_median_iqr reads
Q25/median/Q75 from sorted buffer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 09:06:23 +02:00
jgrusewski
8394e17224 feat: wire config weights to kernel + revert C51 alpha + atom warm-start spec
Config weights wired end-to-end (5 files): price_confirm_weight,
book_aggression_weight, hold_quality_weight, micro_reward_temp now
parsed from [reward] TOML section → DQNHyperparameters → GpuExperienceConfig
→ kernel args. No more hardcoded magic numbers in micro-reward formula.

Reverted c51_alpha_max 1.0→0.5: full C51 collapsed atoms to 3% util
at epoch 30 (death spiral). MSE floor prevents atom collapse.

PopArt warmup 100→10: 100 batches = ~5 epochs unnormalized → unstable.

Added bitonic sort integration spec: 4 uses (atom warm-start, robust
PopArt, experience curriculum, top-K PER).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 08:51:42 +02:00
jgrusewski
8146613cfa fix: skip trivial Hold counterfactual + disable reward_noise + config weights
Counterfactual: Hold(1)/Flat(3) direction mirror maps to self — trivial.
Now falls through to magnitude CF for Hold/Flat instead of wasting a
replay buffer slot on same-action same-reward experiences.

reward_noise_scale: 0.05→0.0 (dense micro-rewards are already noisy,
adding 5% label noise destroys the per-bar signal).

Added micro-reward weight config fields (price_confirm_weight=0.5,
book_aggression_weight=0.3, hold_quality_weight=0.2, micro_reward_temp=3.0,
holding_cost_rate=0.0001) — defined in TOML, kernel plumbing next session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 08:22:56 +02:00
jgrusewski
9d4c9efa05 cleanup: remove legacy Sequential q_network + tune config for dense reward
Removed the legacy non-branching Sequential q_network and target_network
from DQNAgent. These were never used (branching+dueling always active)
but allocated VRAM and ran noise resets every step. -190 lines.

Config tuning for dense micro-reward system:
- n_steps: 5→1 (TD(0), micro-rewards cancel over n>1)
- tau: 0.007→0.01 (faster target tracking for TD(0))
- c51_alpha_max: 0.5→1.0 (full C51, PopArt handles normalization)
- curiosity_weight: 0.1→0.0 (dense micro-reward replaces curiosity)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 08:17:34 +02:00
jgrusewski
f961b6ad64 fix: disable rank normalization + n_steps 5→1 + per_alpha 0.6→0.3
Three signal-killing issues fixed:

1. Rank normalization DISABLED — was double-normalizing with PopArt,
   destroying magnitude difference between micro-rewards (±0.1) and
   trade exits (±5.0). PopArt alone preserves relative magnitude.

2. n_steps 5→1 (TD(0)) — dense micro-rewards alternate ±0.1 each bar.
   With n=5, they cancel out over 5 bars. TD(0) preserves the per-bar
   signal that the temporal pipeline needs to learn from.

3. per_alpha 0.6→0.3 — lower = more uniform PER sampling. Dense micro-
   rewards have tiny TD-errors (easy to predict), so high alpha ignores
   them. Lower alpha ensures micro-reward experiences get sampled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 08:06:36 +02:00
jgrusewski
353c8d81ab tune: revert LR 2e-5 → 1e-5 — too aggressive with architectural changes
Linear scaling rule (2x batch → 2x LR) doesn't hold for DQN+PER with
major architectural changes (Hold action, OFI embed, wider attention).
The model needs stability to learn the new action space, not speed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 07:53:29 +02:00
jgrusewski
55d70b7cc8 feat: dense micro-reward + DSR fix + counterfactual sign fix
Dense micro-reward: OFI momentum × price confirmation (MBP-10 mid-price
mark-to-market) × adaptive cost tolerance (capital_ratio × Sharpe_ema)
+ book aggression + retrospective hold quality bonus. Replaces flat
-0.0001 holding cost. Scale: micro_reward_scale=0.1.

DSR Sharpe EMA: was hardcoded price_change_dsr=0.0 — adaptive cost
tolerance was permanently floored at 0.1. Now uses actual per-bar
returns from ps[PREV_CLOSE_SLOT].

Counterfactual: cf_cycle==1 (magnitude) and cf_cycle==2 (order) now
undo do_flip before computing CF reward, then re-apply. Previously
2/3 of counterfactual experiences had wrong sign when do_flip=true.

Rank normalization threshold: 0.001 → 1e-5 for dense micro-rewards.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 00:03:51 +02:00
jgrusewski
28384836a7 tune: batch 8192→16384, lr 1e-5→2e-5, tau 0.005→0.007
Linear scaling rule: 2x batch → 2x LR to maintain effective update
magnitude. Tau increased to compensate for fewer steps/epoch (target
network tracks faster). H100 VRAM: 41GB free at B=8192, B=16384 adds
~4GB — easily fits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 15:38:31 +02:00
jgrusewski
64e6353a5d fix: IQN num_quantiles 64→32 in all binaries + production config
Default was hardcoded as 64 in train_baseline_rl.rs and evaluate_baseline.rs,
overriding the config default of 32. Added num_quantiles=32 to production
config so it's explicit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 13:06:58 +02:00
jgrusewski
e0d90dd4b3 fix: batch_size 16384→8192 in dqn-production.toml (was overriding gpu profile) 2026-04-19 11:11:41 +02:00
jgrusewski
6dd1aeca7e perf: disable replay buffer VRAM auto-sizing — cap at 500K entries
Auto-sizer inflated replay buffer to 15.8M entries on H100 (45% of 80GB VRAM).
The PER prefix scan inside the CUDA graph processes ALL 15.8M entries every step,
dominating step time. With buffer_size=500K, the scan processes 500K entries instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 09:20:15 +02:00
jgrusewski
c2edb273ca perf: batch_size 16384 → 8192 — 2x faster GEMMs, more frequent updates
At 16384, each GEMM saturates 97% of 132 SMs — no room for multi-stream
parallelism. At 8192, GEMMs use ~50% SMs, allowing branch streams and
aux parallelism to fill idle SMs. 2x more steps/epoch but each ~2x faster.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 09:01:34 +02:00
jgrusewski
b4cf279b73 fix: reduce H100 replay buffer VRAM fraction 55% → 45% — OOM from IQN 8.6GB + Phase 2
IQN with 64 quantiles × batch=16384 allocates 8.6GB for tiled intermediates.
Combined with attention (250MB), Phase 2 aux workspaces (64MB), backward
branch scratch (16MB), the 55% replay fraction left only 349MB free —
not enough for experience collector's 192MB next_states allocation.

45% frees ~8GB headroom: replay buffer 19.4M → ~15.9M (still 60× batch).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 18:42:06 +02:00
jgrusewski
108bb63fce feat: cost-driven hold timing — replace min_hold_bars with learned cost signals
Removed: enforce_hold(), min_hold_bars from config/kernels/backtest.
Added: holding_cost_rate (inventory penalty), churn_threshold_bars +
churn_penalty_scale (graduated flip penalty) to reward in
experience_env_step and backtest kernels.

The model learns optimal hold timing from cost signals:
- Per-trade tx cost prevents churning (existing)
- Inventory penalty makes large positions expensive to hold
- Churn penalty graduates cost for rapid flips
- Temporal attention learns when holding cost > expected profit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 01:04:11 +02:00
jgrusewski
6d58eaa24e fix: min_hold_bars 10→50 + adaptive hold scales with base
min_hold=10 allowed ~1200 trades/day — costs destroyed the edge.
min_hold=50 (~6.5 min) limits to ~230 trades/day max.

Adaptive extension now scales 0-2× base (was hardcoded 0-8 bars).
With base=50: range is 50-150 bars depending on ISV stability.
Losing positions (negative reward_ema) get base only (50 bars).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 22:20:10 +02:00
jgrusewski
709fffe923 fix: revert c51_loss_reduce to (1,1,1) — same Hopper graph hang class
The warp-parallel (32,1,1) change to c51_loss_reduce caused the same
hang as isv_feature_gate: changing block_dim inside graph_mega alters
CUDA Graph node structure on Hopper's TMA scheduler. Must stay (1,1,1).

Also: batch_size 8192→16384, gpu_n_episodes 1024→4096, num_atoms 51→52
to align h100.toml with dqn-production.toml.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 14:46:15 +02:00
jgrusewski
de13c165da fix: production batch_size 16384→8192 — OOM with state_dim=96
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 09:25:15 +02:00
jgrusewski
f1be4619e0 fix: H100 VRAM fraction 0.70→0.55 — OOM with state_dim=96 + 86 tensors
batch_size auto-scaled to 16384 consuming 78GB, leaving only 3GB
for experience collector → OOM. Reducing replay buffer fraction
gives more headroom. state_dim grew 88→96 (plan features), weight
tensors grew 68→86 (ISV + plan head).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 09:23:51 +02:00
jgrusewski
61f2ba3cb2 feat: MBP-10 data loading always enabled by default
mbp10_data_dir and trades_data_dir uncommented in localdev config.
Doc comments updated: 20 microstructure features always appended.
OFI is no longer optional — it's core to the model's feature set.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 01:02:54 +02:00
jgrusewski
c74a687ea8 feat: position-gated episodes + 5000-bar limit — close 45x training/val gap
Episode done flag: timer-based -> position-gated (trade complete = done).
V(flat)=0 is correct terminal anchor. Soft reset keeps equity on
trade completion; hard reset only on data-end or capital breach.
H100: 100 bars -> 5000 bars, gpu_n_episodes -> 1024.
ExperienceProfile gains optional gpu_n_episodes field.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:56:44 +02:00
jgrusewski
b21c6d5cff feat: comprehensive C51 training stability overhaul — Q-stats diagnostics, adaptive atoms, gradient safety
Major architectural fixes discovered through systematic investigation:

Q-gap measurement (3 bugs):
- compute_expected_q never ran during training → q_out_buf was zeros
- epoch_q_gap reset in process_epoch_boundary before logging read it
- flush_q_stats_readback drained async readback before in-loop read

Zero-copy pinned memory (4 hot-path scalars):
- t_buf, tau_buf, v_range_buf, adaptive_clip_buf → pinned device-mapped
- GPU reads via cuMemHostGetDevicePointer, host writes directly, no HtoD

Q-stats-driven adaptive v_range:
- v_range = q_mean ± 3σ + Bellman headroom (was fixed ±1.0)
- Adaptive MIN_RANGE scales with |Q_mean| (was fixed 0.02)
- Per-step adaptation (was every 50 steps)
- 100× finer atom resolution from epoch 1

Gradient stability:
- IS-weight clamp at 10.0 in all loss/grad kernels (PER spike prevention)
- 3 power iterations in spectral norm (was 1 — underestimated sigma)
- Bottleneck w_bn added as 13th spectral-normed matrix (was missing)
- Pre-Adam grad_buf clip via clip_grad_buf_inplace (activation amplification)
- EMA-based adaptive gradient clipping (pinned device buffer)
- Consolidated grad_norm to single buffer (was 2 — eliminated grad_norm_f32_buf)

Adaptive tau from online-target Q-divergence:
- C51 loss kernel accumulates (E[Q_online] - E[Q_target])² per batch
- Tau scales with sqrt(divergence/baseline), clamped [0.5×, 10×] base
- Accelerates target convergence during discovery, stabilizes during plateau

Deterministic evaluation:
- eval_mode in action_select kernel: pure greedy argmax, no Boltzmann/RNG
- Eliminated ±40 val_Sharpe noise from near-uniform Boltzmann sampling
- Backtest evaluator uses adaptive v_range (was config v_min/v_max — 1500× mismatch)

Atom utilization metrics:
- compute_expected_q accumulates entropy + utilization per step
- q_stats_kernel extended to 7 outputs (was 5)
- Logged per epoch: atoms=98%ent/92%util

Pessimistic Q-init removed — incompatible with adaptive v_range (bias was
255× outside ±0.01 support, causing 5-epoch cold-start and late Q-value drift).

903/903 tests passing. val_Sharpe positive from epoch 1 with greedy eval.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:10:25 +02:00
jgrusewski
21bd05c9a2 feat: advantage noise kernel — breaks dueling symmetry trap (dead NoisyLinear replacement)
NoisyLinear was completely dead after cuBLAS migration — neither training
nor experience collection applied ANY exploration noise. With identical
Q-values across actions, Boltzmann selection was uniform, making the
dueling mean-subtraction cancel ALL advantage gradients. The advantage
heads could NEVER learn (Q-gap permanently 0.0000).

Fix: add_advantage_noise CUDA kernel injects per-action Gaussian noise
into Q-values AFTER compute_expected_q, BEFORE action selection.
- Philox PRNG + Box-Muller for GPU-native Gaussian sampling
- Per-sample, per-action noise (breaks within-batch symmetry)
- Only during experience collection (not training targets)
- noise_sigma=0.1 (configurable, TOML + hyperopt tunable)

The noise creates non-uniform Boltzmann selection → different actions
have different frequencies → advantage gradient survives the dueling
mean-subtraction → Q-gap can grow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 16:01:52 +02:00
jgrusewski
6989598150 feat: Q-gap + Q-variance diagnostics — validates state discrimination
Q-gap = max_a Q(s,a) - mean_a Q(s,a): measures action preference per state.
Q-var = Var_s[Q(s,a)]: measures state differentiation across the batch.

Both are 0.0000 through all 10 smoketest epochs despite Sharpe 8+ and
Q-values growing to 0.012. This proves the good metrics are from
reward-induced mechanical bias, NOT learned Q-values.

Also: c51_warmup_epochs=0 (C51 from step 1, bypass MSE dead zone),
smoketest lr=1e-4 (50× higher, makes 40 steps representative).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:07:54 +02:00
jgrusewski
1f6a8436d3 feat: v9 Differential Sharpe Ratio reward — optimize for consistency, not PnL
Fundamental shift: reward optimizes Sharpe ratio contribution per trade
instead of directional PnL magnitude. Targets Sharpe 5+ via many small
consistent gains (spread capture, execution quality) instead of few
large directional bets.

DSR implementation (Moody & Saffell, 2001):
- Per-episode EMA statistics in portfolio state slots [3]-[5]
- DSR = (B*R - 0.5*A*R²) / (B - A²)^1.5 at trade completion
- 10-trade warmup, clamped [-5, +5]
- w_dsr=5.0 (primary reward signal)

Reward hierarchy restructured:
- DSR: 5.0 (NEW — primary, rewards Sharpe consistency)
- Directional PnL: 2.0 (was 10.0 — demoted to secondary)
- Order credit: 1.0 (was 0.1 — spread capture amplified 10×)
- Urgency credit: 0.5 (was 0.1 — fill quality amplified 5×)
- Inventory penalty: -0.005×|pos|/max_pos per bar (NEW)
- Dense micro-reward: 0.0 (removed — noisy direction signal)

Expected: WinRate increases (many small spread captures), per-trade
variance decreases, Sharpe rises from consistency not prediction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 14:33:56 +02:00
jgrusewski
42de872f1a fix: gradient_clip_norm 1.0→10000 — safety-only clip for unclipped gradient pipeline
With per-component clipping removed, raw gradient norms are ~4000.
The old gradient_clip_norm=1.0 (calibrated for pre-clipped norms ~7)
discarded 99.97% of gradient magnitude every step, making the effective
learning rate 570× too small. Q-values grew 9× slower than old run.

10000 lets normal gradients (~4000) pass through unclipped. Only fires
on genuine divergence spikes (>2.5× normal). Adam's internal adaptive
step sizing handles the rest.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:35:27 +02:00
jgrusewski
eb7139c436 feat: adaptive C51 v_range — Q-values 0.008→0.525 (65× improvement)
Fundamental fix: C51 atom spacing (dz=0.6) was larger than the Q-value
range (0.02), making the distributional loss unable to resolve rewards.
The network was blind to its own learning signal.

Adaptive v_range via device buffer (same pattern as tau_buf):
- v_range_buf[2] on GPU, all C51/MSE/CQL/expected_q kernels read via
  pointer (graph captures stable address, reads value at replay time)
- Cold start: v_range=±1.0 → dz=2/51=0.039 → 25× atom resolution
- Monotonic expansion based on Q-stats every 50 training steps
- Never shrinks (avoids invalidating Bellman targets in replay buffer)

Kernel changes (5 files):
- c51_loss_kernel.cu, mse_loss_kernel.cu, mse_grad_kernel.cu,
  cql_grad_kernel.cu, compute_expected_q: scalar v_min/v_max → pointer

Also fixed:
- PopArt warmup 100→1 (start normalizing from step 1)
- PopArt variance floor 0.0001→1e-8 (allow sigma < 0.01)
- Backtest evaluator: allocate own v_range_buf for compute_expected_q
- num_atoms 51→52 in all TOMLs (TF32 4-element alignment)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 07:58:02 +02:00
jgrusewski
dab4478b47 fix: production v_min/v_max ±15→±50 for gamma=0.99 + PopArt GPU buffer reset
v_min/v_max ±15 with gamma=0.99 only covers 15% of theoretical Q range
(Q_max = 1/(1-0.99) = 100 for unit-variance PopArt rewards). C51 top atom
saturates on sustained winners, degrading distributional learning. ±50
covers 95th percentile.

PopArt GPU buffers (popart_mean, popart_var, popart_count) were never
zeroed between walk-forward folds — fold 2's reward normalization was
contaminated by fold 1's statistics. Now reset alongside Adam state in
reset_adam_state().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 06:56:37 +02:00
jgrusewski
e18ab46e13 fix: min_epochs_before_stopping 5→10 — prevents early stopping in 10-epoch smoke tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:00:12 +02:00
jgrusewski
dd6143936e fix: IQN 4-branch runtime + order counterfactual + per-branch entropy + histogram
IQN kernel rewrite (critical — was corrupting 75% of gradient budget):
- Eliminate compile-time BRANCH_*_SIZE defines (BRANCH_0_SIZE was 5, should be 3)
- All 9 IQN kernels now take runtime b0/b1/b2/b3 params
- 4-branch forward, backward, decode (was 3-branch, missing magnitude)
- branch_actions buffer B*3 → B*4

Action diversity infrastructure:
- 3-cycle counterfactual: dir mirror / mag relabel / order-type cost delta (50× amplified)
- Per-branch entropy: order (d==2) gets 3× boost in C51 + MSE grad kernels
- Position histogram expanded 6→12 bins (all 4 branches get entropy bonus)
- Boltzmann temperature floor raised to 0.5 for order + urgency branches
- Smoke test epochs 3→10 for diversity verification

Note: experience collector still needs bottleneck forward path — currently reads
trainer's bottleneck-layout weights with state_dim K, producing misaligned Q-values.
Next: eliminate weight copy, use direct pointer views into trainer's params_buf.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:40:47 +02:00
jgrusewski
7cf2cfdc39 cleanup: remove ALL GpuVarStore from DQN path + re-enable bottleneck_dim=16
- Added OwnedGpuLinear to ml-core (self-contained linear layer with owned
  weight/bias CudaSlice, forward_with_slices bypasses GpuVarStore lookup)
- Removed GpuVarStore from all 12 ml-dqn modules: Sequential, branching,
  distributional_dueling, dueling, network, rmsnorm, residual, curiosity,
  attention, iql, quantile_regression, regime_conditional
- Removed get_q_network_vars/vars/store accessors from DQN, branching,
  distributional_dueling, dueling, network, quantile_regression
- Replaced GpuLinear+GpuVarStore with OwnedGpuLinear in all cold-path modules
- Made load_from_safetensors a no-op (flat buffer path handles checkpoints)
- Made sync_to_varmap a no-op (flat buffer is source of truth)
- Moved branching->VarStore conversion to gpu_weights::branching_to_varstore
- Re-enabled bottleneck_dim=16 in config defaults + all 3 TOML configs
- Removed flatten_online_weights bottleneck skip workaround
- Zero GpuVarStore references in crates/ml-dqn/src + crates/ml/src/trainers/dqn

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 14:42:54 +02:00
jgrusewski
d797661bbe fix: revert bottleneck_dim to 0 — blocked by Candle VarStore dimension mismatch
bottleneck_dim=16 causes w_s1 size mismatch between VarStore (full state_dim)
and flat params_buf (reduced bottleneck_concat_dim). EMA target sync crashes
with CUDA_ERROR_ILLEGAL_ADDRESS.

Root cause: GpuVarStore allocates weights at full state_dim but GpuDqnTrainer's
flat buffer uses bottleneck-reduced dimensions. These two weight sources are
fundamentally incompatible when bottleneck is active.

Fix: eliminate GpuVarStore entirely (next task), then re-enable bottleneck.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 13:28:51 +02:00
jgrusewski
c246afbbdb feat: bottleneck 2→16 — 8× more market feature information for all branches
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 12:59:38 +02:00
jgrusewski
1e133e4ca1 feat: enable PopArt + tau coupling + verify PER/Sharpe reset between folds
Task 3: Enable PopArt reward normalization (default true, all 3 TOML configs,
wiring confirmed in fused_training.rs submit_forward_ops_main()).
Task 4: Couple PopArt variance with tau reset — add prev_popart_var to
FusedTrainingCtx, read_popart_var() to GpuDqnTrainer, read_popart_variance()
+ should_reset_tau() to FusedTrainingCtx, tau reset injected at epoch boundary
in training_loop.rs after log_phase_timing().
Task 5: Add best_sharpe/best_epoch/best_val_loss reset to reset_for_fold() so
each walk-forward fold competes independently. Also wire v8 reward fields
(popart_enabled, micro_reward_scale, td_lambda, max_trace_length,
hindsight_fraction, hindsight_lookahead) through training_profile.rs apply_to().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:02:29 +02:00
jgrusewski
bf3091c6f2 fix: re-enable IQN + remove diagnostics for H100 baseline
- iqn_lambda restored to 0.25 (was 0.0 for NaN isolation)
- STEP_DIAG per-step logging removed (was temporary)
- FOXHUNT_GRAD_DIAG env var removed from Argo template

IQN NaN root cause fixed in 8cbabcef5 (f32-as-bf16 type mismatch).
Ready for H100 baseline deployment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:04:01 +02:00
jgrusewski
796fbd01cf fix: set c51_alpha_max=0.5 — prevent C51 gradient convergence on H100
H100 fold 2 showed grad_norm→0 when C51 fully replaced MSE (alpha=1.0).
MSE provides a non-vanishing gradient floor. Default 0.5 = 50/50 blend.
Hyperopt can search [0.3, 0.9].

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:02:48 +02:00
jgrusewski
5b4561fc27 diag: disable IQN (iqn_lambda=0) to isolate H100 NaN source
Graph recapture ruled out as NaN cause (NaN persists without graph
invalidation). Now testing: does NaN disappear when IQN is disabled?
If yes → IQN backward/Adam produces NaN on H100.
If no → NaN is in the main C51/MSE pipeline.

Also reverts graph invalidation hack from previous diagnostic commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:47:42 +02:00
jgrusewski
fce606a421 fix(critical): d_adv_logits buffer under-allocation — 32 elements short
The f32→bf16 cast in cast_d_logits_to_bf16() reads
b*(b0+b1+b2+b3)*na + 32*4 elements, but d_adv_logits_buf and
d_adv_logits_bf16 were allocated with only +32*3 padding (96 vs 128).

The 32 extra f32 reads went past the allocation into adjacent GPU
memory. On H100 with 80GB and different memory layout, this grabbed
NaN/garbage values. The NaN propagated through the cuBLAS backward
pass into grad_buf, killing training at step ~720 (non-deterministic
depending on what lives in adjacent memory).

Fix: allocate +32*4 padding in d_adv_logits_buf, d_adv_logits_bf16,
and d_adv_logits_mse to match the cast kernel's access pattern.

Also reverts c51_alpha_max default to 1.0 — the gradient collapse
was from this buffer bug, not C51 premature convergence. The
c51_alpha_max parameter is kept for hyperopt exploration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:06:30 +02:00
jgrusewski
a0aab3baa5 feat: add c51_alpha_max to cap MSE→C51 blend and prevent gradient starvation
H100 baseline (20 epochs) showed gradient collapse at epoch 10: C51
cross-entropy converges its distributional fit before the policy converges,
leaving zero gradient signal. The collapse happened 5 epochs after C51
reached alpha=1.0 (pure C51, zero MSE).

Fix: cap the C51 alpha ramp at c51_alpha_max (default 0.5) so MSE always
contributes (1 - alpha_max) of the primary gradient. MSE loss measures
Q-error directly and only goes to zero when Q-values are correct, not
just when the distribution shape is right.

- c51_alpha_max added to DQNHyperparameters (default 0.5)
- Added to PSO search space as 15th dimension (range [0.3, 0.9])
- Added to TOML profiles: smoketest, production, hyperopt
- Training loop caps alpha ramp at alpha_max instead of 1.0
- All 907 ml tests + 300 ml-core tests + 6 smoke tests pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:42:41 +02:00
jgrusewski
30f46c04c1 fix(critical): mean-reduce gradients + fix ExposureLevel 4-branch mismatch
Three root causes found and fixed:

1. SUM-reduced gradients without 1/N: all CUDA loss gradient kernels
   (C51, MSE, IQN backward, CQL) accumulated per-sample gradients as
   raw SUM. At batch=16384 (H100) the raw norm was 282x larger than
   batch=58, causing gradient clipping to destroy signal-to-noise ratio
   and collapse training at epoch 2-3. Now all kernels multiply by
   1/batch_size, making gradient scale batch-invariant.

2. ExposureLevel::target_exposure() used a flat 9-level scale that did
   not match the 4-branch dir*mag encoding. The Rust backtest evaluator
   computed wrong position sizes (e.g. 4x oversize for Short+Small).
   Now uses dir x mag formula. Also fixed is_buy/is_sell/is_hold and
   from_trading_action for 4-branch semantics.

3. Rust epsilon-greedy only explored 5/9 exposure combos (0..5 instead
   of dir*3+mag), ignored the magnitude branch on greedy, and used wrong
   indices for order/urgency (get(1)/get(2) instead of get(2)/get(3)).

LR recalibrated: old gradient_clip_norm was accidentally a batch-size-
dependent LR reducer (~60x at batch=58, ~16000x at batch=16384). With
mean-reduced gradients the clip rarely fires, so LR is now the sole
training speed control. Smoketest 1e-4 -> 2e-6, production 1e-4 -> 1e-5,
hyperopt range [1e-5,3e-4] -> [1e-7,1e-4].

Diagnostics: FOXHUNT_GRAD_DIAG=1 enables per-stage gradient norm logging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:18:36 +02:00
jgrusewski
0d1e01a0c7 fix: all DQN configs — IBKR costs (0.18 bps) + min_hold_bars=10 for volume bars
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 21:05:59 +02:00
jgrusewski
413ec6f643 fix: smoketest config — add tx_cost_multiplier=0.18 (was missing, defaulted to 1.0)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 21:05:20 +02:00
jgrusewski
6b7c7f8b70 fix(critical): spread_cost 50× too high + mirror negates all 17 directional features + IBKR costs
Three fixes:

1. spread_cost removed contract_multiplier ($50) — was computing in dollars
   but PnL is in points, making spread 12.5× actual. The model saw every
   trade as guaranteed loss → preferred Flat.

2. Mirror universe now negates ALL 17 directional features (returns, MACD,
   Bollinger, SMA ratios, regression slope, CUSUM) + flips RSI. Was only
   negating 4, teaching wrong direction on 50% of epochs.

3. tx_cost_multiplier updated to 0.18 bps (IBKR ES RT=$4.50/contract).
   min_hold_bars=10 for volume bars (~26s at 23 bars/min).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 21:02:25 +02:00
jgrusewski
4889b6e968 feat(4branch): config foundation — b0=3 direction, b1=3 magnitude, b2=3 order, b3=3 urgency
Delete exposure_aux_weight, exposure_aux_warmup_epochs.
b0_size 9→3 (direction branch). b3_size=3 added (urgency).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:18:50 +02:00
jgrusewski
af60ddf837 fix: 10x stronger aux optimizer (LR 0.001→0.01, weight 0.01→0.1)
With separate optimizer, no gradient competition — safe to increase.
Smoke test shows within-group differentiation starting (S100≠S25≠L50).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:13:58 +02:00
jgrusewski
58ff8db406 fix: aux GEMM gradient explosion — scale 0.5→0.01 + clip after backward
The exposure aux backward GEMM amplifies gradients through the
hidden activation matrix (d_logits × h_b0), producing grad_norm=5000+.
With max_grad_norm=10, this throttles ALL gradients by 500x (lr/500).

Fix: reduce aux_weight 0.5→0.01 (50x) + clip_grad_buf_inplace after
aux GEMM to cap combined gradient at max_grad_norm.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:03:54 +02:00
jgrusewski
891f4d8715 perf: validation subsampling (4x faster) + batch_size 16384 (halves training steps) 2026-04-08 01:55:10 +02:00