# SP14 — Aux→Q Wire + Earned Gradient Flow Pearl

**Status:** Design v3 (post-second-critical-review; awaiting final user approval)
**Date:** 2026-05-05
**Author:** Brainstormed in session post-Smoke A
**Parent commit:** `d243a6f08` (v2 spec) → v3 with two further corrections
**Depends on:** SP13 chain (B0 → B0.1 → B1.0 → B1.1a → B1.1b) all landed at `6657e5626`
**Branch target:** `sp11-reward-as-controlled-subsystem`

## Changelog

### v2 → v3 (this revision; addresses two further user-flagged issues)

1. **Direction Q-head is K=4, not K=3.** v2 trusted the stale comment at `gpu_dqn_trainer.rs:2438` ("default: 3 — Short/Flat/Long") but `state_layout.cuh:123-126` is the authoritative source: `DIR_SHORT=0, DIR_HOLD=1, DIR_LONG=2, DIR_FLAT=3`. Production callsites (`gpu_iqn_head.rs:123`, `gpu_backtest_evaluator.rs:144`, all smoke configs) all set `branch_0_size: 4`. SP13 added `DIR_HOLD` as a separate fourth action when Hold-pricing landed; the config default comment was never updated. v3 corrects:
   - B.1 action-space context: 4 actions with Hold and Flat both non-committal
   - B.2.3 q_disagreement mapping: K=4↔K=2 with both Hold and Flat masked
   - This is exactly the `feedback_trust_code_not_docs` failure mode — a single stale comment would have silently corrupted the disagreement signal
2. **Adaptive β rate limiter (was structural).** v2 hardcoded `β=0.9`. Per `feedback_isv_for_adaptive_bounds`, β is now signal-driven from variance of `α_grad_raw`, mirroring the k_aux/k_q variance-driven steepness pattern. Adds 2 ISV slots: `ALPHA_GRAD_RAW_VARIANCE_EMA_INDEX` and `BETA_RATE_LIMITER_ADAPTIVE_INDEX`. Net 11→13 slots.

### v1 → v2

Critical review against current code state at `eaf4adcb9` surfaced 8 corrections:

