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>
22 KiB
Determinism Foundation Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan task-by-task. Phase 1 is currently DISPATCHED separately (coder agent af76b910). Phases 2-6 execute after Phase 1 lands. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Make foxhunt ml-alpha training bit-deterministic (rel-tol 1e-5) across same-seed runs, on a per-step basis, with a permanent CI guard preventing regression. Establish a contract: every dev cluster smoke and every local mid-smoke is reproducible.
Architecture: Localize → Fix → Test → Toggle → Guard. Each phase has explicit acceptance criteria and produces an artifact that the next phase consumes.
Tech stack: CUDA kernels (sm_86/sm_89/sm_90), cudarc + cuBLAS + cuDNN, Rust 1.85, Argo Workflows, foxhunt invariant test harness.
Linked spec: docs/superpowers/specs/2026-06-02-determinism-foundation.md
§0 Phase status at plan creation
- Phase 1 (diagnose): DISPATCHED as background coder agent af76b910 — instrumenting 15 checksums + running diagnostic. Output:
docs/superpowers/notes/2026-06-02-determinism-phase1-diagnosis.mdidentifying ONE first-divergent component. - Phase 2 (fix): this plan covers all 6 candidate causes; ship only the relevant subsection based on Phase 1 output.
- Phase 3 (test infra): independent of Phase 1; can ship in parallel.
- Phase 4 (dev/prod toggle): independent; ships after Phase 2 fix lands.
- Phase 5 (CI guard): independent; ships after Phase 3 test infra.
- Phase 6 (validation): gates merge to main; runs entire suite.
Phase 1 — already dispatched
See coder agent af76b910 output. Once it completes, read docs/superpowers/notes/2026-06-02-determinism-phase1-diagnosis.md for the CULPRIT identification, then jump to the matching §2.X subsection below.
Phase 2 — Apply targeted fix based on Phase 1 culprit
§2.A — IF Phase 1 finds: encoder_output diverged first (Mamba2 culprit)
Files:
- Modify:
crates/ml-alpha/cuda/mamba2_selective_scan_fwd.cu - Modify:
crates/ml-alpha/cuda/mamba2_selective_scan_bwd.cu(likely same fix applies) - Possibly:
crates/ml-alpha/cuda/mamba2_block.cu - Test:
crates/ml-alpha/tests/mamba2_determinism.rs(NEW)
§2.A.1 — Reproduce mamba2 non-determinism in isolation
- Step 1: Write
tests/mamba2_determinism.rsthat calls mamba2 forward kernel twice with same input + same weights + same seed → checksum compare - Step 2: Run it. Verify it reproduces the non-determinism (not just a downstream effect)
- Step 3: If it does NOT reproduce, the bug is downstream of mamba2 — re-read Phase 1 output, the culprit is elsewhere
§2.A.2 — Audit chunk-parallel scan for non-deterministic ordering
- Step 4: Read
mamba2_selective_scan_fwd.cupaying attention to:- Chunk size constant (CHUNK_SIZE)
- Block scheduling order
- Any
atomicAdd(should be zero perfeedback_no_atomicadd) - Any "block 0 OR block 1 writes final" patterns
- The chunk reduction operator (associativity errors compound)
- Step 5: Document audit findings as comments in the file. If clean → bug is in the scan associativity, not block order.
§2.A.3 — Ground-truth reference with CHUNK_SIZE=1
- Step 6: Add a
MAMBA2_DETERMINISTICcompile-time flag that forces CHUNK_SIZE=1 (sequential scan) - Step 7: With flag ON, run determinism check — should pass (sequential is trivially deterministic)
- Step 8: Measure speed cost of CHUNK_SIZE=1: expect ~10× slower
§2.A.4 — Fix: deterministic chunk-reduction with fixed accumulation order
- Step 9: In the chunk reduction operator, ensure accumulation order is FIXED (not parallel-reduce with non-deterministic synchronization):
// BAD (non-det if multiple threads accumulate): __shared__ float acc; if (threadIdx.x == 0) acc = 0.0f; __syncthreads(); atomicAdd(&acc, my_partial); // ← non-det order // GOOD (sequential within block, fixed across blocks): __shared__ float partials[WARPS_PER_BLOCK]; partials[warp_id] = warp_reduce(my_partial); __syncthreads(); if (warp_id == 0) { float total = 0.0f; for (int i = 0; i < WARPS_PER_BLOCK; ++i) total += partials[i]; // block 0 always writes final } - Step 10: Apply the fix
- Step 11: Re-run determinism check — should pass with full chunk parallelism
- Step 12: Measure speed cost: expect ~1-3% (negligible)
§2.A.5 — Validation
- Step 13:
cargo test mamba2_determinism --release --ignoredpasses - Step 14: Full
scripts/determinism-check.shpasses (no checksum divergence) - Step 15: Commit:
fix(mamba2): deterministic chunk-reduction accumulation order
§2.B — IF Phase 1 finds: q_logits / pi_logits / v_value diverged first (cuBLAS split-K culprit)
Files:
- Modify:
crates/ml-alpha/src/networks/q_head.rs(cuBLAS handle init) - Modify:
crates/ml-alpha/src/networks/policy_head.rs - Modify:
crates/ml-alpha/src/networks/v_head.rs - Modify:
crates/ml-alpha/src/networks/dqn.rs(if cuBLAS handle is shared) - Modify:
crates/ml-alpha/src/networks/mamba2_block.rs(likely same) - Test:
crates/ml-alpha/tests/cublas_determinism.rs(NEW)
§2.B.1 — Confirm TF32 enabled is the cause
- Step 1: Tasks 30/31 (completed) enabled TF32 on cuBLAS handles. TF32 uses split-K with non-deterministic K-split heuristic.
- Step 2: Audit each cuBLAS handle init site (grep for
cublasSetMathMode) - Step 3: Document current math modes:
q_head: CUBLAS_TF32_TENSOR_OP_MATH ← tasks 30/31 pi_head: CUBLAS_TF32_TENSOR_OP_MATH ← tasks 30/31 mamba2_block: CUBLAS_TF32_TENSOR_OP_MATH ← tasks 30/31
§2.B.2 — Add deterministic mode toggle
- Step 4: Add helper in
crates/ml-alpha/src/cublas_init.rs(NEW or in existing setup file):pub fn cublas_set_math_mode_deterministic(handle: cublasHandle_t) -> Result<()> { let pedantic = std::env::var("FOXHUNT_DETERMINISTIC") .unwrap_or_else(|_| "1".to_string()) == "1"; let mode = if pedantic { CUBLAS_PEDANTIC_MATH // disables TF32 split-K, FP32 strict } else { CUBLAS_TF32_TENSOR_OP_MATH // current default after tasks 30/31 }; unsafe { cublasSetMathMode(handle, mode); } Ok(()) } - Step 5: Replace every
cublasSetMathMode(handle, CUBLAS_TF32_*)call withcublas_set_math_mode_deterministic(handle) - Step 6: Verify with grep: no remaining
CUBLAS_TF32_TENSOR_OP_MATHdirect calls
§2.B.3 — Validation
- Step 7: Build release. Set
FOXHUNT_DETERMINISTIC=1and run determinism-check → should pass - Step 8: Set
FOXHUNT_DETERMINISTIC=0and re-run → should fail (sanity: the toggle works) - Step 9: Measure throughput cost on RTX 3050 mid-smoke:
time scripts/local-mid-smoke.shwith toggle ON vs OFF. Expect 5-15% slower in deterministic mode. - Step 10: Document the cost in
docs/superpowers/notes/2026-06-02-determinism-speed-cost.md
§2.B.4 — Commit
- Step 11:
fix(cublas): FOXHUNT_DETERMINISTIC env var toggles PEDANTIC vs TF32 math mode
§2.C — IF Phase 1 finds: cuDNN involved (less likely but possible)
Files:
- Same network files as §2.B (if they call cuDNN)
- Modify:
crates/ml-alpha/src/cudnn_init.rs(if exists; or relevant module)
§2.C.1 — Check whether ml-alpha uses cuDNN at all
- Step 1:
grep -rn "cudnn\|CUDNN" crates/ml-alpha/src/ crates/ml-alpha/cuda/to confirm cuDNN usage - Step 2: If zero hits → cuDNN not in path → skip §2.C entirely
- Step 3: If cuDNN is used: apply
CUDNN_DEFAULT_MATH+CUDNN_DETERMINISTICflags via similar toggle to §2.B.2
§2.D — IF Phase 1 finds: custom kernel grad/forward output diverged (block-tree audit)
Files:
- Audit: all
.cufiles incrates/ml-alpha/cuda/ - Most likely candidates:
rl_*_backward.cukernels,*_reduce.cukernels
§2.D.1 — Identify the specific kernel(s)
- Step 1: Phase 1 narrows to e.g.
q_grad_checksum diverges first. The Q gradient is computed bydqn_distributional_q_backward.cu(or similar). Identify the exact kernel. - Step 2: Read the kernel for non-deterministic patterns:
- Any atomicAdd (should be 0 per
feedback_no_atomicadd) - Any "if (block_id == 0)" final-write pattern that depends on block scheduling
- Any
volatilememory access without proper fence - Any
cudaDeviceSynchronizeinside kernel (very rare)
- Any atomicAdd (should be 0 per
§2.D.2 — Common fix patterns
- Step 3: If the kernel uses "first block to finish writes final" pattern, replace with:
- Two-pass: first pass each block writes its partial to global memory, second pass single-block reads all partials in fixed order
- OR: cooperative groups grid-wide sync (requires sm_70+, available on all foxhunt GPUs)
- Step 4: If the kernel reads stale values due to write/read race: add
__threadfence_system()+ counter pattern with explicit ordering - Step 5: Apply the fix
- Step 6: Re-run determinism-check
§2.D.3 — Validation
- Step 7: GPU oracle test for the specific kernel passes bit-equivalence
- Step 8: Determinism-check passes
- Step 9: Commit:
fix(rl): deterministic block-reduction in <kernel-name>
§2.E — IF Phase 1 finds: adam_m_sum / adam_v_sum diverged (optimizer iteration order)
Files:
- Modify:
crates/ml-alpha/src/optimizers/adamw.rs(or wherever Adam state is iterated)
§2.E.1 — Audit parameter iteration order
- Step 1: Find the AdamW step function. Look for parameter iteration via HashMap, Vec, or BTreeMap.
- Step 2: If HashMap is used (e.g.,
HashMap<String, CudaSlice<f32>>), iteration order is non-deterministic in Rust by design. - Step 3: Replace HashMap with BTreeMap OR pre-sorted
Vec<(String, CudaSlice<f32>)>at construction time. Sort by parameter name.
§2.E.2 — Validation
- Step 4: Add unit test in
crates/ml-alpha/tests/adam_determinism.rs: build trainer, do 10 steps, dump Adam m + v, repeat, compare — should be bit-equal - Step 5: Determinism-check passes
- Step 6: Commit:
fix(adam): deterministic parameter iteration order via BTreeMap
§2.F — IF Phase 1 finds: replay_sample_indices diverged (PER sampling)
Files:
- Modify:
crates/ml-alpha/cuda/rl_per_sample.cu(or wherever PER sampling happens) - Modify:
crates/ml-alpha/src/replay/per.rs
§2.F.1 — Check PER RNG seeding
- Step 1: Locate the PER sample kernel. Should use device-side xorshift32 seeded from host (per
pearl_rl_sample_tau_xorshift32) - Step 2: Verify the seed is passed via ISV or kernel arg, deterministically advanced per step
- Step 3: If the seed source is
std::time::SystemTime::now()orthread_rng()anywhere → that's the bug
§2.F.2 — Fix
- Step 4: Ensure PER seed derives from a deterministic counter (e.g.,
cli.seed + step_counter) - Step 5: Verify priority update / read ordering uses CudaEvent, not stream-implicit ordering
- Step 6: Re-run determinism-check
§2.F.3 — Commit
- Step 7:
fix(per): deterministic sample-RNG seeded from cli.seed + step
Phase 3 — Test infrastructure (parallel to Phase 2)
§3.1 — tests/determinism_invariants.rs
Files:
-
Create:
crates/ml-alpha/tests/determinism_invariants.rs -
Task 1: Write the test file with 3 tests:
//! GPU-oracle determinism invariants.
//!
//! Test 1: same seed → identical checksums (rel-tol 1e-5)
//! Test 2: different seed → different checksums (sanity check)
//! Test 3: same seed + golden checksum file → match (catches regression)
use anyhow::{Context, Result};
use serde_json::Value;
use std::path::PathBuf;
use std::process::Command;
fn binary_path() -> PathBuf { /* ... */ }
fn data_dir() -> PathBuf { /* ... */ }
fn read_jsonl(path: &Path) -> Result<Vec<Value>> { /* ... */ }
fn extract_checksums(row: &Value) -> Result<Vec<(String, f64)>> { /* ... */ }
#[test]
#[ignore = "requires CUDA + pre-built release binary + MBP-10 test data"]
fn determinism_twin_runs_match() -> Result<()> {
let out_a = run_mid_smoke(seed=42)?;
let out_b = run_mid_smoke(seed=42)?;
let rows_a = read_jsonl(&out_a.join("diag.jsonl"))?;
let rows_b = read_jsonl(&out_b.join("diag.jsonl"))?;
for (i, (a, b)) in rows_a.iter().zip(rows_b.iter()).enumerate() {
let ca = extract_checksums(a)?;
let cb = extract_checksums(b)?;
for ((ka, va), (kb, vb)) in ca.iter().zip(cb.iter()) {
assert_eq!(ka, kb, "checksum key mismatch at row {i}: {ka} vs {kb}");
let rel = ((va - vb) / va.abs().max(1e-12)).abs();
anyhow::ensure!(rel < 1e-5,
"step {i} {ka}: a={va:.6} b={vb:.6} rel_err={rel:.2e}");
}
}
Ok(())
}
#[test]
#[ignore = "requires CUDA + pre-built release binary + MBP-10 test data"]
fn determinism_seed_actually_matters() -> Result<()> {
let out_a = run_mid_smoke(seed=42)?;
let out_b = run_mid_smoke(seed=43)?;
let rows_a = read_jsonl(&out_a.join("diag.jsonl"))?;
let rows_b = read_jsonl(&out_b.join("diag.jsonl"))?;
// After 100 steps, at least one checksum should differ
let row_100_a = &rows_a[99];
let row_100_b = &rows_b[99];
let ca = extract_checksums(row_100_a)?;
let cb = extract_checksums(row_100_b)?;
let any_differs = ca.iter().zip(cb.iter()).any(|((_, va), (_, vb))| {
((va - vb) / va.abs().max(1e-12)).abs() > 1e-3
});
anyhow::ensure!(any_differs, "different seeds produced identical results — seed not used");
Ok(())
}
#[test]
#[ignore = "requires CUDA + pre-built release binary + MBP-10 test data + golden checksum file"]
fn determinism_golden_checksum_match() -> Result<()> {
let out = run_mid_smoke(seed=42)?;
let rows = read_jsonl(&out.join("diag.jsonl"))?;
let last = &rows[rows.len() - 1];
let observed = extract_checksums(last)?;
let golden = read_golden("crates/ml-alpha/tests/fixtures/determinism-golden-seed42.json")?;
for ((k, v_obs), (k_g, v_g)) in observed.iter().zip(golden.iter()) {
assert_eq!(k, k_g);
let rel = ((v_obs - v_g) / v_g.abs().max(1e-12)).abs();
anyhow::ensure!(rel < 1e-5,
"checksum {k} drift from golden: obs={v_obs:.6} gold={v_g:.6} rel_err={rel:.2e}");
}
Ok(())
}
fn run_mid_smoke(seed: u32) -> Result<PathBuf> { /* invoke local-mid-smoke.sh with seed */ }
fn read_golden(path: &str) -> Result<Vec<(String, f64)>> { /* parse JSON */ }
-
Task 2: Generate golden checksum file
crates/ml-alpha/tests/fixtures/determinism-golden-seed42.json:- After Phase 2 fix lands and determinism check passes
- Run mid-smoke with seed=42, extract final checksums, save as golden
- This freezes the post-fix behavior — any future regression detected
-
Task 3: Verify tests pass on a clean checkout
§3.2 — Pre-commit hook
Files:
-
Create:
.claude/hooks/determinism-check.shOR.git/hooks/pre-commit(foxhunt's convention) -
Task 4: Write a fast 50-step micro-determinism-check that runs on pre-commit:
- Detects
.cuorcrates/ml-alpha/src/**/*.rschanges viagit diff --cached --name-only - If no relevant changes → exit 0 immediately (no overhead)
- Otherwise: run 50-step mid-smoke twice, compare checksums
- Exit 1 + clear error message if determinism broke
- Detects
-
Task 5: Document the hook in CLAUDE.md
-
Task 6: Add
git commit --no-verifyescape hatch documentation (must justify in commit message)
§3.3 — Cluster regression-pair workflow
Files:
-
Modify:
scripts/argo-alpha-rl.sh -
Modify:
infra/k8s/argo/alpha-rl-template.yaml -
Task 7: Add
--regression-pairflag toscripts/argo-alpha-rl.sh:- When set, submits TWO identical Argo workflows (same SHA, same seed, same params)
- After both complete, compares
eval_summary.jsonfrom both - Reports
total_pnl_usdΔ in absolute and relative terms - Logs warning if Δ > 0.1% (true determinism = bit-exact)
-
Task 8: Manual test: run
argo-alpha-rl.sh --regression-pair --sha <post-fix-sha>→ both runs should produce identical eval_summary
Phase 4 — Dev/prod toggle
Files:
-
Modify:
crates/ml-alpha/src/lib.rsorcrates/ml-alpha/src/config.rs -
Modify:
CLAUDE.md -
Task 1: Add global config struct:
pub struct DeterminismConfig {
pub enabled: bool,
}
impl DeterminismConfig {
pub fn from_env() -> Self {
let enabled = std::env::var("FOXHUNT_DETERMINISTIC")
.unwrap_or_else(|_| "1".to_string()) // default ON in dev
== "1";
Self { enabled }
}
}
-
Task 2: Plumb through to: cuBLAS init (§2.B.2), checksum kernel (skip if disabled to save 150us/step), CHUNK_SIZE selection if applicable (§2.A.6)
-
Task 3: Production runs (live trading, walk-forward production) set
FOXHUNT_DETERMINISTIC=0for speed. Document in CLAUDE.md. -
Task 4: Add startup log: print determinism mode + expected speed cost so it's obvious in run logs which mode was active
Phase 5 — CI guard
Files:
-
Modify:
.gitlab-ci.yml(per CLAUDE.md, legacy CI exists but disabled; if enabled now use it; otherwise add Argo CI) -
Possibly create:
infra/k8s/argo/determinism-gate-template.yaml -
Task 1: Add CI job
determinism-gate:- Triggers: any push to a branch with
ml-alpha-*prefix, or any PR to main - Runs: full mid-smoke (b=128, 2000+500) twice with
FOXHUNT_DETERMINISTIC=1, then determinism-check.sh - Fails: if any checksum divergence > rel-tol 1e-5
- Reports: which leaf diverged, at what step
- Triggers: any push to a branch with
-
Task 2: Add pre-train determinism check to
infra/k8s/argo/alpha-rl-template.yaml:- New step before main training: run 50-step micro-determinism-check on cluster
- If divergence detected → fail the workflow with clear message
- Cost: ~30 seconds per cluster smoke; saves N×70min if it catches a regression
Phase 6 — Full validation
- Task 1: All 3 tests in
determinism_invariants.rspass - Task 2:
scripts/determinism-check.shpasses for 5 different seeds: 42, 43, 44, 45, 46 - Task 3:
scripts/argo-alpha-rl.sh --regression-pairproduces identical eval_summary.json (Δ < $1) on one full b=1024 20k+5k cluster smoke - Task 4: Speed cost documented: measured time of
local-mid-smoke.shwithFOXHUNT_DETERMINISTIC=1vs=0on RTX 3050; same comparison on cluster L40S - Task 5: Create memory pearls:
pearl_determinism_root_cause: the actual culprit + fixpearl_determinism_speed_cost: measured throughput costpearl_determinism_contract: the rule for future commits
- Task 6: Update
MEMORY.mdindex with the 3 new pearls - Task 7: Atomic commit:
feat(rl): deterministic training foundation + CI gate
Phase 7 — Multi-head policy unblock
Once Phase 6 lands, the multi-head policy spec (2026-06-02-multi-head-policy-with-r-multiple.md) is unblocked. Implementation can proceed with confidence that:
- Same-seed runs produce identical results
- Eval pnl differences between iterations are SIGNAL not noise
- Single-seed verdicts are trustworthy (no need for 5-seed averaging)
Failure modes and replanning
Failure mode 1: Phase 1 finds multiple components diverge simultaneously
Cause: A single upstream bug (e.g., mamba2) cascades to all downstream components. Response: Fix the upstream (likely mamba2 per §2.A). The downstream should fix itself once upstream is deterministic.
Failure mode 2: Phase 2 fix doesn't restore determinism
Cause: We fixed one bug but there are multiple sources. Response: Re-run Phase 1 with the partial fix applied. Identify the NEXT first-divergent component. Apply the matching §2.X. Iterate.
Failure mode 3: Determinism in dev mode but not in prod (FOXHUNT_DETERMINISTIC=0)
Cause: Expected — TF32 split-K is non-deterministic by design. Response: This is acceptable. Dev (where verdict matters) is deterministic; prod (where speed matters) is not. Document this.
Failure mode 4: Speed cost is too high (>20% slowdown)
Cause: CHUNK_SIZE=1 mamba2 path OR aggressive cuDNN deterministic mode. Response: Audit the specific bottleneck. Often a deterministic-by-construction reduction can be added without falling back to fully sequential. If unavoidable, accept the cost — verdict trust > 20% throughput.
Failure mode 5: Phase 1 finds bug is in third-party code (cudarc / cuBLAS internals)
Cause: NVIDIA / cudarc library determinism issue. Response: Pin the cudarc version + CUDA toolkit version. File a bug upstream. Use multi-seed averaging as workaround until library fix lands.
Discipline reminders (HARD)
feedback_no_atomicadd— fixes must use block-tree-reduce only, never atomicAddfeedback_no_nvrtc— all kernels pre-compiled via build.rsfeedback_cpu_is_read_only— checksums computed on GPU, result read on CPU onlyfeedback_no_stubs— every checksum, every fix, every test must run end-to-endfeedback_local_smoke_before_cluster— Phase 6 task 3 (cluster regression-pair) ONLY after Phase 6 task 1+2 pass locallyfeedback_no_partial_refactor— when modifying a cuBLAS handle init, modify ALL of them in the same commit (no half-toggle state)feedback_systematic_debugging— find root cause before fixing; don't patch symptomsfeedback_going_in_circles_pattern— if Phase 2 fixes don't converge after 3 attempts → re-audit Phase 1 diagnostic
Acceptance criteria for the whole plan
The plan is COMPLETE when:
scripts/determinism-check.shexit 0 on 5 different seeds locally- Cluster
--regression-pairproduces eval_summary Δ < $1 - CI determinism-gate is wired and passing
- 3 memory pearls saved + MEMORY.md indexed
- Speed cost documented
- Atomic commit landed on
ml-alpha-regime-observeror main - Multi-head policy spec confirmed unblocked
Estimated total time across all phases: 3-5 working days (heavily depending on which §2.X applies — mamba2 fix is largest, cuBLAS toggle is smallest).