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>
80 lines
3.2 KiB
Plaintext
80 lines
3.2 KiB
Plaintext
// rl_deterministic_checksum.cu — provably deterministic sum-of-squares.
|
|
//
|
|
// Phase 1 of the determinism foundation
|
|
// (`docs/superpowers/specs/2026-06-02-determinism-foundation.md` §1.1).
|
|
//
|
|
// Computes `Σ data[i]^2` over `n` elements with construction-guaranteed
|
|
// determinism. Launch geometry is hard-coded (1,1,1)/(1,1,1) — a single
|
|
// thread iterates sequentially. The accumulator is double precision; no
|
|
// intermediate fp32 round-off varies with launch geometry or block count.
|
|
//
|
|
// Trade-off vs throughput:
|
|
// * RTX 3050 / L40S sum-of-squares throughput at single-thread is on the
|
|
// order of 10^9 elements/s. The largest tensor in foxhunt's RL train
|
|
// loop is ~24k floats (Q-head grad_w). Cost per launch is therefore
|
|
// dominated by launch overhead (~5-20 μs) — 15 launches/step add
|
|
// ~150-300 μs total, well within the dev-mode budget per §4 of the
|
|
// spec.
|
|
// * The deliberately slow accumulation order is the entire point. Any
|
|
// parallel reduction tree introduces order-of-addition variance that
|
|
// would invalidate the diagnostic.
|
|
//
|
|
// Per `feedback_no_atomicadd`: zero atomics — pure sequential accumulation.
|
|
// Per `feedback_no_stubs`: kernel always executes, no early-exit; n=0
|
|
// writes 0.0 (defined behavior).
|
|
// Per `feedback_no_nvrtc`: registered in `crates/ml-alpha/build.rs` for
|
|
// AOT compilation to per-arch cubin.
|
|
//
|
|
// Output is f64 (one element). Reading it from the host is the only CPU
|
|
// roundtrip; the value itself is computed entirely on device, satisfying
|
|
// `feedback_cpu_is_read_only`.
|
|
|
|
#include <cuda_runtime.h>
|
|
#include <stdint.h>
|
|
|
|
// `n` is passed as int (32-bit) to match the kernel arg-passing
|
|
// convention used throughout foxhunt (raw_launch + RawArgs::push_i32).
|
|
// All checksum-able tensors in the RL trainer are < 2 billion elements
|
|
// (the largest, Q-head weights with HIDDEN_DIM * N_ACTIONS * Q_N_ATOMS,
|
|
// is on the order of 1e5 floats), so int is safe.
|
|
|
|
extern "C" __global__ void rl_deterministic_checksum_f32(
|
|
const float* __restrict__ data,
|
|
int n,
|
|
double* __restrict__ out
|
|
) {
|
|
// Single block, single thread — only thread 0 does anything.
|
|
if (threadIdx.x != 0 || blockIdx.x != 0) {
|
|
return;
|
|
}
|
|
double acc = 0.0;
|
|
for (int i = 0; i < n; ++i) {
|
|
const double v = static_cast<double>(data[i]);
|
|
acc += v * v;
|
|
}
|
|
out[0] = acc;
|
|
}
|
|
|
|
// Integer variant for replay indices and other u32/i32 tensors. Same
|
|
// accumulation discipline; the input is cast through int64 to avoid
|
|
// signed-overflow UB on the sum-of-squares accumulation.
|
|
extern "C" __global__ void rl_deterministic_checksum_i32(
|
|
const int* __restrict__ data,
|
|
int n,
|
|
double* __restrict__ out
|
|
) {
|
|
if (threadIdx.x != 0 || blockIdx.x != 0) {
|
|
return;
|
|
}
|
|
double acc = 0.0;
|
|
for (int i = 0; i < n; ++i) {
|
|
// Cast to int64 first to widen — int32^2 fits in int64 exactly;
|
|
// double's 53-bit mantissa may round large sums but the
|
|
// accumulation ORDER is still deterministic, which is the
|
|
// contract.
|
|
const int64_t v = static_cast<int64_t>(data[i]);
|
|
acc += static_cast<double>(v) * static_cast<double>(v);
|
|
}
|
|
out[0] = acc;
|
|
}
|