Commit Graph

3404 Commits

Author SHA1 Message Date
jgrusewski
c05f75d0d8 feat: adaptive v_range reward-scale floor from observed reward_std
The hardcoded REWARD_SCALE_FLOOR=0.01 was tuned for smoketest (reward_std=0.007)
but 940× too small for production (reward_std=6.57). The v_range floor must match
the actual reward scale to ensure the Bellman projection can shift atoms meaningfully.

- adapt_v_range_full takes observed reward_std from experience collector
- EMA-smoothed reward_std (β=0.99) prevents single-epoch noise from jerking floor
- Falls back to 0.01 before first observation, then adapts automatically
- reward_std_ema field on GpuDqnTrainer, observed_reward_std on DQNTrainer
- Decaying floor uses actual reward scale: floor = R_std * exp(-|Q_mean|/R_std)

903/903 tests passing. Smoketest val_Sharpe positive (60.76 epoch 1).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:03:44 +02:00
jgrusewski
9054b6aefb feat: gap-aware v_range with decaying reward-scale floor
Q-gap drives v_range contraction: atoms are distributed to maximize action
discrimination, not just cover the Q-range. Target: Q-gap spans 10 atoms
(20% of 52), giving C51 enough resolution for distributional value.

Decaying reward-scale floor: starts at 0.01 (breaks fixed point at init),
decays as exp(-|Q_mean|/0.01) as Q-values mature. Automatically transitions
from "Bellman projection headroom" to "tight atom resolution" without
requiring step counters or epoch awareness.

903/903 tests passing. Q-gap grows 72× (2.7e-6 → 1.93e-4) through epoch 17.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:34:16 +02:00
jgrusewski
d42a8f4000 fix: reward-scale v_range floor + fold-boundary shrink-and-perturb
Breaks C51 stable fixed point: tight v_range → tiny Bellman shift → tiny gradient
→ Q-values stuck near zero → v_range stays tight (self-reinforcing trap).

- REWARD_SCALE_FLOOR=0.01 replaces ABS_FLOOR=1e-4 (was 1400× too small)
- v_range floor proportional to single-step reward magnitude, not Q-statistics
- Ensures Bellman projection can shift atoms by at least 1 reward unit
- Removes pessimistic Q-init bias (incompatible with adaptive v_range)

Fold transition stability:
- Shrink-and-perturb at fold boundary (alpha=0.8, sigma=0.01)
- Reduces overfit to previous fold's data distribution
- Eliminates val_Sharpe=-33 crash on fold 2/3 transitions

Result: val_Sharpe positive across all 50 epochs and 3 walk-forward folds.
Q-values grow monotonically (0→0.023), Q-gap grows 245× (2.7e-6→6.6e-4).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:25:38 +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
0d534f1dc0 feat: dense OFI-alignment reward — state-conditional direction signal every bar
Replaces dead Gem 1 micro-reward (used future price = hindsight leak) and
moves OFI-alignment from sparse (trade completion) to dense (every positioned bar).

Uses CURRENT-BAR observable OFI features:
- depth_imbalance (70%): bid vs ask volume ratio from MBP-10
- trade_imbalance (30%): inferred trade direction flow
Rewards Long when order flow says buy pressure, Short when sell pressure.

This gives the direction advantage head a STATE-CONDITIONAL signal:
"Long is better than Short WHEN order book shows buy pressure."
Without this, direction rewards are 50/50 (price is random) and the
advantage gradient cancels via dueling mean-subtraction.

Combined with dense DSR, every positioned bar now gets:
1. DSR: "is holding improving Sharpe?" (consistency signal)
2. OFI-alignment: "does order flow confirm your direction?" (microstructure signal)
3. Inventory penalty: "holding costs money" (urgency signal)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 16:17:26 +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
8d9bf15cfa feat: dense DSR — per-bar Sharpe contribution when positioned, flat excluded
Sparse DSR (trade completion only) left 80% of bars with near-zero reward.
Q-values couldn't grow. Dense DSR fires EVERY bar when positioned:
- r_bar = vol-normalized unrealized PnL per bar
- EMA statistics update only from positioned bars (flat excluded)
- DSR reward = how much this bar's PnL improves the running Sharpe
- Flat bars get zero DSR — safe default, no penalty for not trading

