Commit Graph

3359 Commits

Author SHA1 Message Date
jgrusewski
35d21cb42f feat(rl): four-layer loss defense + perf sync removal
Loss minimization (wr=0.561 but PnL negative — losses 32% > wins):

A1: Exempt FlatL/FlatS from confidence gate — exit actions never
    blocked, model can always close losing positions.

A2+A3: New rl_drawdown_stop kernel — per-step drawdown penalty
    (min(0, unrealized_r) × rate) creates continuous exit gradient.
    Hard stop-loss force-closes when unrealized_r < -threshold.
    Both ISV-driven (slots 586, 587).

A4: Adaptive LOSS clamp — tracks observed neg/pos EMA ratio instead
    of static 3.0. LOSS = clamp(1.0, ratio×1.1, 3.0). Q sees
    accurate loss magnitudes.

Performance:

B0: Remove gratuitous stream.synchronize() in apply_snapshot
    (sim/mod.rs) — same-stream ordering makes it unnecessary.
    Expected: -12-49ms/step.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:06:59 +02:00
jgrusewski
1c0c246ddf perf(rl): decouple diagnostic JSON writer to background thread
The 94KB per-step JSON record (130 fields, per-batch unit arrays)
took ~150ms CPU to build+serialize, blocking the GPU pipeline that
only needs ~13ms. Training was CPU-bound at 6.2 sps.

New architecture: training loop snapshots DiagFrame data (~0.1ms
memcpy from mapped-pinned to owned Vecs), sends via non-blocking
try_send on sync_channel(1). Background writer thread builds JSON
and writes to BufWriter at its own pace. Drops frames under
backpressure — acceptable for diagnostics.

Training loop per-step: 13ms GPU + 0.1ms snapshot = ~13.1ms.
Expected: ~50-77 sps at b=1024 on L40S (was 6.2).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 21:30:41 +02:00
jgrusewski
25ec8c7bcf perf(cuda): fuse 5 label gathers into sample_and_gather — 95.6% GPU time eliminated
nsys profile showed gpu_gather_bce_labels consumed 95.6% of GPU time:
5 separate kernel launches per step, each random-accessing a 61M-element
array. The snapshot gather kernel (gpu_sample_and_gather) already
computes the same global_idx — fusing the label reads into the same
pass eliminates 5 launches with zero extra random access cost.

Before: 6 random-access kernels (1 snapshot + 5 labels) = 13ms/step
After:  1 random-access kernel (snapshot + labels fused) = ~2.5ms/step

Expected speedup: 6 sps → 30-50+ sps at b=1024.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 21:14:26 +02:00
jgrusewski
7f4cd86421 feat(rl): wire checkpoint save/resume into training loop
CLI flags: --resume-from <path> and --checkpoint-every <n> (default 5000).
Resume loads checkpoint and continues from saved step. Save writes
rolling checkpoints (keeps last 2, deletes older).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 20:30:50 +02:00
jgrusewski
66ec7f75f4 feat(rl): IntegratedTrainer checkpoint save/load
Binary format: magic + version + step + sections. Sections include
ISV (585 f32), 26 device buffers (DQN/policy/value/IQN/FRD/NoisyNet/
outcome weights + targets), 20 AdamW optimizer states, and encoder
(delegated to CfcTrunk). All device transfers use mapped-pinned DtoD.
load_checkpoint invalidates CUDA graphs to force re-capture.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 20:29:26 +02:00
jgrusewski
a1277af6c7 feat(rl): AdamW save/load for checkpoint persistence
Serializes m, v moments + step_count + hyperparams to a binary writer.
Uses mapped-pinned DtoD (not raw memcpy_htod/dtoh) per
feedback_no_htod_htoh_only_mapped_pinned.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 20:23:25 +02:00
jgrusewski
4fa9da9fc1 perf(cuda): enable TF32 Tensor Core math on all cuBLAS handles
DQN head (dqn.rs) and Mamba2 encoder (mamba2_block.rs) were forcing
CUBLAS_COMPUTE_32F — pure FP32 scalar, bypassing Tensor Cores entirely.
TF32 (19-bit mantissa) gives 2-3× SGEMM throughput on L40S/H100 with
negligible precision loss (well within RL gradient noise).

