# SP16 — Hold-Collapse Structural Fix Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Break the structural Hold-collapse mechanism that caused months-long WR plateau, validated by train-multi-seed-pfh9n (sha 98a81981a, post-13-commit chain).

**Architecture:** Three sequential phases on top of the existing 13-commit Class C/A/B intervention chain. (1) Add Q-value-by-action diagnostics so we can directly observe the Q(Hold) bias hypothesis. (2) Fix the fold-reset producer re-launch bug (slots 460 and 330 stuck at sentinel in Fold 1). (3) Make the per-bar Hold cost large enough (10-20×) to actually compete with per-bar reward magnitudes, plus make it ISV-adaptive.

**Tech Stack:** Rust 1.85 + cudarc 0.16 + CUDA 12.4. ISV slots in `crates/ml/src/cuda_pipeline/sp14_isv_slots.rs`. HEALTH_DIAG emits in `crates/ml/src/trainers/dqn/trainer/training_loop.rs`.

---

## Background — what train-multi-seed-pfh9n proved

The 13-commit chain (Class C measurement bug, Class A 8 adaptive bounds, Class B #1 fold-buffer clear, Class B #2 PER double-α, build.rs SM 90 fix) delivered measurable wins for what each commit was scoped to fix:

- ✅ Action distribution balanced (Hold 15-30% during early epochs)
- ✅ Magnitude WR climbed fast (0.286 → 0.577 across 3 epochs)
- ✅ Fold-buffer-clear verified working
- ✅ PER actually prioritizes (no double-α)

But **the WR plateau persisted with same shape across both folds**:

| Metric | Fold 0 final | Fold 1 final |
|---|---|---|
| val WR | 0.4610 | 0.4558 |
| val Sharpe | 64.24 | 56.86 |
| Hold% | 45.6% | **51.6%** |
| dir_entropy | 0.853 | **0.715** (kill threshold) |
| `min_hold_T` | adapted 50→11.4 | **stuck at 50** (producer broken) |

**Two findings drive this plan:**

1. **MIN_HOLD_TEMPERATURE adaptation doesn't matter for the spiral** — Fold 1 had `min_hold_T` pinned at 50 throughout (max permissive), yet Hold% climbed 15→52% identically to Fold 0 where temp adapted. The temperature mechanism has zero detectable effect on Hold collapse.

2. **Spiral is structural Q-bias, not measurement bug** — Q-value mean climbed 0.19 → 0.41 across Fold 1 epochs while Hold% climbed 15 → 52%. Hypothesis: Adam's `m/sqrt(v)` favors low-variance Hold action over noisy direction Q-targets; Q(Hold) climbs steadily until it dominates regardless of penalty/temp/prio fixes.

---

## Phase 0 — Q-value-by-action diagnostic (foundation for verification)

**Why first:** Without per-action Q observations, we cannot verify whether the Q-bias hypothesis is correct or whether Phase 2 fixes actually break the spiral. Cheap to add (~50 LOC), high diagnostic value.

### Task 0.1: Add Q-value-by-action HEALTH_DIAG emit

**Files:**
- Modify: `crates/ml/src/trainers/dqn/trainer/training_loop.rs` — find the existing HEALTH_DIAG emit block (search "HEALTH_DIAG\[.*\]: mag")
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs` — find the diagnostic computation block (search "mag_stats" or "q_dir_per_branch")
- Test: `crates/ml/tests/sp14_oracle_tests.rs` — add a Q-by-action diagnostic verification test

- [ ] **Step 1: Read the existing per-branch Q diagnostic emit**

```bash
grep -nE "q_var_per_branch|q_dir_per_branch|mag.*q_full|HEALTH_DIAG.*mag" \
    crates/ml/src/trainers/dqn/trainer/training_loop.rs \
    crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs | head -20
```

Identify the GPU buffer holding per-action Q-means (likely already produced by the C51/IQN diagnostic kernel). If not present, add it.

- [ ] **Step 2: Write the failing test**

```rust
#[test]
#[ignore]
fn q_by_action_diagnostic_emits_four_means() {
    // Force a known Q-value distribution where Q[Hold]=0.5, Q[Long]=0.1, etc.
    // Launch the diagnostic.
    // Verify HEALTH_DIAG output contains q_by_action [hold=0.5 long=0.1 short=... flat=...]
    todo!("write after Step 4 lands the emit")
}
```

Run: `cargo test -p ml --test sp14_oracle_tests --release q_by_action_diagnostic_emits_four_means -- --ignored --nocapture`
Expected: FAIL with "todo!()"

- [ ] **Step 3: Add the emit code**

Find the per-epoch HEALTH_DIAG emit block. Add after the existing `mag` line:

```rust
// SP16 Phase 0: Q-by-action diagnostic. Per-action Q-mean, computed
// from the GPU per-action mean buffer (already produced for C51 atom
// expectation; if not, add a producer). This lets us directly observe
// whether Q(Hold) climbs dominantly relative to Q(directional) — the
// structural-Q-bias hypothesis from train-multi-seed-pfh9n post-mortem.
let q_by_action = read_q_by_action_means();  // [hold, long, short, flat]
info!(
    "HEALTH_DIAG[{epoch}]: q_by_action [hold={:.4} long={:.4} short={:.4} flat={:.4}]",
    q_by_action[0], q_by_action[1], q_by_action[2], q_by_action[3]
);
```

If the per-action mean buffer doesn't exist on GPU, add a small kernel to compute it from the existing Q-distribution (block-tree-reduce mean over the C51 atoms, expectation under softmax).

- [ ] **Step 4: Run test, verify it passes**

Run: `cargo test -p ml --test sp14_oracle_tests --release q_by_action_diagnostic_emits_four_means -- --ignored --nocapture`
Expected: PASS

- [ ] **Step 5: Commit**

```bash
git add -A
git commit -m "$(cat <<'EOF'
feat(sp16-p0): Q-by-action HEALTH_DIAG diagnostic for Hold-bias observation

Per train-multi-seed-pfh9n post-mortem: structural-Q-bias hypothesis
needs direct per-action Q observations to verify/refute. Adds emit:

  HEALTH_DIAG[N]: q_by_action [hold=X long=Y short=Z flat=W]

Read from existing per-action Q-mean buffer (or new producer if absent).
Lets us see Q(Hold) trajectory vs Q(direction) per epoch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Phase 1 — MIN_HOLD_TEMPERATURE signal swap (DONE 2026-05-08, plan revised post-investigation)

**REVISED FROM ORIGINAL.** The original plan framed Phase 1 as a "fold-reset producer launch lifecycle bug" with three suspected patterns (one-shot flag / epoch-gate / cross-fold-state). Investigation refuted both starting hypotheses; the actual fix is a signal swap, not a launch-lifecycle fix. See `docs/dqn-wire-up-audit.md` § "SP16 Phase 1 (revised): MIN_HOLD_TEMPERATURE signal swap + slot 330 non-bug closure" for the canonical write-up.

**Why this matters (revised):** Two ISV slots looked stuck at sentinel in Fold 1 of train-multi-seed-pfh9n:
- `MIN_HOLD_TEMPERATURE_ADAPTIVE_INDEX=460` — stuck at 50 throughout Fold 1 (Fold 0 adapted 50→11.4)
- `KELLY_WARMUP_FLOOR_INDEX=330` — shown as 0.0 in Fold 1 (Fold 0 was 0.25)

After investigation:

1. **Slot 330 is NOT a bug.** Producer fires correctly. Reads `KELLY_SAMPLE_COUNT_INDEX=283` which is **intentionally cross-fold-persistent** per `pearl_kelly_cap_signal_driven_floors`. In Fold 1, accumulated sample count ≥ target=100 → `stat_conf=1.0` → `floor = base × (1−1.0) = 0` **by design**. `floor=0` post-warmup is the correct steady-state. Slot 330 untouched in this commit; documented in audit doc as investigated-non-bug.

2. **Slot 460 IS a bug, but not a fold-reset-launch-lifecycle bug.** Producer fires per-epoch (call site at `training_loop.rs:525`). Kernel had an early-return guard on `ISV[AUX_DIR_ACC_SHORT_EMA_INDEX=373]` at sentinel 0.5; after fold reset slot 373 returned to 0.5; either the per-step aux producer didn't push it off sentinel quickly enough or the binary classifier converged within ε of 0.5 in Fold-1 noise. Either way, kernel kept early-returning and slot 460 stayed at sentinel 50 the entire fold.

**Fix applied:** drop slot 373 dependency entirely. Drive temperature from realised hold-rate vs target overrun (slots 382 and 381, populated by the SP13 hold-pricing chain):

```c
overrun           = max(0, observed_hold_rate − target_hold_rate)
overrun_norm      = clamp(overrun / max(target_hold_rate, 0.01), 0, 1)
target_temp       = TEMP_MIN + (TEMP_MAX − TEMP_MIN) × overrun_norm
blended           = (1−α) × current + α × target_temp   // α=0.05
```

Per `compute_min_hold_penalty` at trade_physics.cuh:575 (HIGH T = forgiving exit ramp; LOW T = strict): over-holding pushes temp HIGH (exit ramp out of over-holding regime); at-or-below target pushes temp LOW (strict commitment).

Why this beats the slot-373 mapping:
- Direct closed-loop control on the actual symptom (hold rate, the thing we're trying to fix)
- No chained-input-sentinel masking — hold rates are measured per-epoch from realised actions
- Survives fold reset cleanly — hold-rate measurement starts fresh in Fold 1 with real data

Pearl-A bootstrap preserved (sentinel 50.0 → REPLACE on first valid observation). New cold-start sentinel guard fires when `observed_hold_rate` is at SP13 fold-reset sentinel 0.0 — the bootstrap-protection equivalent of the deleted dir_acc=0.5 guard.

**Instrumentation:** every per-epoch launch now emits `HEALTH_DIAG[N]: min_hold_temp_diag obs_hold_rate=X target_hold_rate=Y overrun_norm=Z target_temp=T blended_temp=B` so the swap can be verified at every fold transition by reading the smoke logs.

**Behavioral tests** (all GPU-gated, all pass on RTX 3050 Ti):
- `sp16_phase1_min_hold_temp_climbs_with_hold_overrun`
- `sp16_phase1_min_hold_temp_strict_when_at_target`
- `sp16_phase1_min_hold_temp_strict_when_under_target`
- `sp16_phase1_min_hold_temp_no_longer_reads_slot_373` (canonical proof of the swap)
- `sp16_phase1_min_hold_temp_mid_cadence_ema_after_bootstrap`
- `sp16_phase1_min_hold_temp_sentinel_observed_preserves_isv`

**Original plan tasks (Tasks 1.1 / 1.2) below are kept for historical reference but are SUPERSEDED by the actual fix above.**

### Task 1.1 (SUPERSEDED): Investigate producer launch lifecycle

**Files:**
- Read: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs` — find launch sites for both producers
- Read: `crates/ml/src/trainers/dqn/state_reset_registry.rs` — fold-reset registry entries
- Read: `crates/ml/src/trainers/dqn/trainer/training_loop.rs` — fold-boundary call ordering

- [ ] **Step 1: Locate the producer launch sites**

```bash
grep -rn "min_hold_temperature_update\|kelly_warmup_floor_compute\|launch.*MIN_HOLD\|launch.*KELLY_WARMUP" \
    crates/ml/src/cuda_pipeline/ | head -20
```

Find the launchers and identify what state guards them (epoch counter? warmup flag? fold-local flag?).

- [ ] **Step 2: Trace fold-reset path**

```bash
grep -nE "reset_for_fold|fold.*boundary|new_fold" \
    crates/ml/src/trainers/dqn/trainer/training_loop.rs \
    crates/ml/src/trainers/dqn/trainer/mod.rs \
    crates/ml-dqn/src/dqn.rs | head -20
```

Identify the call sequence at fold boundary. Check whether the launchers are actually invoked in Fold 1 (may be guarded by an epoch>=warmup check that the per-fold reset doesn't address).

- [ ] **Step 3: Identify root cause (one of three patterns)**

**Pattern A — One-shot flag**: launcher has a `did_warmup` or `producer_initialized` flag that's set on first launch. Reset clears the buffer but not this flag.

**Pattern B — Epoch-gate**: launcher only fires when `training_steps > some_threshold`. After fold reset, `training_steps=0` which keeps the launcher gated for several epochs.

**Pattern C — Cross-fold state**: launcher reads from a slot that's reset to 0 (sentinel), but the producer's bootstrap relies on the slot already having a valid value.

Document which pattern applies in the audit doc.

### Task 1.2: Fix the producer launch

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs` — at the producer launch sites
- Modify (if needed): `crates/ml/src/trainers/dqn/trainer/training_loop.rs` — fold-boundary path

- [ ] **Step 1: Write the failing test**

```rust
#[test]
#[ignore]
fn fold_reset_producer_relaunch_min_hold_temperature() {
    // Setup: trainer at end of Fold 0 with min_hold_T=11.4 (adapted)
    // Action: call reset_for_fold()
    // Action: simulate epoch 1 of Fold 1 (training step + producer launch)
    // Verify: isv[MIN_HOLD_TEMPERATURE_ADAPTIVE_INDEX=460] != 50.0
    // (50.0 is the sentinel/cold-start fallback; producer must override on first epoch)
    todo!("write after Task 1.1 identifies root cause")
}

#[test]
#[ignore]
fn fold_reset_producer_relaunch_kelly_warmup_floor() {
    // Same pattern for KELLY_WARMUP_FLOOR_INDEX=330
    todo!()
}
```

Run: `cargo test -p ml --test sp14_oracle_tests --release fold_reset_producer_relaunch -- --ignored --nocapture`
Expected: FAIL

- [ ] **Step 2: Apply the fix per Task 1.1's root-cause identification**

If Pattern A (one-shot flag): reset the flag in `reset_for_fold()` per `feedback_no_partial_refactor` (atomic with the buffer clear).

If Pattern B (epoch-gate): change the gate from `training_steps > N` to `(training_steps > N) || sentinel_detected`, where sentinel detection reads ISV[slot] and detects it's at the reset value.

If Pattern C (bootstrap dependency): ensure the bootstrap fallback path is taken on first launch after reset.

- [ ] **Step 3: Run tests, verify they pass**

```bash
SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo test -p ml --test sp14_oracle_tests --release fold_reset_producer_relaunch -- --ignored --nocapture
```
Expected: 2/2 PASS

- [ ] **Step 4: Run full sp14/sp15 oracle suite to catch regressions**

```bash
SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo test -p ml --test sp14_oracle_tests --test sp15_phase1_oracle_tests --release -- --ignored --nocapture 2>&1 | tail -10
```
Expected: all pass.

- [ ] **Step 5: Commit**

```bash
git add -A
git commit -m "$(cat <<'EOF'
fix(sp16-p1): fold-reset producer re-launch for slots 460 + 330

Per train-multi-seed-pfh9n post-mortem: MIN_HOLD_TEMPERATURE_ADAPTIVE_INDEX
(slot 460) and KELLY_WARMUP_FLOOR_INDEX (slot 330) producers fail to fire
in Fold 1 — Fold 0 adapted 50→11.4 / 0→0.25, Fold 1 stuck at sentinel.

Root cause: <Pattern A/B/C from Task 1.1>
Fix: <description>

Behavioral tests:
- fold_reset_producer_relaunch_min_hold_temperature
- fold_reset_producer_relaunch_kelly_warmup_floor

Both verify slot value escapes sentinel within first epoch of Fold 1.

Per feedback_no_partial_refactor — atomic with the original Class B #1
fold-reset buffer clear (commit 658fec493).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Phase 2 — Per-bar Hold cost: increase magnitude + ISV-adaptive

**Why this matters:** Train-multi-seed-pfh9n epoch 2 emit:

```
HEALTH_DIAG[2]: hold_pricing observed_rate=0.2501 target=0.2000 cost=0.006251
```

Realized Hold rate is 25%, target is 20%, but the *cost penalty* on Hold is only 0.006251 — tiny relative to per-bar reward magnitudes (popart=0.97, cf=0.65 in `reward_split`). The Hold action is essentially free, so Q-bias toward low-variance Hold has nothing to push against.

### Task 2.1: Make Hold cost magnitude ISV-driven and 10-20× larger

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/sp14_isv_slots.rs` — add `HOLD_COST_SCALE_INDEX` slot
- Modify: `crates/ml/src/cuda_pipeline/state_layout.cuh` — C #define mirror
- Modify: existing hold-pricing kernel (find via grep)
- Modify: `crates/ml/src/trainers/dqn/state_reset_registry.rs` — register new slot
- Modify: `crates/ml/src/trainers/dqn/trainer/training_loop.rs` — dispatch arm

- [ ] **Step 1: Find the existing hold-cost computation**

```bash
grep -rn "hold_pricing\|hold.*cost\|hold_pen\|HOLD_COST" \
    crates/ml/src/cuda_pipeline/*.cu \
    crates/ml/src/cuda_pipeline/*.cuh | head -20
```

Read the existing computation: `cost = base_cost × (observed_rate - target_rate)` or similar. Note the base coefficient.

- [ ] **Step 2: Allocate ISV slot 461**

Edit `crates/ml/src/cuda_pipeline/sp14_isv_slots.rs`:

```rust
// SP16 Phase 2 (2026-05-08): adaptive Hold cost magnitude. Train-multi-seed-pfh9n
// post-mortem showed observed Hold rate climbed 25% → 52% across training while
// the cost penalty (~0.006) was 100× smaller than per-bar reward magnitudes
// (popart=0.97). Hold became effectively free, allowing Q(Hold) to dominate.
// Producer: hold_cost_scale_update (per-epoch, signal-driven from
// |observed_hold_rate - target_hold_rate|).
pub const HOLD_COST_SCALE_INDEX: usize = 461;
// ISV_TOTAL_DIM bumps 461 → 462
```

Update `ISV_TOTAL_DIM` and add C #define mirror in `state_layout.cuh`.

- [ ] **Step 3: Write the failing test**

```rust
#[test]
#[ignore]
fn hold_cost_scale_climbs_with_overrun() {
    // Setup: observed_hold_rate = 0.50, target = 0.20 (i.e., 30% overrun)
    // Launch hold_cost_scale_update kernel
    // Verify: isv[HOLD_COST_SCALE_INDEX] >= 5.0× base
    // (intent: at 30% overrun, scale should be aggressive)
    todo!()
}

#[test]
#[ignore]
fn hold_cost_scale_decays_when_in_target() {
    // Setup: observed_hold_rate = 0.20, target = 0.20 (perfect)
    // Launch hold_cost_scale_update kernel a few times
    // Verify: isv[HOLD_COST_SCALE_INDEX] decays toward 1.0 (base scale)
    todo!()
}
```

Run: `cargo test -p ml --test sp14_oracle_tests --release hold_cost_scale -- --ignored --nocapture`
Expected: FAIL

- [ ] **Step 4: Implement producer kernel**

Create `crates/ml/src/cuda_pipeline/hold_cost_scale_update_kernel.cu`:

```c
/* hold_cost_scale_update — adaptive hold cost magnitude.
 *
 * Reads:  observed_hold_rate, target_hold_rate (from existing diagnostic)
 * Writes: ISV[HOLD_COST_SCALE_INDEX=461]
 *
 * Logic: scale = clamp(base + 20 × (observed - target)+, 1.0, 25.0)
 *   - base scale = 1.0 (no penalty when at target)
 *   - 30% overrun → scale = 1.0 + 20×0.3 = 7.0× (aggressive)
 *   - 50% overrun → scale = 11.0× (very aggressive, clipped at 25)
 *
 * Pearl-A bootstrap: first observation replaces sentinel.
 * Welford slow EMA α=0.05 for steady state.
 *
 * Per-epoch boundary cadence.
 */
#include "state_layout.cuh"

extern "C" __global__ void hold_cost_scale_update(
    const float* __restrict__ isv,
    float*       __restrict__ isv_out,
    float observed_hold_rate,
    float target_hold_rate
) {
    if (threadIdx.x != 0 || blockIdx.x != 0) return;

    float overrun = fmaxf(0.0f, observed_hold_rate - target_hold_rate);
    float new_scale = 1.0f + 20.0f * overrun;
    new_scale = fmaxf(1.0f, fminf(new_scale, 25.0f));

    float prev_scale = isv[HOLD_COST_SCALE_INDEX];
    float blended;
    if (prev_scale <= 0.0f) {
        // Pearl-A bootstrap from sentinel
        blended = new_scale;
    } else {
        // Welford slow EMA, α=0.05 (responds in ~20 epochs to steady drift)
        const float alpha = 0.05f;
        blended = (1.0f - alpha) * prev_scale + alpha * new_scale;
    }
    blended = fmaxf(1.0f, fminf(blended, 25.0f));

    isv_out[HOLD_COST_SCALE_INDEX] = blended;
}
```

Add cubin entry in `crates/ml/build.rs`. Wire launch site in `gpu_dqn_trainer.rs` per-epoch boundary, beside other adaptive producers.

- [ ] **Step 5: Migrate consumer**

Find the existing hold-cost computation site (Task 2.1 Step 1). Multiply the base cost by `isv[HOLD_COST_SCALE_INDEX]`:

```c
float cost_scale = isv[HOLD_COST_SCALE_INDEX];
if (cost_scale <= 0.0f) cost_scale = 1.0f;  // cold-start fallback
float cost = base_cost * cost_scale;  // was: float cost = base_cost;
```

- [ ] **Step 6: Add dispatch arm**

`crates/ml/src/trainers/dqn/state_reset_registry.rs` — add entry for slot 461 with sentinel value.
`crates/ml/src/trainers/dqn/trainer/training_loop.rs` — add reset_named_state dispatch arm.

- [ ] **Step 7: Run tests**

```bash
SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo test -p ml --test sp14_oracle_tests --test sp15_phase1_oracle_tests --release -- --ignored --nocapture 2>&1 | tail -15
```
Expected: all pass including 2 new hold_cost_scale tests.

- [ ] **Step 8: Commit**

```bash
git add -A
git commit -m "$(cat <<'EOF'
feat(sp16-p2): adaptive Hold cost scale via ISV[461]

Per train-multi-seed-pfh9n post-mortem: observed_hold_rate climbed 0.25 → 0.52
across training while cost penalty (~0.006) was 100× smaller than per-bar
reward magnitudes (popart=0.97). Hold action was effectively free.

Fix: scale Hold cost adaptively. ISV[HOLD_COST_SCALE_INDEX=461] tracks:
  scale = clamp(1.0 + 20 × max(0, observed - target), 1.0, 25.0)

At 30% overrun (observed=0.5, target=0.2): scale → ~7.0×, aggressive.
At target: scale → 1.0× (no penalty).

Pearl-A bootstrap + Welford slow EMA (α=0.05).

ISV_TOTAL_DIM: 461 → 462.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Phase 3 — Wiener-optimal adaptive α (T3)

### Why this exists (validation result, not original spec)

The original Phase 3 was a placeholder for "validation L40S/H100 smoke" — but `train-multi-seed-hjzss` running the SP16 T1+T2 chain showed the producer trajectory was **bit-identical to the pfh9n baseline through epoch 3**:

- `hold_cost_scale_diag`: 1.0 → 1.0 → 1.27 (target 7.0 — only 21% of headroom in 3 epochs)
- `min_hold_temp_diag`: 0.0 → 5.0 → 5.50 (target 16.0 — only 35% of headroom in 3 epochs)

**Root cause**: hardcoded `const float alpha = 0.05f` in both producer kernels. With α=0.05 the EMA needs ~60 epochs from cold start to converge — far beyond the 5-epoch smoke and likely beyond any practical training horizon. Per `feedback_isv_for_adaptive_bounds`, the constant α was itself an adaptive bound being hardcoded; per `pearl_wiener_optimal_adaptive_alpha` the proper fix is:

```
α = diff_var / (diff_var + sample_var + ε)
```

Where `sample_var` and `diff_var` are estimated via Welford accumulators stored in ISV.

### Task 3.0: Wiener-optimal adaptive α (LANDED 2026-05-08)

Both producer kernels (T1 `min_hold_temperature_update_kernel.cu` and T2 `hold_cost_scale_update_kernel.cu`) migrated atomically:

- 12 new ISV slots [462..474), 6 per producer (target mean/M2, diff mean/M2, prev_target, sample_count). `ISV_TOTAL_DIM` 462 → 474.
- Welford accumulators updated on every launch (after sentinel-guard early-return).
- α computed as `diff_var / (diff_var + sample_var + ε)`, clamped to `[WELFORD_ALPHA_MIN=0.01, WELFORD_ALPHA_MAX=0.95]`.
- Cold-start path: when `sample_count < 3`, α = 1.0 (REPLACE) so the producer is responsive in epochs 1-2 while Welford state stabilises.
- Pearl-A first-observation bootstrap preserved (sentinel → REPLACE) and runs BEFORE the blend formula so it has priority over a coincidental cold-start α=1.0.
- FoldReset registers all 12 slots with sentinel 0.0 + 12 dispatch arms.
- Diagnostic emit (`HEALTH_DIAG[N]: hold_cost_scale_diag` + `min_hold_temp_diag`) extended with `alpha` and `sample_count` fields for direct trajectory observation.
- 5 behavioral tests (4 GPU + 1 host-only): α high during signal jump, α low in steady state, Pearl-A bootstrap fires, α naturally bounded, no `0.05f` literal in kernel sources.

### Task 3.1: Push and dispatch validation smoke

- [ ] **Step 1: Push the SP16 chain**

```bash
git push origin sp15-phase1-honest-numbers
```

- [ ] **Step 2: Dispatch H100 5-epoch smoke**

```bash
SHORT=$(git rev-parse --short HEAD)
./scripts/argo-train.sh dqn --sha "$SHORT" --branch sp15-phase1-honest-numbers \
    --epochs 5 --gpu-pool ci-training-h100 --baseline --multi-seed 1 --folds 2
```

- [ ] **Step 3: Monitor trajectory**

Stream logs to `/tmp/train-sp16-${SHORT}.log` and watch for:

**Success signals (pass to Phase 4):**
- `q_by_action [hold=X long=Y short=Z flat=W]` — Q(Hold) NOT climbing dominantly relative to Q(direction)
- Hold% stays below 30% across all 5 epochs
- Sharpe stable (not peak-then-decay)
- `min_hold_T` adapts in BOTH folds (not stuck at 50 in Fold 1)
- `sp9_kelly_warmup floor` non-zero in BOTH folds

**Failure signals (kill-fast):**
- `q_by_action [hold]` climbs >2× faster than max(long, short, flat)
- Hold% reaches >40% in any epoch
- Sharpe decays >25% peak-to-final
- NaN/Inf, Q-explosion, dir_entropy<0.7

### Task 3.2: Diagnose and iterate

- [ ] **If success signals**: dispatch 30-epoch L40S validation, compare to pfh9n trajectory.

- [ ] **If failure signals (Q-bias persists despite Hold cost increase)**: structural Q-bias is deeper than reward shaping. Recommended next step is Phase 4 (out of scope for this plan): dueling Q-network architecture with advantage normalization.

---

## Out of scope (defer to follow-up)

These were considered and explicitly excluded from SP16:

1. **Dueling Q-network architecture** — separate `V(s)` and `A(s,a)` heads with advantage normalization. Largest possible fix for structural Q-bias, but requires architecture refactor (~500-800 LOC across encoder/decoder, GRN trunk, IQN/C51 heads). Defer to SP17 if Phase 2 doesn't break the spiral.

2. **Class B #3 PER beta annealing** — beta resets to start when `training_steps=0` at fold boundary, but old fold's transitions still in buffer have priorities computed under late-fold beta. Now mitigated by Class B #1 fold-buffer clear, so this issue is closed in practice. Re-evaluate if needed.

3. **Class B #4 collapse-warmup mismatch** — gate uses `replay_buffer_capacity * 0.2 = 20K` instead of `min_replay_size = 1K`. Cosmetic at current scale; defer.

4. **Class B #6 PER alpha adaptive** — `per_alpha=0.6` hardcoded. Class B #5 marked it MEDIUM severity. Adaptive α would be nice but not urgent.

---

## Discipline rules (per project memory)

- **`feedback_no_partial_refactor`** — fold-reset producer fixes migrate atomically (signature + all callers).
- **`feedback_isv_for_adaptive_bounds`** — Hold cost scale lives in ISV slot 461, not hardcoded.
- **`feedback_no_stubs`** — every test has a real body; no `todo!()` in committed code (Step 2 placeholders get filled in Step 4 of each task).
- **`feedback_no_atomicadd`** — block-tree-reduce only if any reduction needed.
- **`feedback_kill_runs_on_anomaly_quickly`** — Phase 3 smoke kills early on Q(Hold) climb signal.
- **`feedback_push_before_deploy`** — Phase 3 pushes before argo-train.

---

## Self-review notes

- **Phase 0 first** is non-negotiable: without `q_by_action` we have no falsifier for the Q-bias hypothesis.
- **Phase 1 fixes a separate bug** that surfaced during the smoke. It's small but worth doing atomically with Phase 2 (single L40S validation covers both).
- **Phase 2's 20× scale coefficient** is heuristic — Welford EMA + adaptive scale should self-tune at convergence. If validation shows oscillation, lower α from 0.05 to 0.01.
- **Total LOC estimate**: Phase 0 ~80, Phase 1 ~50-150 depending on root cause, Phase 2 ~250. Total ~400-500 LOC across 3 commits + 1 validation run.
