Files
foxhunt/docs/superpowers/plans/2026-05-31-regime-observer.md
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

45 KiB
Raw Blame History

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.51 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_INDEXRL_REGIME_TRANSITION_REMAINING_INDEX (slot 685, value unchanged)
  • RL_EVAL_WARMUP_STEPS_CONFIG_INDEXRL_REGIME_TRANSITION_STEPS_CONFIG_INDEX (slot 686)
  • RL_EVAL_WARMUP_DECAY_STEPS_CONFIG_INDEXRL_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_INDEXRL_REGIME_TRANSITION_REMAINING_INDEX
  • RL_EVAL_WARMUP_STEPS_CONFIG_INDEXRL_REGIME_TRANSITION_STEPS_CONFIG_INDEX
  • RL_EVAL_WARMUP_DECAY_STEPS_CONFIG_INDEXRL_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:

  1. No permanent kelly_f=0 trap (or TIMEOUT_FLAG fires if regime adverse)
  2. v9 mechanics fire correctly at train→eval boundary
  3. ε-recovery cycle visible if any dead-zone occurs
  4. F4 popart_sigma_effective tracks tail magnitudes
  5. 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-investigator from 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