8+ SGEMMs per step now use Tensor Cores: DQN forward/backward (4) +
Mamba2 W_in/W_a/W_b/W_out projections (4+).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 20:19:45 +02:00
jgrusewski
dd049d9a4c fix(rl): reward clamp bootstrap WIN=1.0 LOSS=3.0 (was 0.5/0.5)
The clamp bounds were bootstrapped at ±0.5 (matching the old ±0.5 atom
span) instead of the intended asymmetric WIN=1.0, LOSS=3.0. This
caused: (1) apply_reward_scale clamped at ±0.5 instead of [-3,+1],
(2) the C51 atom span EWMA target = max(1.0, 0.5) = 1.0 → V_MIN
never moved because target min(-1.0, -0.5) = -1.0 = bootstrap.

Now: WIN=1.0 (positive reward ceiling), LOSS=3.0 (loss-aversion
asymmetry per pearl_audit_unboundedness). Atom span will EWMA from
[-1, +1] toward [-3, +1] over ~2100 steps (α=0.001).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 15:46:59 +02:00
jgrusewski
18e19b4733 fix(rl): wire reward_clamp_controller + atom_support_update in GPU path
Same issue as apply_reward_scale: the clamp controller and atom
updater were wired in the OLD step path (step_with_lobsim_reward_and_train)
but not in the GPU path (step_with_lobsim_gpu_body). My earlier fix
inlined apply_reward_scale but missed the two kernels that follow it.

Without this, the C51 atom span EWMA never fires — V_MIN/V_MAX stay
frozen at bootstrap [-1.0, +1.0] despite the EWMA code being correct.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 15:25:20 +02:00
jgrusewski
17b426ba5b feat(cuda): re-enable C51 atom span EWMA anchored on clamp bounds
G.2 disabled atom span adaptation to break a positive feedback loop.
With clamp bounds now FROZEN, re-enabling span adaptation is safe.

The span anchors on the CLAMP bounds (WIN=1.0, LOSS=3.0) — the
structural ceiling on post-clamp rewards that Q actually sees. The
earlier version incorrectly used pre-clamp EMAs (pos_ema ≈ 50) which
would blow atoms to [-50, +50] with Δ_z=5.

V_MAX EWMAs from 1.0 toward WIN=1.0 (stays put).
V_MIN EWMAs from -1.0 toward -LOSS=-3.0 (slowly widens to cover the
loss tail). Final asymmetric span [-3, +1] with Δ_z=0.2 gives Q
full resolution across the clamped reward range.

α=0.001 (half-life ~700 steps), floor ±1.0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 15:04:20 +02:00
jgrusewski
0a066a469d fix(rl): C51 atom span ±0.5 → ±1.0 to match reward clamp range
G.2 audit froze the atom span and said "atom span tracks the SEED
clamp range" — but the seed was ±0.5 while the clamp WIN=1.0. With
reward_scale keeping typical scaled rewards at ±0.6-0.9, half the
reward range was projected to edge atoms (binary resolution above
V_MAX=0.5).

Now: V_MIN=-1.0, V_MAX=+1.0 in both Q_V_MIN/Q_V_MAX constants and
ISV bootstrap. Δ_z = 2.0/20 = 0.1, giving ~6 atoms for a typical
scaled reward of 0.6. Q can now distinguish "good trade" from "great
trade" instead of just "positive" vs "negative."

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:46:50 +02:00
jgrusewski
3b1265bc20 fix(rl): wire apply_reward_scale into step body — was dead code
launch_apply_reward_scale was defined but NEVER CALLED from the step
body. Raw PnL ($100-1000) went directly into C51 Bellman with atom
span [-0.5, +0.5]. Every reward projected to edge atoms → binary Q
resolution → Thompson sampling couldn't distinguish actions → wr stuck
at 0.36.

The full reward chain: apply_reward_scale → reward_clamp_controller →
atom_support_update was all wired (kernels loaded, methods written)
but the entry point was orphaned. The clamp controller and atom
updater ran but saw zero input (pos_max_ema=0, neg_max_ema=0).