Naturally adaptive: high-vol → variance grows → DSR shrinks per unit
return → model becomes conservative. Low-vol → DSR amplifies.

Smoketest: WinRate 52-55% (was 44%), Sharpe peaks at 11.53, PF 1.67.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 14:48:15 +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
3a16aeacfd fix: remove per-component gradient clipping — restores Adam's adaptive step size
Per-component budget clipping (C51→4.5, IQN→1.0, CQL→1.0) produced a
CONSTANT gradient norm every step (0.655 old split, 0.756 new split).
Adam's second moment v[i] converged to a constant, making
m_hat/sqrt(v_hat) → sign(m_hat) — Adam degenerated into SignSGD.
The optimizer could not adapt step sizes to loss landscape curvature.

Fix: remove all per-component clips. Loss weights (iqn_lambda=0.25,
CQL coefficient, ensemble weight) control component balance. Single
global clip inside Adam kernel is the sole safety mechanism.

Saves 9 kernel launches per training step:
- 3× grad_norm phase1+phase2 removed (IQN, CQL, ensemble)
- 3× clipped_saxpy → plain saxpy_f32 (no norm computation needed)
- 1× primary budget clip_grad_buf_inplace removed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:48:26 +02:00
jgrusewski
fbe185cb92 feat: configurable gradient budget (IQN 75%→40% default, C51 10%→45%)
Hardcoded IQN_GRAD_BUDGET=0.75 starved C51 directional learning:
grad_norm frozen at 0.655 for 20+ epochs, WinRate stuck at 44-46%.
IQN dominated the gradient, C51's contribution after budget clipping
was effectively zero.

Now configurable via DQNHyperparameters:
- iqn_grad_budget: 0.40 (was 0.75 const)
- cql_grad_budget: 0.10 (was 0.10 const)
- ens_grad_budget: 0.05 (was 0.05 const)
- C51 gets remainder: 1.0 - 0.40 - 0.10 - 0.05 = 0.45 (was 0.10)

Wired through GpuDqnTrainConfig, TOML profiles, hyperopt search space.
Hyperopt can now tune the gradient budget split.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 11:58:48 +02:00
jgrusewski
91e05990a9 fix: node-bootstrap resolv.conf keeps VPC upstream as DNS fallback
Root cause of DNS deadlock (2026-03-18, 2026-04-12): node-bootstrap
DaemonSet overwrote /etc/resolv.conf with ONLY nameserver 10.32.0.10
(cluster DNS). When nodes rebuild, CoreDNS can't pull its image because
containerd resolves via 10.32.0.10 which requires CoreDNS to be running.

Fix: resolv.conf now has DUAL nameservers:
  nameserver 10.32.0.10          # primary: cluster DNS
  nameserver 169.254.169.254     # fallback: Scaleway metadata DNS

Containerd tries kube-dns first (fast, resolves .svc.cluster.local),
falls back to VPC resolver when kube-dns is unreachable. Breaks the
chicken-and-egg permanently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 10:49:45 +02:00
jgrusewski
51dd530c35 fix: update training_profile test assertions for epochs=10, num_atoms=52, explicit v_min/v_max
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 08:51:34 +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
d391e308d1 fix: max_dirmag 7→9 — report against full dir×mag combinatorial space
Flat+epsilon can hit any of the 9 dir×mag combos. Reporting against 7
(old reachable-without-epsilon count) showed 9/7 which is confusing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 23:44:13 +02:00
jgrusewski
320a64bb90 fix: monitoring 7→9 bin readback mismatch — diversity 15/81 → 81/81
The monitoring summary layout was shifted by 2 positions:
- Kernel writes: summary[5..14]=exp[9], [14..17]=order[3], [17..20]=urgency[3]
- Rust read (old): raw[5..12]=exp[7], raw[12..15]=order, raw[15..18]=urgency
- Rust read (new): raw[5..14]=exp[9], raw[14..17]=order, raw[17..20]=urgency

When num_actions changed 7→9, exp[7] and exp[8] became non-zero but Rust
read them as order_counts — the "order=3/3" was actually exp[7]+exp[8]+order[0].

