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>
11 KiB
Eval-Boundary Normalization-EMA Preservation — Design
Date: 2026-05-31
Status: Draft (pre-implementation)
Branch target: ml-alpha-eval-boundary-norm-fix (off ml-alpha-regime-observer)
Related work: Phase A (eval_diag), E.1-E.5 (eval-summary aggregation), v9 defensive eval-boundary calibration
Pearls referenced:
pearl_popart_reset_at_eval_boundary_shocks_normalization— full diagnosispearl_adaptive_carryover_discipline— being refined here- F4 envelope design
feedback_isv_for_adaptive_bounds,feedback_adaptive_not_tuned
1. Motivation
Fold-1 eval cluster runs (alpha-rl-8ll7j, alpha-rl-jz48s, alpha-rl-6kghr) all show the same pattern: total eval pnl = −$185M, dominated by a catastrophic shock window in the first ~1000 eval steps where popart σ explodes from train-final ~57 to >4400 within 14 steps.
Phase A's eval_diag.jsonl enabled the per-step diagnosis. Mechanism (verified empirically):
reset_session_stateat the train→eval fold boundary resets reward-clamp scale EMAs to 0:RL_POS_SCALED_REWARD_MAX_EMA_INDEX → 0RL_NEG_SCALED_REWARD_MAX_EMA_INDEX → 0RL_REWARD_CLAMP_CLIP_RATE_EMA_INDEX → 0RL_POPART_MAX_ABS_REWARD_EMA_INDEX → 0(F4 envelope)
- The reward clamp's cap is
cap = pos_max_ema × margin_ratio. Withpos_max_ema = 0, cap = 0 — the clamp is functionally disabled. All raw rewards pass through unclamped. - Eval's first ~10 trades produce realized pnl swings 10-300× larger than train's typical (Kelly resets to bootstrap=1.0 → larger positions; eval data may also have different volatility).
- Unclamped large rewards feed
rl_popart_normalize. F4's envelope ("fast-up, slow-decay" by design — see F4 spec) captures the outliers wholesale. σ_effective =max(σ_welford, envelope)becomes envelope-driven. - σ explodes 57 → 4471 (78× over train calibration) by eval step 14.
- PPO surrogate's
A_unnorm = σ × A_normscales 78×. Policy gradient magnitudes are catastrophically wrong. Next ~1000 eval steps thrash with mis-scaled updates. - Per-trade losses accumulate to dominate the -$185M aggregate.
This is NOT model overfitting in the classical sense. The model's policy survives the shock (qpa recovers to 0.99 by eval step 1500). It IS a state-management bug at the regime boundary.
2. Goals
G1 — Preserve normalization-state EMAs across regime boundaries
At reset_session_state, do not reset:
RL_POS_SCALED_REWARD_MAX_EMA_INDEXRL_NEG_SCALED_REWARD_MAX_EMA_INDEXRL_REWARD_CLAMP_CLIP_RATE_EMA_INDEXRL_POPART_MAX_ABS_REWARD_EMA_INDEX
These are NORMALIZATION EMAs (calibrate signal scale). The model was trained against rewards at this scale; preserving the scale lets the model interpret eval rewards with the same calibration.
G2 — Keep resetting predictive EMAs
Continue resetting Kelly EMAs (win_rate, avg_win/loss, cumulative_dones), dd_trip, cooldown, recency, ε_recovery, session_pnl. These predict future behavior from past, and past behavior in train doesn't predict eval regime.
G3 — Update the pearl rule
Distill the refined rule into pearl_adaptive_carryover_discipline: "RESET PREDICTIVE EMAs (Kelly, dd_trip, cooldown). PRESERVE NORMALIZATION EMAs (clamp pos/neg_max, popart envelope, clamp clip-rate). Don't reset blindly."
G4 — Non-goals
- Don't fix policy-side overfitting yet. If the model's policy is genuinely bad on eval data, that's a separate (Layer 3) problem. Fix the boundary-normalization shock first; the resulting eval pnl is the true measure of model quality.
- Don't introduce a pure-eval mode (frozen weights). That's a separate, larger refactor — defer until we know if normalization preservation alone is sufficient.
- Don't widen
TRADE_LOG_CAPfurther. Already at 4096 (E.3), sufficient.
3. Design
3.1 The change
crates/ml-alpha/src/trainer/integrated.rs:3231-3239 — remove four lines from reset_session_state's reset list:
// BEFORE:
// Reward-clamp EMAs — NEW in v9.
(crate::rl::isv_slots::RL_POS_SCALED_REWARD_MAX_EMA_INDEX, 0.0_f32),
(crate::rl::isv_slots::RL_NEG_SCALED_REWARD_MAX_EMA_INDEX, 0.0_f32),
(crate::rl::isv_slots::RL_REWARD_CLAMP_CLIP_RATE_EMA_INDEX, 0.0_f32),
// ...
// Regime observer boundary policy:
// ...
// - POPART_MAX_ABS_REWARD_EMA → 0 (F4 envelope reset)
(crate::rl::isv_slots::RL_POPART_MAX_ABS_REWARD_EMA_INDEX, 0.0_f32),
// AFTER (4 lines removed; comments around the regime-observer block updated):
// Normalization EMAs intentionally NOT reset — these calibrate signal scale.
// The model was trained against rewards at this scale; preserving the scale
// keeps the reward clamp armed (cap = pos_max_ema × ratio) and lets popart
// continue with the same normalization. RESETTING them disabled the clamp
// (cap=0×ratio=0) and let outliers blow up the envelope → σ shock window
// → -$185M eval loss (validated alpha-rl-6kghr 2026-05-31).
3.2 What stays the same
Everything else in reset_session_state is unchanged:
- Layer 1 CMDP summary slots (session_pnl, worst_pnl, dd_triggered, consec_loss, cooldown) — PREDICTIVE, correctly reset
- Layer 4 (Kelly) EMAs (win_rate, avg_win/loss, cumulative_dones, kelly_fraction) — PREDICTIVE, correctly reset
- Layer 3 (Inventory) EMAs (inventory_penalty_β, inventory_variance) — PREDICTIVE, correctly reset
- Defensive warmup arm (
RL_REGIME_TRANSITION_REMAINING_INDEX= 500 steps) - Regime observer state (dead_zone flag/duration/timeout, tail recency, recovery_factor, ε_recovery_live, prev_worst_pnl) — PREDICTIVE/transient, correctly reset
- Per-batch buffer memsets (session_pnl_per_batch_d, etc.)
3.3 Why not preserve popart.σ / .var / .mean themselves?
popart's Welford state (σ/var/mean) is ALREADY preserved across the boundary — it's not in the reset list. The bug was that env.max (which dominates σ_effective via max(σ_welford, env.max)) was being reset to 0, exposing the bare σ_welford = 1. The 4-line fix preserves env.max so σ_effective stays at train-calibrated level.
3.4 Why preserve clip_rate_ema?
RL_REWARD_CLAMP_CLIP_RATE_EMA_INDEX is the rate at which rewards were being clipped during train. The clamp's margin controller uses this to adjust the margin ratio adaptively (target clip rate ~5%). Resetting it to 0 makes the controller think it's never clipping → grows margin → defeats clamp purpose further. Preserve.
3.5 Will preserving NORMALIZATION cause its own problems?
Scenarios to consider:
| Scenario | Preserved env.max | New regime ACTUAL scale | Behavior |
|---|---|---|---|
| Eval ≈ train scale | 57 | 57 | clamp works as in train, σ stable ✓ |
| Eval larger scale | 57 | 200 | clamp clips some real signal, but pos_max_ema adapts up over ~100 steps; σ grows smoothly |
| Eval smaller scale | 57 | 10 | clamp lets everything through (most rewards ≤ cap), pos_max_ema slowly adapts down; σ stays at 57 until rewards consistently smaller |
None of these are catastrophic. The current "reset to 0" scenario is uniquely bad because it produces an unbounded amplification rather than mild miscalibration.
4. Validation
V1 — Local smoke
Run alpha_rl_train with fold-1 walk-forward at b=16, 1k train + 250 eval. Verify:
- Pre-eval pos_max_ema is non-zero (preserved)
- First eval step's clamp cap > 0
- popart σ stays within 5× of train-final value across all 250 eval steps
- eval_summary n_eval_trades_dropped = 0
V2 — Cluster smoke
Same as v11/Phase A config (fold 1, b=1024, 20k+5k eval, seed 16962). Compare:
- eval_diag.jsonl first 50 steps: popart.σ should stay near train-final (~57), NOT spike to >4000
- eval_summary.json total_pnl_usd: should be dramatically less negative than -$185M (predicted: -$5M to -$30M range, but exact value unknown — that's the residual model overfit)
V3 — Pearl update
Patch pearl_adaptive_carryover_discipline with the predictive-vs-normalization distinction.
5. Implementation phases
Phase 1 — Code change (5 min)
Remove the 4 reset lines from reset_session_state. Update the comment to explain the new policy.
Phase 2 — Local smoke (15 min)
Same fold-1 b=16 smoke as the eval-summary fix. Inspect eval_diag.jsonl popart sigma trajectory.
Phase 3 — Cluster validation (~95 min)
Push, submit, wait, inspect.
Phase 4 — Pearl update (5 min)
Refine pearl_adaptive_carryover_discipline with the predictive-vs-normalization distinction.
6. Open decisions
| # | Question | Recommendation |
|---|---|---|
| 1 | Preserve ALL 4 EMAs, or just the 3 reward-clamp ones (skipping popart envelope)? | Preserve all 4. Without env.max, the clamp may suppress the worst overshoots but the envelope itself still re-bootstraps from 0 and can spike on a single large reward. Both layers of defense matter. |
| 2 | Add a controller-level invariant test? | Yes, V1 local smoke — assert popart.σ after eval step 50 is within 5× of train-final. Cheap to verify. |
| 3 | Update pearl_adaptive_carryover_discipline NOW or wait for cluster confirmation? | After cluster. Update the pearl WITH the empirical evidence of the fix working. |
| 4 | Risk that preserving env.max masks true regime shift in eval data? | Low. EMAs adapt over time; preserving the train-final value just means eval starts calibrated rather than at 0. If eval has truly larger scale, env.max grows there too, just from 57 instead of 0. |
7. Done means
- Lines removed from
reset_session_state, commit made - Local b=16 fold-1 smoke shows popart.σ bounded across eval
- Cluster b=1024 fold-1 run completes, eval_diag.jsonl shows σ never exceeds 5× train-final
- eval_summary.json total_pnl significantly less negative than -$185M (number TBD; if still very negative, that's true Layer-3 overfit work)
- Pearl
pearl_adaptive_carryover_disciplineupdated with the refined rule
8. Risks
- Risk A (low): If train-final pos_max_ema was somehow off (e.g. from train tail event), preserving it carries the mis-calibration into eval. Mitigation: EMA decays over eval steps anyway.
- Risk B (medium): If eval shock losses come from MULTIPLE compounding factors beyond just clamp-disable (e.g. Kelly reset compounded with mis-calibrated normalization), preserving only normalization may only partially fix. Mitigation: cluster validation will measure exact remaining gap.
- Risk C (low): Pearl rule refinement might be over-broad if there are other PREDICTIVE-vs-NORMALIZATION EMAs we haven't catalogued. Mitigation: only apply the rule to the 4 EMAs we're changing today; document the principle but require future cases to verify per-EMA.