Two same-seed runs now produce bit-equal eval_summary.json, alpha_rl_train_summary.json,
and diag.jsonl (modulo wall-clock elapsed_s). The 5-phase falsification chain landed:
Phase 2 PER tree-rebuild: __threadfence is NOT a grid-wide barrier; multiple blocks
raced across sum-tree levels. Fix: Grid=(1) Block=(1024) + __syncthreads
in rl_per_tree_rebuild.cu.
Phase 2.3 cuBLAS GEMM_DFALT + TF32 default-math allowed split-K non-deterministic
accumulation at 3 sites. New crates/ml-alpha/src/cublas_determinism.rs
applies CUBLAS_PEDANTIC_MATH via FOXHUNT_DETERMINISTIC env toggle
(0=TF32 prod, 1=PEDANTIC dev default, 2=DEFAULT_MATH control).
Phase 2.6 Two bugs surfaced sequentially in the backward kernel chain:
(1) rl_iqn_tau_cos_features had a multi-block r/w race on prng_state[batch]
— all N_TAU=32 blocks read seed; only tau_idx==0 wrote back; no
inter-block barrier. Fix: split into READ-ONLY rl_iqn_tau_cos_features
+ new sibling rl_iqn_advance_prng_state launched on same stream
(kernel-launch ordering = grid-wide barrier).
(2) OutcomeHead::new called near_zero_xavier without scoped_init_seed,
falling back to time+thread-id RNG. Stayed dormant until first done
event activated non-sentinel labels and divergent weights flowed via
grad_h_t_outcome into encoder gradient. Fix: add seed param + install
scoped_init_seed(dqn_seed.wrapping_add(0x0CE0)) guard.
Validation (./scripts/determinism-check.sh --quick, RTX 3050, b=128, 200+50 steps):
- All 200 rows of checksums.* leaves match (rel-tol 1e-5, abs-tol 1e-7)
- eval_summary.json, alpha_rl_train_summary.json byte-equal between runs
- diag.jsonl byte-equal modulo elapsed_s
- Eval pnl identical run-A vs run-B at seed 42
Pre-fix baseline (Phase 2.5 measurement): same-seed eval pnl spread $450k
($187k vs -$261k). Post-fix: $0 spread.
Speed cost: ~1.5ms/step amortised; ~10-15% slower than TF32 production
(PEDANTIC tax — acceptable in dev, toggle to FOXHUNT_DETERMINISTIC=0 for prod).
Mapped-pinned discipline: all 11 NEW memcpy_dtoh sites in diagnostic dump methods
+ per-step checksum readback use a new pub(crate) helper
read_slice_d_into<T: Copy>(stream, src, dst) — MappedRecordBuffer + raw
memcpy_dtod_async + raw_stream_sync + volatile read. Generic over T (f32, f64,
i32, u32, u8). Satisfies feedback_no_htod_htoh_only_mapped_pinned + hook guard.
Bundled Tier 1.5 fast-dev-cycle infrastructure (spec
docs/superpowers/specs/2026-06-02-fast-dev-cycle.md):
- scripts/local-mid-smoke.sh b=128, 2000+500, ~10min on RTX 3050
- scripts/determinism-check.sh runs mid-smoke twice, diffs checksums
- scripts/tier1_5_verdict.py behavioral kill verdict
- AdamW checkpoint save/load (crates/ml-alpha/src/trainer/optim.rs)
- IntegratedTrainer checkpoint save/load (resume from checkpoint)
- 15 Phase 1 checksum leaves in build_diag_value
- Env-gated dump methods (FOXHUNT_DETERMINISM_DEBUG_PER/MAMBA2/RL/BACKWARD)
for future divergence-chasing — never run in production
Documentation:
- docs/superpowers/specs/2026-06-02-determinism-foundation.md
- docs/superpowers/specs/2026-06-02-fast-dev-cycle.md
- docs/superpowers/plans/2026-06-02-determinism-foundation-implementation.md
- docs/superpowers/notes/2026-06-02-determinism-phase{1,2,2.2,2.5,2.6}-*.md
- Adjacent specs/plans/notes from the analytical chain that surfaced determinism
as the load-bearing blocker (eval-summary, eval-boundary, regime-observer,
multi-head policy, regime-invariance, Phase 3 IQN-complement post-mortem)
Unlocks: every controller / architecture / reward-shaping A/B from this commit
onward attributes outcome differences to the change, not random-init kernel-race
drift cascading through training x eval LOB-sim trajectories. The eval-collapse
investigation (pearl_reward_signal_anti_aligned_with_pnl, multi-head spec,
regime-invariance spec) is now testable with trustworthy verdicts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
14 KiB
Phase 2 v2 — Proper Dueling Distributional C51 Design
Date: 2026-05-29
Status: Spec (informed by 2026-05-29 post-mortem + RL literature research)
Supersedes: 2026-05-28-state-conditional-q-synthesis.md (architecturally flawed)
Baseline: Plan A v2 (commit fd3174262, branch ml-alpha-plan-a-v2)
0. Why this spec exists
The original Phase 2 spec proposed three sub-phases that interacted incorrectly:
- Phase 2.0 (V envelope clamp): Caused PPO advantage corruption when V was clamped tighter than actual return range. DROPPED in Plan A v2 — confirmed harmful.
- Phase 2.1 (Dueling C51): Implementation used scalar V as Bellman bootstrap, making Q-update action-INDEPENDENT (target = projection of
r + γ V(s') - V(s)is the same for all actions in a given state). Provably collapses to no action preference at convergence. REVERTED in Plan A v2. - Phase 2.2 (Per-action heterogeneous atoms): Architecturally flawed per RL literature — categorical C51 with per-action atom supports requires a "support-alignment operator" that has no clean mathematical formulation. NEVER ADOPTED in Plan A v2.
RL literature consensus (Perplexity research, 2026-05-29):
For C51 distributional Q, use a single unified canonical support across all actions. Per-action heterogeneous atoms are "last resort" — naturally handled by quantile methods (IQN/QR-DQN), NOT categorical C51.
This spec proposes a properly-grounded Phase 2 v2 design.
1. Goal
Match the SURFER baseline (dd049d9a4, wr=0.56 + $10.6M @ 18k) AND beat the TREND baseline (6d33f18b7, $40M peak) by addressing the TWO architectural shortcomings the original Phase 2 spec correctly identified:
- Cross-regime value mediation — FLAT-state Q values are small (per-step rewards), OPEN-state Q values are large (cumulative position value). Without a unified baseline, Q-learning can't transition between regimes coherently.
- Action-specific resolution — Long/Short/Hold benefit from fine [-3, +1] resolution; FullFlat/HalfFlat benefit from wider range to capture trade-realization variance.
The architecturally correct way to achieve both is proper dueling distributional Q with:
- V_head: scalar value baseline (action-independent, shared across all actions)
- A_dist: per-action advantage distribution on a SHARED atom support with mean-centering identifiability constraint
- Joint loss: composed
Q = V + A − mean_a Aregressed against TD target
This is exactly what Phase 2.1 intended but implemented incorrectly.
2. Architectural Decisions
2.A Dueling decomposition (proper)
Q(s, a) = V(s) + A(s, a) − mean_{a'} A(s, a')
Where:
V(s)= scalar state value, action-independentA(s, a)= advantage distribution (atoms) per actionmean_{a'} A(s, a')= action-mean subtraction, enforces identifiability
Identifiability: Without mean subtraction, V and A are unidentifiable (any V+K and A−K gives same Q). The subtraction zero-centers A, making the decomposition unique.
2.B Shared atom support for A_dist
All actions share the same atom support: [-A_MAX, +A_MAX] with Q_N_ATOMS=21 atoms.
Bootstrap value: A_MAX = 3.0 (matches Plan A v2's atom span scale).
Why shared not per-action: Per RL literature, advantages have similar magnitudes across actions (centered around 0 by mean-subtraction). The differential is in the magnitudes, NOT the typical range. Shared atoms with reward_scale normalization captures both regimes:
- Long/Short: typical A in [-1, +0.3] (small per-step deviation)
- FullFlat/HalfFlat: typical A in [-2, +2] (realization variance)
Both fit comfortably in [-3, +3] with adequate resolution.
2.C Bellman target — joint composed Q loss
The KEY architectural difference from broken Phase 2.1:
Phase 2.1 (WRONG): A trained against scalar y − V(s) = r + γ V(s') − V(s) projected onto A atoms. Target is action-INDEPENDENT.
Phase 2 v2 (CORRECT): Composed Q distribution Q_dist(s, a_t) = V(s) + A_dist(s, a_t) − mean_{a'} A_dist(s, a') is regressed against TD target distribution. Target is the C51 projection of r + γ × Q_target_dist(s', a*) onto shared atom support, where a* = argmax_a E[Q_target(s', a)] (Double-DQN).
This is action-DEPENDENT through a* → A naturally learns action-specific structure.
Gradient flows through composed Q to BOTH V and A:
- ∂L/∂V: full TD error magnitude
- ∂L/∂A(s, a_t): TD error per-atom contribution
- Mean-subtraction term: gradient distributes negatively to all A(s, a') for a' ≠ a_t
2.D V head training — auxiliary regression
V head trains independently via MSE against returns (n-step):
loss_V = (V(s) − R_n(s))²
where R_n = r + γ R_{n-1} ... + γ^n V_target(s_{t+n}).
This is in ADDITION to V's gradient from composed Q loss. Dual training:
- From composed Q loss (gradient via dueling decomposition)
- From auxiliary return regression (PPO advantage signal)
Both push V toward the true value function. The auxiliary loss accelerates V convergence and provides PPO with stable advantages.
2.E State-conditional action mask (deferred)
Phase 2.3 (state mask) is intentionally DEFERRED. The mask is independently orthogonal to dueling — can be added later as a wrapper on top of either Phase 2 v1 or v2 architecture.
3. Implementation Sub-phases
Sub-phase 2v2.0 — Plan A v2 baseline merge (already done)
Branch: ml-alpha-plan-a-v2 (commit fd3174262).
- ✅ Distributional C51 Q (
dd049d9a4architecture) - ✅ V_head for PPO advantage (
dd049d9a4architecture) - ✅ Bug fixes: VSN stride, h_mag multi-warp, compute_advantage_return done-branch
- ✅ Cluster validated: wr=0.57 + $3.5M @ step 7000 (run
alpha-rl-8ksnj)
This is the foundation. All sub-phases build on this.
Sub-phase 2v2.1 — V joint training (preparation, 1 day)
Goal: V gets gradient from BOTH PPO advantage signal AND the upcoming composed Q loss.
Files to modify:
crates/ml-alpha/cuda/v_head_fwd_bwd.cu— V head already done as scalar regression. Keep.crates/ml-alpha/src/trainer/integrated.rs— ensure V_head.backward fires on EVERY step (already does for PPO).- New:
crates/ml-alpha/cuda/composed_q_grad_to_v.cu— kernel that adds ∂L_Q/∂V into V's grad_h_t accumulator.
Specific changes:
- After distributional Q loss kernel fires, add a kernel that:
- Reads per-batch CE-loss gradient w.r.t. composed Q
- Sums contribution to V (constant across atoms):
dL/dV = Σ_z (dL/dQ_atom[z]) × 1(V adds the same to every atom) - Adds to
grad_h_t(V's encoder gradient accumulator)
Smoke test: local b=128 × 1000 steps. Pass if l_v decreases monotonically and V_pred converges to observed return mean within 2σ.
Sub-phase 2v2.2 — Dueling composed Q kernel (the core change, 2-3 days)
Goal: Implement Q = V + A − mean_a A composition in forward + backward kernels.
Files to modify:
crates/ml-alpha/cuda/dqn_distributional_q.cu— reinterpret Q-head output as A_dist (no architectural change in shape, just semantics).- New:
crates/ml-alpha/cuda/composed_q_fwd.cu— kernel computing composed Q distribution per batch per action from V + A_dist. - New:
crates/ml-alpha/cuda/composed_q_bwd.cu— gradient through composition. crates/ml-alpha/cuda/bellman_target_projection.cu— UNCHANGED (still projects scalar bootstrap onto shared atoms).crates/ml-alpha/src/rl/dqn.rs— wire new kernels;forwardreturns composed Q;backwardroutes gradient.
Specific changes:
-
Forward composition (per batch, per action, per atom):
composed_logits[b, a, z] = V(s_b) + A_logits[b, a, z] − mean_{a'} A_logits[b, a', z]Note: subtraction happens PER ATOM (z) for proper distributional decomposition.
-
Action selection (rollout):
Q_value(s, a) = softmax(composed_logits)[z] · atom_z action = argmax_a E[Q(s, a)] // or Thompson sampleAt rollout time, computed from composed Q.
-
Bellman target (TD bootstrap):
a* = argmax_a E[Q_target(s', a)] // Double-DQN y = r + γ × atom_z_values // bootstrap per atom (same atoms shared) target_dist = C51 projection of (y, Q_target_softmax(s', a*)) onto shared atoms -
Loss: CE between composed_Q_dist(s_t, a_t) and target_dist.
-
Backward:
∂L/∂V[s_b]: sum gradient over atoms → V's grad∂L/∂A[b, a_t, z]: (1 − 1/N_ACTIONS) × ∂L/∂composed_logits[b, a_t, z]∂L/∂A[b, a, z]for a ≠ a_t: −(1/N_ACTIONS) × ∂L/∂composed_logits[b, a_t, z]
The mean-subtraction Jacobian distributes negative gradient to all OTHER actions, which is the identifiability constraint.
Smoke test: local b=128 × 2000 steps. Pass criteria:
l_q(composed Q loss) stable, declining trend after warmupl_v(V loss) decliningq_pi_agree(action-mean A agreement) > 0.3 sustained (vs Phase 2.1's 0.01 collapse)- Action distribution diverse (entropy > 1.5)
Sub-phase 2v2.3 — Cluster validation (1 day)
Submission:
./scripts/alpha-rl-run.sh --branch ml-alpha-phase2-v2-dueling --steps 20000 --batch 1024 --gpu l40s
Pass criteria:
- Hold% at step 100 ≥ 50% (sub-90% is acceptable; dueling shifts initial Mamba2 RNG chain consumption)
- wr trajectory ≥ Plan A v2 baseline by step 5000
- pnl_cum profitable (> $0) by step 10000
- entropy stable in [1.5, 2.2] band
- No PPO l_pi explosion (l_pi < 1M by step 20k — Plan A v2 ranges 0-millions, that's normal)
Failure handling:
- Hold% crashes to 95%+ early → mean-subtraction term has wrong sign → revisit backward kernel signs
- l_q diverges (> 100 sustained) → composed Q computation has bug → revisit forward kernel
- wr stuck at 0.50 (no improvement) → V is being trained but A isn't getting useful gradient → check V's contribution dominates A's
4. Future Phase 2.3 (state mask) — design only, defer implementation
Per the original spec's §2.C. The mask is independent of dueling — it operates on action SELECTION (force illegal actions to -∞ before argmax/Thompson). Can be added as a thin layer over the dueling Q outputs.
Defer until v2 dueling is validated at cluster.
5. Future Phase 2.4 (per-action atom support) — abandoned
Per RL literature: this is the wrong direction for categorical C51. If we ever need per-action ranges (we may not — dueling A with shared support should suffice), the proper path is IQN, not per-action C51 atoms.
The existing IqnHead infrastructure (already in codebase) can be promoted from secondary head to primary Q-learning head if cluster experiments show shared-support C51 insufficient. Defer until v2 dueling is validated and we have evidence for IQN need.
6. Risks + Mitigations
| Risk | Impact | Mitigation |
|---|---|---|
| Mean-subtraction backward gradient sign wrong | Q diverges; opposite of dueling | Unit test backward against finite-difference oracle BEFORE cluster |
| V's auxiliary regression overwhelms composed Q signal | A doesn't learn action-specific structure | Balance λ_v and λ_q in loss balance controller; both already exist |
Composed Q forward kernel race condition on mean A |
NaN; non-determinism | Compute mean in single thread-0 reduce per batch (no atomicAdd, follows feedback_no_atomicadd pattern) |
| Cluster Mamba2 RNG init shift makes start state different | Hold% at step 100 won't match dd049d9a4's 90% | Document expected delta; rely on long-horizon convergence to wr=0.56+ |
| Adam optimizer not loading new kernel weights correctly | Silent training failure | Smoke test before cluster: log w_d.shape and grad_w per Adam step |
7. What this spec does NOT do
- No state-conditional action mask (defer to 2.3 if v2 succeeds)
- No per-action heterogeneous atoms (abandoned per research)
- No V envelope clamp (Phase 2.0 — confirmed harmful in Plan A v1)
- No new ISV controllers (P1-P4 from May 29 controller work was based on broken Phase 2.1 — also abandoned)
8. Success definition
Phase 2 v2 succeeds if it achieves:
- wr ≥ 0.57 at step 10k cluster (matches
dd049d9a4) - pnl_cum ≥ $5M at step 20k cluster (matches mid-trajectory
dd049d9a4) q_pi_agree_ema≥ 0.3 sustained (proves Q-π coupling — was 0.01 in broken Phase 2.1)- No NaN spikes (V envelope clamp not needed because V regresses to bounded scaled targets naturally)
If it surpasses ($10M+, beats trend's $40M plateau): pursued architectural intent achieved.
9. Implementation order
2v2.0 ✅ Plan A v2 baseline merge (DONE)
↓ (smoke gate: wr=0.57 confirmed @ cluster)
2v2.1 V joint training preparation
↓ (smoke gate: V converges with composed Q gradient)
2v2.2 Dueling composed Q kernel (forward + backward)
↓ (smoke gate: q_pi_agree > 0.3 sustained at local b=128)
2v2.3 Cluster validation
↓ (cluster gate: wr=0.57+, pnl profitable by step 10k)
Future: 2.3 state mask if needed
Future: IQN promotion if per-action resolution proves needed
Each sub-phase has a standalone smoke test. Failures halt progression and trigger root-cause analysis.
10. Lessons captured
This redesign is informed by an exhausting 2026-05-29 day of cluster failures (alpha-rl-77c54, fcw67, z76v4, rkzkg, tdtqv, zss69, 4sjzw — 7 failed cluster runs). Pearls written:
pearl_state_mask_starves_q_for_closing_actions(D test)pearl_violation_rate_blind_to_hold_attractor(H controller postmortem)pearl_atomicadd_masks_v_instability(atomicAdd removal)pearl_dd049d9a4_surfer_baseline_verified(dd049d9a4reproduction)
The meta-lesson: architectural intent ≠ implementation correctness. The original Phase 2 spec captured real RL design intuitions (V mediation for cross-regime, per-action resolution for varying scales) but implemented them incorrectly. The proper implementation requires careful attention to:
- Identifiability (mean-subtraction in dueling)
- Cross-action mathematical coherence (shared atom support)
- Joint training (composed Q loss, not separate Q and V losses)
Plan A v2 is the proven foundation. Phase 2 v2 is the proper architectural completion of the original intent.