Also fixed:
- MonitoringSummary.action_counts: [usize; 7] → [usize; 9]
- All downstream: monitoring.rs, metrics.rs, financials.rs, training_loop.rs
- Flat cell indices 3→{3,4,5} in diversity threshold
- exposure_names array expanded to 9 entries

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 23:42:28 +02:00
jgrusewski
e2bf655be2 fix: order diversity 1/3→3/3 — three root causes found and fixed
1. expected_q_kernel layout mismatch: cuBLAS writes branch-major logits
   [Branch0: B×b0×NA | Branch1: ...] but kernel read sample-major
   [Sample0: 12×NA | Sample1: ...]. Fixed to use branch_logit_offset
   accumulator matching cuBLAS output layout.

2. Duplicate expected_q_kernel.cu deleted — single implementation in
   experience_kernels.cu. Trainer loads from experience_kernels.cubin.

3. Monitoring num_actions=7 (old 7-level exposure) → 9 (dir×mag = 3×3).
   Actions with dir*mag≥7 silently dropped from order/urgency counts,
   making it appear only 1 order type was active.

Result: Action diversity 15/81 (18.5%) → 45/81 (55.6%), order=3/3.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 23:34:53 +02:00
jgrusewski
ad5c2b7ded refactor: exp collector zero-copy — direct pointer into trainer params_buf
Dead code sweep (167 lines deleted):
- online_params_flat (synced but never read by forward)
- online_params_f32 (never synced, always zeros — ROOT CAUSE of garbage Q-values)
- DuelingWeightSet/BranchingWeightSet/CuriosityWeightSet/RmsNormWeightSet fields
- sync_weights_flat, sync_weights_f32, flatten_online_weights methods
- sync_gpu_weights epoch-boundary DtoD copy

Zero-copy replacement:
- trainer_params_ptr: u64 points directly into trainer's params_buf
- param_sizes computed from real config (bottleneck_dim=16, market_dim=42)
- CublasForward with correct s1_input_dim=54 for bottleneck
- Bottleneck forward activated (bn_hidden + tanh + concat)
- Fused context init moved before Phase 2 (experience collection)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 23:10:25 +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
63006bb870 fix: xavier_init_params_buf + cuBLAS tile padding + bottleneck GEMM dims — 0 compute-sanitizer errors
VarStore elimination completion:
- xavier_init_params_buf(): CPU Xavier init directly into flat params_buf + target_params_buf
  using compute_param_sizes() for correct bottleneck dims (s1_input_dim=54, not state_dim=80)
- Pad all cuBLAS-accessed flat buffers (grad/m/v/cql/vaccine + IQN + evaluator)
  with cutlass_tile_pad to prevent SGEMM 32-element M-tile overreads
- Fix forward+backward GEMM for layer s1: use s1_input_dim/s1_ldb when bottleneck
  active (was state_dim=80, writing 80-wide rows into 54-wide bn_d_concat_buf)
- Pad bn_concat_buf/bn_d_concat_buf for K-tile overreads (concat_dim=54 not multiple of 32)
- Vaccine gradient: eliminate 2× DtoD copies with std::mem::swap on CudaSlice structs
- Fix IQN Xavier init to include branch_3 (was [b0,b1,b2], now [b0,b1,b2,b3])
- Pad backtest evaluator buffers (params_flat, activations, logits)

Verified: compute-sanitizer --tool memcheck → 0 errors, 286+57 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 21:43:27 +02:00
jgrusewski
31c610b69a WIP: training quality fixes + VarStore elimination (INCOMPLETE — needs xavier_init_params_buf)
Phase 1 — Training quality (14 structural bugs):
- Reward: sqrt micro-reward scaling, remove sparse normalization, Kelly penalty,
  additive OFI, magnitude-scaled capital floor penalty
- Gradient: CQL 25→10%, IQN 60→75%, MSE magnitude 4×, 3-phase counterfactual,
  tau_anneal 100K→500K, epsilon floor 2%, PopArt fold reset, dd_threshold 0.5%
- Monitoring: per-branch Q-value instrumentation
- Hold: min_hold_bars 5→1