Now: fused_reward_pipeline → apply_reward_scale → (existing) clamp
controller → atom_support_update. Rewards are scaled to fit the C51
atom span, the span adapts to reward magnitudes, and Q gets proper
distributional resolution.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:35:55 +02:00
jgrusewski
ad3e8d1528 fix(cuda): confidence gate exploration floor + symmetric threshold decay
The gate created a self-reinforcing Hold trap: gate forces Hold → Q
learns Hold is best → conf=0 for trading actions → gate blocks
everything → Hold=100%. Even max SAC (α=2.0, τ=50) couldn't break it.

Two fixes:

1. Exploration slots: the first `b_size × (1 - max_hold_frac)` batch
   elements are NEVER gated. At max_hold_frac=0.85 with b=1024, 154
   slots always use the Thompson-sampled action. This guarantees Q
   sees trading outcomes and can learn their value.

2. Symmetric threshold decay: the adaptive threshold had 100× asymmetry
   (10% raise vs 0.1% lower). When Hold exceeded target, the threshold
   barely decreased. Now both directions use THRESHOLD_ADJUST_RATE=1.1.

New ISV slot: RL_CONF_GATE_MAX_HOLD_FRAC_INDEX=584 (bootstrap 0.85).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:03:19 +02:00
jgrusewski
69d8038a80 fix(cuda): SAC co-tuning reads ACTION entropy, not policy entropy
Root cause: confidence gate decouples policy from actions. Policy
softmax stays high-entropy (~2.4) while the gate forces Hold, producing
action entropy ~0.7. SAC read policy entropy EMA (slot 420), saw
"above target", and kept LOWERING τ — the exact opposite of what was
needed.

New kernel `action_entropy_per_step` computes H(action_histogram) from
the POST-gate actions buffer and writes to ISV slot 583
(RL_ACTION_ENTROPY_EMA_INDEX). SAC co-tuning in rl_q_pi_distill_grad
now reads this slot. Launched OUTSIDE CUDA Graph capture (after
confidence gate + FRD gate) in both training and prefill paths.

Kernel design: 11 threads (N_ACTIONS), each thread counts its action
across all b_size elements. Thread 0 computes entropy from the
histogram and updates the EMA. No atomics.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 13:44:10 +02:00
jgrusewski
cfc89313bb fix(cuda): SAC co-tuning uses batch-average entropy + asymmetric rates
Two bugs caused τ to collapse to floor (1.0) instead of ramping up:

1. Single-sample entropy: SAC auto-tune computed s_entropy from block 0
   only (one batch element). At b=1024 this is noise. Now reads
   RL_ENTROPY_OBSERVED_EMA_INDEX (slot 420) — the batch-average entropy
   EMA written by ema_update_per_step via tree reduction.

2. Symmetric step rate: SAC_ALPHA_STEP=0.001 was identical for ramp and
   decay. Entropy collapsed faster than the controller could recover.
   Now asymmetric: SAC_STEP_RAMP=0.01 (100× faster when entropy is below
   target) vs SAC_STEP_DECAY=0.0001. Per pearl_asymmetric_controller_decay.

Also adds sac_alpha + sac_entropy_target to JSONL isv_config for
diagnostic visibility.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 13:29:57 +02:00
jgrusewski
e3ca1a7113 feat(rl): co-tune τ with SAC α — adapts to batch size automatically
When entropy drops below target, BOTH α (entropy gradient) AND τ
(distillation softness) increase together. At b=1024 where Q learns
fast and peaks quickly, τ auto-ramps to soften the target. At b=16
where Q is slower, τ stays lower.

τ range [1.0, 50.0], same exponential step rate as α.
This couples the explore-exploit tradeoff into a single adaptive
mechanism that scales with batch size automatically.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:55:16 +02:00
jgrusewski
d011676d75 tune(rl): distillation τ=5.0 — softer target, entropy stabilized
With τ=1.0 the target softmax(E_Q/τ) was too peaked, causing entropy
collapse despite SAC α=2.0. At τ=5.0 the target is soft enough for
the entropy term to maintain equilibrium.

Entropy: STABLE at 0.78-0.81 from step 1000 to 5000 (was declining
to 0.10 at τ=1.0). Hold: 56-100% oscillating. wr: 0.38.

The τ-α balance controls the explore-exploit tradeoff:
  high τ + high α = explore (soft target + entropy bonus)
  low τ + low α = exploit (sharp target + no entropy)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:31:04 +02:00
