Files
foxhunt/crates/ml-alpha/cuda/rl_per_tree_rebuild.cu
jgrusewski 629ebd667c feat(ml-alpha): deterministic same-seed training + Tier 1.5 fast-dev-cycle
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>
2026-06-02 17:56:00 +02:00

87 lines
4.0 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
* rl_per_tree_rebuild.cu — GPU-resident PER: bottom-up sum-tree rebuild
*
* Grid=(1), Block=(1024). Single-block deterministic rebuild.
*
* Rebuilds the entire sum-tree from leaf priorities (at indices
* [capacity..2*capacity)) up to the root (index 1). All levels are
* processed within ONE block: __syncthreads() between levels is a
* proper barrier guaranteeing every thread in the block sees every
* other thread's writes from the previous level.
*
* Determinism rationale (Phase 2 §2.F fix — Option C):
* The previous Grid=(128) launch used __threadfence() between levels.
* __threadfence() is a memory-ordering primitive, NOT a grid-wide
* barrier — it orders THIS thread's writes globally but does not
* wait for OTHER blocks' writes to be visible. Under the previous
* geometry, block N could read level-L nodes while block M's
* level-L writes were still in flight, producing different
* addition orderings across same-seed runs. Same-seed runs of the
* determinism diagnostic at b=128 saw root priority diverge
* ~592.7 vs ~695.7 at step 2 (Phase 2 sub-investigation dumps).
*
* By collapsing to a single block, we lose grid-level parallelism
* but gain bit-deterministic execution: __syncthreads() is a hard
* intra-block barrier, and every internal node is the deterministic
* sum of its two specific children written in a definite order
* (sequential grid-stride within one block, fixed thread→node
* mapping via i = tid + k*blockDim.x).
*
* Speed cost: capacity=32768 → ~65k floating-point adds total, 15
* levels = 15 syncs. On RTX 3050 the kernel runs ~30-80 μs which
* is a ~10× slowdown vs the parallel version but well within the
* per-step budget (the step itself is ~30-50 ms at b=128).
*
* Launch: Grid=(1,1,1), Block=(1024,1,1). Block size 1024 is the
* CUDA hard maximum; works on all foxhunt GPUs (sm_86 / sm_89 /
* sm_90). If capacity grows past 2^21 in the future, switch to
* cooperative_groups::grid().sync() for a multi-block deterministic
* path.
*
* No atomicAdd — each internal node is written by exactly one thread.
* No __threadfence() — __syncthreads() is the only inter-level barrier.
* ===================================================================== */
extern "C" __global__ void rl_per_tree_rebuild(
float* __restrict__ priority_tree, /* [2 * capacity] */
int capacity
)
{
/* Single block, single-block-stride loop per level. blockIdx.x is
* guaranteed 0 by the launch geometry, but guard anyway so a
* future caller passing Grid>(1,1,1) is a silent no-op rather than
* a corrupted tree. */
if (blockIdx.x != 0) return;
const int tid = threadIdx.x;
const int block_size = blockDim.x;
/* Bottom-up: level 0 = parents of leaves, last level = root.
* At level L, there are (capacity >> (L+1)) internal nodes,
* with indices [capacity >> (L+1) .. capacity >> L). */
int nodes_at_level = capacity >> 1; /* level 0: cap/2 nodes */
int start = nodes_at_level; /* first node index at this level */
while (nodes_at_level >= 1) {
/* Each thread processes multiple nodes via block-stride loop.
* Mapping `i = tid + k*block_size` is FIXED across runs:
* thread t always writes nodes (start + t), (start + t + B),
* (start + t + 2B), ... ensuring a deterministic write order.
*/
for (int i = tid; i < nodes_at_level; i += block_size) {
const int node = start + i;
priority_tree[node] = priority_tree[2 * node]
+ priority_tree[2 * node + 1];
}
/* Intra-block barrier — proper synchronisation, unlike the
* previous __threadfence() which did NOT wait for other
* blocks' writes. */
__syncthreads();
/* Move up one level */
nodes_at_level >>= 1;
start >>= 1;
}
}