Phase 2 — VarStore elimination:
- Deleted copy_weights_from from all network types
- Deleted to_varstore from DuelingQNetwork + DistributionalDuelingQNetwork
- Deleted branching_to_varstore, 7 extract/sync VarStore functions
- Deleted update_target_networks, legacy CPU training paths
- Deleted iqn_target_network, target_network fields
- Deleted gpu_kernel_parity_test.rs (705 lines)
- Refactored GpuExperienceCollector to single flat buffer
- Added weight_sets_from_branching() zero-copy replacement
- EMA tau=1.0 init for target_params_buf

KNOWN ISSUE: params_buf is zero-initialized but from_flat_buffer() creates
weight set pointers into it BEFORE flatten_online_weights runs. The online
forward reads zeros → NaN. Needs xavier_init_params_buf() at construction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 21:03:05 +02:00
jgrusewski
9f18772527 fix: update all test/example files for VarStore removal + 7-level exposure
Add to_varstore() compatibility shims on DuelingQNetwork and
DistributionalDuelingQNetwork so test/example code can rebuild a
GpuVarStore snapshot when needed. Delete dead tests that referenced
removed DQNAgent, PrioritizedReplayBuffer, and ReplayBufferType.
Fix action index references (action_19/21 -> action_28/30) and
type annotation issues (sin ambiguity, remainder operator).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 15:21:21 +02:00
jgrusewski
9c139586f3 refactor: zero-copy weight views from params_buf — VarStore bypass complete
DuelingWeightSet/BranchingWeightSet now created via from_flat_buffer()
pointing directly into GpuDqnTrainer's params_buf. No intermediate
VarStore extraction, no D2D copies, no flatten/unflatten cycle for
the primary weight sets.

bottleneck_dim=16 now works — smoke test passes. The dimension mismatch
is resolved because params_buf is allocated at the correct reduced
dimension, and weight sets point directly into it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 14:49:53 +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
1c3eb3e331 refactor: eliminate GpuVarStore — DuelingWeightSet is zero-copy pointer view into params_buf
DuelingWeightSet and BranchingWeightSet fields changed from owned
CudaSlice<f32> to raw u64 device pointers + usize element counts.
This enables zero-copy views directly into the flat params_buf for
the training hot path (no D2D copies, no shadow allocations).

Key changes:
- Weight sets store u64 + usize pairs instead of CudaSlice<f32>
- from_flat_buffer() creates zero-copy views into params_buf
- from_slices() creates pointer views from owned CudaSlice allocations
- DuelingWeightBacking/BranchingWeightBacking hold owned CudaSlice
  arrays for callers that need independent allocations (experience
  collector, ensemble heads, tests)
- flatten/unflatten become no-ops when weight sets point into params_buf
- extract functions return (Backing, WeightSet) tuples
- KernelWeightPack::build() uses u64 fields directly (no raw_device_ptr)
- All sync functions updated for pointer-based signatures
- Ensemble clone functions return backing + pointer view pairs
- gradient_budget tests updated (7/7 pass)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 14:02:55 +02:00
jgrusewski
a662e54e86 plan: VarStore elimination — zero-copy pointer views into flat params_buf (8 tasks)
DuelingWeightSet becomes raw u64 pointers into params_buf, not separate
CudaSlice allocations. Eliminates ALL D2D copies between weight
representations. flatten/unflatten cycle deleted entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 13:35:15 +02:00
jgrusewski
1a3da6f6b0 spec: Candle GpuVarStore elimination — 40+ call sites, 15 files, blocks bottleneck
VarStore is a CPU-trip corrupting data + degrading performance. Must be
replaced with direct pointer buffers (flat params_buf is already the
source of truth). Checkpoint, weight extraction, and polyak updates
all need porting to direct CudaSlice operations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 13:30:12 +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
0a68bb9713 feat: ensemble variance as exploration bonus — high disagreement = explore
Launch ensemble_aggregate_kernel per training step to populate
ensemble_var_q_buf with per-atom C51 logit variance across K heads.
Expose via ensemble_var_ptr() on FusedTrainingContext. Add
ensemble_exploration_beta config (default 0.2). Wire into
set_ensemble_variance_bonus() on GpuExperienceCollector which computes
beta * sqrt(mean_var) and tiles it as a constant SAXPY bonus over all
Q-values before Boltzmann action selection. No-op when ensemble_count <= 1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 13:05:40 +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
c57bd250a6 feat: IQN quantile spread (IQR) as anti-collapse exploration bonus
IQR (Q75-Q25) from IQN's 32 quantile estimates is computed per-action
after each training step and added to Q-values before Boltzmann action
selection. High-IQR actions (uncertain outcomes, typically large-magnitude
positions) get an exploration boost, countering C51's structural bias
toward low-variance Small actions that causes magnitude collapse.

