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>
45 KiB
Regime Observer Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking. Spec reference:docs/superpowers/specs/2026-05-31-regime-observer-design.md(v3).
Goal: Implement the unified regime_observer state machine that closes all 3 confirmed failure modes from alpha-rl-zwj68 — Kelly trade-stream-death (Theorem 1), v9 eval-boundary preservation (Theorem 2), and popart blindness (Theorem 6).
Architecture: New CUDA kernel rl_regime_observer.cu emits 5+ shared regime signals via ISV slots; consumed by Kelly (resurrection override), v9 (slot rename), popart (per-account max envelope floor), and IQN τ (tail-recency boost). Single-thread single-block kernel runs once per step between LobSim and risk-stack. Two helper additions: rl_regime_flat_count.cu (batch tree-reduce) and warp_reduce_max (popart kernel extension).
Tech Stack: Rust 1.85, cudarc 0.19, CUDA 12.4, mapped-pinned ISV bus, single-stream graph capture compatible. NVIDIA-grade kernels per feedback_nvidia_grade_perf_for_kernels: no atomics, no nvrtc, no host branches in capture.
Branch: new branch ml-alpha-regime-observer from current ml-alpha-adaptive-controller-floors head (commit baf971ba5).
Phases & parallelism:
- F1 (foundation): land first, ~1 day, zero behavior change
- F2/F3/F4/F5: parallel after F1, each ~0.5–1 day
- F6 (validation): after F1-F5 all landed, ~2h wall + 1h GPU
Pre-implementation setup
Task 0: Branch & worktree
Files: —
- Step 0.1: Create new branch
cd /home/jgrusewski/Work/foxhunt
git checkout ml-alpha-adaptive-controller-floors
git pull origin ml-alpha-adaptive-controller-floors
git checkout -b ml-alpha-regime-observer
git push -u origin ml-alpha-regime-observer
- Step 0.2: Verify baseline compile + smoke
SQLX_OFFLINE=true cargo check -p ml-alpha 2>&1 | tail -5
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored --nocapture 2>&1 | tail -10
Expected: clean check; integrated_trainer_step_with_lobsim_runs_without_panic ... ok
Phase F1 — Foundation (zero behavior change)
Goal: regime_observer kernel emits signals; NO consumer reads them yet. Validates wiring before consumers depend on it (per feedback_smoke_validation_before_structural_priors).
Task F1.1: Add ISV slot constants
Files: Modify crates/ml-alpha/src/rl/isv_slots.rs
- Step 1.1.1: Read isv_slots.rs to find insertion point
grep -n "RL_SLOTS_END\|pub const RL_EVAL_WARMUP\|pub const RL_KELLY_EPS" crates/ml-alpha/src/rl/isv_slots.rs
Insertion point: after the v9 block (slots 685-695, ending around line ~1507). Current RL_SLOTS_END = 696.
- Step 1.1.2: Rename 3 v9 constants (no physical move)
Find each and rename:
RL_EVAL_WARMUP_REMAINING_INDEX→RL_REGIME_TRANSITION_REMAINING_INDEX(slot 685, value unchanged)RL_EVAL_WARMUP_STEPS_CONFIG_INDEX→RL_REGIME_TRANSITION_STEPS_CONFIG_INDEX(slot 686)RL_EVAL_WARMUP_DECAY_STEPS_CONFIG_INDEX→RL_REGIME_TRANSITION_DECAY_STEPS_CONFIG_INDEX(slot 687)
The 8 v9 defensive/normal slots (688-695) stay v9-owned with unchanged names.
- Step 1.1.3: Add 20 new ISV slot constants after slot 695
// ──────────────────────────────────────────────────────────────────
// Regime observer (spec: 2026-05-31-regime-observer-design.md v3)
// Unified state-machine signals for risk-stack controllers.
// ──────────────────────────────────────────────────────────────────
// regime_observer outputs (6 surface signals + 4 internal Welford state)
pub const RL_REGIME_DEAD_ZONE_FLAG_INDEX: usize = 696;
pub const RL_REGIME_DEAD_ZONE_DURATION_INDEX: usize = 697;
pub const RL_REGIME_DEAD_ZONE_TIMEOUT_FLAG_INDEX: usize = 698;
pub const RL_REGIME_RECOVERY_FACTOR_INDEX: usize = 699;
pub const RL_REGIME_SESSION_PNL_VARIANCE_EMA_INDEX: usize = 700;
pub const RL_REGIME_TAIL_EVENT_RECENCY_INDEX: usize = 701;
pub const RL_REGIME_SESSION_PNL_VAR_M2_INDEX: usize = 702;
pub const RL_REGIME_SESSION_PNL_VAR_MEAN_INDEX: usize = 703;
pub const RL_REGIME_SESSION_PNL_VAR_COUNT_INDEX: usize = 704;
pub const RL_REGIME_PREV_WORST_PNL_INDEX: usize = 705;
// Kelly resurrection (consumed by Kelly kernel in F2)
pub const RL_KELLY_EPS_RECOVERY_LIVE_INDEX: usize = 706;
pub const RL_KELLY_EPS_RECOVERY_MIN_INDEX: usize = 707;
pub const RL_KELLY_EPS_RECOVERY_MAX_INDEX: usize = 708;
pub const RL_KELLY_EPS_RECOVERY_N_RECOVERY_INDEX: usize = 709;
// Safety net
pub const RL_REGIME_DEAD_ZONE_MAX_DURATION_INDEX: usize = 710;
// IQN τ tail-boost (consumed in F5)
pub const RL_IQN_TAU_TAIL_BOOST_FACTOR_INDEX: usize = 711;
pub const RL_IQN_TAU_TAIL_BOOST_N_WINDOW_INDEX: usize = 712;
// regime_observer config
pub const RL_REGIME_TAIL_SIGMA_THRESHOLD_INDEX: usize = 713;
// Popart per-account max envelope (consumed by popart in F4)
pub const RL_POPART_MAX_ABS_REWARD_EMA_INDEX: usize = 714;
pub const RL_POPART_MAX_DECAY_ALPHA_INDEX: usize = 715;
And update RL_SLOTS_END:
pub const RL_SLOTS_END: usize = 716;
- Step 1.1.4: Verify compile
SQLX_OFFLINE=true cargo check -p ml-alpha 2>&1 | tail -10
Expected: clean. Some downstream sites will reference the renamed v9 constants; those updates land in F3.
- Step 1.1.5: Commit
git add crates/ml-alpha/src/rl/isv_slots.rs
git commit -m "$(cat <<'EOF'
feat(rl): regime_observer ISV slot allocation (F1.1)
20 new slots (696-715) + 3 v9 slot renames (685-687).
Per spec docs/superpowers/specs/2026-05-31-regime-observer-design.md v3.
Slot map:
- 696-705: regime_observer outputs + internal Welford state
- 706-709: Kelly resurrection config + live value
- 710: dead-zone timeout config
- 711-712: IQN τ tail-boost config
- 713: tail σ threshold
- 714-715: popart max envelope (F4)
v9 renames are zero-move (same physical addresses):
- 685 RL_EVAL_WARMUP_REMAINING → RL_REGIME_TRANSITION_REMAINING
- 686 RL_EVAL_WARMUP_STEPS_CONFIG → RL_REGIME_TRANSITION_STEPS_CONFIG
- 687 RL_EVAL_WARMUP_DECAY_STEPS_CONFIG → RL_REGIME_TRANSITION_DECAY_STEPS_CONFIG
RL_SLOTS_END: 696 → 716
EOF
)"
Task F1.2: Implement rl_regime_flat_count.cu
Files: Create crates/ml-alpha/cuda/rl_regime_flat_count.cu
- Step 1.2.1: Write kernel
Block-reduce lots[b] → flat_count_d (single int). Spec lines 331-353.
// Tree-reduce flat count from per-account lots buffer.
// Per feedback_no_atomicadd: shared-mem tree-reduce only, no atomics.
#include <stdint.h>
extern "C" __global__ void rl_regime_flat_count(
const int* __restrict__ lots,
int* __restrict__ flat_count_out,
int b_size
) {
extern __shared__ int s_count[];
int tid = threadIdx.x;
int sum = 0;
for (int b = tid; b < b_size; b += blockDim.x) {
sum += (lots[b] == 0) ? 1 : 0;
}
s_count[tid] = sum;
__syncthreads();
for (int s = blockDim.x / 2; s > 0; s >>= 1) {
if (tid < s) s_count[tid] += s_count[tid + s];
__syncthreads();
}
if (tid == 0) flat_count_out[0] = s_count[0];
}
- Step 1.2.2: Register in build.rs
Find the cubin list in crates/ml-alpha/build.rs (alphabetical around the other rl_regime* or rl_* entries) and add "rl_regime_flat_count".
- Step 1.2.3: Compile-verify
SQLX_OFFLINE=true cargo build -p ml-alpha --profile dev-release 2>&1 | tail -5
ls -la target/dev-release/build/ml-alpha-*/out/rl_regime_flat_count.cubin
Expected: cubin exists, ~3-5 KB.
- Step 1.2.4: Commit
git add crates/ml-alpha/cuda/rl_regime_flat_count.cu crates/ml-alpha/build.rs
git commit -m "feat(rl): rl_regime_flat_count kernel (F1.2)"
Task F1.3: Implement rl_regime_observer.cu
Files: Create crates/ml-alpha/cuda/rl_regime_observer.cu
- Step 1.3.1: Write kernel
Single-thread single-block. Spec lines 228-307. Header #defines reference slot numbers explicitly (kernel can't include Rust constants):
// rl_regime_observer.cu — unified state-machine for risk-stack controllers.
//
// Spec: docs/superpowers/specs/2026-05-31-regime-observer-design.md v3
// Pearls:
// - [[pearl_kelly_trade_stream_death]] — empirical motivation
// - [[pearl_dead_signal_resurrection_discipline]] — meta-principle
//
// Per feedback_no_atomicadd: single-thread single-block, no atomics.
// Per feedback_cpu_is_read_only: pure device kernel.
// Per feedback_nvidia_grade_perf_for_kernels: no host branches in capture.
#include <stdint.h>
// ─── ISV slot indices (mirror Rust constants from isv_slots.rs) ─────
#define RL_KELLY_FRACTION_INDEX 676
#define RL_COOLDOWN_REMAINING_STEPS_INDEX 668 // adjust per actual slot
#define RL_SESSION_PNL_WORST_INDEX 658 // adjust per actual slot
#define RL_REGIME_DEAD_ZONE_FLAG_INDEX 696
#define RL_REGIME_DEAD_ZONE_DURATION_INDEX 697
#define RL_REGIME_DEAD_ZONE_TIMEOUT_FLAG_INDEX 698
#define RL_REGIME_RECOVERY_FACTOR_INDEX 699
#define RL_REGIME_SESSION_PNL_VARIANCE_EMA_INDEX 700
#define RL_REGIME_TAIL_EVENT_RECENCY_INDEX 701
#define RL_REGIME_SESSION_PNL_VAR_M2_INDEX 702
#define RL_REGIME_SESSION_PNL_VAR_MEAN_INDEX 703
#define RL_REGIME_SESSION_PNL_VAR_COUNT_INDEX 704
#define RL_REGIME_PREV_WORST_PNL_INDEX 705
#define RL_KELLY_EPS_RECOVERY_LIVE_INDEX 706
#define RL_KELLY_EPS_RECOVERY_MIN_INDEX 707
#define RL_KELLY_EPS_RECOVERY_MAX_INDEX 708
#define RL_KELLY_EPS_RECOVERY_N_RECOVERY_INDEX 709
#define RL_REGIME_DEAD_ZONE_MAX_DURATION_INDEX 710
#define RL_REGIME_TAIL_SIGMA_THRESHOLD_INDEX 713
extern "C" __global__ void rl_regime_observer(
float* __restrict__ isv,
const int* __restrict__ flat_count_d,
int b_size
) {
if (threadIdx.x != 0 || blockIdx.x != 0) return;
// Phase 1: dead-zone detection (composite kelly==0 ∧ all-flat ∧ no-cooldown)
const float kelly = isv[RL_KELLY_FRACTION_INDEX];
const float cooldown = isv[RL_COOLDOWN_REMAINING_STEPS_INDEX];
const int flat_count = flat_count_d[0];
const int dead_zone = (kelly == 0.0f)
&& (flat_count == b_size)
&& (cooldown == 0.0f) ? 1 : 0;
isv[RL_REGIME_DEAD_ZONE_FLAG_INDEX] = (float)dead_zone;
float duration = isv[RL_REGIME_DEAD_ZONE_DURATION_INDEX];
if (dead_zone) duration += 1.0f; else duration = 0.0f;
isv[RL_REGIME_DEAD_ZONE_DURATION_INDEX] = duration;
const float max_dur = isv[RL_REGIME_DEAD_ZONE_MAX_DURATION_INDEX];
const int timeout = (duration > max_dur) ? 1 : 0;
isv[RL_REGIME_DEAD_ZONE_TIMEOUT_FLAG_INDEX] = (float)timeout;
// Phase 2: session_pnl_change Welford variance
const float worst_now = isv[RL_SESSION_PNL_WORST_INDEX];
const float worst_prev = isv[RL_REGIME_PREV_WORST_PNL_INDEX];
const float dx = worst_now - worst_prev;
isv[RL_REGIME_PREV_WORST_PNL_INDEX] = worst_now;
float mean = isv[RL_REGIME_SESSION_PNL_VAR_MEAN_INDEX];
float m2 = isv[RL_REGIME_SESSION_PNL_VAR_M2_INDEX];
float count = isv[RL_REGIME_SESSION_PNL_VAR_COUNT_INDEX];
if (worst_now != 0.0f || worst_prev != 0.0f) {
count += 1.0f;
const float delta = dx - mean;
mean += delta / count;
const float delta2 = dx - mean;
m2 += delta * delta2;
isv[RL_REGIME_SESSION_PNL_VAR_MEAN_INDEX] = mean;
isv[RL_REGIME_SESSION_PNL_VAR_M2_INDEX] = m2;
isv[RL_REGIME_SESSION_PNL_VAR_COUNT_INDEX] = count;
const float variance = (count > 1.0f) ? (m2 / (count - 1.0f)) : 0.0f;
isv[RL_REGIME_SESSION_PNL_VARIANCE_EMA_INDEX] = variance;
}
// Phase 3: tail-event detection (3σ threshold, gated by count >= 10)
const float var = isv[RL_REGIME_SESSION_PNL_VARIANCE_EMA_INDEX];
const float sigma_thr = isv[RL_REGIME_TAIL_SIGMA_THRESHOLD_INDEX];
const float sigma = sqrtf(var);
const int sufficient_count = (count >= 10.0f) ? 1 : 0;
const int is_tail = sufficient_count && (sigma > 0.0f) && (fabsf(dx) > sigma_thr * sigma);
float recency = isv[RL_REGIME_TAIL_EVENT_RECENCY_INDEX];
if (is_tail) recency = 0.0f; else recency += 1.0f;
isv[RL_REGIME_TAIL_EVENT_RECENCY_INDEX] = recency;
// Phase 4: emit recovery_factor and ε_recovery_live (drift-free for consumers + diag)
const float n_recovery = isv[RL_KELLY_EPS_RECOVERY_N_RECOVERY_INDEX];
const float eps_min = isv[RL_KELLY_EPS_RECOVERY_MIN_INDEX];
const float eps_max = isv[RL_KELLY_EPS_RECOVERY_MAX_INDEX];
const float recovery_factor = fminf(1.0f, recency / fmaxf(n_recovery, 1.0f));
const float eps_live = eps_min + (eps_max - eps_min) * recovery_factor;
isv[RL_REGIME_RECOVERY_FACTOR_INDEX] = recovery_factor;
isv[RL_KELLY_EPS_RECOVERY_LIVE_INDEX] = eps_live;
}
IMPORTANT: verify RL_COOLDOWN_REMAINING_STEPS_INDEX and RL_SESSION_PNL_WORST_INDEX against actual values in isv_slots.rs before writing the kernel. The values shown above are placeholders.
grep -nE "RL_(COOLDOWN_REMAINING_STEPS|SESSION_PNL_WORST|KELLY_FRACTION)_INDEX:[[:space:]]*usize" crates/ml-alpha/src/rl/isv_slots.rs
- Step 1.3.2: Register in build.rs
Add "rl_regime_observer" to cubin list.
- Step 1.3.3: Compile-verify
SQLX_OFFLINE=true cargo build -p ml-alpha --profile dev-release 2>&1 | tail -5
ls -la target/dev-release/build/ml-alpha-*/out/rl_regime_observer.cubin
- Step 1.3.4: Commit
git add crates/ml-alpha/cuda/rl_regime_observer.cu crates/ml-alpha/build.rs
git commit -m "feat(rl): rl_regime_observer kernel (F1.3)"
Task F1.4: Trainer kernel loaders + struct fields + launchers
Files: Modify crates/ml-alpha/src/trainer/integrated.rs
- Step 1.4.1: Add cubin include_bytes! constants
Find the existing cubin constants (around line 200-260) and add:
const RL_REGIME_FLAT_COUNT_CUBIN: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/rl_regime_flat_count.cubin"));
const RL_REGIME_OBSERVER_CUBIN: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/rl_regime_observer.cubin"));
- Step 1.4.2: Add struct fields
In IntegratedTrainer struct, after the existing v9 fields:
_rl_regime_flat_count_module: Arc<CudaModule>,
rl_regime_flat_count_fn: CudaFunction,
_rl_regime_observer_module: Arc<CudaModule>,
rl_regime_observer_fn: CudaFunction,
flat_count_d: CudaSlice<i32>, // per-batch helper buffer (length 1)
- Step 1.4.3: Add cubin loaders in new()
After the v9 cubin loader block:
let rl_regime_flat_count_module = ctx
.load_cubin(RL_REGIME_FLAT_COUNT_CUBIN.to_vec())
.context("load rl_regime_flat_count cubin")?;
let rl_regime_flat_count_fn = rl_regime_flat_count_module
.load_function("rl_regime_flat_count")
.context("load rl_regime_flat_count function")?;
let rl_regime_observer_module = ctx
.load_cubin(RL_REGIME_OBSERVER_CUBIN.to_vec())
.context("load rl_regime_observer cubin")?;
let rl_regime_observer_fn = rl_regime_observer_module
.load_function("rl_regime_observer")
.context("load rl_regime_observer function")?;
let flat_count_d = stream.alloc_zeros::<i32>(1)
.context("alloc flat_count_d")?;
- Step 1.4.4: Assign struct fields in new()'s return value
_rl_regime_flat_count_module: rl_regime_flat_count_module,
rl_regime_flat_count_fn,
_rl_regime_observer_module: rl_regime_observer_module,
rl_regime_observer_fn,
flat_count_d,
- Step 1.4.5: Add launcher methods
After existing risk-stack launchers, add:
pub fn launch_rl_regime_flat_count(&mut self, lots_d: &CudaSlice<i32>, b_size: usize) -> Result<()> {
let mut args = RawArgs::new();
args.push_ptr(lots_d.raw_ptr());
args.push_ptr(self.flat_count_d.raw_ptr());
args.push_i32(b_size as i32);
let mut ptrs = args.build_arg_ptrs();
let smem_bytes = (256 * std::mem::size_of::<i32>()) as u32;
unsafe {
raw_launch(
self.rl_regime_flat_count_fn.cu_function(),
(1, 1, 1), (256, 1, 1), smem_bytes,
self.raw_stream,
&mut ptrs[..args.len()],
).map_err(|e| anyhow::anyhow!("rl_regime_flat_count: {:?}", e))?;
}
Ok(())
}
pub fn launch_rl_regime_observer(&self, b_size: usize) -> Result<()> {
let mut args = RawArgs::new();
args.push_ptr(self.isv_dev_ptr);
args.push_ptr(self.flat_count_d.raw_ptr());
args.push_i32(b_size as i32);
let mut ptrs = args.build_arg_ptrs();
unsafe {
raw_launch(
self.rl_regime_observer_fn.cu_function(),
(1, 1, 1), (1, 1, 1), 0,
self.raw_stream,
&mut ptrs[..args.len()],
).map_err(|e| anyhow::anyhow!("rl_regime_observer: {:?}", e))?;
}
Ok(())
}
- Step 1.4.6: Compile-verify
SQLX_OFFLINE=true cargo check -p ml-alpha 2>&1 | tail -10
- Step 1.4.7: Commit
git add crates/ml-alpha/src/trainer/integrated.rs
git commit -m "feat(rl): regime_observer trainer wiring — loaders + struct + launchers (F1.4)"
Task F1.5: Wire into step_with_lobsim
Files: Modify crates/ml-alpha/src/trainer/integrated.rs
- Step 1.5.1: Find launch position between LobSim and risk-stack
Find where launch_rl_cmdp_constraints_check is called in step_with_lobsim. Insert regime_observer launch BEFORE the risk-stack controllers (specifically before CMDP).
- Step 1.5.2: Add launches
// Regime observer (spec 2026-05-31-regime-observer-design.md v3).
// Reads PRIOR-step kelly_f + CURRENT-step flat_count + worst_pnl + cooldown.
// Writes 6 surface signals (dead_zone_flag, duration, timeout, recovery_factor,
// session_pnl_var_ema, tail_event_recency) + internal Welford state +
// drift-free ε_recovery_live for Kelly resurrection (F2).
self.launch_rl_regime_flat_count(&self.lots_d, b_size)
.context("launch_rl_regime_flat_count")?;
self.launch_rl_regime_observer(b_size)
.context("launch_rl_regime_observer")?;
(verify self.lots_d is the correct name for the position lots buffer; adjust if needed)
- Step 1.5.3: Compile-verify
SQLX_OFFLINE=true cargo check -p ml-alpha 2>&1 | tail -10
- Step 1.5.4: Commit
git add crates/ml-alpha/src/trainer/integrated.rs
git commit -m "feat(rl): wire regime_observer + flat_count into step_with_lobsim (F1.5)"
Task F1.6: Bootstrap entries
Files: Modify crates/ml-alpha/src/trainer/integrated.rs
- Step 1.6.1: Find with_controllers_bootstrapped() and the isv_constants array
grep -n "with_controllers_bootstrapped\|isv_constants:" crates/ml-alpha/src/trainer/integrated.rs
Currently [(usize, f32); 179] (per v9 commit).
- Step 1.6.2: Add 20 new bootstrap entries
Increase the array size to [(usize, f32); 199] (179 + 20). Append after the v9 block:
// regime_observer foundation (F1)
(crate::rl::isv_slots::RL_REGIME_DEAD_ZONE_FLAG_INDEX, 0.0),
(crate::rl::isv_slots::RL_REGIME_DEAD_ZONE_DURATION_INDEX, 0.0),
(crate::rl::isv_slots::RL_REGIME_DEAD_ZONE_TIMEOUT_FLAG_INDEX, 0.0),
(crate::rl::isv_slots::RL_REGIME_RECOVERY_FACTOR_INDEX, 1.0), // start at max recovery
(crate::rl::isv_slots::RL_REGIME_SESSION_PNL_VARIANCE_EMA_INDEX, 0.0),
(crate::rl::isv_slots::RL_REGIME_TAIL_EVENT_RECENCY_INDEX, 1.0e6), // sentinel "no tails seen"
(crate::rl::isv_slots::RL_REGIME_SESSION_PNL_VAR_M2_INDEX, 0.0), // Welford
(crate::rl::isv_slots::RL_REGIME_SESSION_PNL_VAR_MEAN_INDEX, 0.0), // Welford
(crate::rl::isv_slots::RL_REGIME_SESSION_PNL_VAR_COUNT_INDEX, 0.0), // Welford
(crate::rl::isv_slots::RL_REGIME_PREV_WORST_PNL_INDEX, 0.0), // sentinel
// Kelly resurrection config
(crate::rl::isv_slots::RL_KELLY_EPS_RECOVERY_LIVE_INDEX, 0.50), // = ε_max
(crate::rl::isv_slots::RL_KELLY_EPS_RECOVERY_MIN_INDEX, 0.05),
(crate::rl::isv_slots::RL_KELLY_EPS_RECOVERY_MAX_INDEX, 0.50),
(crate::rl::isv_slots::RL_KELLY_EPS_RECOVERY_N_RECOVERY_INDEX, 100.0),
// Safety
(crate::rl::isv_slots::RL_REGIME_DEAD_ZONE_MAX_DURATION_INDEX, 1000.0),
// IQN τ tail boost config
(crate::rl::isv_slots::RL_IQN_TAU_TAIL_BOOST_FACTOR_INDEX, 1.5),
(crate::rl::isv_slots::RL_IQN_TAU_TAIL_BOOST_N_WINDOW_INDEX, 100.0),
// regime_observer config
(crate::rl::isv_slots::RL_REGIME_TAIL_SIGMA_THRESHOLD_INDEX, 3.0),
// F4 popart envelope (bootstrap only; kernel modification lands in F4)
(crate::rl::isv_slots::RL_POPART_MAX_ABS_REWARD_EMA_INDEX, 0.0), // sentinel
(crate::rl::isv_slots::RL_POPART_MAX_DECAY_ALPHA_INDEX, 0.01), // ~69-step half-life
- Step 1.6.3: Compile-verify
SQLX_OFFLINE=true cargo check -p ml-alpha 2>&1 | tail -10
- Step 1.6.4: Commit
git add crates/ml-alpha/src/trainer/integrated.rs
git commit -m "feat(rl): regime_observer bootstrap entries (F1.6) — array 179→199"
Task F1.7: Extend reset_session_state with regime boundary policy
Files: Modify crates/ml-alpha/src/trainer/integrated.rs
- Step 1.7.1: Find reset_session_state
grep -n "fn reset_session_state" crates/ml-alpha/src/trainer/integrated.rs
- Step 1.7.2: Add regime_observer slot resets
Per spec section "Regime observer reset policy at fold/eval boundaries". Add inside reset_session_state, in the existing direct-ISV-write loop:
// Regime observer transient state (resets per regime boundary per
// [[pearl_adaptive_carryover_discipline]]).
(crate::rl::isv_slots::RL_REGIME_DEAD_ZONE_FLAG_INDEX, 0.0_f32),
(crate::rl::isv_slots::RL_REGIME_DEAD_ZONE_DURATION_INDEX, 0.0_f32),
(crate::rl::isv_slots::RL_REGIME_DEAD_ZONE_TIMEOUT_FLAG_INDEX, 0.0_f32),
(crate::rl::isv_slots::RL_REGIME_TAIL_EVENT_RECENCY_INDEX, 1.0e6_f32), // sentinel
(crate::rl::isv_slots::RL_REGIME_RECOVERY_FACTOR_INDEX, 1.0_f32),
(crate::rl::isv_slots::RL_KELLY_EPS_RECOVERY_LIVE_INDEX, 0.50_f32),
// CRITICAL: PREV_WORST_PNL must be aligned with the just-reset session_pnl_worst
// (which is reset to 0 by the existing loop above) to prevent spurious tail event.
// Issue γ from v2 critical review.
(crate::rl::isv_slots::RL_REGIME_PREV_WORST_PNL_INDEX, 0.0_f32),
// F4 popart envelope (sentinel reset at boundary so new regime starts fresh).
(crate::rl::isv_slots::RL_POPART_MAX_ABS_REWARD_EMA_INDEX, 0.0_f32),
// Note: Welford state (M2, MEAN, COUNT) intentionally NOT reset — variance
// of session_pnl_change is a regime-invariant property.
- Step 1.7.3: Compile + smoke
SQLX_OFFLINE=true cargo check -p ml-alpha 2>&1 | tail -10
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored --nocapture 2>&1 | tail -10
Expected: smoke passes.
- Step 1.7.4: Commit
git add crates/ml-alpha/src/trainer/integrated.rs
git commit -m "feat(rl): reset_session_state regime_observer boundary policy (F1.7)"
Task F1.8: Diag emit structure
Files: Modify crates/ml-alpha/examples/alpha_rl_train.rs
- Step 1.8.1: Add ISV slot imports
Add to the use ml_alpha::rl::isv_slots::{...} block:
RL_REGIME_DEAD_ZONE_FLAG_INDEX, RL_REGIME_DEAD_ZONE_DURATION_INDEX,
RL_REGIME_DEAD_ZONE_TIMEOUT_FLAG_INDEX, RL_REGIME_RECOVERY_FACTOR_INDEX,
RL_REGIME_SESSION_PNL_VARIANCE_EMA_INDEX, RL_REGIME_TAIL_EVENT_RECENCY_INDEX,
RL_REGIME_SESSION_PNL_VAR_COUNT_INDEX,
RL_REGIME_DEAD_ZONE_MAX_DURATION_INDEX, RL_REGIME_TAIL_SIGMA_THRESHOLD_INDEX,
RL_REGIME_TRANSITION_REMAINING_INDEX, // existing slot, renamed
RL_REGIME_TRANSITION_STEPS_CONFIG_INDEX,
RL_REGIME_TRANSITION_DECAY_STEPS_CONFIG_INDEX,
RL_KELLY_EPS_RECOVERY_LIVE_INDEX, RL_KELLY_EPS_RECOVERY_MIN_INDEX,
RL_KELLY_EPS_RECOVERY_MAX_INDEX, RL_KELLY_EPS_RECOVERY_N_RECOVERY_INDEX,
RL_IQN_TAU_TAIL_BOOST_FACTOR_INDEX, RL_IQN_TAU_TAIL_BOOST_N_WINDOW_INDEX,
RL_POPART_MAX_ABS_REWARD_EMA_INDEX, RL_POPART_MAX_DECAY_ALPHA_INDEX,
- Step 1.8.2: Add regime block to risk_stack diag
Find the existing "risk_stack": { ... } JSON literal and append a new "regime": {...} block per spec section "Diag emit structure" (spec lines 748-790). Use direct isv[...] reads (no inline computation per drift-free principle).
- Step 1.8.3: Compile + smoke + check JSONL
SQLX_OFFLINE=true cargo check -p ml-alpha --examples 2>&1 | tail -10
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored --nocapture 2>&1 | tail -10
- Step 1.8.4: Commit
git add crates/ml-alpha/examples/alpha_rl_train.rs
git commit -m "feat(rl): regime_observer diag emit (F1.8)"
Task F1.9: Local 1k smoke validation
Files: None (test only)
- Step 1.9.1: Run integrated trainer smoke
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored --nocapture 2>&1 | tail -10
Expected: PASS.
- Step 1.9.2: Run all existing risk_stack invariants (G1-G14)
SQLX_OFFLINE=true cargo test -p ml-alpha --release --test risk_stack_invariants -- --ignored --nocapture --test-threads=1 2>&1 | tail -20
Expected: 13/13 PASS (no regression).
- Step 1.9.3: Push F1
git push origin ml-alpha-regime-observer
F1 complete. Foundation in place, zero behavior change confirmed.
Phase F2 — Kelly resurrection (Problem 1 fix)
Goal: Kelly reads DEAD_ZONE_FLAG and overrides with ε_recovery_live, respecting TIMEOUT_FLAG.
Task F2.1: Audit + extend kelly fraction kernel
Files: Modify crates/ml-alpha/cuda/rl_kelly_fraction_controller.cu
- Step 2.1.1: Audit per
feedback_extending_existing_code_audits_for_existing_violations
grep -E "TODO|FIXME|XXX|atomicAdd|#allow|_ =" crates/ml-alpha/cuda/rl_kelly_fraction_controller.cu
Expected: clean. If any violations, fix in a separate commit before F2.
- Step 2.1.2: Add #define for new ISV slots
#define RL_REGIME_DEAD_ZONE_FLAG_INDEX 696
#define RL_REGIME_DEAD_ZONE_TIMEOUT_FLAG_INDEX 698
#define RL_KELLY_EPS_RECOVERY_LIVE_INDEX 706
- Step 2.1.3: Append resurrection block at END of kernel (after final clamp + ISV write)
// Kelly resurrection (Theorem 1): override analytic kelly if DEAD_ZONE_FLAG fires.
// TIMEOUT_FLAG safety net: if dead-zone has exceeded MAX_DURATION, stop trying.
const int dead_zone = (int)isv[RL_REGIME_DEAD_ZONE_FLAG_INDEX];
const int timeout = (int)isv[RL_REGIME_DEAD_ZONE_TIMEOUT_FLAG_INDEX];
if (dead_zone && !timeout) {
isv[RL_KELLY_FRACTION_INDEX] = isv[RL_KELLY_EPS_RECOVERY_LIVE_INDEX];
}
- Step 2.1.4: Compile
SQLX_OFFLINE=true cargo build -p ml-alpha --profile dev-release 2>&1 | tail -5
- Step 2.1.5: Commit
git add crates/ml-alpha/cuda/rl_kelly_fraction_controller.cu
git commit -m "feat(rl): Kelly resurrection override (F2.1)"
Task F2.2: Mirror in fused controllers
Files: Modify crates/ml-alpha/cuda/rl_fused_controllers.cu
- Step 2.2.1: Find Layer-4 (Kelly) branch in fused kernel
grep -n "Layer 4\|kelly_fraction\|RL_KELLY_FRACTION_INDEX" crates/ml-alpha/cuda/rl_fused_controllers.cu
- Step 2.2.2: Append same resurrection block at end of Kelly branch
Same 5-line snippet as Task F2.1.3.
- Step 2.2.3: Compile + commit
SQLX_OFFLINE=true cargo build -p ml-alpha --profile dev-release 2>&1 | tail -5
git add crates/ml-alpha/cuda/rl_fused_controllers.cu
git commit -m "feat(rl): Kelly resurrection — fused controller mirror (F2.2)"
Task F2.3: Update existing tests for back-compat
Files: Modify crates/ml-alpha/tests/risk_stack_invariants.rs
- Step 2.3.1: Add explicit dead-zone disable to G10/G11/G12
In each of g10_kelly_at_known_edge_returns_half_kelly, g11_kelly_at_negative_edge_clamps_to_zero, g12_kelly_held_at_bootstrap_during_warmup, before the Kelly kernel launch:
// Explicit dead-zone disable: these tests verify analytic-Kelly behavior,
// not resurrection. Without this, alloc_zeros leaves DEAD_ZONE_FLAG = 0
// which is fine, but make it explicit for clarity.
write_isv(&stream, &mut isv, RL_REGIME_DEAD_ZONE_FLAG_INDEX, 0.0).unwrap();
write_isv(&stream, &mut isv, RL_REGIME_DEAD_ZONE_TIMEOUT_FLAG_INDEX, 0.0).unwrap();
- Step 2.3.2: Run G10/G11/G12 to confirm no regression
SQLX_OFFLINE=true cargo test -p ml-alpha --release --test risk_stack_invariants g10 g11 g12 -- --ignored --nocapture --test-threads=1 2>&1 | tail -15
- Step 2.3.3: Commit
git add crates/ml-alpha/tests/risk_stack_invariants.rs
git commit -m "test(rl): G10/G11/G12 back-compat — explicit dead-zone disable (F2.3)"
Task F2.4: Add new resurrection tests G15-G21
Files: Modify crates/ml-alpha/tests/risk_stack_invariants.rs
- Step 2.4.1: Add fixtures
Per spec lines 627-674. Each is ~30 LOC following the existing G1-G14 pattern. Specifically:
-
G15: DEAD_ZONE_FLAG fires when composite (kelly=0 ∧ flat=n_batch ∧ cooldown=0)
-
G16: DEAD_ZONE_FLAG = 0 when any condition violated
-
G17: Kelly resurrection sets kelly_f = ε_recovery_live when flag set
-
G18: Kelly retains analytic value when flag NOT set
-
G19: ε_recovery ramps linearly from ε_min at T=0 to ε_max at T≥N_recovery
-
G20: TIMEOUT_FLAG fires when DURATION > MAX_DURATION
-
G21: Kelly resurrection NOT triggered when TIMEOUT_FLAG = 1
-
Step 2.4.2: Run all G15-G21
SQLX_OFFLINE=true cargo test -p ml-alpha --release --test risk_stack_invariants -- --ignored --nocapture --test-threads=1 2>&1 | grep -E "g1[5-9]|g2[0-1]" | head -20
Expected: 7 PASSES.
- Step 2.4.3: Commit
git add crates/ml-alpha/tests/risk_stack_invariants.rs
git commit -m "test(rl): G15-G21 Kelly resurrection invariants (F2.4)"
Task F2.5: Synthetic dead-zone smoke
Files: None (manual probe)
- Step 2.5.1: Run integrated trainer smoke to confirm no regression
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored --nocapture 2>&1 | tail -10
- Step 2.5.2: Push F2
git push origin ml-alpha-regime-observer
F2 complete. Kelly trap fix landed.
Phase F3 — v9 slot rename refactor
Goal: v9 reads renamed slot constants; bit-identical behavior.
Task F3.1: Update kernel + trainer references
Files: Modify crates/ml-alpha/cuda/rl_eval_warmup_decay.cu AND crates/ml-alpha/src/trainer/integrated.rs AND crates/ml-alpha/examples/alpha_rl_train.rs
- Step 3.1.1: Find all references
grep -rn "RL_EVAL_WARMUP_REMAINING\|RL_EVAL_WARMUP_STEPS_CONFIG\|RL_EVAL_WARMUP_DECAY_STEPS_CONFIG" crates/ml-alpha/
- Step 3.1.2: Search-and-replace constant names
Use Edit tool with replace_all to rename in each file:
RL_EVAL_WARMUP_REMAINING_INDEX→RL_REGIME_TRANSITION_REMAINING_INDEXRL_EVAL_WARMUP_STEPS_CONFIG_INDEX→RL_REGIME_TRANSITION_STEPS_CONFIG_INDEXRL_EVAL_WARMUP_DECAY_STEPS_CONFIG_INDEX→RL_REGIME_TRANSITION_DECAY_STEPS_CONFIG_INDEX
For the .cu file the kernel uses #define not Rust consts; rename the #defines too.
- Step 3.1.3: Verify no stragglers
grep -rn "RL_EVAL_WARMUP_" crates/ml-alpha/
Expected: empty (all renamed).
- Step 3.1.4: Compile + smoke
SQLX_OFFLINE=true cargo check -p ml-alpha 2>&1 | tail -10
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored --nocapture 2>&1 | tail -10
- Step 3.1.5: Commit
git add -A
git commit -m "$(cat <<'EOF'
refactor(rl): v9 slot rename — RL_EVAL_WARMUP_* → RL_REGIME_TRANSITION_* (F3.1)
Zero physical migration: slots 685-687 keep their addresses; only the
Rust/C constant identifiers change. Theorem 2 of spec proves
bit-identical behavior.
EOF
)"
Task F3.2: Push F3
- Step 3.2.1
git push origin ml-alpha-regime-observer
F3 complete.
Phase F4 — Popart per-account max envelope (Problem 3 fix)
Goal: popart_sigma responds to single-account tail shocks within one step.
Task F4.1: Audit + extend popart kernel
Files: Modify crates/ml-alpha/cuda/rl_popart_normalize.cu
- Step 4.1.1: Audit existing kernel
grep -E "TODO|FIXME|XXX|atomicAdd|#allow|_ =" crates/ml-alpha/cuda/rl_popart_normalize.cu
- Step 4.1.2: Add warp_reduce_max helper (after existing warp_reduce_sum)
__device__ __forceinline__ float warp_reduce_max(float val) {
for (int offset = 16; offset > 0; offset >>= 1) {
val = fmaxf(val, __shfl_down_sync(0xFFFFFFFF, val, offset));
}
return val;
}
- Step 4.1.3: Add #define for new ISV slots
#define RL_POPART_MAX_ABS_REWARD_EMA_INDEX 714
#define RL_POPART_MAX_DECAY_ALPHA_INDEX 715
- Step 4.1.4: Extend Pass 1 with local_max_abs
Modify the existing for (int i = tid; ...) loop:
float local_max_abs = 0.0f;
for (int i = tid; i < b_size; i += block_dim) {
float r = rewards[i];
local_sum += r;
local_sum_sq += r * r;
local_max_abs = fmaxf(local_max_abs, fabsf(r)); // NEW
}
- Step 4.1.5: Add warp + shared-mem max reduction
After existing warp_reduce_sum calls:
float warp_max_abs = warp_reduce_max(local_max_abs); // NEW
// In shared-mem section, allocate 3rd bank:
// float* s_max_abs = sdata + block_dim * 2; // NEW
if (lane_id == 0) {
s_sum[warp_id] = warp_sum;
s_sum_sq[warp_id] = warp_sum_sq;
s_max_abs[warp_id] = warp_max_abs; // NEW
}
__syncthreads();
if (tid < 32) {
float v_sum = (tid < n_warps) ? s_sum[tid] : 0.0f;
float v_ssq = (tid < n_warps) ? s_sum_sq[tid] : 0.0f;
float v_max = (tid < n_warps) ? s_max_abs[tid] : 0.0f;
v_sum = warp_reduce_sum(v_sum);
v_ssq = warp_reduce_sum(v_ssq);
v_max = warp_reduce_max(v_max); // NEW
if (tid == 0) {
// ... existing batch_mean, batch_var, Welford updates, mean_old/sigma_old saves ...
// ... new_sigma computation ...
// NEW: envelope detector + sigma floor
const float max_r_this_step = v_max;
const float decay_alpha = isv[RL_POPART_MAX_DECAY_ALPHA_INDEX];
float max_r_ema = isv[RL_POPART_MAX_ABS_REWARD_EMA_INDEX];
if (max_r_this_step > max_r_ema) {
max_r_ema = max_r_this_step; // fast-up
} else {
max_r_ema = (1.0f - decay_alpha) * max_r_ema + decay_alpha * max_r_this_step;
}
isv[RL_POPART_MAX_ABS_REWARD_EMA_INDEX] = max_r_ema;
new_sigma = fmaxf(new_sigma, max_r_ema); // floor applied
isv[POPART_SIGMA_INDEX] = new_sigma;
// CRITICAL: broadcast slots moved (3rd bank inserted)
sdata[block_dim * 3 + 0] = new_mean;
sdata[block_dim * 3 + 1] = new_sigma;
__threadfence_system();
}
}
__syncthreads();
- Step 4.1.6: Update Pass 3 broadcast read indices (CRITICAL — Issue β fix)
Change Pass 3:
// WAS: float mean = sdata[block_dim * 2 + 0];
// WAS: float sigma = sdata[block_dim * 2 + 1];
float mean = sdata[block_dim * 3 + 0]; // updated indices
float sigma = sdata[block_dim * 3 + 1];
- Step 4.1.7: Update shared mem allocation in extern declaration if needed
The extern __shared__ float sdata[] size is set at kernel launch; the kernel itself doesn't size it. Just need to update the launcher (next task).
- Step 4.1.8: Compile
SQLX_OFFLINE=true cargo build -p ml-alpha --profile dev-release 2>&1 | tail -10
- Step 4.1.9: Commit
git add crates/ml-alpha/cuda/rl_popart_normalize.cu
git commit -m "feat(rl): popart per-account max envelope (F4.1)"
Task F4.2: Update trainer launch smem_bytes
Files: Modify crates/ml-alpha/src/trainer/integrated.rs
- Step 4.2.1: Find popart launch in step_with_lobsim
grep -n "rl_popart_normalize_fn\|popart.*smem_bytes\|block_x \* 2 + 2" crates/ml-alpha/src/trainer/integrated.rs
- Step 4.2.2: Update smem_bytes calculation
Find the existing smem_bytes expression like (block_x * 2 + 2) * (std::mem::size_of::<f32>() as u32) and change to:
let smem_bytes = (block_x * 3 + 3) * (std::mem::size_of::<f32>() as u32);
- Step 4.2.3: Compile + smoke
SQLX_OFFLINE=true cargo check -p ml-alpha 2>&1 | tail -10
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored --nocapture 2>&1 | tail -10
- Step 4.2.4: Commit
git add crates/ml-alpha/src/trainer/integrated.rs
git commit -m "feat(rl): popart launch smem_bytes update — (block*2+2) → (block*3+3) (F4.2)"
Task F4.3: Add F4 tests G26-G28
Files: Modify crates/ml-alpha/tests/risk_stack_invariants.rs
-
Step 4.3.1: Add 3 fixtures
-
G26: Inject rewards =
[0, 0, ..., -100, ..., 0](single outlier) → popart_sigma jumps to ≥100 in one step -
G27: After G26 spike, run 100 more steps with quiet rewards → max_r_ema decays per α=0.01 (predicted half-life 69 steps)
-
G28: Run G26 then V regression step → V loss bounded (no spike) thanks to V-correct affine fixup
-
Step 4.3.2: Run tests
SQLX_OFFLINE=true cargo test -p ml-alpha --release --test risk_stack_invariants -- --ignored --nocapture --test-threads=1 2>&1 | grep -E "g2[6-8]" | head -10
- Step 4.3.3: Commit + push
git add crates/ml-alpha/tests/risk_stack_invariants.rs
git commit -m "test(rl): G26-G28 popart envelope invariants (F4.3)"
git push origin ml-alpha-regime-observer
F4 complete.
Phase F5 — IQN τ tail-recency consumer
Goal: τ_min temporarily raised when TAIL_EVENT_RECENCY < N_window.
Task F5.1: Extend IQN τ kernel + fused mirror
Files: Modify crates/ml-alpha/cuda/rl_iqn_action_tau_controller.cu AND crates/ml-alpha/cuda/rl_fused_controllers.cu
- Step 5.1.1: Audit IQN τ kernel
grep -E "TODO|FIXME|XXX|atomicAdd|#allow|_ =" crates/ml-alpha/cuda/rl_iqn_action_tau_controller.cu
- Step 5.1.2: Add #define for new ISV slots
#define RL_REGIME_TAIL_EVENT_RECENCY_INDEX 701
#define RL_IQN_TAU_TAIL_BOOST_FACTOR_INDEX 711
#define RL_IQN_TAU_TAIL_BOOST_N_WINDOW_INDEX 712
- Step 5.1.3: Insert tail-boost block before final τ clamp
const float recency = isv[RL_REGIME_TAIL_EVENT_RECENCY_INDEX];
const float tail_window = isv[RL_IQN_TAU_TAIL_BOOST_N_WINDOW_INDEX];
const float boost = isv[RL_IQN_TAU_TAIL_BOOST_FACTOR_INDEX];
float tau_min_eff = isv[RL_IQN_ACTION_TAU_MIN_INDEX];
if (recency < tail_window) {
tau_min_eff *= boost;
}
tau_action = fmaxf(tau_action, tau_min_eff);
- Step 5.1.4: Mirror in Layer-2 branch of rl_fused_controllers.cu
Same block.
- Step 5.1.5: Compile + commit
SQLX_OFFLINE=true cargo build -p ml-alpha --profile dev-release 2>&1 | tail -5
git add crates/ml-alpha/cuda/rl_iqn_action_tau_controller.cu crates/ml-alpha/cuda/rl_fused_controllers.cu
git commit -m "feat(rl): IQN τ_min tail-boost consumer (F5.1)"
Task F5.2: Add F5 tests G24/G25
Files: Modify crates/ml-alpha/tests/risk_stack_invariants.rs
-
Step 5.2.1: Add fixtures
-
G24: With TAIL_EVENT_RECENCY < N_window, τ_action ≥ τ_min × boost_factor
-
G25: With TAIL_EVENT_RECENCY ≥ N_window, τ_action behavior unchanged
-
Step 5.2.2: Run + commit + push
SQLX_OFFLINE=true cargo test -p ml-alpha --release --test risk_stack_invariants -- --ignored --nocapture --test-threads=1 2>&1 | grep -E "g2[4-5]" | head -5
git add crates/ml-alpha/tests/risk_stack_invariants.rs
git commit -m "test(rl): G24-G25 IQN τ tail-boost invariants (F5.2)"
git push origin ml-alpha-regime-observer
F5 complete.
Phase F6 — Validation (local + cluster)
Task F6.1: Local validation gate
Files: None
- Step 6.1.1: Full risk_stack_invariants suite
SQLX_OFFLINE=true cargo test -p ml-alpha --release --test risk_stack_invariants -- --ignored --nocapture --test-threads=1 2>&1 | tail -30
Expected: ALL of G1-G28 PASS.
- Step 6.1.2: Controller adaptive floors
SQLX_OFFLINE=true cargo test -p ml-alpha --release --test controller_adaptive_floors -- --ignored --nocapture --test-threads=1 2>&1 | tail -10
- Step 6.1.3: Integrated trainer smoke
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored --nocapture 2>&1 | tail -10
- Step 6.1.4: 1k local smoke (RTX 3050 Ti)
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo run -p ml-alpha --bin alpha_rl_train --release -- \
--n-steps 1000 --n-backtests 128 --seq-len 32 --per-capacity 1024 --seed 16962 --log-every 500 2>&1 | tail -20
Expected: completes without anomalies; diag JSONL shows risk_stack.regime.* fields populated.
Task F6.2: Bit-equivalence verification (F3)
Files: None
- Step 6.2.1: Run v9-only test before F3 took effect (use git stash on F3 changes)
This verification ensures F3's rename was truly zero-behavior-change. Approach:
# Save the F3 commit hash
F3_COMMIT=$(git log --format=%h --grep="v9 slot rename" -1)
PRE_F3_COMMIT=$(git rev-parse ${F3_COMMIT}^)
# Run smoke at PRE_F3 (current state minus the rename)
git stash # save any uncommitted work
git checkout $PRE_F3_COMMIT
# (run smoke, capture diag.jsonl as PRE)
# checkout back to current
git checkout ml-alpha-regime-observer
git stash pop
# (run smoke, capture diag.jsonl as POST)
# diff
Note: this is exploratory; if v9 mechanics aren't exercised in local smoke, the bit-equivalence check is automatically satisfied. The real test is cluster behavior at the fold boundary.
Task F6.3: Cluster validation
Files: None
- Step 6.3.1: Push commit + cluster smoke
git push origin ml-alpha-regime-observer
SHA=$(git rev-parse --short HEAD)
# Cluster smoke (5k b=128) for graph stability
./scripts/argo-alpha-rl.sh \
--sha $SHA \
--branch ml-alpha-regime-observer \
--gpu-pool ci-training-l40s \
--n-steps 5000 \
--seq-len 32 \
--n-backtests 128 \
--per-capacity 4096 \
--seed 16962 \
--instrument-mode all \
--fold-idx 0 \
--n-folds 1 \
--n-eval-steps 0
Watch for: no crashes, sps stable, diag emits regime signals.
- Step 6.3.2: Cluster full fold-1 walk-forward
./scripts/argo-alpha-rl.sh \
--sha $SHA \
--branch ml-alpha-regime-observer \
--gpu-pool ci-training-l40s \
--n-steps 20000 \
--seq-len 32 \
--n-backtests 1024 \
--per-capacity 4096 \
--seed 16962 \
--instrument-mode all \
--fold-idx 1 \
--n-folds 3 \
--n-eval-steps 5000
- Step 6.3.3: Diag analysis
Per spec success criteria:
- No permanent kelly_f=0 trap (or TIMEOUT_FLAG fires if regime adverse)
- v9 mechanics fire correctly at train→eval boundary
- ε-recovery cycle visible if any dead-zone occurs
- F4 popart_sigma_effective tracks tail magnitudes
- Eval pnl ≥ -$507k (v8 fold-1 baseline)
Validate by querying diag.jsonl for risk_stack.regime.* fields.
Self-review checklist
After all phases complete, before considering this plan executed:
- All 6 phases committed and pushed
- G1-G28 all pass on RTX 3050 Ti
- Local smoke unaffected (no behavior change in normal regime)
- Cluster fold-1 walk-forward completes
- Eval pnl ≥ baseline
- Diag JSONL contains
risk_stack.regime.*fields throughout - No NaN or anomaly in regime signals
- Pre-commit hooks pass on every commit (no skip-hooks)
- Branch ready to merge per
superpowers:finishing-a-development-branch
Notes
- Per
feedback_commit_per_phase_for_long_coder_tasks: each task commits independently so progress survives session interruption. - Per
feedback_local_smoke_before_cluster: F1.9, F2.5, F4.2.3 run local smoke before any cluster submit. - Per
feedback_push_before_deploy: F6.3.1 pushes before cluster invocation. - Per
feedback_verify_cluster_before_submit: F6.3 doesn't include the cluster pre-flight checks (kubectl get pod, billing) — implementer must do those manually before each submit. - The investigator pod
v9-investigatorfrom the prior debug session is still running on the L40S pool (4h TTL). If still alive, can be reused for cluster diag analysis; otherwise spawn a fresh one.
Linked
- Spec:
docs/superpowers/specs/2026-05-31-regime-observer-design.md(v3) - Pearls: pearl_kelly_trade_stream_death, pearl_dead_signal_resurrection_discipline, pearl_popart_blind_to_session_signals, pearl_adaptive_carryover_discipline