Commit Graph

3501 Commits

Author SHA1 Message Date
jgrusewski
b1b5682e2b fix: Expected SARSA tau floor scales with Q magnitude — was fixed 0.01
Floor = max(|mean_Q| * 0.01, 1e-6). At Q-mean=0.005: floor=5e-5.
At Q-mean=1.0: floor=0.01. Fully adaptive, zero hardcoded constants.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:10:10 +02:00
jgrusewski
69b911a611 fix: Expected SARSA tau=Q_gap (scale-invariant) — tau=Q_gap/3 caused convergence trap
tau=Q_gap/3 gave 20:1 weight ratio (95%/4%/1%), making the target
essentially argmax. Once the online distribution matched this sharp
target, cross-entropy gradient vanished → Q-gap froze at 0.158,
val_Sharpe locked at 2.40 from epoch 22.

tau=Q_gap gives a CONSTANT e:1 ≈ 2.72:1 ratio (63%/23%/14% for 3
actions) regardless of Q-gap magnitude. The target always maintains
meaningful mixture → perpetual gradient pressure → Q-values keep
evolving. Scale-invariant: the softmax ratio depends only on relative
Q-differences, not their absolute magnitude.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:00:09 +02:00
jgrusewski
0b59b30549 fix: eval mode uses Boltzmann sampling — argmax froze val_Sharpe at 0.00
All 4 branches (dir, mag, order, urgency) now use Boltzmann sampling
in eval mode instead of argmax. argmax(softmax(Q/tau)) = argmax(Q) —
temperature is meaningless with argmax. Every state picked the same
winning action → identical trades → Sharpe=0.00 from epoch 8 onward.
Boltzmann sampling with Philox seed gives deterministic but
action-diverse evaluation. When Q-values differentiate, Boltzmann
naturally sharpens toward the best action.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:58:26 +02:00
jgrusewski
c55d9d0276 fix: Expected SARSA temperature prevents uniform-averaging plateau
Softmax weights used implicit tau=1, producing near-uniform weights
when Q-gap=0.06: exp(0.06)≈1.06 → weights [0.35, 0.33, 0.32].
The mixture averaged all distributions → zero gradient to differentiate
actions → Q-gap frozen → val_Sharpe locked at 0.00 after epoch 19.

Now uses tau = max(Q_gap/3, 0.001). With Q-gap=0.06 and tau=0.02:
exp(0.06/0.02) = exp(3) ≈ 20:1 weight ratio between best and worst.
The 0.001 floor means even tiny Q-differences produce sharp weights,
preventing the uniform-averaging fixed point.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:10:20 +02:00
jgrusewski
d06a18ef9f fix: IQN readiness update moved outside graph capture — cuStreamSynchronize invalidated capture
read_total_loss() calls cuStreamSynchronize which is illegal during
CUDA Graph stream capture. Was inside submit_aux_ops (captured in
graph_mega/graph_aux). Moved to Step 4 (conditional ops outside graph).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:26:53 +02:00
jgrusewski
7c00793c9f fix: exposure action select uses Boltzmann everywhere — argmax caused flips
branching_action_select exposure head used hard argmax while order/
urgency used Boltzmann. experience_action_select eval mode used pure
argmax for direction. Both replaced with Boltzmann softmax:
  tau = max(Q_max - Q_min, 0.01)
When Q-values differentiate: converges to argmax. When flat: spreads
evenly. Eval mode takes argmax of softmax probs (deterministic).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:19:44 +02:00
jgrusewski
1634dec042 fix: eval v_range width from Q-gap, baseline from Q-std — eliminates -40 dips
Two fixes:
1. Width: half = max(10*q_gap, 3*q_std, 0.1) instead of fixed 3*q_std+1.0.
   With q_gap=0.06 and 51 atoms, gives ~5 atoms of action resolution
   (was 1.5 atoms with the 1.0 floor). No more catastrophic -40 dips
   from atom underresolution.

2. Baseline: uses q_std_ema (proportional to natural Q oscillation)
   instead of fixed 0.01. Normal Q-oscillation (±q_std) no longer
   triggers aggressive alpha — only abnormal shifts do.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:13:36 +02:00