Pipeline: IQN train -> compute_iqr() -> set_iqr_bonus() -> SAXPY on
q_values [N, q_stride] each experience timestep. Alpha default 0.3.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:56:26 +02:00
jgrusewski
81538081bc feat: hindsight magnitude relabeling via alternating counterfactual
Even timesteps use the existing directional mirror (Short<->Long, negated
reward).  Odd timesteps substitute a different magnitude and linearly scale
the reward by alt_mag_frac / actual_mag_frac, giving the magnitude branch
direct gradient signal about what bigger or smaller positions would have
earned.  Falls back to directional mirror when reward is near-zero (scaling
uninformative).  No extra buffer needed -- reuses the existing N*L
counterfactual slot.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:47:36 +02:00
jgrusewski
8d568158a7 feat: gradient vaccine every 5 steps (was 10) — more diversity correction 2026-04-11 12:42:28 +02:00
jgrusewski
59695d67b7 feat: per-branch epsilon — magnitude 2× exploration, Boltzmann for order/urgency
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:39:02 +02:00
jgrusewski
d6ab83d37c fix: c51_alpha starts at 0.5 from epoch 0 — eliminates 3 graph recaptures per fold
Set c51_warmup_epochs default to 0. The ramp logic in training_loop.rs
already returns alpha_max immediately when c51_warmup_epochs==0, so all
epochs now see c51_alpha=0.5 from the first step. Stable loss landscape,
no CUDA graph recaptures from alpha transitions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 12:30:59 +02:00
jgrusewski
370319f7c8 plan: ABI Complete — per-branch epsilon, IQN anti-collapse, hindsight relabeling, vaccine, ensemble, bottleneck (8 tasks)
Unified plan combining Sub-plans B+C + novel findings:
1. Fix c51_alpha ramp (eliminate 3 graph recaptures)
2. Per-branch epsilon (magnitude 2×, Boltzmann for order/urgency)
3. IQN quantile spread as anti-collapse bonus
4. Hindsight magnitude relabeling
5. Gradient vaccine with diverse batches
6. Ensemble variance at inference
7. Per-branch bottleneck width
8. Full verification + deploy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:27:56 +02:00
jgrusewski
dd4255c6c9 fix: update 5 ml tests for 7-level ExposureLevel
- Rename test_c2_dqn_default_num_actions_is_9 → _is_7, assert 7
- PPO adapter tests: NaN-safe direction assertions (fresh model = random weights)
- Curriculum: NUM_ACTIONS 5→7, phase1_mask 7-element (ShortHalf/Flat/LongHalf),
  update all index/count assertions for ShortSmall..LongFull layout
- Fixes 903 pass / 0 fail (was 898 pass / 5 fail)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:21:52 +02:00
jgrusewski
3696cb475b fix: update action reporting from 9-level to 7-level ExposureLevel
ShortSmall=0, ShortHalf=1, ShortFull=2, Flat=3, LongSmall=4, LongHalf=5,
LongFull=6. Updates all array sizes, bounds checks, names, and distribution
logic across gpu_monitoring, monitoring, training_loop, metrics, and financials.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 12:12:18 +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
a401f03f9a fix: remove 30% Flat floor + fix magnitude masking — ABI exploration replaces it
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 11:56:32 +02:00
jgrusewski
448b61d095 refactor: collapse 9-level to 7-level ExposureLevel — eliminate degenerate Flat variants
The 4-branch DQN (direction x magnitude) had 3 degenerate variants
(Short25, Flat, Long25) that all mapped to 0.0 target exposure when
direction=Flat, causing 82% Flat collapse. Collapse these into a
single Flat variant, giving 7 levels (ShortSmall/Half/Full, Flat,
LongSmall/Half/Full) and 63 total factored actions (7x3x3).