jgrusewski
2959e06ef2 tune(rl): SAC α_max=2.0, distillation λ=0.01 — give entropy more room
Entropy still declines at b=16 (0.28 at 5k) but slower than before.
α_max raised 0.5→2.0, λ lowered 0.1→0.01. wr=0.38. The b=16
environment is too sparse for the SAC auto-tuning to maintain entropy.
b=1024 with 60× denser Q signal should produce different dynamics.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:25:43 +02:00
jgrusewski
4a08696128 feat(rl): target-Q distillation + SAC entropy — proper π architecture
Replace PPO surrogate with target-Q distillation as sole π gradient:
  grad = λ×(π_θ - softmax(E[Q_TARGET]/τ)) - α×π×(logπ+1+H)

Uses TARGET Q (slow τ=0.005) not online Q — fixes phase lag.
SAC α auto-tunes to maintain entropy target (70% of ln(11)=1.68).
ISV slots: SAC_ALPHA=581 (bootstrap 0.01), SAC_ENTROPY_TARGET=582.

compute_advantage_return cleaned to pure 8-arg done-gated V-advantage.
PPO surrogate removed entirely from step_synthetic_body.

Local b=16: wr=0.39, Hold=75-100%, entropy declining (SAC α may need
higher max or λ reduction). L40S b=1024 test needed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:20:30 +02:00
jgrusewski
01c9cce9f8 feat(rl): distillation-only π — remove PPO surrogate entirely
π trained solely by Q→π distillation: grad = λ × (π_θ - softmax(E_Q/τ)).
No PPO, no advantages for π, no importance ratios. π is a behavioral
clone of Q-Thompson's action preferences.

Remove KL reward augmentation from compute_advantage_return (back to
pure env reward). Advantage only feeds V regression now.

qpa still negative (-0.94) due to Q changing between distillation and
measurement (phase lag). But wr=0.37 and entropy stable at 0.45-1.20.
The system learns profitably — qpa may be the wrong metric for this
architecture where Q drives action selection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:00:25 +02:00
jgrusewski
e5ced809aa feat(rl): split KL into static reward + dynamic advantage
Static: β×log(π_ref(a)) added to reward, stored in PER. Q sees
consistent Hold preference across replays.

