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
Determinism Foundation — Stop Chasing Ghosts
Date: 2026-06-02
Status: Spec draft — MANDATORY PREREQUISITE for any further controller / architecture work
Priority: BLOCKER for multi-head policy (2026-06-02-multi-head-policy-with-r-multiple.md), R-multiple reward, and the entire regime-invariance roadmap
Empirical motivation: alpha-rl-determinism-check.sh on ml-alpha-regime-observer @ 63fc16f17 showed same-seed runs diverging at step 2, eval pnl Δ=$364,925, run A produced 0 trades while run B produced 347 trades. Bit-equivalence requirement: same seed → same diag rows to fp32 rel-tol 1e-5.
§0. Motivation — the going-in-circles diagnosis was incomplete
The session of 2026-06-01/02 cycled through 4 controller iterations (v4 → v5.2) plus today's grwwh eval. Each iteration produced a different result. We attributed the variance to:
- Bad controller designs (the v5/v5.1 bugs)
- Architectural mismatch (single policy can't generalize)
- Reward signal contamination (Phase 5 hold-bonus)
What we did NOT consider: part of the variance was non-determinism.
The 2026-06-02 mid-smoke determinism check revealed:
- Same seed, same code, same hardware → divergence at step 2
- By step 199: 654 diag leaves diverging
- Eval pnl Δ=$365k between identical configurations
- Behavioral Δ: run A=0 trades vs run B=347 trades (different agent entirely)
Implications:
- Every controller iteration today might have been measuring seed-noise, not controller effect
- The "scaffold v5 stuck at w=1.0" might be partially seed-dependent (would different seeds also stick?)
- The "grwwh eval +$74M then collapse to -$160M" might partially be intra-run variance
- Until determinism is fixed, every future iteration is also seed-dependent and unfalsifiable
Per feedback_going_in_circles_pattern: "audit the foundation". The foundation has a bug. Fix the foundation before adding more layers.
§1. Investigation strategy — localize divergence to ONE component
Per feedback_systematic_debugging Phase 1 "Gather evidence in multi-component systems": instrument every boundary, find FIRST divergence.
§1.1 Deterministic checksum kernel (NEW infrastructure)
Add crates/ml-alpha/cuda/rl_deterministic_checksum.cu:
- Computes sum-of-squares of any device tensor with provably deterministic accumulation
- Single-block, single-thread accumulation if tensor < 1024 elements (trivially deterministic)
- For larger tensors: block-tree-reduce with fixed accumulation order (block 0 always reads block 1's result, never vice versa;
__threadfence_system+ countercudaDeviceSynchronizeif needed; NO atomic ops, NO "whichever-block-finishes-last" patterns) - Returns one f64 per tensor (sum-of-squares, full precision)
- Compile-time guaranteed determinism via construction, not configuration
§1.2 Per-step component checksums in diag
Instrument every major component output in build_diag_value:
"checksums": {
"encoder_output": checksum(encoder_out_d, b_size × seq_len × hidden_dim),
"q_logits": checksum(q_logits_d, b_size × n_actions × n_atoms),
"pi_logits": checksum(pi_logits_d, b_size × n_actions),
"v_value": checksum(v_d, b_size),
"replay_sample_indices": checksum_int(per_sampled_indices_d, b_size),
"rewards_after_shape": checksum(rewards_d, b_size),
"advantages": checksum(advantages_d, b_size),
"q_grad": checksum(q_grad_d, all_params),
"pi_grad": checksum(pi_grad_d, all_params),
"v_grad": checksum(v_grad_d, all_params),
"encoder_grad": checksum(encoder_grad_d, all_params),
"adam_m_sum": checksum(adam_first_moments_d, all_params),
"adam_v_sum": checksum(adam_second_moments_d, all_params),
"isv_state": checksum(isv_dev_ptr, RL_SLOTS_END),
}
Cost: ~15 small kernel launches per step, each ~10us → 150us/step overhead. Acceptable in dev mode; toggle off in production.
§1.3 Run determinism-check + localization
scripts/determinism-check.sh now compares checksums leaf-by-leaf. Output: first leaf to diverge + step number + Δ value.
Expected outcomes (one of these wins):
checksums.encoder_outputdiverges first → mamba2 culprit (most likely)checksums.q_logitsdiverges first (encoder same) → Q head GEMM split-K culpritchecksums.pi_logitsfirst → π head GEMM culpritchecksums.replay_sample_indicesfirst → PER sampling RNG culpritchecksums.q_gradfirst (forward same) → backward kernel reduction culpritchecksums.adam_m_sumfirst (grads same) → optimizer iteration-order culprit
§2. Standard fixes by component (apply based on §1 localization)
§2.A Mamba2 selective scan (most-likely culprit)
If encoder checksum diverges first:
- Chunk-size determinism: mamba2 selective scan uses chunk-parallel scan. Setting
CHUNK_SIZE=1reduces to sequential scan (trivially deterministic but ~10× slower). Use as ground-truth oracle. - Reduction-order fix: within each chunk, the scan reduction must be fixed-order. Audit
mamba2_selective_scan_fwd.cufor anyatomicAdd, any "block 0 OR block 1 writes final" patterns, any non-deterministic chunk ordering. - Reference: pearl
pearl_mamba2_selective_scan_nan_source(Phase 2 v2 work) — same code path was extensively debugged for NaN issues. May have introduced non-det then.
§2.B cuBLAS GEMM split-K
If Q/π/V head checksums diverge first (encoder same):
- Set
cublasSetMathMode(cublas_handle, CUBLAS_PEDANTIC_MATH)— disables auto-selection of non-deterministic algorithms - Disable TF32 (
cublasSetMathMode(CUBLAS_DEFAULT_MATH)) — TF32 split-K is non-deterministic per NVIDIA docs - This REVERSES tasks 30/31 (TF32 enable). Document the perf cost trade-off (~10-15% on H100 GEMMs).
- Alternative: use FP32 explicitly via
cublasSgemmStridedBatchedwith explicit algorithm selection
§2.C cuDNN deterministic mode
If conv/RNN kernels involved:
cudnnSetMathMode(CUDNN_DEFAULT_MATH)(no TF32)- Force
CUDNN_DETERMINISTICvia algorithm selection - Foxhunt may not use cuDNN heavily (most ops are custom CUDA kernels) — verify before applying
§2.D Custom kernel block-tree-reduce audit
Per feedback_no_atomicadd, all reductions use block-tree-reduce. Audit each kernel for:
- ANY use of "last block to finish writes final" patterns — must become "block 0 always writes final" with proper synchronization
- ANY use of
cudaDeviceSynchronizeinside kernels (rare) — implies non-graph-capturable patterns - The
block_reduce_to_global_with_counterpattern (if used) — non-deterministic by construction
§2.E Adam optimizer iteration order
If adam_m_sum checksum diverges (grads same):
- Audit AdamW step in
crates/ml-alpha/src/networks/*for iteration order - HashMap iteration is non-deterministic in Rust — use
BTreeMapor pre-sortedVec<(key, value)>for parameter groups - Per-tensor iteration MUST be a fixed order
§2.F PER (Prioritized Experience Replay) sampling
If replay_sample_indices checksum diverges:
- The PER index sampling kernel must use a deterministic RNG (device-side xorshift32 seeded from host per
pearl_rl_sample_tau_xorshift32) - Verify the seed is correctly propagated and not re-randomized between runs
- Check that priority updates and reads are properly synchronized via events (not just stream ordering)
§3. Test infrastructure — make determinism a CONTRACT
§3.1 Local determinism gate
scripts/determinism-check.sh (already created by fast-dev-cycle items 1-5) is the gate. Add:
- Exit 0 if all checksums match to rel-tol 1e-5
- Exit 1 with first-divergent-leaf report otherwise
- Verbose mode shows checksum trajectory side-by-side
§3.2 GPU-oracle determinism invariant test
Add crates/ml-alpha/tests/determinism_invariants.rs:
- Test 1: 200-step mid-smoke twice with seed=42 → all checksums match
- Test 2: 200-step mid-smoke with seed=42 vs seed=43 → checksums DIFFER (sanity: seed actually does something)
- Test 3: 200-step mid-smoke with seed=42 on RTX 3050 → checksum at step 199 matches a frozen golden value (catches regression to non-det)
- All 3 must pass before any cluster smoke can submit (CI gate)
§3.3 Pre-commit hook
.git/hooks/pre-commit (or .claude/hooks/):
- If any
.cuor.rsfile incrates/ml-alpha/changed → run 50-step micro-determinism-check - If divergence → BLOCK commit
- Override with
git commit --no-verify(logged + requires justification)
§3.4 Cluster determinism regression test
Once local determinism is fixed:
- One re-run of any cluster smoke with same SHA should produce identical eval_summary.json
- Add to
scripts/argo-alpha-rl.sh: optional--regression-pairflag that submits TWO identical workflows and verifies they match
§4. Speed cost — document the dev-vs-prod trade-off
Determinism almost always costs throughput:
- TF32 → FP32: ~10-15% on H100 GEMMs (per NVIDIA docs)
- Mamba2 chunk-1: ~10× slower (if needed; should not be needed if reduction-order fix works)
- cuDNN deterministic: ~5-20% varies
- Checksum kernels: ~150us/step overhead
Acceptable in dev (user explicitly said "we are in dev, determism is most important").
Production toggle via env var FOXHUNT_DETERMINISTIC (default 1 for dev, 0 for production):
let pedantic = std::env::var("FOXHUNT_DETERMINISTIC").unwrap_or_else(|_| "1".to_string()) == "1";
if pedantic {
cublasSetMathMode(handle, CUBLAS_PEDANTIC_MATH);
} else {
cublasSetMathMode(handle, CUBLAS_TF32_TENSOR_OP_MATH);
}
Production runs (live trading, walk-forward production) MAY use non-deterministic mode for the 10-15% throughput gain. ALL dev / cluster smokes MUST use deterministic mode.
§5. CI guard
Add to GitLab CI (or whatever CI foxhunt uses — see feedback_argo_workflows_primary):
- Job:
determinism-gate- Triggers: PR to main, or any branch with
ml-alpha-*prefix - Runs:
scripts/determinism-check.shon the PR commit - Fails the PR if any divergence > rel-tol 1e-5
- Triggers: PR to main, or any branch with
- Argo workflow template: add
pre-train-determinism-checkstep toalpha-rl-template.yamlthat runs determinism-check on the cluster GPU before submitting the actual training run
§6. Falsification criteria
PASS criteria (ALL must hold):
determinism-check.shpasses (exit 0) for 5 different seeds (42, 43, 44, 45, 46) on RTX 3050- One b=1024 cluster smoke re-run produces eval_summary.json
total_pnl_usdmatching to ±$1 (true bit-exact at scalar level) determinism_invariants.rstest 1 passes (twin run match)determinism_invariants.rstest 3 passes (golden checksum match) — proves no regression
FAIL criteria:
- Any test above fails after applying §2 fixes
- If failure persists after auditing all 6 candidate causes (mamba2, GEMM, cuDNN, custom kernels, Adam, PER) → escalate to NVIDIA support OR accept multi-seed averaging as the workaround (5× cluster cost per verdict, but trustworthy)
§7. Sequencing
Phase 1 ships first (the diagnostic):
- §1.1 Write
rl_deterministic_checksum.cu+ register inbuild.rs(~2 hours) - §1.2 Instrument 15 component checksums into
build_diag_value(~3 hours) - §1.3 Modify
determinism-check.shto compare checksums leaf-by-leaf, report first-divergent (~1 hour) - §3.1 Local determinism gate output (~1 hour)
- Run the diagnostic: identify first-divergent component (~30 min)
Phase 1 total: ~8 hours / 1 day.
Phase 2 fixes depend on Phase 1 results (apply the relevant §2.X). Cost: ~1-3 days depending on which component is the culprit.
Phase 3 validation (§3.2, §3.3, §3.4) + Phase 4 documentation (§4): ~1 day.
Total: ~3-5 days to ship the foundation. This is the prerequisite for multi-head policy, R-multiple reward, and all subsequent regime-invariance work.
§8. Out of scope (defer)
- Multi-GPU determinism (foxhunt is single-GPU per process; defer until production)
- Cross-hardware determinism (RTX 3050 vs L40S vs H100 may produce different exact bits even with all fixes; rel-tol 1e-5 should hold but bit-exact across hardware is not required)
- Cross-CUDA-version determinism (pin CUDA 12.4 per current setup; if upgraded, re-audit)
- Floating-point order across compiler versions (pin rustc + cudart versions in build env)
§9. Why this comes BEFORE multi-head policy
Multi-head policy adds 3× the policy head parameters + a gating network + new aux loss term. ANY of these new components could introduce its own non-determinism (more GEMM calls, more block-tree-reduces, possibly more PER samples).
If we ship multi-head on a non-deterministic foundation:
- Multi-head eval shows +$3M → we cheer, but it's a lucky seed
- Multi-head eval shows -$5M → we revert, but it was an unlucky seed
- Multiple seeds → 5× cluster cost
- We never know if the architecture worked
If we ship determinism FIRST:
- Multi-head eval shows +$3M → reproducible, trust it, build on it
- Multi-head eval shows -$5M → reproducible, abandon it, learn from it
- Single seed per iteration → fast cluster cadence
- Every iteration produces real signal
The 3-5 day investment in determinism pays back on the FIRST controller iteration. Over the next 10 iterations, it saves 50% of cluster compute (no need for multi-seed averaging) and 100% of the "is this real or noise?" confusion that drove today's going-in-circles.
§10. Memory pearls to create after Phase 1 lands
pearl_determinism_root_cause— documents which component(s) were the actual culprit + fix appliedpearl_determinism_speed_cost— documents the actual measured throughput cost on H100 + L40S + RTX 3050pearl_determinism_contract— the rule going forward: every commit passes determinism gate, no exceptions