- ExposureLevel enum: 9 variants -> 7 (add direction/magnitude/from_dir_mag)
- FactoredAction: 81 -> 63 total actions, from_index/to_index updated
- DQN epsilon-greedy: use from_dir_mag() instead of dir*3+mag indexing
- DQN config: num_actions default 9 -> 7
- PPO action space: 45 -> 63 actions, action masking updated
- Signal adapter CUDA kernel: 5-bin -> 7-bin exposure aggregation
- All tests updated for new variant names and index ranges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:54:09 +02:00
jgrusewski
55be1776b5 plan: ABI Foundation — 7-level exposure + PopArt + tau coupling + PER clear (6 tasks, 27 steps)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:28:59 +02:00
jgrusewski
553289545a spec: Autonomous Branch Intelligence (ABI) — fix DQN action collapse
10-component design to fix 82% Flat/Small action concentration:
1. Collapse degenerate 9→7 level exposure space
2. Per-branch reward decomposition (direction/magnitude/order/urgency)
3. Hindsight action relabeling for magnitude
4. Position-aware temporal exploration with per-branch epsilon
5. IQN quantile spread as anti-collapse signal
6. Activate PopArt + couple with tau reset
7. Feed the gradient vaccine with diverse batches
8. Per-branch bottleneck widths
9. Ensemble variance at inference
10. Clear PER between folds

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:23:23 +02:00
jgrusewski
bdcc3b7d30 perf: cublasLt RELU_BIAS epilogue fusion for trunk + branch hidden GEMMs
Fuses GEMM + bias-add + ReLU into a single cublasLtMatmul kernel via
CUBLASLT_EPILOGUE_RELU_BIAS. Eliminates 7 separate add_bias_relu
kernel launches per forward pass (3 trunk + 4 branch hidden layers).

Creates separate cached descriptors with epilogue enabled at init time.
Falls back to separate kernels if the epilogue heuristic isn't available.
Bias pointer set dynamically per-call via set_matmul_desc_attribute.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 10:57:57 +02:00
jgrusewski
7c34003f03 perf+fix: cuBLAS descriptor caching, per-branch workspace, shrink-perturb fix
Performance:
- Cache cuBLAS descriptors: pre-create matmul_desc + layouts + algo at init.
  requestedAlgoCount=3 for better algorithm selection. Zero per-GEMM overhead.
- Per-branch workspace: 4 × 32MB separate workspace buffers for multi-stream
  branch dispatch. Eliminates workspace contention on parallel execution.

Training stability:
- Remove hardcoded shrink-perturb that fired every epoch on short runs (3-5 epochs).
  With epochs=5, interval = epochs/4 = 1 → fired every epoch, destroying epoch 1
  learned weights. This caused Sharpe to collapse from +0.60 to -0.29 after epoch 1.
- Phase 3 shrink-perturb now uses config values (was hardcoded alpha=0.9, sigma=0.01).
- The config-defined shrink_perturb_interval=20 now controls all shrink-perturb timing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 10:40:56 +02:00
jgrusewski
74f27d18f3 fix: revert workspace 16MB→32MB — H100 CUDA 13.0 CUBLAS_STATUS_NOT_SUPPORTED
The backward GEMM heuristic on H100 with CUDA 13.0 requires 32MB
workspace for TF32 algorithm selection. 16MB causes
CUBLAS_STATUS_NOT_SUPPORTED on dW_only (m=256,n=128,k=16384).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 10:19:35 +02:00
jgrusewski
1e5ed3bde1 perf: target branch parallelism + attention Q,K,V cache + workspace reduction
Three optimizations targeting the 345ms mega-graph bottleneck:

1. Target forward multi-stream branches (est -15ms):
   Replaced single aliased tg_h_b_scratch with 4 separate buffers.
   Target forward now uses fork-join multi-stream dispatch (same as
   online forward) — 4 branch GEMMs run in parallel instead of serial.

2. Attention Q,K,V projection caching (est -10ms):
   Forward kernel saves Q,K,V projections to a 14MB buffer.
   Backward kernel reads saved projections instead of recomputing
   from weights — eliminates 12 O(D) inner products per sample.

3. cuBLAS workspace reduction (32MB → 16MB):
   Sufficient for H100 TF32 tile sizes at our matrix dimensions.
   Reduces L2 cache pressure from workspace allocation.

Combined with IQN h_s2 reuse from prior commit: target 345ms → <300ms.

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