Files
foxhunt/scripts/local-mid-smoke.sh
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

149 lines
5.1 KiB
Bash
Executable File
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.
#!/usr/bin/env bash
# Tier 1.5 local mid-smoke — RTX 3050 Ti, b=128, 2000 train + 500 eval steps.
#
# Closes the 700× dev-cycle gap between Tier 1 (6 sec local correctness) and
# Tier 2 (70 min cluster eval). Gates 80%+ of bad hypotheses before they reach
# cluster. Target wall-clock: ≤ 10 min on RTX 3050.
#
# Spec: docs/superpowers/specs/2026-06-02-fast-dev-cycle.md §3.2
# Verdict: python3 scripts/tier1_5_verdict.py <out-dir>
#
# Data: test_data/futures-baseline-mid/ES.FUT/ — symlinks to 2024-Q1 (train,
# 4.8GB) + 2024-Q2 (eval, 14GB) MBP-10 files from the canonical PVC.
# Walk-forward split: --n-folds 2 --fold-idx 0 → train=Q1, eval=Q2.
#
# Usage:
# ./scripts/local-mid-smoke.sh # default seed=42
# ./scripts/local-mid-smoke.sh --seed 123 # override seed
# ./scripts/local-mid-smoke.sh --out /tmp/foxhunt-alt # custom out dir
# ./scripts/local-mid-smoke.sh --n-backtests 64 # smaller batch
#
# Per `feedback_local_smoke_before_cluster`: run this BEFORE every cluster
# smoke when touching .cu / trait impls / buffers / N_ACTIONS / ISV slots.
set -euo pipefail
SEED=42
OUT_DIR="/tmp/foxhunt-mid-smoke"
N_BACKTESTS=128
N_STEPS=2000
N_EVAL_STEPS=500
N_FOLDS=2
FOLD_IDX=0
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS]
Tier 1.5 mid-smoke — b=128, 2000+500 steps, ~10 min on RTX 3050.
Options:
--seed N RNG seed (default: $SEED — reproducible)
--out DIR Output directory (default: $OUT_DIR)
--n-backtests N Batch size (default: $N_BACKTESTS; drop to 64 if OOM)
--n-steps N Train steps (default: $N_STEPS)
--n-eval-steps N Eval steps (default: $N_EVAL_STEPS)
--n-folds K Walk-forward fold count (default: $N_FOLDS)
--fold-idx K Walk-forward fold index (default: $FOLD_IDX)
-h, --help Show this help
Output:
\$OUT_DIR/diag.jsonl — per-step train diag (642 scalars/step)
\$OUT_DIR/eval_diag.jsonl — per-step eval diag
\$OUT_DIR/alpha_rl_train_summary.json — final summary
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--seed) SEED="$2"; shift 2 ;;
--out) OUT_DIR="$2"; shift 2 ;;
--n-backtests) N_BACKTESTS="$2"; shift 2 ;;
--n-steps) N_STEPS="$2"; shift 2 ;;
--n-eval-steps) N_EVAL_STEPS="$2"; shift 2 ;;
--n-folds) N_FOLDS="$2"; shift 2 ;;
--fold-idx) FOLD_IDX="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
esac
done
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
# --mbp10-data-dir points DIRECTLY at the instrument's .dbn.zst directory
# (matches argo-rl template line: --mbp10-data-dir /data/futures-baseline-mbp10/ES.FUT).
MBP10_DIR="$REPO_ROOT/test_data/futures-baseline-mid/ES.FUT"
PREDECODED_DIR="$REPO_ROOT/test_data/futures-baseline-mid"
# Sanity checks.
if [[ ! -d "$MBP10_DIR" ]]; then
echo "ERROR: $MBP10_DIR not found." >&2
echo "Run setup: see CLAUDE.md 'Tier 1.5 data path' or docs/superpowers/specs/2026-06-02-fast-dev-cycle.md §3.1" >&2
exit 3
fi
n_mbp_files=$(find -L "$MBP10_DIR" -maxdepth 1 -name '*.dbn.zst' 2>/dev/null | wc -l)
if [[ "$n_mbp_files" -lt 2 ]]; then
echo "ERROR: $MBP10_DIR has $n_mbp_files MBP-10 files; need ≥ 2 for walk-forward split." >&2
exit 3
fi
mkdir -p "$OUT_DIR"
echo "=== Tier 1.5 mid-smoke ==="
echo " Repo: $REPO_ROOT"
echo " Seed: $SEED"
echo " Batch: $N_BACKTESTS"
echo " Steps: $N_STEPS train + $N_EVAL_STEPS eval"
echo " Walk-forward: fold $FOLD_IDX of $N_FOLDS"
echo " MBP-10 dir: $MBP10_DIR (${n_mbp_files} files)"
echo " Out dir: $OUT_DIR"
echo
# Build (sccache + dev-release for fast iteration; --release matches cluster).
echo "[build] cargo build --release --example alpha_rl_train -p ml-alpha"
SQLX_OFFLINE=true cargo build --release --example alpha_rl_train -p ml-alpha
BINARY="$REPO_ROOT/target/release/examples/alpha_rl_train"
if [[ ! -x "$BINARY" ]]; then
echo "ERROR: binary not found at $BINARY after build." >&2
exit 4
fi
echo
echo "[run] $BINARY ..."
START_TS=$(date +%s)
# Per `feedback_no_stubs`: the binary owns its full life cycle. We just
# invoke it and capture wall time. Exit code 2 = G8 NaN abort (per binary
# docstring); we propagate it so the caller knows the run died.
"$BINARY" \
--mbp10-data-dir "$MBP10_DIR" \
--predecoded-dir "$PREDECODED_DIR" \
--out "$OUT_DIR" \
--diag-jsonl "$OUT_DIR/diag.jsonl" \
--eval-diag-jsonl "$OUT_DIR/eval_diag.jsonl" \
--n-steps "$N_STEPS" \
--n-eval-steps "$N_EVAL_STEPS" \
--n-backtests "$N_BACKTESTS" \
--n-folds "$N_FOLDS" \
--fold-idx "$FOLD_IDX" \
--seed "$SEED" \
--instrument-mode front-month \
--log-every 200 \
--gpu-idx 0
END_TS=$(date +%s)
ELAPSED=$((END_TS - START_TS))
ELAPSED_MIN=$((ELAPSED / 60))
ELAPSED_SEC=$((ELAPSED % 60))
echo
echo "=== Mid-smoke complete in ${ELAPSED_MIN}m ${ELAPSED_SEC}s ==="
echo "Outputs:"
ls -lh "$OUT_DIR" 2>/dev/null
echo
echo "Next:"
echo " python3 $REPO_ROOT/scripts/tier1_5_verdict.py $OUT_DIR"