1. **Action-class asymmetry corrected** (further corrected in v3 to K=4).
2. **Forward launch order constraint added** (B.1). Wire creates new serial dep: aux forward must complete before direction Q-head forward. Cited `pearl_canary_input_freshness_launch_order`.
3. **Adaptive sigmoid k formula fixed** (B.2.5). v1 had unreachable `k_max = 50` because formula caps `k ≤ k_base`. v2 uses `max(...)` instead of `clip(..., k_min, k_max)`, with `k_max = k_base` implicit. `k_base_aux = 20`, `k_base_q = 15`.
4. **α_grad rate limiter promoted from nice-to-have to v1** (new B.2.8). Schmitt state-flip introduces sigmoid discontinuity (~0.15-0.4 jump). Rate limiter EMA smoothing damps the transition. v3 makes β ISV-adaptive.
5. **q_disagreement_baseline drop** (B.2.3). v1 had adaptive baseline as long-EMA — risk of feedback loop. v2/v3 uses structural 0.5 (analytic K-with-Hold-Flat-masked random alignment).
6. **Backward gradient scaling clarified** (B.4). α_grad scales `dL/dx` (input gradient flowing back to aux), NOT `dL/dW` (Q-head's weight gradient).
7. **4 hard rules added** (B.6): `feedback_no_hiding`, `feedback_no_htod_htoh_only_mapped_pinned`, `feedback_kill_runs_on_anomaly_quickly`, `pearl_canary_input_freshness_launch_order`.
8. **Smoke A2 explicit kill criteria table added** (B.7).

---

## Goal

Connect the SP13 supervised aux signal to the Q-head's directional decision-making, and fix three structural stability issues that Smoke A surfaced. After SP14 lands, the K=2 softmax CE aux head's directional confidence should actually reach the action-selection path, gradient clipping should drop to baseline frequency, and the aux_w controller should reach its designed dynamic range.

## Motivation

Smoke A on `6657e5626` (multi_fold_convergence × 3 folds × 5 epochs) revealed a striking diagnostic pattern:

- `aux_dir_acc` reached **0.61** in fold 1 — supervised signal extraction works, the data has directional signal, the K=2 softmax CE head finds it
- `val_win_rate` stayed glued at **45-48%** across all 15 epochs and all 3 folds, even when aux_dir_acc varied 0.16 → 0.61
- `val_dir_dist` was uniform `[short=0.24, hold=0.26, long=0.26, flat=0.24]` — Hold-pricing eliminated the prior Hold dominance ✓
- `val_sharpe` oscillated 9-16 — sizing/timing branches respond to fold variation
- `GRAD_CLIP_OUTLIER` fired **1109 times** (vs 5 in pre-SP13 baseline) — chronic gradient throttling

The "WR-flat-while-aux-varies" pattern is a structural tell: directional weights are *frozen* (no path to action selection) while size/timing branches respond normally. Three parallel diagnostic agents triangulated three interlocking root causes, documented in the next section.

The user's intuition was correct on all three counts: longer runs won't fix chronic clipping (gradient information is being lost at every step); the model isn't adapting because the aux signal can't reach the Q-head; and adaptive LR is the right next adaptation lever *but only after* these structural bugs are fixed.

## Smoke A diagnostic — three interlocking bugs

| # | Bug | Evidence | Bug class |
|---|---|---|---|
| 1 | **Aux→Q wire absent.** Slot 375 (`AUX_DIR_PREDICTION_INDEX`) has zero readers in any Q-head pipeline. The deferred Layer B Task B.T3 Step 2 (Q-head input concat) was scoped but never built. | Exhaustive grep across 120 `.cu` files returns no consumers. Self-aware comments at `gpu_dqn_trainer.rs:13385-13387` and `training_loop.rs:4878-4880` admit "Phase 0b wires the consumers — deferred." | Architectural |
| 2 | **C51 distributional Q-loss gradient blow-up.** Raw c51 grad reaches `9.5×10⁶` in fold 2; SP7 budget controller saturates at `1e-4` minimum but post-scale grad is still `~10³`, dominating IQN by 50-500×. | HEALTH_DIAG `grad_split_bwd c51=9522607` at fold 0 ep 1; `c51_budget` collapses to `1e-4` and pins. Pre-SP13 baseline had c51 grad 0.006-0.5. | Numerical |
| 3 | **aux_w controller muzzled by stale clamp.** P0b's deficit+stagnation controller computes raw aux_w in `[0.15, 1.5]`, but `set_aux_weight` at `gpu_dqn_trainer.rs:14722` re-clamps with the SP11-era `[0.05, 0.3]` cap. Deficit-amplification term is silently dead. Fold 2 stagnation drops aux_w 45% below working value. | Trace at line 14720-14723. Aux_w reaches floor 0.164 in fold 2 vs ceiling 0.3 in folds 0/1 (clamp masks reality). | Configuration |

Plus two related issues:
- **Stagnation gate fires inappropriately at fold boundary** because Pearl-A first-observation bootstrap makes short-EMA = long-EMA at fold reset, giving `improvement = 0` by construction.
- **Producer kernel writes batch-mean scalar** to slot 375 instead of per-bar tile (Plan B.T3 sketched per-bar). For per-step Q-head decisions the batch-mean shape is structurally wrong, but the trainer-side `aux_nb_softmax_buf` is already `[B, K]` per-bar — so the wire reads from the source buffer directly, sidestepping the slot-375 shape question.

## Decomposition into three sub-projects

| Sub-project | Scope | Size | Brainstorm needed? |
|---|---|---|---|
| **A — Stability fixes** | Bugs 2, 3, plus stagnation warmup gate | ~3 files, ~50 LOC | No — direct to writing-plans |
| **B — Wire + Earned Gradient Flow pearl** | Bug 1 + the new gradient-gating pearl | 12-15 files, ~1200 LOC | Yes — this spec |
| **C — Adaptive LR (per-loss-group)** | Bug 6 (the user's open question) | TBD | Deferred until A+B's effect is measured |

A and B are independent and parallel-safe. A goes straight to a plan; B is the focus of this spec. C is intentionally deferred — only revisit if the post-SP14 30-epoch validation shows WR not climbing past 50%.

## Sub-project A — stability fixes (this spec defers to plan)

A is small enough that brainstorming would be theatrical. The plan will land:

1. **C51 atom-probability floor** in `c51_grad_kernel.cu`: `p_atom = fmaxf(p_atom, ATOM_FLOOR)` before the `−log(p)/p · ∂p/∂z` divide. Floor source: ISV-driven from existing SP4 `atom_pos_p99` per-branch producer. Per `pearl_isv_for_adaptive_bounds`, the floor must be ISV-driven not hardcoded.
2. **`set_aux_weight` clamp lift** at `gpu_dqn_trainer.rs:14722`: change `clamp(0.05, 0.3)` to `clamp(AUX_W_BASE * AUX_W_HARD_FLOOR_RATIO, AUX_W_BASE * AUX_W_HARD_CEIL_RATIO)` = `clamp(0.15, 1.5)`, importing constants from `sp13_isv_slots.rs`.
3. **Stagnation warmup gate** in `compute_aux_w_p0b` at `training_loop.rs:96-135`: skip stagnation logic when `epochs_in_fold < 1`. Pearl-A first-observation makes short=long at fold reset, so `improvement = 0` by construction in the first epoch — the stagnation gate fires on a non-stagnation. Add `epochs_in_fold > 0` guard.

Validation: Smoke A re-run on commit landing A should show GRAD_CLIP_OUTLIER count drop from 1109 → ~50 or less, and aux_w trace should show values in `[0.15, 1.5]` range during deficit-amplification phases.

A and B can land in either order. If A lands first (simpler), B's smoke validation has cleaner gradient signals. If B lands first, A's smoke shows whether the wire alone moves WR. Recommendation in plan: **A first** as a cleanup commit, then B as the main feature work.

## Sub-project B — Aux→Q wire + Earned Gradient Flow pearl

This is the architectural piece. Two coupled changes that must land atomically per `feedback_no_partial_refactor`:

1. **Forward wire**: direction Q-head input concat gains 1 column from `aux_softmax_diff = aux_nb_softmax_buf[b, 1] - aux_nb_softmax_buf[b, 0]` ∈ [−1, +1]
2. **Backward gating pearl**: gradient flow back through that wire is gated by an adaptive scalar `α_grad ∈ [0, 1]` driven by ISV signals

### B.1 — Forward wire design

**Input shape change:** direction Q-head's input layer was `[h_s2]` (trunk features, shape `[B, SH2]`). Becomes `[h_s2 || aux_softmax_diff]` shape `[B, SH2 + 1]`. The new column is per-bar, one float per batch row.

**Source of per-bar signal:** read directly from `aux_nb_softmax_buf` (existing `CudaSlice<f32>` shape `[B, K=2]`, populated by `aux_next_bar_forward` post-B1.1a). The diff `softmax[:, 1] - softmax[:, 0]` is bounded structurally in `[−1, +1]` because softmax components are non-negative and sum to 1. Per `pearl_bounded_modifier_outputs_require_structural_activation`, this is the right structural bound — no runtime clamp needed.

**Slot 375 fate:** kept as-is (batch-mean scalar) for HEALTH_DIAG observability. The existing `aux_pred_to_isv_tanh_kernel` continues to write `mean(softmax[1] - softmax[0])` for diagnostic purposes. The wire bypasses slot 375 and reads `aux_nb_softmax_buf` directly per-bar — slot 375 becomes pure diagnostic, not a critical data path.

**Q-head SGEMM weight matrix:** the direction Q-head's first FC layer (`w_b0fc`-equivalent — implementer to identify exact tensor index in `compute_param_sizes()` against current code) grows its INPUT dimension from `SH2` to `SH2 + 1`. Output dim (advantage hidden / direction logits) unchanged. Production `branch_0_size = 4` (Short/Hold/Long/Flat per `state_layout.cuh:123-126` — **the comment at `gpu_dqn_trainer.rs:2438` saying "default: 3" is STALE pre-SP13**; verified against production callsites in `gpu_iqn_head.rs:123`, `gpu_backtest_evaluator.rs:144`, smoke configs). The new row of weights is initialized to **zero** at trainer construction. Rationale: model starts ignoring the new input (no perturbation to existing learned behavior), then learns to use it through gradient descent. Xavier-init would inject noise on day 0 that the trunk would have to learn to denoise.

**Action-space context for the wire's downstream meaning:** the direction Q-head emits **4 actions**, indexed per `state_layout.cuh`:
- `DIR_SHORT = 0` — open/maintain short position
- `DIR_HOLD = 1` — keep current position (no-op; SP13 Hold-pricing applies here)
- `DIR_LONG = 2` — open/maintain long position
- `DIR_FLAT = 3` — close all positions to zero

The aux head emits 2 classes: down (0), up (1) [plus -1 skip mask]. The wire's signal `softmax[1] - softmax[0]` is positive when aux thinks "up" (favoring Long), negative when aux thinks "down" (favoring Short). Hold and Flat are non-committal actions — Hold keeps existing position (no new directional commitment), Flat exits to zero (also not a new commitment). The wire makes aux's directional confidence available as input; the Q-head learns whether to trust it for the Short/Long commitment decision.

**Forward launch order constraint** (per `pearl_canary_input_freshness_launch_order`): the wire creates a new serial dependency in the per-step graph capture. The aux head forward (`aux_next_bar_forward` writing `aux_nb_softmax_buf [B, K=2]`) must complete BEFORE the direction Q-head's first FC reads the softmax diff. Pre-SP14, aux and direction Q-head ran in parallel from `h_s2`; post-SP14, they're serial: trunk → aux forward → direction Q-head forward. Implementer must order the two launches in graph capture; failure to do so produces a stale-input bug (direction Q-head reads previous step's softmax, exactly the failure mode the canary pearl warns about).

**Param tensor layout:** the direction Q-head weight tensor is one of the indices in `compute_param_sizes()` — implementer to identify exactly which index against current code at HEAD `6657e5626` per `feedback_trust_code_not_docs`. Its size grows by `branch_0_size`. Update `compute_param_sizes()` and the corresponding `layout_fingerprint_seed` entry. Per `pearl_build_rs_rerun_if_env_changed` and Invariant 8, fingerprint must bump (rename the entry suffix to indicate the new shape, e.g., `PARAM_W_DIR_QHEAD → PARAM_W_DIR_QHEAD_AUX1` or similar — implementer chooses convention matching the codebase).

**Forward kernel modifications:** the direction Q-head's forward SGEMM (likely in `gpu_dqn_trainer.rs` near the per-branch forward block) needs its `K` (input dim) bumped by 1 and an extra concat operation that copies the per-bar `softmax_diff` into the input vector. Implementer to choose between (a) a new fused concat-SGEMM kernel, or (b) a separate concat into a scratch buffer followed by the existing SGEMM. (a) is more efficient but more invasive; (b) reuses existing paths.

**Symmetry decision:** does the magnitude / order / urgency Q-head also see `softmax_diff`? **No, v1 keeps it direction-only.** The aux signal is directional — adding it to mag/ord/urg branches dilutes the signal and adds compute without clear benefit. If post-validation magnitude-conditional-on-direction shows benefit, that's a v2 lever.

### B.2 — Earned Gradient Flow pearl

The forward wire is one-way information flow by default. Backward gradient flow through the wire is **earned** by the aux head demonstrating label competence and the Q-head demonstrating it's not already aligned with aux. This is the new pearl — adaptive ISV-driven gating on backward-pass gradient flow, a new application of the codebase's existing Pearl pattern (which previously gated forward-pass features only).

#### B.2.1 — Mathematical form

```
α_grad = σ(k_aux · sgn_open_1 · (aux_dir_acc_short_ema − threshold_1))
       × σ(k_q   · (q_disagreement_short_ema   − q_disagreement_baseline))
       × post_warmup_gate

where:
  σ(x) = 1 / (1 + exp(-clip(x, -30, 30)))     # numerically safe sigmoid
  k_aux, k_q = ISV-adaptive sigmoid steepness
  sgn_open_1, threshold_1 = state-dependent (Schmitt trigger; see B.2.4)
  post_warmup_gate = linear ramp over first epoch of each fold
```

`α_grad` is a single batch-mean scalar in `[0, 1]`. It scales the backward gradient column at the wire position (i.e., the new column in the direction Q-head's weight gradient that corresponds to the `softmax_diff` input). When `α_grad = 0`, the aux head receives no gradient from Q-loss (pure stop-gradient). When `α_grad = 1`, full Q-loss gradient flows back through the wire to the aux softmax outputs, then through aux head backward to the trunk.

#### B.2.2 — Gate 1: aux competence

**Driver signals:** `aux_dir_acc_short_ema` (ISV[373], existing) and `target_dir_acc` (ISV[372], existing).

**Behavior:** opens when aux head has demonstrated label accuracy above target. Closes when aux head's accuracy drops back below target.

**Threshold:** in v1, `threshold_1 = target_dir_acc` (so default behavior is to open at exactly target). The Schmitt-trigger (B.2.4) modifies this dynamically.

**Why this gate exists:** prevents Q-loss from hijacking aux training before aux has earned its label. The aux head's primary job is supervised directional prediction; co-training is a privilege earned by performing that primary job.

#### B.2.3 — Gate 2: Q-head disagreement (NEW signal)

**Driver signals:** `q_disagreement_short_ema` (NEW ISV slot, fast EMA α=0.3) and `q_disagreement_long_ema` (NEW ISV slot, slow EMA α=0.05 for HEALTH_DIAG drift visibility). Baseline is **structural 0.5** — not an ISV slot. Long-EMA-as-baseline was considered and rejected because adaptive baseline drift creates a feedback loop (disagreement drops → baseline drops → gate sees lower values as "above baseline" → opens spuriously).

**K=4 ↔ K=2 mapping** (the action-class asymmetry): aux head emits K=2 (down=0, up=1). Direction Q-head emits K=4 (`DIR_SHORT=0`, `DIR_HOLD=1`, `DIR_LONG=2`, `DIR_FLAT=3` per `state_layout.cuh:123-126`). Naive `argmax(Q) != argmax(aux)` doesn't apply across different cardinality. The disagreement signal masks BOTH Hold and Flat picks — neither represents a NEW directional commitment (Hold = keep current position; Flat = exit to zero):

```
For each row b in batch:
    aux_class    = argmax(aux_softmax[b])              # 0 (down) or 1 (up)
    q_dir_class  = argmax(Q_dir_logits[b])             # 0, 1, 2, or 3

    if q_dir_class == DIR_HOLD or q_dir_class == DIR_FLAT:  # 1 or 3
        # neutral; no new directional commitment; mask out
        q_disagreement[b]   = 0.0
        contribution_count  = 0
    else:
        # Q picked DIR_SHORT (0) or DIR_LONG (2)
        # Map Q to aux space: Short=0 ↔ down=0, Long=2 ↔ up=1
        q_dir_mapped = 0 if q_dir_class == DIR_SHORT else 1
        q_disagreement[b]   = (q_dir_mapped != aux_class) ? 1.0 : 0.0
        contribution_count  = 1

q_disagreement_batch_mean = sum(q_disagreement) / max(1, sum(contribution_count))
```

Then EMA over steps via standard Pearl-A bootstrap pattern. Skip-rows from Hold/Flat picks don't pollute the EMA — they're treated as "no information about directional disagreement", not "no disagreement."

**Note on Hold semantics:** Hold could be argued as a *soft* directional commitment ("I think the current position is right"), but resolving that requires the current position state which the disagreement kernel doesn't have access to. v1 keeps the simple "no new commitment → mask" rule. If post-30-epoch validation shows the disagreement signal is too noisy because of frequent Hold picks during training, v2 could thread the current-position bit into the disagreement kernel.

**Behavior:** opens when persistent disagreement is meaningfully above the structural 0.5 baseline. Specifically:
- `q_disagreement_short_ema > 0.5 + ε` → Q-head is fighting aux signal (with Hold/Flat masked out, this means the model is actively picking Short when aux says up, or Long when aux says down) → co-training would help align them
- `q_disagreement_short_ema ≤ 0.5` → Q-head agreement is at-or-better-than chance → no benefit from gradient flow

**Edge case — all rows masked:** if the entire batch picks Hold/Flat (early training abstention), `contribution_count = 0` everywhere. The `max(1, ...)` denominator prevents division by zero; q_disagreement stays at sentinel 0.5 via Pearl-A bootstrap (first observation). Gate 2 stays closed because raw signal hasn't moved away from baseline. This is correct behavior — "no committed directional decisions to evaluate" → "no co-training signal to feed."

**Argmax source:** use the deterministic `argmax(Q_dir_logits)` at both training and eval time — i.e., the Q-head's preferred direction *before* Thompson sampling. This tracks the Q-head's underlying directional belief; the disagreement signal should not be polluted by exploration noise. Thompson-sampled action selection diverges from argmax stochastically (especially early in training when Q values are flat), so using the post-Thompson selection would conflate two signals: directional belief and exploration. We want the belief.

**Why this gate exists:** open Gate 1 alone would unlock co-training even when Q-head is already aligned with aux — wasted gradient flow. Gate 2 ensures co-training happens only when the system shows it's productive.

#### B.2.4 — Schmitt-trigger hysteresis on Gate 1

Single-threshold sigmoid oscillates around target by ±50% if `aux_dir_acc` jitters around target by training noise. Hysteresis prevents this:

```
Two thresholds:
  threshold_1_open  = target_dir_acc + 0.03  # cross above this from below to OPEN
  threshold_1_close = target_dir_acc - 0.03  # cross below this from above to CLOSE

State persistence:
  gate1_open_state ∈ {0, 1}  (NEW ISV slot)

Logic:
  if gate1_open_state == 0 and aux_dir_acc_short_ema > threshold_1_open:
      gate1_open_state = 1
  elif gate1_open_state == 1 and aux_dir_acc_short_ema < threshold_1_close:
      gate1_open_state = 0

Sigmoid argument:
  if gate1_open_state == 1:
      arg_1 = aux_dir_acc_short_ema − threshold_1_close
  else:
      arg_1 = aux_dir_acc_short_ema − threshold_1_open
```

Once open, gate stays open until aux drops 0.06 below the open-threshold (i.e., 0.03 below target). Once closed, stays closed until aux rises 0.06 above the close-threshold. The ±0.03 band is structural (numerical-stability anchor; not adaptive).

**State reset:** `gate1_open_state` resets to 0 at fold boundary via state-reset-registry. Otherwise stale gate state from fold N pollutes fold N+1.

#### B.2.5 — ISV-adaptive sigmoid steepness

Both `k_aux` and `k_q` self-adapt based on the variance of their respective driver signals:

```
k_x = max(k_base / (1 + variance_ema_x / variance_ref), k_min)
```

- **Welford EMA variance** of the driver signal computed per-step
- High variance → noisy signal → low k → smoother sigmoid → less reactive to noise
- Low variance → stable signal → k near k_base → sharper sigmoid → more decisive
- At variance = 0, k = k_base (structural maximum; the formula bounds k naturally above by k_base)
- At variance = variance_ref, k = k_base / 2
- At variance >> variance_ref, k decays toward k_min (structural floor)

**Bounds:** only `k_min = 1.0` is a separate constant (numerical-stability floor — prevents the sigmoid from becoming completely flat under huge variance). `k_max = k_base` is implicit in the formula — k can never exceed `k_base`. This is correct semantics: "k_base is the maximum steepness we trust when the signal is perfectly reliable; less reliability → less reactive sigmoid." Set `k_base_aux = 20.0` and `k_base_q = 15.0` (structural numerical-stability anchors per Invariant 1; calibrated so a 0.05 deviation from threshold gives sigmoid output ≈ 0.73 at full reactivity). The variance bound `variance_ref` is structural per-signal: e.g., `variance_ref_aux = 0.01` (1% deviation = signal becomes noisy), `variance_ref_q = 0.05`.

**Per-fold reset:** variance EMAs reset to a sentinel that gives `k = k_base` at fold boundary (e.g., `var_ema = 0.0` so initial k = `k_base`). Without reset, fold N's variance carries into fold N+1's sigmoid steepness during cold start. (Note: starting at `var_ema = 0` deliberately gives maximum reactivity at fold start; the warmup ramp + Pearl-A bootstrap on EMAs prevent the high-k from acting on bad data.)

#### B.2.6 — Per-epoch warmup ramp

```
post_warmup_gate = min(steps_in_fold / WARMUP_STEPS, 1.0)
```

where `WARMUP_STEPS` is one epoch's worth of training steps (structural constant, derived from `config.steps_per_epoch`). This ramps the gate from 0 → 1 linearly over the first epoch of each fold, avoiding Δα discontinuity at the warmup boundary.

**State source:** `steps_in_fold` is already tracked by the trainer for other purposes; reuse it.

#### B.2.7 — Anti-gradient-hacking circuit breaker

The pathological dynamic to defend against: Q-head learns that disagreeing with aux unlocks aux co-training, which lets Q-loss pull aux toward Q-favored features, which makes aux less label-aligned, which → Q-head disagrees more → cycle. This is mesa-optimization risk.

**Detection:** track `aux_dir_acc_post_open_min` (NEW ISV slot — lowest aux_dir_acc since gate 1 opened in current fold). Reset when gate 1 closes.

**Trigger:** if `aux_dir_acc_post_open_min < threshold_1_open - 0.05` AND `q_disagreement_short_ema > q_disagreement_baseline + 0.10` simultaneously → suspected gradient hacking → force gate 1 closed for `LOCKOUT_EPOCHS` (structural constant; 2 epochs).

**Implementation:** `gradient_hack_lockout_remaining` (NEW ISV slot, integer epoch count). When zero, normal gate behavior. When >0, force `gate1_open_state = 0` and decrement at each epoch boundary.

This is the "smart toddler with circuit breaker" pattern: the model can't game the wire if doing so triggers a forced reset.

#### B.2.8 — α_grad rate limiter (ISV-driven adaptive smoothing)

The Schmitt-trigger hysteresis (B.2.4) introduces a sigmoid-argument discontinuity at the moment `gate1_open_state` flips between {0, 1}. The argument shifts by 0.06 (the hysteresis band). With `k_aux ≈ 10`, the sigmoid output jumps by ~0.15; with high `k_aux ≈ 20` (low-variance regime), the jump is ~0.4. That's a meaningful gradient discontinuity at a single step boundary.

**Mitigation:** exponentially-smoothed α_grad output with **ISV-driven adaptive β**:

```
α_grad_smoothed = β · α_grad_smoothed_prev + (1 − β) · α_grad_raw
```

Per `feedback_isv_for_adaptive_bounds`, β is signal-driven, not a hardcoded constant. The driver is the variance of `α_grad_raw` itself — the same Welford pattern used for k_aux/k_q in B.2.5:

```
β = clip(β_base + variance_alpha_raw_ema / variance_ref_alpha, [β_base, β_max])
```

- **Welford EMA variance** of `α_grad_raw` updates per-step (NEW ISV slot `ALPHA_GRAD_RAW_VARIANCE_EMA_INDEX`)
- Stable α_grad_raw (low variance) → β = β_base (light smoothing; preserves gate's directional intent)
- Volatile α_grad_raw (high variance from frequent state flips or noisy sigmoids) → β → β_max (heavy smoothing dampens jitter)
- At variance = 0 (sentinel/cold start), β = β_base
- At variance = variance_ref_alpha, β midway through range
- At variance >> variance_ref_alpha, β saturates at β_max

The kernels reading α_grad for backward gradient scaling (B.4 launch site) consume `α_grad_smoothed`, NOT `α_grad_raw`. Both emit to HEALTH_DIAG (and adaptive β too) so we can see the raw decision, the actuation, and the rate-limiter's response.

**Bounds (structural numerical-stability anchors):**
- `β_base = 0.5` (light smoothing baseline; ~2-step half-life when α_grad_raw is stable)
- `β_max = 0.95` (heavy smoothing; ~20-step half-life under volatile signal)
- `variance_ref_alpha = 0.01` (1% variance threshold for the adaptive transition)

**Adaptive β slot:** `BETA_RATE_LIMITER_ADAPTIVE_INDEX` (NEW) — current β value, written by `alpha_grad_compute_kernel`, read by HEALTH_DIAG only (the smoothing math uses β inline). Keeps the pearl pattern consistent (k_aux and k_q have their own ISV slots; β joins them).

**Per-fold reset:** `α_grad_smoothed` resets to 0 at fold boundary via state-reset-registry. Variance EMA resets to 0 (sentinel) → initial β = β_base. Fresh fold starts with closed gate and light smoothing; both adapt as signal emerges.

### B.3 — ISV slot allocations

13 new ISV slots required:

| Slot name | Purpose | Reset at fold? |
|---|---|---|
| `Q_DISAGREEMENT_SHORT_EMA_INDEX` | Fast EMA (α=0.3) of per-step disagreement (Hold/Flat-masked) | Yes (sentinel 0.5) |
| `Q_DISAGREEMENT_LONG_EMA_INDEX` | Slow EMA (α=0.05) — HEALTH_DIAG drift visibility only, NOT used as adaptive baseline | Yes (sentinel 0.5) |
| `K_AUX_ADAPTIVE_INDEX` | Current `k_aux` value | Yes (sentinel `k_base_aux`) |
| `K_Q_ADAPTIVE_INDEX` | Current `k_q` value | Yes (sentinel `k_base_q`) |
| `BETA_RATE_LIMITER_ADAPTIVE_INDEX` | Current β value for α_grad rate limiter | Yes (sentinel `β_base`) |
| `AUX_DIR_ACC_VARIANCE_EMA_INDEX` | Welford variance of aux_dir_acc | Yes (sentinel 0.0 → initial k = k_base) |
| `Q_DISAGREEMENT_VARIANCE_EMA_INDEX` | Welford variance of disagreement | Yes (sentinel 0.0 → initial k = k_base) |
| `ALPHA_GRAD_RAW_VARIANCE_EMA_INDEX` | Welford variance of α_grad_raw (drives adaptive β) | Yes (sentinel 0.0 → initial β = β_base) |
| `GATE1_OPEN_STATE_INDEX` | Schmitt-trigger persistent state | Yes (0 — closed) |
| `ALPHA_GRAD_RAW_INDEX` | Raw computed α_grad value (HEALTH_DIAG visibility) | Yes (0) |
| `ALPHA_GRAD_SMOOTHED_INDEX` | Rate-limited α_grad consumed by backward path (B.2.8) | Yes (0) |
| `AUX_DIR_ACC_POST_OPEN_MIN_INDEX` | Anti-gradient-hacking detection | Yes (1.0 — no min observed) |
| `GRADIENT_HACK_LOCKOUT_REMAINING_INDEX` | Circuit-breaker epoch counter | Yes (0 — no lockout) |

**Baseline for Gate 2 is structural 0.5** — not an ISV slot. The K=2-aux ↔ K=4-Q-with-Hold/Flat-masked random alignment at random init is 0.5; this is the analytic baseline (when Q picks Short or Long uniformly, half the time it agrees with aux's binary prediction). Long-EMA-as-baseline was rejected (feedback loop risk; see B.2.3).

All slots register in `state_reset_registry.rs` with appropriate sentinels following the existing pattern (lines 913-922 for the two pre-existing aux_dir_acc EMAs are the canonical template). Slot indices to be allocated in `sp14_isv_slots.rs` (NEW file; mirrors `sp13_isv_slots.rs` pattern).

### B.4 — Kernels

Three new GPU kernels:

1. **`q_disagreement_update_kernel`** — per-step compute K=3↔K=2 mapped argmax mismatch (B.2.3 logic, with Flat masked) from `Q_dir_logits` and `aux_nb_softmax_buf`, EMA update for short (α=0.3) and long (α=0.05), Welford variance update. Block tree-reduce per `feedback_no_atomicadd`; tracks both numerator (sum of disagreements) and denominator (count of non-Flat picks) separately, then divides at the end. ~100 LOC.
2. **`alpha_grad_compute_kernel`** — read all driver signals, evaluate Schmitt-trigger logic for Gate 1 (B.2.4), compute adaptive `k_aux` and `k_q` from variance EMAs (B.2.5), evaluate both sigmoids, multiply with warmup gate (B.2.6), write `ALPHA_GRAD_RAW_INDEX`. Then update Welford variance of α_grad_raw → compute adaptive `β` (B.2.8) → apply rate-limiter `α_grad_smoothed = β · α_grad_smoothed_prev + (1−β) · α_grad_raw`, write `ALPHA_GRAD_SMOOTHED_INDEX` and `BETA_RATE_LIMITER_ADAPTIVE_INDEX`. Three Welford variance EMAs total (aux_dir_acc, q_disagreement, α_grad_raw); each is associative + parallelizable (block tree-reduce). ~100 LOC.
3. **`gradient_hack_detect_kernel`** — read aux_dir_acc, gate1_open_state, q_disagreement, decide lockout state, write to `GRADIENT_HACK_LOCKOUT_REMAINING_INDEX` and force-clear `GATE1_OPEN_STATE_INDEX` if triggered. ~40 LOC.

Plus modifications to:
- **Direction Q-head forward kernel** (or new pre-SGEMM concat kernel mirroring `mag_concat_qdir` precedent at `experience_kernels.cu:4560`): in_dim+1, read `softmax_diff = aux_nb_softmax_buf[b, 1] − aux_nb_softmax_buf[b, 0]` per-bar. **Launch order: aux forward must complete before this concat reads `aux_nb_softmax_buf`** (B.1 forward launch order constraint). ~50-100 LOC.
- **Direction Q-head backward gradient scaling**: scale `dL/dx[wire_col]` (the **input** gradient flowing back to aux head's softmax) by `α_grad_smoothed`. The Q-head's **weight** gradient `dL/dW` for the new column is NOT scaled — Q-head learns to use the wire freely; α_grad only gates gradient flow to aux. ~30 LOC.
- **`compute_param_sizes()`** and **`layout_fingerprint_seed()`**: bump the direction Q-head's first FC weight tensor (`w_b0fc` or equivalent — implementer to identify exact index) input dim by 1; corresponding layout-fingerprint entry renamed (e.g., `PARAM_W_DIR_FC1 → PARAM_W_DIR_FC1_AUX`) to force fingerprint hash bump per Invariant 8. ~10 LOC.
- **Trainer construction**: zero-initialize the new weight column. ~5 LOC.

### B.5 — HEALTH_DIAG observability

New per-epoch `pearl_egf_diag` line:
```
pearl_egf_diag α_smoothed=0.42 α_raw=0.45 β=0.62 gate1=0.92 gate2=0.49
              k_aux=15.3 k_q=8.7 var_aux=0.008 var_q=0.024 var_alpha=0.018
              q_dis_short=0.523 q_dis_long=0.498
              gate1_state=open aux_post_open_min=0.55 lockout_remaining=0
              warmup=1.0
```

All three adaptive scalars emit (`α_smoothed`, `α_raw`, `β`, `k_aux`, `k_q`) plus their driving variances (`var_aux`, `var_q`, `var_alpha`) so the chain from variance → steepness/smoothing → output is fully observable. `q_dis_long` is included even though it's not used as baseline — it's there to track whether disagreement is drifting (training-side directional alignment improving over folds). Without these emit fields, diagnosing α_grad behavior in the smoke logs is forensic-only.

### B.6 — Hard rules upheld

- `feedback_no_partial_refactor` — wire (forward) and pearl (backward) land atomically. Cannot ship one without the other.
- `feedback_no_atomicadd` — all three new kernels use block tree-reduce. Welford variance specifically benefits from this pattern (Welford's online algorithm is associative and parallelizable).
- `feedback_cpu_is_read_only` — all signals computed GPU-side. CPU only reads ISV for HEALTH_DIAG emit.
- `feedback_no_stubs` — every new ISV slot has a real producer and a real consumer in this commit. No zero-stub.
- `feedback_no_hiding` — no `_` prefixes on unused params, no `#[allow]` to suppress warnings. Every new identifier serves its declared purpose.
- `feedback_no_htod_htoh_only_mapped_pinned` — all 11 new ISV slots live in the existing mapped-pinned ISV buffer (SP4 infrastructure). HEALTH_DIAG reads via the same mapped-pinned snapshot path used by all other ISV consumers; no new HtoD or HtoH copies.
- `feedback_isv_for_adaptive_bounds` — adaptive bounds (k_aux, k_q from variance) live in ISV. Numerical-stability anchors (k_min, k_base_aux, k_base_q, variance_ref, hysteresis ±0.03 band, lockout epochs, smoothing β) are structural constants per Invariant 1.
- `feedback_trust_code_not_docs` — implementer verifies all line numbers and tensor indices against current code at HEAD `eaf4adcb9` before editing.
- `feedback_no_legacy_aliases` — fingerprint entry rename has no `_DEPRECATED` shim.
- `feedback_kill_runs_on_anomaly_quickly` — explicit kill criteria enumerated in B.7 below; smoke A2 monitor terminates the workflow at first signal of pathological behavior.
- `pearl_first_observation_bootstrap` — Pearl-A bootstrap applied to all new EMAs (q_disagreement short/long, variances, post_open_min).
- `pearl_bounded_modifier_outputs_require_structural_activation` — softmax IS the structural activation; α_grad's sigmoid composition produces structurally-bounded `[0, 1]`.
- `pearl_zscore_normalization_for_magnitude_asymmetric_signals` — variance-driven k adjustment is the appropriate analog.
- `pearl_canary_input_freshness_launch_order` — direction Q-head's input concat reads aux_nb_softmax_buf; aux forward MUST launch before direction Q-head forward in graph capture, otherwise stale-input bug manifests as fold-1-wire-works-fold-2-doesn't pattern.
- `pearl_event_driven_reward_density_alignment` — N/A; this is a backward-pass gradient gate, not a per-step reward shaping.
- `feedback_no_quickfixes` — the pearl is the canonical answer to "stop-gradient vs full-gradient" tradeoff, not a hack to make Q-head train.

### B.7 — Validation criteria

**Smoke A2 (5-epoch L40S, post-SP14) — pass criteria:**
- `α_grad_smoothed` and `α_grad_raw` both emit per epoch in HEALTH_DIAG; observable across all folds
- `gate1_open_state` opens (=1) at least once during fold 1 (when aux_dir_acc reaches 0.55-0.60)
- `q_disagreement_short_ema` non-zero and varying across epochs (at random init, expect ~0.5; with training, expect drift)
- `aux_dir_acc` reaches target (0.55) in fold 1 — confirms aux head still trains on its label even with the new wire
- `val_dir_dist` remains balanced (Hold-pricing still works)
- `GRAD_CLIP_OUTLIER` count < 100 (down from 1109 — assumes A's C51 floor lands first)
- No new pathological behaviors: aux_dir_acc doesn't drop catastrophically; circuit breaker `lockout_remaining` stays at 0 in normal operation

**Smoke A2 — explicit KILL criteria** (per `feedback_kill_runs_on_anomaly_quickly`; smoke monitor terminates the workflow at first signal):

| # | Kill trigger | Mechanism implication |
|---|---|---|
| 1 | Workflow phase = Failed / Error | Already done; report + investigate |
| 2 | Build/compile failure (CUDA error, missing symbol, link error) | Wire infrastructure broken |
| 3 | `α_grad_smoothed` stuck at 0.0 across 5 epochs in fold 1 (when aux_dir_acc demonstrably reaches 0.55+) | Wire backward path not reaching the gate; either backward order wrong or scaling factor not applied |
| 4 | `gate1_open_state` never flips to 1 across full smoke despite aux_dir_acc above threshold_1_open=0.58 | Schmitt-trigger logic broken or state slot not persisting |
| 5 | `aux_dir_acc` collapses below 0.40 after gate1 opens AND `lockout_remaining` stays at 0 | Gradient hacking happening; circuit breaker not firing — bug in `gradient_hack_detect_kernel` |
| 6 | `val_sharpe` < 0 sustained across 2+ epochs | Full-stack regression; the wire is destabilizing training |
| 7 | `n_up = 0` in HEALTH_DIAG aux_b1_diag (regression of B1.1b producer) | Producer broke during SP14 implementation; should have been caught by unit tests but kill-on-detect is the safety |
| 8 | Forward stale-input pattern: `aux_dir_acc` healthy but `α_grad` oscillates wildly with no apparent driver | Launch order constraint violated — direction Q-head reading previous-step softmax (canary pearl signature) |

**30-epoch validation A (post-Smoke-A2-pass):**
- `val_win_rate` rises above 0.50 by ep 20-25 — confirms the user's hypothesis that aux signal can move WR when wired
- If WR stays at 45-48% across 30 epochs even with α_grad demonstrably opening: hypothesis falsified at this layer; next intervention is on the aux signal richness (sub-project C — adaptive LR, OR longer prediction horizons, OR microstructure feature additions)
- `val_sharpe` recovers toward B1.0 baseline (~30) or stabilizes positive — assesses whether the new wire contention with Q-loss is destabilizing

**Multi-seed validation (post-30-epoch-pass):**
- 3-seed × 30-epoch reproducibility check
- Validates the WR climb is signal, not seed-luck

### B.8 — Implementation cost

| Component | LOC |
|---|---|
| 13 new ISV slots + constants in `sp14_isv_slots.rs` | ~95 |
| `q_disagreement_update_kernel` (NEW; K=4↔K=2 mapping with Hold/Flat masking) | ~100 |
| `alpha_grad_compute_kernel` (NEW; Schmitt + adaptive k + adaptive β rate limiter + 3 Welford variances) | ~100 |
| `gradient_hack_detect_kernel` (NEW) | ~40 |
| Direction Q-head forward modification (in_dim+1, concat reading aux_nb_softmax_buf) | ~100 |
| Direction Q-head backward gradient scaling (dL/dx scaling, NOT dL/dW) | ~30 |
| `compute_param_sizes` + `layout_fingerprint_seed` updates | ~20 |
| `state_reset_registry` entries (11 new) | ~50 |
| HEALTH_DIAG emit (pearl_egf_diag line) | ~30 |
| Trainer construction (zero-init new weight column) | ~10 |
| Three orchestrator launchers (kernel launches + launch-order constraint enforcement) | ~80 |
| Audit doc SP14 section | ~80 |
| **Production code total** | **~700** |
| GPU oracle unit tests (12-15 tests covering each gate, hysteresis, rate limiter, K=3↔K=2 mapping) | ~450 |
| **Grand total** | **~1150** |

3-5 days of focused work for a fresh implementer with clear brief. Comparable in scope to SP13 B1.1a (which was 175 tool uses for the implementer agent — at the upper bound of what fits in a single session).

**Decomposition recommendation:** B should land as a single atomic commit per `feedback_no_partial_refactor`. The forward wire and the backward pearl are coupled (gating a non-existent wire is meaningless; an ungated wire bypasses the safety design). However, the spec writer (this doc) anticipates that the implementer may need to decompose internally into sub-commits the way B1 split into B1.0/B1.1a/B1.1b — the structure of "the wire alone gives degraded but valid behavior" is similar.

## Sub-project C — Adaptive LR (deferred)

Not in scope for this spec. Addressed only if post-SP14 30-epoch shows WR stuck at 45-48% despite `α_grad` opening and `gate1_open_state` showing healthy behavior. In that case, the next lever is per-loss-group LR (per `pearl_adam_normalizes_loss_weights` — the lever that escapes Adam normalization). Spec'd separately if needed.

## Open questions for implementer

These are decisions the implementer should make by reading current code (not pre-decided in this spec):

1. **Forward concat strategy** — fused concat-SGEMM kernel vs. separate concat-then-SGEMM vs. extending an existing concat kernel like `mag_concat_qdir`. Implementer to evaluate compute cost vs. code clarity.
2. **Argmax source for q_disagreement** — Thompson selector output buffer vs. recomputing argmax inline in `q_disagreement_update_kernel`. Implementer to choose the cleaner integration with existing action-selection path.
3. **Per-bar α vs batch-mean α** — spec ships batch-mean for v1. If profiling shows the gradient column scaling has high variance per-row, per-row α may be needed. Implementer flags this if observable.
4. **Sub-decomposition** — if implementer agent capacity is at risk, may split into "wire only with stop-gradient hardcoded" + "pearl on top" as B1.1a/B1.1b style. Decide based on dispatch session size.

## References

- **Smoke A:** `smoke-test-qfxjw` on commit `6657e5626`, 22m 59s, 3-fold × 5-epoch
- **Three-agent diagnostic findings:** in conversation 2026-05-05 between Smoke A completion and this spec
- **Memory pearls referenced:**
  - `pearl_first_observation_bootstrap` — sentinel = 0; first observation replaces directly
  - `pearl_zscore_normalization_for_magnitude_asymmetric_signals` — variance-driven adaptive bounds
  - `pearl_bounded_modifier_outputs_require_structural_activation` — sigmoid/softmax as structural bounds
  - `pearl_event_driven_reward_density_alignment` — ruled out; backward-pass gating is not reward shaping
  - `pearl_adam_normalizes_loss_weights` — drives the C deferral decision
  - `feedback_isv_for_adaptive_bounds` — adaptive in ISV, structural anchors as constants
  - `feedback_no_partial_refactor` — atomic commit constraint
- **Plan reference:** `docs/superpowers/plans/2026-05-04-sp13-redefine-success-for-predictive-skill.md` §B.T3 — the deferred Q-head input concat sketch (this spec implements that, with the EGF pearl as new addition)
- **Prior SP13 chain commits:** `f934ea171` (P0a) → `bdc5cb8bb` (P0b) → `62ab8ed85` (B0) → `6a869ad36` (B0.1) → `75e94858c` (B1.0) → `7d10ea8b3` (B1.1a) → `6657e5626` (B1.1b)

## Next step

Spec self-review (placeholder scan, internal consistency, ambiguity check), then user review. After user approval, invoke `superpowers:writing-plans` to produce the implementation plan for sub-project B (and a smaller plan for sub-project A if A is to land first).