jgrusewski
60528390e1 fix: adaptive IQN lambda + complete bf16 elimination (312 calls total)
Adaptive IQN lambda: reads IQN total_loss synchronously, tracks EMA
with adaptive alpha, computes readiness = (loss_initial - loss_ema) /
loss_initial. SAXPY scale = iqn_lambda_base * readiness. Suppresses
noisy IQN gradient early (readiness≈0), ramps to full weight as IQN
converges (readiness→1).

bf16 cleanup: 185 wrapper calls replaced across 12 .cu files
(attention, backtest_ppo, backtest_supervised, curiosity, dqn_utility,
dt, ensemble, her, iqn_cvar, monitoring, mse_loss, statistics).
Definitions removed from common_device_functions.cuh (legacy compat
block retained for PPO kernels only). Total: 312 bf16 calls eliminated
across both sessions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:46:17 +02:00
jgrusewski
7000151493 fix: IQL readiness uses adaptive EMA — was binary pop-on
Readiness jumped from 0 to ~0.8 in one step when CV first improved,
causing per-sample support to snap from [-1,1] to V(s)-centered
discontinuously. Same adaptive alpha pattern as eval_v_range:
α = |error| / (|error| + 0.05), clamped [0.01, 0.3]. Smooth ramp-in.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:26:05 +02:00
jgrusewski
e3bd44f170 fix: eval EMA seeds from neutral v_range — snap-to-first caused epoch 3 spike
EMA init used == 0.0 sentinel and snapped to first non-zero Q-stats,
causing a discontinuous v_range transition → val_Sharpe=30 spike at
epoch 3 followed by -1 correction. Now seeds from neutral center
(mean=0, std=1 → v_range=[-4,4]) and the adaptive alpha tracks
to actual Q-stats smoothly over 2-3 steps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:24:59 +02:00
jgrusewski
9c2f5d75b5 fix: eval v_range EMA uses adaptive alpha — fixed α=0.05 lagged on Q-shifts
When Q-mean shifted from +0.01 to -0.01, fixed α=0.05 took ~20 updates
to catch up → misaligned backtest atoms → val_Sharpe dipped to -20.
Adaptive alpha: α = |error| / (|error| + 0.01), clamped [0.01, 0.5].
Tracks fast on Q-shifts (α→0.5), smooths when stable (α→0.01).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:01:49 +02:00
jgrusewski
8c3b7f5c5e fix: validation backtest uses EMA-smoothed eval v_range — was fixed [-240, 240]
Fixed v_range from hyperparams (reward_scale/gamma=±240) was 4800x too
wide for Q-values near 0. All Q-values mapped to the same 2-3 center
atoms, making backtest action selection essentially random → val_Sharpe
jumped between -11 and +0.14 arbitrarily. Now uses the same
EMA-smoothed eval_v_range that the experience collector uses.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 13:39:44 +02:00
jgrusewski
5d877234b3 fix: Q-stats readback is synchronous — stale double-buffer caused 2-state oscillation
reduce_current_q_stats returned PREVIOUS call's results via async
double-buffer. When the eval_v_range EMA converged, stale readback
created a limit cycle: Q-mean alternated between exactly -0.0190 and
-0.2102 for 20+ epochs. Now uses synchronous cuStreamSynchronize +
cuMemcpyDtoH (5µs cost every 50 steps). Removes dead
flush_q_stats_readback and q_stats_ready field.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 13:16:26 +02:00
jgrusewski
5c725f7c9d fix: C51 target uses softmax-weighted mixture instead of hard argmax
Hard argmax picks actions based on noise when Q-values are flat
(all near zero). This causes policy collapse — the model locks into
a single action pattern after 1 epoch (Trades=56647 locked, action
distribution frozen). Expected SARSA-style softmax mixture:
  p_target(z) = Σ_a softmax(E[Q(s',a)])[a] * p_target_a(z)
When Q-values differentiate, converges to argmax. When flat, averages
across actions → stable gradient signal → breaks policy collapse.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 12:07:59 +02:00
jgrusewski
dea2ce1db7 fix: EMA-smooth eval v_range updates — per-step snapping caused val_Sharpe instability
update_eval_v_range snapped to current Q-stats every 50 steps. With
per-sample IQL support (the circular dependency fix), Q-stats fluctuate
between steps, causing the backtest atom placement to jump → val_Sharpe
oscillated between -42 and 0. EMA (α=0.05) smooths the transition,
giving the backtest consistent atoms across evaluations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:38:15 +02:00
jgrusewski
36c803f520 fix: Q-stats use per-sample IQL support — breaks circular eval_v_range feedback
compute_expected_q used eval_v_range to compute Q-values, then Q-stats
updated eval_v_range from those Q-values — circular dependency that
amplified any drift. Q-mean collapsed 0 → -0.69 in 10 epochs despite
mean_reward ≈ 0. Now uses per_sample_support from IQL (independently
trained V(s)), breaking the feedback loop.

Also removes 127 bf16 identity wrapper calls across 5 CUDA kernels
(c51_loss, epsilon_greedy, dqn_utility, experience, attention_backward).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:01:21 +02:00
jgrusewski
2b612b478f cleanup: remove bf16 wrappers from training guard Q-value kernels
qvalue_stats_reduce and qvalue_divergence_check used bf16() identity
wrappers on native f32 data — legacy from the bf16 era. These
wrappers can cause subtle precision differences across GPU
architectures. Replaced with native fminf/fmaxf/__shfl_xor_sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 09:35:32 +02:00
jgrusewski
9260100200 fix: forward cuBLASLt also uses heuristic — AlgoGetIds caused Q-value drift on H100
Forward cached GEMMs (plain + RELU_BIAS) used AlgoGetIds which can
select graph-incompatible algorithms on H100 SM90. The DDQN target
forward pass uses these — biased target Q-values cause bootstrapping
collapse (Q-mean drifted from 0 to -0.69 over 10 epochs despite
mean_reward≈0). Heuristic selects graph-safe algorithms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 09:30:30 +02:00
jgrusewski
6bdda454b5 fix: adversarial regime uses ratio with hysteresis — consecutive counter was too brittle
Consecutive counter reset on a single epoch above threshold, so the
trigger=5 was never reached when max_dd naturally bounced every 3-4
epochs. Now uses EMA of binary low-dd signal (α=0.15) with hysteresis:
activates at ratio>0.6 (sustained low dd), deactivates at ratio<0.4
(recovered). No fixed trigger count, no single-epoch reset.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 09:00:57 +02:00
jgrusewski
bdad095de1 fix: adversarial regime uses adaptive EMA threshold — was permanently locked on
Fixed threshold 0.5% MaxDD was calibrated for zero-gradient era.
With real gradients, max_dd=0.01-0.08% is always below 0.5%, so
adversarial activated at epoch 5 and never released — every epoch
trained under 3x spread, 2x tx_cost, 0.5x fill permanently.
Now uses EMA(max_dd) * 0.5 as threshold, self-calibrating to the
model's actual drawdown scale.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 08:41:12 +02:00
jgrusewski
f74d8d018c fix: episode starts use epoch counter in Philox seed — breaks deterministic repetition
domain_rand_episode_starts used constant seed (i, 0, 9999) producing
identical episode starting bars every epoch. With deterministic starts
and alternating weight updates, the OOS Sharpe oscillated perfectly
between +0.85 and -1.0. Adding epoch to the Philox hash key diversifies
starting positions across epochs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 08:21:37 +02:00
jgrusewski
bf4e92bdb7 cleanup: remove BUFFER_DIAG, RECAPTURE_DIAG, GRAD_DIAG diagnostic infrastructure
Root causes are fixed (cuBLASLt heuristic, entropy inv_batch, overflow,
IQL OOB). Removes: run_buffer_diagnostics, run_recapture_diagnostics,
debug_buffer_norm_f32, diag_recapture_remaining field,
gradient_stage_diagnostics field + FOXHUNT_GRAD_DIAG env var.
Eliminates ~4 stream syncs per step for first 3 steps each fold.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 08:10:46 +02:00
jgrusewski
a5a754ec53 cleanup: remove FOXHUNT_NO_GRAPH diagnostic code path
The ungraphed path served its purpose — confirmed cuBLASLt AlgoGetIds
was selecting graph-incompatible algorithms on H100. Now that the
heuristic fix is in place, the diagnostic is dead code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 08:06:36 +02:00
jgrusewski
172ed8690f fix: MSE grad uses IQL branch_scales — removes hardcoded 4.0x magnitude
MSE gradient kernel had hardcoded branch_scale=(d==1)?4.0:1.0 for
magnitude branch. Now uses the same IQL branch_scales [B,4] buffer
as c51_grad_kernel. Also fixes MSE entropy missing inv_batch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 07:53:00 +02:00
jgrusewski
91613c45b4 fix: MSE gradient entropy also missing inv_batch — same bug as C51
Per-branch entropy boost in mse_grad_kernel added O(1) term without
1/B normalization. Same pattern as the C51 entropy fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 07:49:02 +02:00
jgrusewski
1393cf36f8 fix: entropy gradient missing inv_batch — grad_norm scaled with batch_size
The entropy regularization term in c51_grad_kernel was added without
the 1/B factor, making it O(1) per sample vs O(1/B) for the C51 loss
term. At batch_size=16384, entropy dominated by 16384x, causing
grad_norm=640 (should be ~33). Local batch=16: 5.5 → 0.29.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 07:44:59 +02:00
jgrusewski
15369c8f50 fix: grad_norm_partials sized for d_logits clipping — 12KB overflow per step
d_logits clipping uses val_blocks(2048) + adv_blocks(2304) = 4352
partials, but buffer was sized for total_params(1298). 3054-slot
(12KB) overflow corrupted adjacent GPU memory on EVERY training step
inside graph_forward. This was captured in the CUDA Graph and
replayed every step — the actual root cause of zero grad_buf on H100.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 07:26:20 +02:00
jgrusewski
d3d9c4a145 fix: IQL kernels decode factored actions into branch indices — eliminates OOB
iql_gather_q_taken and iql_compute_advantage_weights indexed q_out
with factored action (0..80) into a [B,12] buffer — massive OOB.
compute-sanitizer found 2586 errors. Now decodes factored action
(dir*b1*b2*b3 + mag*b2*b3 + ord*b3 + urg) into 4 branch indices
and sums per-branch Q-values. Also fixes total_actions config
(was product 81, now sum 12). Sanitizer: 0 errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 00:19:26 +02:00
jgrusewski
2d704eaa07 fix: backward cuBLASLt uses heuristic algo — AlgoGetIds selects graph-incompatible algo on H100
AlgoGetIds picks algorithms by ID order. On H100 (SM90), the first
valid algo uses split-K/stream-K with internal workspace allocation
that silently produces zero output when replayed via CUDA Graph.
The heuristic selects graph-safe algorithms by design.

Also removes FOXHUNT_NO_GRAPH temp env var and raw DtoH diagnostic probe.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 00:01:34 +02:00
jgrusewski
d6ada5266b diag: enable FOXHUNT_NO_GRAPH in Argo train template — isolate H100 graph issue
Temporarily sets FOXHUNT_NO_GRAPH=1 in train-best pods to run
forward+backward+Adam ungraphed. If grad_buf is non-zero ungraphed,
the root cause is CUDA Graph capture of cuBLASLt on H100 SM90.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 23:44:10 +02:00
jgrusewski
407b3b04ff diag: FOXHUNT_NO_GRAPH env var bypasses all CUDA Graph capture
Runs forward+backward+Adam ungraphed to isolate whether H100
grad_buf=0 is caused by graph capture or the cuBLAS operation itself.
NVIDIA docs confirm cublasLtMatmul inside graphs can silently fail
on Hopper when algorithm uses internal workspace allocation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 23:42:59 +02:00
jgrusewski
9dce707348 diag: raw DtoH probe on grad_buf — bypass kernel to confirm H100 data
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 23:30:17 +02:00
jgrusewski
fee5a7cb3a fix: guard diagnostic buffer overflow — d_adv needed 2304 blocks, partials had 1137
debug_buffer_norm_f32 reuses grad_norm_partials (sized for total_params)
as scratch. d_adv_logits with batch_size*tba=589K elements needs 2304
blocks but partials only has 1137 — GPU memory overflow corrupting
adjacent allocations. On H100 this likely trashed grad_norm_buf,
explaining the persistent grad_norm=0.000000. Guard added to skip
oversized norms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 23:23:03 +02:00
jgrusewski
4159d201a0 fix: eliminate grad_buf pointer duplication — avg_grad=0 root cause
All code now uses ptrs.grad_buf (u64) as single source of truth.
Vaccine rewritten to use launch_cublas_backward_to(scratch) instead
of std::mem::swap on CudaSlice which caused pointer divergence
between diagnostics and Adam/graphs. Locally verified: avg_grad=6.13.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 23:05:43 +02:00
jgrusewski
03cac6ffda perf: accumulator uses mapped pinned memory — eliminates memcpy_dtoh
volatile float* bypasses GPU L2 cache for correct read-modify-write
across kernel launches. CPU reads directly via read_volatile at epoch
boundary. Removes dead ptr variable in update_eval_v_range.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:15:05 +02:00
jgrusewski
40cf48868a fix: tile attention backward per-sample grads — eliminates H100 OOM
attention d_params_per_sample was B*P*4 = 1.6GB at batch_size=16384.
Caused CUDA_ERROR_OUT_OF_MEMORY on H100.

Same tile fix as IQL: process min(B,256) samples per tile, accumulate
d_params via +=. Buffer shrinks from 1.6GB to 26MB.

Also fixed: attn_weight_grad_reduce uses += for tile accumulation.
Also fixed: IQL VRAM log now shows tiled allocation size.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 21:55:13 +02:00
jgrusewski
b11eec6ec5 fix: tiled IQL backward — 1.7GB→27MB per-sample gradient buffer
At batch_size=16384, per-sample gradient buffer was B*P*4 = 1.7GB
per IQL trainer (3.4GB for dual tau). Caused OOM on H100 for attention.

Fix: tile backward+reduce into chunks of 256 samples. Same kernels,
launched multiple times with offset pointers. Per-sample buffer
shrinks to min(B,256)*P*4 = 27MB. 64x smaller.

Also fixed: weight_grad_reduce uses += (accumulate across tiles)
instead of = (overwrite). Backward kernel takes full_batch_size
param for correct 1/N gradient scaling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 21:43:13 +02:00
jgrusewski
e306f9f0e0 fix: eval v_range tracks Q-stats — was [-100,100] producing constant val_Sharpe
Root cause: eval compute_expected_q used [-100,100] atom range. With 51
atoms that's delta_z=4.0 — all Q-values collapsed to ~0. Model weights
changed but eval actions didn't → identical val_Sharpe=-9.16 every epoch.

Fix: eval_v_range_buf initialized from config v_min/v_max, then updated
per-epoch from observed Q-stats (q_mean ± 3σ). Eval atoms now track
the actual Q-value distribution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 21:26:46 +02:00
jgrusewski
68bbdd050c fix: revert accumulator to device memory — mapped memory breaks read-modify-write
GPU read-modify-write (acc_buf[0] += loss) requires device memory.
Mapped host memory doesn't support atomic-free += from GPU kernels
on all architectures — H100 showed avg_grad=0.000000.

Reverted to CudaSlice<f32> with DtoH at epoch boundary (gated by
FOXHUNT_GPU_SYNC_DIAG). Removed __threadfence_system from accumulator
writes (not needed for device memory).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 21:18:00 +02:00
jgrusewski
6c8486774b perf: gate GPU sync points behind FOXHUNT_GPU_SYNC_DIAG
Default: enabled (syncs ON for debugging).
Production: set FOXHUNT_GPU_SYNC_DIAG=0 to disable.

Gated:
- Experience collector progress logging sync (3x per collection)
- Training guard read_accumulators epoch-boundary sync

Not gated (required for correctness):
- Drop sync in experience collector (prevents VRAM leak)
- compute_q_stats DtoH (returns data to caller — needs mapped memory refactor)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 21:14:07 +02:00
jgrusewski
61f1b0a1d2 fix: evaluate_baseline compile — make q_provider Optional, remove dead evaluate_dqn path
evaluate_dqn_graphed now takes Option<&mut dyn QValueProvider>.
Training eval passes Some(fused_ctx), standalone eval uses closure path.
Removed dead evaluate_dqn non-graphed branch from evaluate_baseline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 21:05:01 +02:00
jgrusewski
9bbd725fd1 refactor: eliminate ALL hardcoded constants — fully adaptive IQL
Every constant now derives from data:
- cv_max decays at 0.999/step (1000-step half-life) — stale spikes
  don't permanently anchor readiness
- p5 estimator inits from first batch's mean spread (not hardcoded 1.0)
- Frugal step uses 1/sqrt(step_count) — convergence guarantee
- Support floor fraction = 1/num_atoms (guarantees ≥1 atom resolution)
- Staleness decay_k = ln(20) — derived from "oldest = 5% weight" invariant

Zero manual tuning. Zero hardcoded constants. All self-calibrating.

50-epoch: fold 2 Sharpe 16.20, fold 3 Sharpe 6.42 at epoch 42.
No regression from making constants adaptive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 20:49:12 +02:00
jgrusewski
5617c6c9b5 feat: IQL H100 hardening — p5 support floor, buffer-relative staleness, cv_max readiness
Three production fixes, all self-calibrating:

1. Readiness: adaptive CV with cv_max regime-reset anchor.
   When regime change spikes CV, cv_max updates → readiness drops
   until V(s) re-adapts. Drift-based approach tested and rejected
   (V(s) drift is normal training, not an error signal).

2. Support floor: Frugal p5 quantile estimator floors delta_z at
   10% of batch's 5th-percentile Q-spread. No sample contributes
   zero loss. Scale-free, one GPU scalar.

3. Staleness: exp(-3 * age/capacity) replaces exp(-λ * age/τ).
   Buffer-relative normalization. Oldest transition gets 5% weight.
   Removed staleness_lambda and staleness_tau config fields.

50-epoch walk-forward: fold 2 Sharpe 16.65, fold 3 Sharpe 6.43.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 20:38:06 +02:00
jgrusewski
33b03a86a6 docs: IQL H100 hardening spec — drift readiness, p5 floor, buffer-relative staleness
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 20:13:39 +02:00
jgrusewski
3a8a756332 feat: adaptive CV readiness — relative improvement from initial CV
readiness = (cv_initial - cv_current) / cv_initial

No fixed threshold. Captures initial CV at step 1, measures fractional
improvement. Fully domain-adaptive: works for any batch size, reward
scale, or Q-value magnitude.

Result: fold 2 Sharpe 2.47 → 16.15, peak epoch 18 → 42/50.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 20:02:02 +02:00
jgrusewski
7e8150bdf6 feat: CV-based readiness gate for IQL features — self-calibrating warmup
Single GPU-resident scalar: CV = sigma_adv / |mean_adv|.
When CV > 1 (noisy advantages), readiness < 1 → features suppressed.
When CV ≤ 1 (stable advantages), readiness = 1 → features fully active.

All 5 IQL downstream kernels blend toward neutral defaults at readiness=0:
- Per-sample C51 support: blends to [-1, 1] default
- PER modulation: blends to td_errors unmodified
- Branch scales: blends to uniform 0.25
- Advantage weights: blends to neutral 1.0
- Expectile gap epsilon: gap scaled by readiness

Zero new hyperparameters. Computed from existing adv_stats_buf.
Best Sharpe improved 11.39 → 14.23 on 10-epoch stability test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 19:55:20 +02:00
jgrusewski
85794c92ca feat: wire up all IQL features + remove dead code
Wired:
- modulate_td_errors: advantage-weighted PER priorities with staleness decay
- per_sample_epsilon: IQL expectile gap drives state-dependent exploration
  in experience_action_select (NULL fallback for backtest evaluator)
- replay_write_cursor/capacity accessors for staleness computation

Removed:
- use_iql config flag (IQL is mandatory)
- rng_states from ALL 7 experience kernel signatures + Rust launchers
  + backtest evaluator (pure stateless Philox, zero LCG)
- Dead v_range comments cleaned

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 19:28:00 +02:00
jgrusewski
86ad2703dd refactor: remove dead rng_states from experience kernels — pure stateless Philox
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 19:26:55 +02:00
jgrusewski
ece0a42786 fix: replace acc_buf DtoH sync with pinned device-mapped memory
training_guard accumulator (loss_sum, grad_sum, step_count) now uses
MappedBuffer — GPU writes via device pointer, CPU reads via
read_volatile. Eliminates synchronous memcpy_dtoh at epoch boundary.

Also added __threadfence_system() after accumulator writes for
CPU visibility guarantee.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:53:14 +02:00
jgrusewski
3177cde6d1 fix: stateless Philox for stochastic depth RNG (replace LCG)
stochastic_depth_rng kernel now uses Philox hash(step, layer)
instead of stateful LCG. Monotonic step counter replaces
accumulated RNG state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:39:46 +02:00