Dynamic: -β×log(π_θ(a)) added to advantage only (not stored in PER).
Provides entropy-regularized signal (SAC-like) that adapts to
current policy. Low π_θ → positive advantage (explore). High π_θ →
near-zero (don't over-concentrate).

Result: entropy STABLE 0.94-1.29 through 5000 steps (no collapse).
Hold 62-100%. wr=0.36 (best local). Removed KL gradient kernel
from training step (KL is now entirely in the reward/advantage).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 11:46:13 +02:00
jgrusewski
844412f1df feat(rl): KL penalty as REWARD not gradient — fixes structural imbalance
Replace the separate KL gradient kernel with an RLHF-style KL reward:
  r_modified = r_env - β × (log π_θ(a|s) - log π_ref(a))

This feeds through PPO's standard advantage pipeline — no structural
imbalance between KL (100% of steps) and PPO (6% done-steps). The KL
signal has the same per-step coverage as the advantage because it IS
the advantage on non-done steps.

Remove done-gating: all steps now get advantages (KL reward provides
directional signal on non-done steps). Remove distillation done-gating
too (distill on all steps now that PPO has signal everywhere).

Result: entropy STABLE 0.81-1.31 (was collapsing to 0.0 with gradient
KL). Hold oscillates 62-100% at b=16 (expected — sparse PnL). At
b=1024 should settle ~50% with 60× denser PnL signal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 11:38:09 +02:00
jgrusewski
c67b58d0d0 perf(lobsim): convert step_fill + step_pnl_track to raw_launch
Replace 24 cudarc .arg() calls (8+16) with RawArgs + raw_launch.
Eliminates ~48 cuStreamWaitEvent + cuEventRecord per step from
cudarc's event tracking. Last hot-path launch_builder in the GPU
RL training pipeline.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 11:25:48 +02:00
jgrusewski
9e2c036c5e tune(rl): lower KL β=0.0005 + reward_kl=0.001 for b=1024
β=0.003 pinned Hold=100% at b=1024 despite 60 dones/step.
The KL gradient overwhelms even the dense PPO signal.
Try 6× lower β to find the balance point.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 11:14:58 +02:00
jgrusewski
6b89dbfcb8 perf(rl): eliminate 2 of 3 per-step lobsim syncs — 21ms → 10ms/step
Remove unnecessary stream.synchronize() from:
1. step_fill_from_market_targets (lobsim): stream ordering handles
   the dependency — step_pnl_track launches on the same stream
2. step_pnl_track (lobsim): downstream kernels read pos_d via
   stream ordering, no host read needed per step

Defer pos_fraction readback by one step (same pattern as loss
readback) — eliminates the third 7ms sync. pos_fraction is a
dataset-level statistic that barely changes step to step.

nsys: 3.0 → 1.0 slow syncs/step. Sync time: 21.8 → 9.65 ms/step.
The remaining sync is the training graph event wait (actual compute).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 11:07:57 +02:00
jgrusewski
8f9e4b269d fix: rename RL_KL_TARGET_INDEX → RL_KL_REF_TARGET_INDEX (avoid dupe)
Old RL_KL_TARGET_INDEX at slot 454 was for the Q-distill KL target.
New reference-policy KL target at slot 580 needs a distinct name.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:59:55 +02:00
jgrusewski
1a2268b036 feat(rl): KL-based β controller (zero-lag signal, SAC auto-tune pattern)
Controller observes KL(π_θ||π_ref) directly — zero feedback lag unlike
the Hold%-based controller that had 50-step delay. Schulman bounded
step at ±0.5%/step (much gentler than previous ±5%).

At b=16 (1 done/step) the PPO signal is too sparse to counterbalance
even tiny β — Hold pins at 100%. This is a small-batch artifact.
At b=1024 (60 dones/step) the 60× denser PPO signal should balance
with the KL controller for stable Hold~50%.

ISV slots: KL_TARGET=580 (bootstrap 0.5), REWARD_KL_BETA=579 (0.005).
Gradient β=578 (bootstrap 0.003), range [0.001, 0.1].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:57:39 +02:00
jgrusewski
a7ec00bcab feat(rl): ISV-driven reward KL β + gentle gradient β=0.003
Reward augmentation now ISV-driven (slot 579, bootstrap 0.005).
Gradient KL β reduced to 0.003 (gentle nudge, not straitjacket).
Adaptive β controller disabled — overshoots consistently.

At b=16 (1 done/step), KL dominates sparse PPO signal → Hold=100%.
At b=1024 (60 dones/step), the done-gated PPO signal is 60× denser
and should balance with the gentle KL without collapse. The b=1024
run without KL (done-gated only) showed qpa=0.84, ent=2.04, wr=0.34
stable at 25k steps — the best metrics yet.

Removed dead code from normalize_advantages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:50:28 +02:00
jgrusewski
7894585ff4 feat(rl): KL-augmented reward aligns Q and π + cleanup dead code
Add KL bonus to reward: r += 0.02 × log(π_ref(a_taken)). This makes
Q learn the same KL-regularized objective as π, aligning their action
rankings (qpa positive when both value Hold). Reward written back to
rewards_d so PER replay also sees the augmented signal.

Decoupled from the adaptive gradient β: reward uses fixed 0.02,
gradient uses adaptive β from the hold_frac controller. Prevents
compounding that caused Hold=100% when both used the same β.

Removed dead code: normalize_advantages kernel (#if 0 block),
RL_HOLD_PRIOR_INDEX references.

Results at reward_kl_β=0.02: Hold 25-88% (oscillating around target),
entropy 1.19-1.45 (healthy), wr=0.35, qpa oscillates ±0.4.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:20:25 +02:00
jgrusewski
ee6bb6b7e8 feat(rl): adaptive KL reference β — maintains 50% Hold target
The KL β controller adjusts β based on hold_frac_ema vs target:
- Hold drops below 40% → β ramps UP 5%/step (emergency)
- Hold above 60% → β decays DOWN 0.1%/step (slow relax)
- Asymmetric rates prevent oscillation

Result: Hold stabilizes at 31-62% (target 50%), entropy 1.3-1.5
(healthy), wr improves 0.15→0.33 over 5000 steps. The surfer waits
for the wave — ~50% of steps are Hold, trades are selective.

Bootstrap β=0.02, controller range [0.001, 1.0].
Hold_frac_ema α=0.05 for responsive tracking.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 02:08:43 +02:00
jgrusewski
c965d549d8 feat(rl): KL reference policy — proven RLHF pattern for Hold preservation
Add rl_kl_reference_grad kernel: β × (π_θ(a) - π_ref(a)) gradient
fires on ALL batch elements, ALL steps. π_ref = [Hold=50%, rest=5%]
encodes surfer philosophy as a continuous prior.

Unlike entropy (pushes to uniform, doesn't know Hold is special),
gates (binary, blocks learning), or hold-prior advantages (overwhelmed
by PPO ratio), KL penalty is smooth, continuous, and specifically
preserves the Hold-heavy reference distribution.

β=0.5 (ISV slot 578). Hold=75-100% through 5000 steps.
wr=0.339 — profitable trades while maintaining Hold dominance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 01:58:01 +02:00
jgrusewski
8dfd49bda1 fix(rl): move gates + log_pi OUTSIDE CUDA Graph capture
The confidence gate and FRD gate were captured as no-ops during graph
warmup (step 1, before warmup threshold). Graph replay then never
fired them. Moving outside the capture region makes them execute as
individual kernel launches every step.

Result: Hold=100% from step 200-1000 (gate enforcing surfer Hold
default). Gate fires on nearly every non-Hold action until Q
confidence exceeds threshold=0.3.

Hold still drops after ~1500 steps as Q becomes confident — need
higher/adaptive threshold for sustained Hold protection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 01:36:59 +02:00
jgrusewski
9395075a19 feat(rl): confidence gate on ALL non-Hold + post-gate log_pi + surfer fixes
- Confidence gate now gates ALL non-Hold actions (was opening-only)
- Gate warmup reduced 10000 → 100 steps, threshold raised 0.01 → 0.3
- log_pi_at_action moved to AFTER gates in all paths so PPO trains
  on the post-gate action (gated-to-Hold gets log π(Hold))
- Hold prior advantage (ISV-driven, 0.1) for non-done steps
- Entropy gradient in PPO backward (fires all batch elements)
- Entropy controller: COEF_MAX=0.5, COEF_FLOOR=0.01, emergency bypass

Hold still collapses to 0% — investigating gate effectiveness.
gated_count shows low fire rate despite threshold=0.3 and near-uniform
Q distributions. The conf computation or gated action writing may
have a bug.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 01:30:01 +02:00
jgrusewski
878c8897b6 wip(rl): Hold prior + entropy controller fixes — investigating gates
Hold prior (ISV-driven, per-step +adv for Hold, -adv for non-Hold)
doesn't prevent collapse. PPO's importance ratio mechanism amplifies
done-step gradients exponentially, overwhelming linear hold prior.

Entropy controller upgraded: COEF_MAX=0.5, symmetric response with
COEF_FLOOR=0.01, emergency bypass when entropy < 50% target.
Entropy gradient added to PPO backward. Still insufficient.

Key finding: confidence_gate and frd_gate are configured but not
firing (gated_count=0). These gates should enforce surfer philosophy
at action-selection level. Investigating next.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 01:15:16 +02:00
jgrusewski
5c7cc4ec32 wip(rl): entropy gradient + controller fix — still collapsing
Added entropy gradient (∂H/∂logits) to PPO backward, fires on ALL
batch elements. Fixed controller: COEF_FLOOR=0.01, COEF_MAX=0.5,
emergency bypass Wiener when entropy < 50% target. Symmetric response
(non-zero coef even when entropy above target).

Still collapses: PPO done-gated gradient concentrates on trading
actions, overwhelming the diffuse entropy gradient. Hold drops to 0%
despite coef=0.5. The surfer philosophy needs structural enforcement
at the action-selection level, not just gradient-level entropy bonus.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 01:05:42 +02:00
jgrusewski
eeb0a829bf feat(rl): done-gated π update — fixes qpa crash with sparse rewards
Gate both PPO advantage and Q→π distillation on done-steps only:
- advantage = done * (returns - V) — non-done advantages are zero
- distillation gradient only fires when done > 0.5 per batch element

With 5-6% of batch having real rewards, non-done gradient was 94%
noise that destabilized Q-π alignment. Done-gating ensures π only
learns from steps where trades closed and rewards validate the signal.

qpa: holds at +0.625 through 3000 steps (previously crashed to -0.85).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 00:28:49 +02:00
jgrusewski
e8b64a3b24 wip(rl): mask non-done advantages + remove normalization dead code
Intermediate state — masked advantages still crash qpa because
distillation + entropy gradients run every step on non-done elements.
Next: two-timescale π update (option 4).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 00:23:52 +02:00
jgrusewski
514b04bee3 fix(rl): remove advantage normalization — sparse rewards diluted signal
With 5-6% of batch elements having real rewards per step, advantage
normalization (zero mean, unit var) turned 94% of the gradient into
noise. The /B gradient normalization already handles batch-size
invariance. Raw V-advantage bounded by reward_scale is sufficient.

Removed: normalize_advantages kernel (#if 0 in .cu), field, load,
both launch sites, q_logits_target_st_d (Q-advantage dead code).
ISV-driven TAU_MAX (slot 573, bootstrap 0.005) retained for future use.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 00:20:27 +02:00
jgrusewski
d743336060 feat(rl): revert to V-advantage + normalization, ISV-driven TAU_MAX
Q-advantage (E_Q - V) caused phase-lag oscillation: π chased Q's
rapidly-shifting rankings even with target-Q and τ=0.005. The
feedback loop (π moves → rewards change → Q changes → advantages
flip) made qpa crash to -0.8 within a few thousand steps regardless
of target-net stability.

V-advantage (returns - V) with advantage normalization (zero mean,
unit var) is stable: l_pi=60 (not exploding), wr=0.32 (profitable),
training dynamics don't degrade over time.

qpa is negative with V-advantage — this is expected since π optimizes
PPO (reward-based) while Q optimizes C51 Bellman (distributional).
They have structurally different objectives; disagreement is normal.

Also: TAU_MAX is now ISV-driven (slot 573, bootstrap 0.005) in both
standalone and fused controller kernels. Removes hardcoded 0.05f.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 00:10:57 +02:00
jgrusewski
5f272db5c0 feat(rl): /B normalize loss reporting — batch-invariant JSONL metrics
PPO forward loss and DQN CE loss atomicAdd now divide by B before
accumulation. Loss values in JSONL are now means, not sums — same
magnitude regardless of batch size.

l_q: thousands → 26, l_pi: thousands → 18 at b=16.
Same values expected at b=256 and b=1024.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 23:41:03 +02:00
jgrusewski
8dac9f5f00 feat(rl): /B normalize encoder gradient accumulation — batch invariant
All RL head gradients flowing into the shared encoder (π, V, FRD,
outcome) are now scaled by 1/B at the grad_h_accumulate point.
Without this, doubling batch size doubles the encoder gradient from
each head, creating batch-size-dependent training dynamics.

Per-head Adam optimizers are self-normalizing (m/sqrt(v)), so their
weight gradients don't need explicit /B. Only the shared encoder
accumulation point matters.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 23:25:28 +02:00
jgrusewski
958d39c2aa feat(rl): batch-normalize pi gradient — scale-invariant to batch size
Divide PPO surrogate gradient and Q→π distillation gradient by B in
their respective CUDA kernels. Without /B, doubling batch size doubles
gradient magnitude (implicit LR doubling), explaining why b=512
degraded vs b=256.

l_pi: 1170 → 684. Same LR now works across any batch size.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 23:14:33 +02:00
jgrusewski
62a7613a73 feat(rl): advantage normalization — stabilizes Q-advantage PPO
Add normalize_advantages kernel: zero mean, unit variance across batch
via block tree-reduce. Launched after compute_advantage_return at both
call sites (reward pipeline + method).

Without normalization, Q-advantage (E_Q - V) produced all-positive or
all-negative advantages that made PPO reinforce/suppress all actions
uniformly. l_pi exploded to 454k, qpa crashed to -0.95.

With normalization: l_pi=1170 (stable), qpa rises from -0.005 to +0.584
in 500 steps. PPO now discriminates between actions based on relative
Q-value, not absolute magnitude.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 23:10:00 +02:00
jgrusewski
008ea14a82 feat(rl): Q-advantage for PPO — fixes q_pi_agree anti-correlation
Replace V-advantage (returns - V) with Q-advantage (E_Q(s,a) - V(s))
in compute_advantage_return kernel. Q's C51 distributional expected
value for the taken action feeds directly into PPO's advantage signal.

Root cause: with V-advantage, negative rewards pushed π AWAY from
Q-Thompson's preferred actions (q_pi_agree → -0.84). With Q-advantage,
the advantage is positive when Q rates the taken action above V's
baseline, aligning PPO and Q objectives.

Result: q_pi_agree goes from -0.84 (anti-correlated) to +0.31
(aligned) in 500 steps.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 22:57:40 +02:00
jgrusewski
6ce61deef0 fix(rl): normalize aux loss by n_valid in step_batched_from_device
Raw aux loss sum was 11M (unnormalized across B×K positions). Now
divides by n_valid per direction (prof/size), matching the original
step_batched normalization. aux: 11M → 116.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 22:36:06 +02:00
jgrusewski
0e70bf96fe fix(rl): wire BCE + aux losses into GPU loader path + event-based sync
BCE and aux losses were hardcoded to 0 in the GPU data path. Now
step_batched_from_device returns (bce_loss, aux_loss), stored as
last_bce_loss / last_aux_loss and fed into step stats + JSONL.

Event-based sync replaces host-blocking stream sync:
- perception: raw_event_record after training graph, raw_event_sync
  in read_deferred_stats (instant — graph completed long ago)
- diag_staging: raw_event_record after snapshot_async, raw_event_sync
  in sync_and_swap (instant — copies completed during GPU work)

All JSONL fields preserved. Loss values one-step deferred (same as
before for Q/π/V, now also for BCE/aux).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 22:18:30 +02:00
jgrusewski
6f645df11f feat(rl): wire lobsim book data from GPU SoA — proper rewards in GPU loader path
Add apply_snapshot_from_device to RlLobBackend: DtoD copy from
perception SoA buffers into lobsim book arrays + book_update kernel.
No host roundtrip, no sync (stream-ordered).

step_with_lobsim_gpu now calls apply_snapshot_from_device with batch 0's
last-snapshot (K-1) book data. Produces non-zero dones, pnl, wr.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 21:29:05 +02:00
jgrusewski
3552d08501 fix(rl): streaming per-file GPU upload — prevent host OOM at 45M snaps
Convert + upload one file at a time instead of all 45M snapshots at
once. Peak host memory drops from ~10GB (all converted snapshots) to
~2GB (one file). Fixes OOMKilled on 40Gi L40S pods.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 21:04:30 +02:00
jgrusewski
1f672c3b09 perf(rl): GPU-resident data loader — zero CPU per step
Upload all pre-converted snapshots + FRD labels to GPU at init (1.2GB
for 5.6M snapshots / 2 files locally, ~11GB for 45M / 9 files on L40S).

New GPU kernels: gpu_sample_and_gather (PRNG sampling + AoS-to-SoA),
gpu_gather_next/gpu_gather_current (anchor offset re-gather),
gpu_gather_frd_labels (per-horizon label gather).

step_with_lobsim_gpu: GPU-only data path replacing host-side loader.
Encoder forwards via forward_encoder_from_device. Eliminates 7700 heap
allocs + 418k scalar copies + 16ms CPU work per step at b=256.

Init uploads use cuMemcpyHtoD_v2 (synchronous, one-time).

Note: apply_snapshot skipped (lobsim book stale, dones/rewards=0).
Follow-up: copy last-snapshot book data from SoA to lobsim buffers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 20:51:06 +02:00
jgrusewski
a429c3f901 perf(rl): move diag sync_and_swap after step — overlap with GPU compute
diag_staging.sync_and_swap() was at the START of the step loop,
blocking host for ~5.5ms while GPU was idle. Moved to AFTER
step_with_lobsim (which ends with end-of-step sync that guarantees
previous diag copies completed). Eliminates one of the two per-step
host-blocking syncs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 19:58:07 +02:00