Files
foxhunt/crates
jgrusewski 3737feb664 audit: π drives actions (proper actor-critic) + bump b_size 1 → 16
Two coordinated architectural fixes addressing the deepest blockers
exposed by the audit:

## Option B: π-driven action selection

Per `pearl_q_thompson_actor_makes_pi_dead_weight`: the prior
architecture had Q acting as BOTH actor (via Thompson sample) AND
critic (via Bellman target). π trained by PPO surrogate against
Q's actions but never drove any decision — `q_pi_agree_ema`
decayed to 0 by step 5000 in every smoke because π converged to
Q's Thompson SAMPLING distribution, not Q's argmax. π was
dead-weight: 4 dedicated controllers (ε, ratio_clamp,
entropy_coef, KL EMA), shared encoder gradient interference, and
zero contribution to actor decisions.

### New kernel: rl_pi_action_kernel.cu

Single-thread-per-batch CUDA kernel that:
  1. Computes numerically-stable softmax(pi_logits[b, :])
  2. Draws u ∈ [0, 1) from per-batch xorshift32 PRNG
  3. CDF-walks to pick the multinomial-sampled action

Per-batch xorshift32 PRNG state is the SAME `prng_state_d` buffer
already used by rl_action_kernel — no new state needed. Sampling
deterministic given (seed, b_size, pi_logits).

### Trainer wiring (1 site change in step_with_lobsim)

Replaced `rl_action_kernel(q_logits, atom_supports, ...)`
(Q-Thompson) with `rl_pi_action_kernel(pi_logits, ...)`
(π-multinomial). The argmax_expected_q call on h_{t+1} is
unchanged — Q remains the critic via canonical Double-DQN target.

PPO importance-ratio surrogate now has its canonical actor-critic
semantics: π_new(a|s) / π_old(a|s) where `a` was actually sampled
from π_old. Was nonsensical before (a was sampled from Q-Thompson,
not π, so the ratio measured something incoherent).

The rl_action_kernel (Q-Thompson) cubin + function field are kept
loaded for backward-compat tests and diagnostic comparison; no
longer in the hot path.

## b_size: 1 → 16

Per `pearl_b_size_1_signal_starvation_blocks_q_learning`: at
b_size=1 with 11% done-step rate and 70% loss rate per trade, Q
stayed at uniform baseline ln(21)=3.04 across all 16+ smokes
regardless of controller fixes. The architecture was structurally
signal-starved — 1 gradient sample per Adam step is fundamentally
too noisy.

LobSimCuda already supports b_size>1 (n_backtests parameter at
`crates/ml-backtesting/src/sim/mod.rs:355`). Trainer code is
already b_size-parametric throughout. The blocker was just the
CLI default at `--n-backtests=1`.

Default bumped to 16 (matches the doc note "production sweep at
32-64; L40S 48GB"). 16× more gradient samples per Adam step
gives Q proper batch variance reduction. The K-loop multiplier
(`isv[404]/2048`) will likely settle at K=1 since the
advantage_var_ratio drops with batch size.

## Expected behaviour

  * `q_pi_agree_ema` becomes tautological/dropped (π IS the
    policy now — comparing argmax(Q) to argmax(π) doesn't measure
    a real consistency invariant any more)
  * π gradient flows naturally drive π toward an actor that
    optimises the PPO surrogate — Q's encoder gradient is no
    longer competing with a different policy's gradient
  * l_q should drop meaningfully below 3.04 for the first time
    (was stuck at 2.7-2.9 across all prior smokes)
  * reward/trade should approach 0 (was -$0.5 to -$0.8 across
    every prior run)
  * Wall-clock per env step ~16× slower (b_size=16) but training
    cost per gradient step similar (denser sample = more
    progress per step)

## Verified gates (local sm_86)

  G1 isv_bootstrap   
  G3 controllers     
  G4 target_update   
  integrated_smoke   

## Caveat: integrated_trainer_smoke runs at b_size=1

The default for the CLI is bumped to 16, but the local
`integrated_trainer_smoke` test passes its own b_size=1 to
verify the trainer mechanics. Real-world signal verification
happens via cluster smokes which now use b_size=16 by default.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 10:37:42 +02:00
..