# Introspective State Vector (ISV) 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.

**Goal:** Replace external Q-drift penalty with a learnable self-awareness module (ISV) that feeds 8 training dynamics signals into the model as features — enabling context-dependent adaptive responses to drift, uncertainty, and gradient pressure.

**Architecture:** 8 pinned scalars updated by a pure-GPU kernel (`isv_signal_update`), encoded by a small MLP (8→16→8), producing branch gating + drift-conditioned gamma + risk branch conditioning. Includes recursive confidence head and branch confidence routing. 10 new weight tensors (68→78), ~582 new params. All GPU, zero CPU.

**Tech Stack:** Rust 1.85, CUDA 12.4, cudarc, NVRTC cubins via build.rs

---

## Task 1: ISV Scratch Scalars — Piggyback on Existing Kernels

Add 4 pinned device-mapped scalars written by existing GPU kernels. These become data sources for `isv_signal_update`.

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`
- Modify: `crates/ml/src/cuda_pipeline/c51_loss_kernel.cu`
- Modify: `crates/ml/src/cuda_pipeline/experience_kernels.cu`

- [ ] **Step 1: Add 4 pinned scalar fields to GpuDqnTrainer**

In `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`, find the struct fields around line 1297 (after `risk_adam_step`). Add:

```rust
    // ── ISV scratch scalars (pinned device-mapped, written by GPU kernels) ──
    td_error_scratch_pinned: *mut f32,
    td_error_scratch_dev_ptr: u64,
    ensemble_var_scratch_pinned: *mut f32,
    ensemble_var_scratch_dev_ptr: u64,
    reward_scratch_pinned: *mut f32,
    reward_scratch_dev_ptr: u64,
    atom_util_scratch_pinned: *mut f32,
    atom_util_scratch_dev_ptr: u64,
```

- [ ] **Step 2: Allocate pinned memory in constructor**

Find the pinned memory allocation block (around line 4920, near `homeostatic_targets_pinned`). Add 4 allocations using the same pattern:

```rust
        let (td_error_scratch_pinned, td_error_scratch_dev_ptr) = {
            let mut ptr: *mut u8 = std::ptr::null_mut();
            let mut dp: u64 = 0;
            unsafe {
                let rc = cudarc::driver::sys::cuMemAllocHost_v2(
                    &mut ptr as *mut *mut u8, std::mem::size_of::<f32>());
                assert_eq!(rc, cudarc::driver::sys::cudaError_enum::CUDA_SUCCESS,
                    "cuMemAllocHost for td_error_scratch");
                *(ptr as *mut f32) = 0.0;
                let rc2 = cudarc::driver::sys::cuMemHostGetDevicePointer_v2(
                    &mut dp as *mut u64, ptr.cast(), 0);
                assert_eq!(rc2, cudarc::driver::sys::cudaError_enum::CUDA_SUCCESS,
                    "cuMemHostGetDevicePointer for td_error_scratch");
            }
            (ptr as *mut f32, dp)
        };
        let (ensemble_var_scratch_pinned, ensemble_var_scratch_dev_ptr) = {
            let mut ptr: *mut u8 = std::ptr::null_mut();
            let mut dp: u64 = 0;
            unsafe {
                let rc = cudarc::driver::sys::cuMemAllocHost_v2(
                    &mut ptr as *mut *mut u8, std::mem::size_of::<f32>());
                assert_eq!(rc, cudarc::driver::sys::cudaError_enum::CUDA_SUCCESS,
                    "cuMemAllocHost for ensemble_var_scratch");
                *(ptr as *mut f32) = 0.0;
                let rc2 = cudarc::driver::sys::cuMemHostGetDevicePointer_v2(
                    &mut dp as *mut u64, ptr.cast(), 0);
                assert_eq!(rc2, cudarc::driver::sys::cudaError_enum::CUDA_SUCCESS,
                    "cuMemHostGetDevicePointer for ensemble_var_scratch");
            }
            (ptr as *mut f32, dp)
        };
        let (reward_scratch_pinned, reward_scratch_dev_ptr) = {
            let mut ptr: *mut u8 = std::ptr::null_mut();
            let mut dp: u64 = 0;
            unsafe {
                let rc = cudarc::driver::sys::cuMemAllocHost_v2(
                    &mut ptr as *mut *mut u8, std::mem::size_of::<f32>());
                assert_eq!(rc, cudarc::driver::sys::cudaError_enum::CUDA_SUCCESS,
                    "cuMemAllocHost for reward_scratch");
                *(ptr as *mut f32) = 0.0;
                let rc2 = cudarc::driver::sys::cuMemHostGetDevicePointer_v2(
                    &mut dp as *mut u64, ptr.cast(), 0);
                assert_eq!(rc2, cudarc::driver::sys::cudaError_enum::CUDA_SUCCESS,
                    "cuMemHostGetDevicePointer for reward_scratch");
            }
            (ptr as *mut f32, dp)
        };
        let (atom_util_scratch_pinned, atom_util_scratch_dev_ptr) = {
            let mut ptr: *mut u8 = std::ptr::null_mut();
            let mut dp: u64 = 0;
            unsafe {
                let rc = cudarc::driver::sys::cuMemAllocHost_v2(
                    &mut ptr as *mut *mut u8, std::mem::size_of::<f32>());
                assert_eq!(rc, cudarc::driver::sys::cudaError_enum::CUDA_SUCCESS,
                    "cuMemAllocHost for atom_util_scratch");
                *(ptr as *mut f32) = 0.0;
                let rc2 = cudarc::driver::sys::cuMemHostGetDevicePointer_v2(
                    &mut dp as *mut u64, ptr.cast(), 0);
                assert_eq!(rc2, cudarc::driver::sys::cudaError_enum::CUDA_SUCCESS,
                    "cuMemHostGetDevicePointer for atom_util_scratch");
            }
            (ptr as *mut f32, dp)
        };
```

Wire into struct init (around line 5342):

```rust
            td_error_scratch_pinned,
            td_error_scratch_dev_ptr,
            ensemble_var_scratch_pinned,
            ensemble_var_scratch_dev_ptr,
            reward_scratch_pinned,
            reward_scratch_dev_ptr,
            atom_util_scratch_pinned,
            atom_util_scratch_dev_ptr,
```

- [ ] **Step 3: Add td_error_scratch write to C51 loss kernel**

In `crates/ml/src/cuda_pipeline/c51_loss_kernel.cu`, the kernel signature (around line 196) already has `float* __restrict__ total_loss` as an atomicAdd accumulator. Add a new parameter after `cvar_alpha_buf`:

```cuda
    float* __restrict__ td_error_scratch       /* [1] pinned — batch mean |TD-error| */
```

At the end of the kernel (just before the closing `}`), one thread writes the batch mean:

```cuda
    /* ISV: write batch-mean |TD-error| to pinned scratch (single writer, thread 0) */
    __shared__ float td_err_sum;
    if (threadIdx.x == 0) td_err_sum = 0.0f;
    __syncthreads();
    atomicAdd(&td_err_sum, fabsf(per_sample_td));
    __syncthreads();
    if (threadIdx.x == 0 && blockIdx.x == 0) {
        /* Approximate: only block 0's contribution. Good enough for EMA. */
        td_error_scratch[0] = td_err_sum / fminf((float)BLOCK_THREADS, (float)batch_size);
    }
```

- [ ] **Step 4: Pass td_error_scratch_dev_ptr in launch_c51_loss**

In `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`, in `launch_c51_loss()` (line 7864), add the new arg after the `cvar_alpha_buf` arg (line 7926):

```rust
                .arg(&self.td_error_scratch_dev_ptr)
```

- [ ] **Step 5: Add atom_util_scratch write to q_stats path**

In `reduce_current_q_stats()` (line 6555), after `self.update_q_mean_ema()` (line 6689), the atom utilization is already read from `host[5]`. Write it to the pinned scalar:

```rust
        // ISV: write atom utilization to pinned scratch for GPU-side ISV signal update
        unsafe { *self.atom_util_scratch_pinned = host[5].clamp(0.0, 1.0); }
```

Similarly, write the batch-mean reward from `host[4]` (which is the mean Q-value used as a reward proxy):

```rust
        // ISV: write reward proxy to pinned scratch
        unsafe { *self.reward_scratch_pinned = host[0]; } // host[0] = Q-mean = reward proxy
```

- [ ] **Step 6: Free pinned memory in Drop**

In the `Drop` impl (around line 1539), add:

```rust
        if !self.td_error_scratch_pinned.is_null() {
            let _ = unsafe { cudarc::driver::result::free_host(self.td_error_scratch_pinned.cast()) };
        }
        if !self.ensemble_var_scratch_pinned.is_null() {
            let _ = unsafe { cudarc::driver::result::free_host(self.ensemble_var_scratch_pinned.cast()) };
        }
        if !self.reward_scratch_pinned.is_null() {
            let _ = unsafe { cudarc::driver::result::free_host(self.reward_scratch_pinned.cast()) };
        }
        if !self.atom_util_scratch_pinned.is_null() {
            let _ = unsafe { cudarc::driver::result::free_host(self.atom_util_scratch_pinned.cast()) };
        }
```

- [ ] **Step 7: Verify compilation**

```bash
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
```

- [ ] **Step 8: Commit**

```bash
cd /home/jgrusewski/Work/foxhunt && git add crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs crates/ml/src/cuda_pipeline/c51_loss_kernel.cu crates/ml/src/cuda_pipeline/experience_kernels.cu && git commit -m "feat(isv): add 4 pinned scratch scalars for ISV signal sources

td_error_scratch (C51 loss mean |TD|), ensemble_var_scratch,
reward_scratch, atom_util_scratch — all pinned device-mapped.
C51 loss kernel writes td_error_scratch via block-0 atomicAdd.
Pure GPU writes, zero CPU involvement in hot path.

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

---

## Task 2: ISV Signal Buffer + `isv_signal_update` GPU Kernel

Create the ISV signal vector [8], temporal history [K=4, 8], and the GPU kernel that computes all ISV signals from existing pinned pointers.

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/experience_kernels.cu`
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`

- [ ] **Step 1: Write `isv_signal_update` kernel**

In `crates/ml/src/cuda_pipeline/experience_kernels.cu`, at the end (before the final closing comment), add:

```cuda
/* ================================================================== */
/* Kernel: isv_signal_update — pure GPU ISV signal computation        */
/* ================================================================== */

/**
 * Single-thread kernel that reads GPU-accessible pinned scalars,
 * computes EMA updates for 8 ISV signals, and shifts the temporal
 * history buffer. Runs after graph_adam replay, before next forward.
 *
 * Launch config: grid=(1,1,1), block=(1,1,1).
 */
extern "C" __global__ void isv_signal_update(
    float* __restrict__ isv_signals,          /* [8] pinned device-mapped */
    float* __restrict__ isv_history,          /* [K*8] pinned device-mapped */
    float* __restrict__ lagged_td_error,      /* [1] pinned — for recursive confidence target */
    const float* __restrict__ q_mean_ptr,     /* [1] pinned — batch Q-mean */
    const float* __restrict__ q_mean_ema_ptr, /* [1] pinned — Q-mean EMA */
    const float* __restrict__ grad_norm_ptr,  /* [1] pinned — gradient norm² */
    const float* __restrict__ td_error_ptr,   /* [1] pinned — batch mean |TD-error| */
    const float* __restrict__ ens_var_ptr,    /* [1] pinned — batch mean ensemble var */
    const float* __restrict__ reward_ptr,     /* [1] pinned — reward proxy */
    const float* __restrict__ atom_util_ptr,  /* [1] pinned — atom utilization */
    const float* __restrict__ loss_ptr,       /* [1] pinned — total loss */
    int K                                     /* temporal history length (4) */
) {
    if (threadIdx.x != 0) return;

    float alpha = 0.05f;

    /* Save current td_error_ema as lagged target for recursive confidence */
    lagged_td_error[0] = isv_signals[2];

    /* Shift temporal history: newest at [0], oldest at [K-1] */
    for (int t = K - 1; t > 0; t--)
        for (int k = 0; k < 8; k++)
            isv_history[t * 8 + k] = isv_history[(t - 1) * 8 + k];
    for (int k = 0; k < 8; k++)
        isv_history[k] = isv_signals[k];

    /* [0] Q-drift: normalized deviation from EMA */
    float q_mean = q_mean_ptr[0];
    float q_ema  = q_mean_ema_ptr[0];
    isv_signals[0] = (q_mean - q_ema) / fmaxf(fabsf(q_ema), 0.1f);

    /* [1] Gradient norm EMA (ptr stores norm², take sqrt) */
    float gn = sqrtf(fmaxf(grad_norm_ptr[0], 0.0f));
    isv_signals[1] = (1.0f - alpha) * isv_signals[1] + alpha * gn;

    /* [2] TD-error EMA */
    isv_signals[2] = (1.0f - alpha) * isv_signals[2] + alpha * td_error_ptr[0];

    /* [3] Ensemble variance EMA (save old for delta) */
    float old_ens_ema = isv_signals[3];
    isv_signals[3] = (1.0f - alpha) * old_ens_ema + alpha * ens_var_ptr[0];

    /* [4] Ensemble variance velocity */
    isv_signals[4] = (isv_signals[3] - old_ens_ema) / fmaxf(old_ens_ema, 1e-6f);

    /* [5] Reward EMA */
    isv_signals[5] = (1.0f - alpha) * isv_signals[5] + alpha * reward_ptr[0];

    /* [6] Atom utilization EMA */
    isv_signals[6] = (1.0f - alpha) * isv_signals[6] + alpha * atom_util_ptr[0];

    /* [7] Loss EMA */
    isv_signals[7] = (1.0f - alpha) * isv_signals[7] + alpha * loss_ptr[0];
}
```

- [ ] **Step 2: Add ISV pinned buffers + kernel field to GpuDqnTrainer**

In the struct fields (after the scratch scalars from Task 1), add:

```rust
    // ── ISV core buffers (pinned device-mapped, GPU read/write) ──
    isv_signals_pinned: *mut f32,        // [8]
    isv_signals_dev_ptr: u64,
    isv_history_pinned: *mut f32,        // [ISV_K * 8] = [32]
    isv_history_dev_ptr: u64,
    isv_decay_pinned: *mut f32,          // [8] learned decay weights
    isv_decay_dev_ptr: u64,
    lagged_td_error_pinned: *mut f32,    // [1] recursive confidence target
    lagged_td_error_dev_ptr: u64,
    isv_signal_update_kernel: CudaFunction,
```

Add constant:

```rust
const ISV_K: usize = 4;  // Temporal ISV history length
const ISV_DIM: usize = 8; // ISV signal count
```

- [ ] **Step 3: Allocate ISV pinned memory**

In the constructor, allocate all 4 ISV pinned buffers using the same `cuMemAllocHost` + `cuMemHostGetDevicePointer` pattern. `isv_signals` needs 8 floats (32 bytes), `isv_history` needs 32 floats (128 bytes), `isv_decay` needs 8 floats (32 bytes), `lagged_td_error` needs 1 float (4 bytes). Initialize all to 0.0. Initialize `isv_decay` to 0.0 (sigmoid(0)=0.5 moderate decay).

- [ ] **Step 4: Load isv_signal_update kernel**

In constructor, near where other experience_kernels functions are loaded (around line 4016):

```rust
        let isv_signal_update_kernel = exp_module_for_mag.load_function("isv_signal_update")
            .map_err(|e| MLError::ModelError(format!("isv_signal_update load: {e}")))?;
```

- [ ] **Step 5: Add `update_isv_signals()` method**

```rust
    /// Pure GPU ISV signal update — reads all pinned pointers, updates ISV [8] + history [K,8].
    /// Call after graph_adam replay, before next graph_forward.
    pub(crate) fn update_isv_signals(&self) -> Result<(), MLError> {
        let k = ISV_K as i32;
        unsafe {
            self.stream.launch_builder(&self.isv_signal_update_kernel)
                .arg(&self.isv_signals_dev_ptr)
                .arg(&self.isv_history_dev_ptr)
                .arg(&self.lagged_td_error_dev_ptr)
                .arg(&self.q_mean_scratch_dev_ptr)
                .arg(&self.q_mean_ema_dev_ptr)
                .arg(&self.grad_norm_dev_ptr)
                .arg(&self.td_error_scratch_dev_ptr)
                .arg(&self.ensemble_var_scratch_dev_ptr)
                .arg(&self.reward_scratch_dev_ptr)
                .arg(&self.atom_util_scratch_dev_ptr)
                .arg(&self.total_loss_dev_ptr)
                .arg(&k)
                .launch(LaunchConfig {
                    grid_dim: (1, 1, 1),
                    block_dim: (1, 1, 1),
                    shared_mem_bytes: 0,
                })
                .map_err(|e| MLError::ModelError(format!("isv_signal_update: {e}")))?;
        }
        Ok(())
    }
```

- [ ] **Step 6: Wire into training loop**

In `reduce_current_q_stats()` (around line 6689, after `self.update_q_mean_ema()`), add:

```rust
        // ISV signal update — pure GPU, reads all pinned scalars, updates ISV vector
        self.update_isv_signals()?;
```

- [ ] **Step 7: Free ISV pinned memory in Drop**

Add to `Drop` impl for all 4 ISV pinned buffers.

- [ ] **Step 8: Verify compilation**

```bash
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
```

- [ ] **Step 9: Commit**

```bash
cd /home/jgrusewski/Work/foxhunt && git add crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs crates/ml/src/cuda_pipeline/experience_kernels.cu && git commit -m "feat(isv): isv_signal_update GPU kernel — pure GPU ISV signal computation

8 ISV signals (q_drift, grad_norm_ema, td_error_ema, ensemble_var_ema,
ensemble_var_delta, reward_ema, atom_util, loss_ema) computed by
single-thread GPU kernel. Temporal history [K=4, 8] with ring buffer.
Lagged TD-error for recursive confidence target. Zero CPU involvement.

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

---

## Task 3: ISV Encoder + Weight Tensors (68→78)

Add the ISV encoder MLP kernel, branch gating output, gamma modulation output, and extend `compute_param_sizes` for 10 new weight tensors.

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/experience_kernels.cu`
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`

- [ ] **Step 1: Write `isv_forward` kernel**

In `experience_kernels.cu`, add:

```cuda
/* ================================================================== */
/* Kernel: isv_forward — ISV encoder MLP + branch gate + gamma mod    */
/* ================================================================== */

/**
 * Single-thread kernel: encodes ISV signals through temporal decay-weighted
 * average + 2-layer MLP (8→16→8), outputs branch gate [4] and gamma mod [1].
 * All outputs are single-instance (not per-sample) — downstream kernels
 * broadcast via pointer.
 *
 * Launch config: grid=(1,1,1), block=(1,1,1).
 */
extern "C" __global__ void isv_forward(
    const float* __restrict__ isv_signals,    /* [8] pinned */
    const float* __restrict__ isv_history,    /* [K*8] pinned */
    const float* __restrict__ isv_decay,      /* [8] pinned learned decay */
    const float* __restrict__ w_fc1,          /* [16, 8] */
    const float* __restrict__ b_fc1,          /* [16] */
    const float* __restrict__ w_fc2,          /* [8, 16] */
    const float* __restrict__ b_fc2,          /* [8] */
    const float* __restrict__ w_gate,         /* [4, 8] */
    const float* __restrict__ b_gate,         /* [4] */
    const float* __restrict__ w_gamma,        /* [8] (flat) */
    const float* __restrict__ b_gamma,        /* [1] */
    float* __restrict__ isv_embedding_out,    /* [8] */
    float* __restrict__ branch_gate_out,      /* [4] */
    float* __restrict__ gamma_mod_out,        /* [1] */
    int K
) {
    if (threadIdx.x != 0) return;

    /* Step 1: Temporal ISV — decay-weighted average */
    float isv_temporal[8];
    for (int k = 0; k < 8; k++) {
        float decay = 1.0f / (1.0f + expf(-isv_decay[k]));
        float weighted_sum = isv_signals[k];
        float weight_sum = 1.0f;
        for (int t = 0; t < K; t++) {
            float w = powf(decay, (float)(t + 1));
            weighted_sum += w * isv_history[t * 8 + k];
            weight_sum += w;
        }
        isv_temporal[k] = weighted_sum / weight_sum;
    }

    /* Step 2: MLP — FC1 (8→16, ReLU) → FC2 (16→8) */
    float hidden[16];
    for (int j = 0; j < 16; j++) {
        float val = b_fc1[j];
        for (int k = 0; k < 8; k++)
            val += w_fc1[j * 8 + k] * isv_temporal[k];
        hidden[j] = fmaxf(val, 0.0f);
    }
    float embedding[8];
    for (int k = 0; k < 8; k++) {
        float val = b_fc2[k];
        for (int j = 0; j < 16; j++)
            val += w_fc2[k * 16 + j] * hidden[j];
        embedding[k] = val;
        isv_embedding_out[k] = val;
    }

    /* Step 3: Branch gating — softmax(W_gate @ embedding + b_gate) */
    float logits[4];
    float max_logit = -1e9f;
    for (int d = 0; d < 4; d++) {
        float val = b_gate[d];
        for (int k = 0; k < 8; k++)
            val += w_gate[d * 8 + k] * embedding[k];
        logits[d] = val;
        max_logit = fmaxf(max_logit, logits[d]);
    }
    float sum_exp = 0.0f;
    for (int d = 0; d < 4; d++) {
        logits[d] = expf(logits[d] - max_logit);
        sum_exp += logits[d];
    }
    for (int d = 0; d < 4; d++)
        branch_gate_out[d] = logits[d] / sum_exp;

    /* Step 4: Drift-conditioned gamma — 0.5 + 0.5*sigmoid(w@emb+b) → [0.5, 1.0] */
    float gamma_raw = b_gamma[0];
    for (int k = 0; k < 8; k++)
        gamma_raw += w_gamma[k] * embedding[k];
    gamma_mod_out[0] = 0.5f + 0.5f / (1.0f + expf(-gamma_raw));
}
```

- [ ] **Step 2: Extend `compute_param_sizes` to 78 tensors**

In `gpu_dqn_trainer.rs`, change:

```rust
pub(crate) const NUM_WEIGHT_TENSORS: usize = 78;
```

In `compute_param_sizes()`, after the risk branch entries (indices 64-67), add:

```rust
        // ── Introspective State Vector (ISV) encoder + conditioning ──
        16 * ISV_DIM,                                   // [68] w_isv_fc1 [16, 8]
        16,                                              // [69] b_isv_fc1 [16]
        ISV_DIM * 16,                                   // [70] w_isv_fc2 [8, 16]
        ISV_DIM,                                         // [71] b_isv_fc2 [8]
        4 * ISV_DIM,                                    // [72] w_isv_gate [4, 8]
        4,                                               // [73] b_isv_gate [4]
        ISV_DIM,                                         // [74] w_isv_gamma [8]
        1,                                               // [75] b_isv_gamma [1]
        cfg.shared_h2,                                   // [76] w_conf_fc [SH2]
        1,                                               // [77] b_conf_fc [1]
```

Update the doc comment to document indices 68-77.

- [ ] **Step 3: Update `fan_dims` in `xavier_init_params_buf`**

Find the `fan_dims` array (around line 8709). Extend it with 10 new entries:

```rust
            // ISV encoder
            (16, ISV_DIM),           // [68] w_isv_fc1
            (1, 16),                 // [69] b_isv_fc1
            (ISV_DIM, 16),           // [70] w_isv_fc2
            (1, ISV_DIM),            // [71] b_isv_fc2
            (4, ISV_DIM),            // [72] w_isv_gate
            (1, 4),                  // [73] b_isv_gate
            (1, ISV_DIM),            // [74] w_isv_gamma
            (1, 1),                  // [75] b_isv_gamma
            (1, cfg.shared_h2),      // [76] w_conf_fc
            (1, 1),                  // [77] b_conf_fc
```

- [ ] **Step 4: Allocate ISV output buffers**

Add device buffers for ISV forward outputs:

```rust
    // ── ISV forward outputs ──
    isv_embedding_buf: CudaSlice<f32>,   // [8]
    branch_gate_buf: CudaSlice<f32>,     // [4]
    gamma_mod_buf: CudaSlice<f32>,       // [1]
    gamma_buf: CudaSlice<f32>,           // [B] per-sample effective gamma
    predicted_error_buf: CudaSlice<f32>, // [B] recursive confidence output
```

Allocate in constructor:

```rust
        let isv_embedding_buf = stream.alloc_zeros::<f32>(ISV_DIM)
            .map_err(|e| MLError::ModelError(format!("isv_embedding_buf alloc: {e}")))?;
        let branch_gate_buf = stream.alloc_zeros::<f32>(4)
            .map_err(|e| MLError::ModelError(format!("branch_gate_buf alloc: {e}")))?;
        let gamma_mod_buf = stream.alloc_zeros::<f32>(1)
            .map_err(|e| MLError::ModelError(format!("gamma_mod_buf alloc: {e}")))?;
        let gamma_buf = stream.alloc_zeros::<f32>(b)
            .map_err(|e| MLError::ModelError(format!("gamma_buf alloc: {e}")))?;
        let predicted_error_buf = stream.alloc_zeros::<f32>(b)
            .map_err(|e| MLError::ModelError(format!("predicted_error_buf alloc: {e}")))?;
```

- [ ] **Step 5: Load `isv_forward` kernel + add launch method**

```rust
        let isv_forward_kernel = exp_module_for_mag.load_function("isv_forward")
            .map_err(|e| MLError::ModelError(format!("isv_forward load: {e}")))?;
```

Add the field and launch method:

```rust
    pub(crate) fn launch_isv_forward(&self) -> Result<(), MLError> {
        let param_sizes = compute_param_sizes(&self.config);
        let w_fc1 = self.ptrs.params_ptr + padded_byte_offset(&param_sizes, 68);
        let b_fc1 = self.ptrs.params_ptr + padded_byte_offset(&param_sizes, 69);
        let w_fc2 = self.ptrs.params_ptr + padded_byte_offset(&param_sizes, 70);
        let b_fc2 = self.ptrs.params_ptr + padded_byte_offset(&param_sizes, 71);
        let w_gate = self.ptrs.params_ptr + padded_byte_offset(&param_sizes, 72);
        let b_gate = self.ptrs.params_ptr + padded_byte_offset(&param_sizes, 73);
        let w_gamma = self.ptrs.params_ptr + padded_byte_offset(&param_sizes, 74);
        let b_gamma = self.ptrs.params_ptr + padded_byte_offset(&param_sizes, 75);
        let k = ISV_K as i32;

        unsafe {
            self.stream.launch_builder(&self.isv_forward_kernel)
                .arg(&self.isv_signals_dev_ptr)
                .arg(&self.isv_history_dev_ptr)
                .arg(&self.isv_decay_dev_ptr)
                .arg(&w_fc1).arg(&b_fc1)
                .arg(&w_fc2).arg(&b_fc2)
                .arg(&w_gate).arg(&b_gate)
                .arg(&w_gamma).arg(&b_gamma)
                .arg(&self.isv_embedding_buf)
                .arg(&self.branch_gate_buf)
                .arg(&self.gamma_mod_buf)
                .arg(&k)
                .launch(LaunchConfig {
                    grid_dim: (1, 1, 1),
                    block_dim: (1, 1, 1),
                    shared_mem_bytes: 0,
                })
                .map_err(|e| MLError::ModelError(format!("isv_forward: {e}")))?;
        }
        Ok(())
    }
```

- [ ] **Step 6: Invalidate CUDA graphs**

After changing NUM_WEIGHT_TENSORS, all graphs must be recaptured. In `reset_model_state()` (around line 1543):

```rust
        self.graph_forward = None;
        self.graph_forward_ddqn = None;
        self.graph_adam = None;
```

This is already there — just verify it's still correct after the param buffer resize.

- [ ] **Step 7: Verify compilation**

```bash
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
```

- [ ] **Step 8: Commit**

```bash
cd /home/jgrusewski/Work/foxhunt && git add crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs crates/ml/src/cuda_pipeline/experience_kernels.cu && git commit -m "feat(isv): ISV encoder MLP + branch gating + gamma modulation

isv_forward kernel: temporal decay-weighted average + 8→16→8 MLP.
Outputs: isv_embedding [8], branch_gate [4] (softmax), gamma_mod [1]
(0.5+0.5*sigmoid → [0.5,1.0]). NUM_WEIGHT_TENSORS: 68→78.
10 new tensors: w/b_isv_fc1, w/b_isv_fc2, w/b_isv_gate,
w/b_isv_gamma, w/b_conf_fc. Single-thread, ~50 FLOPs.

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

---

## Task 4: Drift-Conditioned Gamma — C51 Loss Kernel Change

Change `c51_loss_kernel` from scalar `float gamma` to per-sample `float* gamma_buf`. Add `fill_gamma_buf` kernel.

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/c51_loss_kernel.cu`
- Modify: `crates/ml/src/cuda_pipeline/experience_kernels.cu`
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`

- [ ] **Step 1: Add `fill_gamma_buf` kernel**

In `experience_kernels.cu`:

```cuda
/* ================================================================== */
/* Kernel: fill_gamma_buf — broadcast scalar gamma to per-sample buf  */
/* ================================================================== */
extern "C" __global__ void fill_gamma_buf(
    float* __restrict__ gamma_buf,          /* [B] output */
    float base_gamma,
    const float* __restrict__ gamma_mod,    /* [1] from isv_forward (0.5-1.0) */
    int B
) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i >= B) return;
    gamma_buf[i] = base_gamma * gamma_mod[0];
}
```

- [ ] **Step 2: Change C51 loss kernel signature**

In `c51_loss_kernel.cu`, change line 202:

From:
```cuda
    float gamma,
```

To:
```cuda
    const float* __restrict__ gamma_buf,   /* [B] per-sample effective gamma */
```

Change line 334 from:
```cuda
    float gamma_eff = gamma;
```

To:
```cuda
    float gamma_eff = gamma_buf[sample_id];
```

Also update `block_bellman_project_f` (line 113) — it receives gamma as a parameter from the main kernel. Trace the caller: the main kernel calls it with the `gamma_eff` value, which now reads from `gamma_buf`. No change needed to the helper — only the main kernel's `gamma_eff` initialization changes.

- [ ] **Step 3: Update `launch_c51_loss` in Rust**

In `gpu_dqn_trainer.rs`, in `launch_c51_loss()` (line 7850), change:

From:
```rust
        let gamma = self.adaptive_gamma.powi(self.config.n_steps as i32);
```
```rust
                .arg(&gamma)
```

To:
```rust
        let gamma_buf_ptr = self.gamma_buf.raw_ptr();
```
```rust
                .arg(&gamma_buf_ptr)
```

- [ ] **Step 4: Add `fill_gamma_buf` launch before C51 loss**

Add method:

```rust
    pub(crate) fn fill_gamma_buf(&self) -> Result<(), MLError> {
        let base_gamma = self.adaptive_gamma.powi(self.config.n_steps as i32);
        let blocks = ((self.config.batch_size as u32 + 255) / 256).max(1);
        let b = self.config.batch_size as i32;
        unsafe {
            self.stream.launch_builder(&self.fill_gamma_buf_kernel)
                .arg(&self.gamma_buf)
                .arg(&base_gamma)
                .arg(&self.gamma_mod_buf)
                .arg(&b)
                .launch(LaunchConfig {
                    grid_dim: (blocks, 1, 1),
                    block_dim: (256, 1, 1),
                    shared_mem_bytes: 0,
                })
                .map_err(|e| MLError::ModelError(format!("fill_gamma_buf: {e}")))?;
        }
        Ok(())
    }
```

Wire into `submit_forward_ops_main()`, just before `self.launch_c51_loss()` (line 7224):

```rust
        self.fill_gamma_buf()?;
```

- [ ] **Step 5: Load fill_gamma_buf kernel**

```rust
        let fill_gamma_buf_kernel = exp_module_for_mag.load_function("fill_gamma_buf")
            .map_err(|e| MLError::ModelError(format!("fill_gamma_buf load: {e}")))?;
```

- [ ] **Step 6: Verify compilation**

```bash
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
```

- [ ] **Step 7: Commit**

```bash
cd /home/jgrusewski/Work/foxhunt && git add crates/ml/src/cuda_pipeline/c51_loss_kernel.cu crates/ml/src/cuda_pipeline/experience_kernels.cu crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs && git commit -m "feat(isv): drift-conditioned gamma — C51 scalar→per-sample buffer

c51_loss_kernel: float gamma → const float* gamma_buf [B].
fill_gamma_buf kernel: broadcasts base_gamma * gamma_mod[0] to [B].
gamma_mod from isv_forward → [0.5, 1.0] → effective_gamma in
[base_gamma*0.5, base_gamma]. Model learns its own planning horizon.

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

---

## Task 5: Branch Confidence Routing — Replace `regime_branch_gate`

Replace the regime gate with ISV-aware branch confidence routing.

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/experience_kernels.cu`
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`

- [ ] **Step 1: Write `branch_confidence_routing` kernel**

In `experience_kernels.cu`:

```cuda
/* ================================================================== */
/* Kernel: branch_confidence_routing — ISV gate × Q-value confidence  */
/* ================================================================== */

/**
 * Replaces regime_branch_gate. Combines ISV importance gating (from
 * isv_forward) with per-branch Q-value separation confidence.
 * Market regime (ADX, CUSUM) still flows through trunk → h_s2 → branches.
 *
 * Grid: ceil(B/256), Block: 256. One thread per sample.
 */
extern "C" __global__ void branch_confidence_routing(
    float* __restrict__ q_values,           /* [B, total_actions] in-place */
    const float* __restrict__ branch_gate,  /* [4] shared from isv_forward */
    int B,
    int b0_size, int b1_size, int b2_size, int b3_size
) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i >= B) return;

    int branch_sizes[4] = { b0_size, b1_size, b2_size, b3_size };
    int total_actions = b0_size + b1_size + b2_size + b3_size;
    int branch_offset = 0;

    for (int d = 0; d < 4; d++) {
        int A_d = branch_sizes[d];

        float q_max = -1e9f;
        float q_sum = 0.0f;
        for (int a = 0; a < A_d; a++) {
            float q = q_values[(long long)i * total_actions + branch_offset + a];
            q_max = fmaxf(q_max, q);
            q_sum += q;
        }
        float q_mean_branch = q_sum / (float)A_d;
        float separation = q_max - q_mean_branch;
        float confidence = 1.0f / (1.0f + expf(-5.0f * separation));

        float isv_gate = branch_gate[d];
        float effective_weight = isv_gate * fmaxf(confidence, 0.3f);

        for (int a = 0; a < A_d; a++) {
            q_values[(long long)i * total_actions + branch_offset + a] *= effective_weight;
        }
        branch_offset += A_d;
    }
}
```

- [ ] **Step 2: Replace `apply_regime_gate` with `apply_branch_confidence_routing`**

In `gpu_dqn_trainer.rs`, replace the `apply_regime_gate` method (line 1974-2013) with:

```rust
    pub(crate) fn apply_branch_confidence_routing(&self, batch_size: usize) -> Result<(), MLError> {
        let blocks = ((batch_size as u32 + 255) / 256).max(1);
        let q_out_ptr = self.q_out_buf.raw_ptr();
        let gate_ptr = self.branch_gate_buf.raw_ptr();
        let b_i32 = batch_size as i32;
        let b0 = self.config.branch_0_size as i32;
        let b1 = self.config.branch_1_size as i32;
        let b2 = self.config.branch_2_size as i32;
        let b3 = self.config.branch_3_size as i32;

        unsafe {
            self.stream.launch_builder(&self.branch_confidence_routing_kernel)
                .arg(&q_out_ptr)
                .arg(&gate_ptr)
                .arg(&b_i32)
                .arg(&b0).arg(&b1).arg(&b2).arg(&b3)
                .launch(LaunchConfig {
                    grid_dim: (blocks, 1, 1),
                    block_dim: (256, 1, 1),
                    shared_mem_bytes: 0,
                })
                .map_err(|e| MLError::ModelError(format!("branch_confidence_routing: {e}")))?;
        }
        Ok(())
    }
```

- [ ] **Step 3: Replace all `apply_regime_gate` call sites**

In `reduce_current_q_stats()` (line 6626), change:

```rust
        self.apply_regime_gate(batch_size)?;
```

to:

```rust
        self.apply_branch_confidence_routing(batch_size)?;
```

Search for any other call sites — the field is also referenced in `submit_forward_ops_ddqn`. Replace all occurrences.

- [ ] **Step 4: Replace kernel field**

Change `regime_gate_kernel: CudaFunction` to `branch_confidence_routing_kernel: CudaFunction`. Update the load:

```rust
        let branch_confidence_routing_kernel = exp_module_for_mag.load_function("branch_confidence_routing")
            .map_err(|e| MLError::ModelError(format!("branch_confidence_routing load: {e}")))?;
```

Keep loading `regime_branch_gate` under the new field name if needed for backward compatibility, or just replace entirely. The `regime_branch_gate` kernel code in `experience_kernels.cu` can stay (dead code removal is separate).

- [ ] **Step 5: Wire `launch_isv_forward` into `reduce_current_q_stats`**

Before the branch confidence routing call, add ISV forward:

```rust
        // ISV forward: encoder MLP → branch gate + gamma mod
        self.launch_isv_forward()?;
```

This should go after `self.mamba2_step(batch_size)?;` and before `self.risk_budget_forward(batch_size)?;`.

- [ ] **Step 6: Verify compilation**

```bash
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
```

- [ ] **Step 7: Commit**

```bash
cd /home/jgrusewski/Work/foxhunt && git add crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs crates/ml/src/cuda_pipeline/experience_kernels.cu && git commit -m "feat(isv): branch confidence routing replaces regime_branch_gate

ISV gate [4] × Q-value separation confidence per branch.
Market regime (ADX, CUSUM) still flows through trunk → branches.
ISV adds training dynamics awareness on top.
Confidence = sigmoid(5 * (Q_max - Q_mean)) per branch, floor 0.3.

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

---

## Task 6: Risk Branch Wider Input (SH2 → SH2+9)

Widen `risk_budget_forward` and `risk_budget_backward` to accept ISV signals + predicted_error.

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/experience_kernels.cu`
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`

- [ ] **Step 1: Update `risk_budget_forward` kernel**

In `experience_kernels.cu`, replace the `risk_budget_forward` kernel (line 4692-4721):

```cuda
extern "C" __global__ void risk_budget_forward(
    const float* __restrict__ h_s2,
    const float* __restrict__ isv_signals_dev_ptr,  /* [8] pinned — raw ISV */
    const float* __restrict__ predicted_error,      /* [B] recursive confidence output */
    const float* __restrict__ w_risk_fc,
    const float* __restrict__ b_risk_fc,
    const float* __restrict__ w_risk_out,
    const float* __restrict__ b_risk_out,
    float* __restrict__ risk_hidden,
    float* __restrict__ risk_budget_out,
    int B, int SH2, int AH
) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i >= B) return;

    int input_dim = SH2 + 9;  /* SH2 + 8 ISV + 1 predicted_error */
    const float* h = h_s2 + (long long)i * SH2;
    float* h_risk = risk_hidden + (long long)i * AH;

    for (int j = 0; j < AH; j++) {
        float val = b_risk_fc[j];
        for (int k = 0; k < SH2; k++)
            val += w_risk_fc[(long long)j * input_dim + k] * h[k];
        for (int k = 0; k < 8; k++)
            val += w_risk_fc[(long long)j * input_dim + SH2 + k] * isv_signals_dev_ptr[k];
        val += w_risk_fc[(long long)j * input_dim + SH2 + 8] * predicted_error[i];
        h_risk[j] = fmaxf(val, 0.0f);
    }

    float raw = b_risk_out[0];
    for (int j = 0; j < AH; j++)
        raw += w_risk_out[j] * h_risk[j];
    risk_budget_out[i] = 1.0f / (1.0f + expf(-raw));
}
```

- [ ] **Step 2: Update `risk_budget_backward` kernel**

Replace `risk_budget_backward` (line 4750-4792):

```cuda
extern "C" __global__ void risk_budget_backward(
    const float* __restrict__ h_s2,
    const float* __restrict__ isv_signals_dev_ptr,  /* [8] NEW */
    const float* __restrict__ predicted_error,      /* [B] NEW */
    const float* __restrict__ risk_hidden,
    const float* __restrict__ risk_budget,
    const float* __restrict__ d_q_mag,
    const float* __restrict__ q_mag_pre,
    const float* __restrict__ w_risk_out,
    float* __restrict__ d_w_risk_fc,
    float* __restrict__ d_b_risk_fc,
    float* __restrict__ d_w_risk_out,
    float* __restrict__ d_b_risk_out,
    int B, int SH2, int AH, int b1_size
) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i >= B) return;

    int input_dim = SH2 + 9;
    float R = risk_budget[i];

    float d_R = 0.0f;
    const float* dq = d_q_mag + (long long)i * b1_size;
    const float* qp = q_mag_pre + (long long)i * b1_size;
    if (b1_size > 1) d_R += dq[1] * qp[1] * 0.5f / fmaxf(sqrtf(fmaxf(R, 1e-6f)), 1e-6f);
    if (b1_size > 2) d_R += dq[2] * qp[2];

    float d_raw = d_R * R * (1.0f - R);

    const float* h_risk = risk_hidden + (long long)i * AH;
    atomicAdd(d_b_risk_out, d_raw);
    for (int j = 0; j < AH; j++) {
        atomicAdd(&d_w_risk_out[j], d_raw * h_risk[j]);
    }

    const float* h = h_s2 + (long long)i * SH2;
    for (int j = 0; j < AH; j++) {
        float d_hj = d_raw * w_risk_out[j];
        if (h_risk[j] <= 0.0f) d_hj = 0.0f;

        atomicAdd(&d_b_risk_fc[j], d_hj);
        /* h_s2 columns */
        for (int k = 0; k < SH2; k++)
            atomicAdd(&d_w_risk_fc[(long long)j * input_dim + k], d_hj * h[k]);
        /* ISV columns (broadcast — same for all samples) */
        for (int k = 0; k < 8; k++)
            atomicAdd(&d_w_risk_fc[(long long)j * input_dim + SH2 + k], d_hj * isv_signals_dev_ptr[k]);
        /* predicted_error column */
        atomicAdd(&d_w_risk_fc[(long long)j * input_dim + SH2 + 8], d_hj * predicted_error[i]);
    }
}
```

- [ ] **Step 3: Update `compute_param_sizes` for wider `w_risk_fc`**

Change index [64] from:

```rust
        cfg.adv_h * cfg.shared_h2,                      // [64] w_risk_fc [AH, SH2]
```

To:

```rust
        cfg.adv_h * (cfg.shared_h2 + 9),                // [64] w_risk_fc [AH, SH2+9]
```

Update the doc comment.

- [ ] **Step 4: Update `risk_budget_forward` Rust launch**

In `risk_budget_forward()` (line 2017-2037), add the new ISV and predicted_error args:

```rust
    pub(crate) fn risk_budget_forward(&self, batch_size: usize) -> Result<(), MLError> {
        let param_sizes = compute_param_sizes(&self.config);
        let w_fc_ptr = self.ptrs.params_ptr + padded_byte_offset(&param_sizes, 64);
        let b_fc_ptr = self.ptrs.params_ptr + padded_byte_offset(&param_sizes, 65);
        let w_out_ptr = self.ptrs.params_ptr + padded_byte_offset(&param_sizes, 66);
        let b_out_ptr = self.ptrs.params_ptr + padded_byte_offset(&param_sizes, 67);

        let blocks = ((batch_size as u32 + 255) / 256).max(1);
        let sh2 = self.config.shared_h2 as i32;
        let ah = self.config.adv_h as i32;
        let b_i32 = batch_size as i32;

        unsafe {
            self.stream.launch_builder(&self.risk_forward_kernel)
                .arg(&self.save_h_s2)  // h_s2
                .arg(&self.isv_signals_dev_ptr)  // ISV raw [8]
                .arg(&self.predicted_error_buf)  // predicted_error [B]
                .arg(&w_fc_ptr)
                .arg(&b_fc_ptr)
                .arg(&w_out_ptr)
                .arg(&b_out_ptr)
                .arg(&self.risk_hidden_buf)
                .arg(&self.risk_budget_buf)
                .arg(&b_i32)
                .arg(&sh2)
                .arg(&ah)
                .launch(LaunchConfig {
                    grid_dim: (blocks, 1, 1),
                    block_dim: (256, 1, 1),
                    shared_mem_bytes: 0,
                })
                .map_err(|e| MLError::ModelError(format!("risk_budget_forward: {e}")))?;
        }
        Ok(())
    }
```

- [ ] **Step 5: Update `step_risk_sgd` backward launch**

Find the `step_risk_sgd` method that calls `risk_budget_backward`. Update the kernel launch to pass the new ISV and predicted_error args (same pattern as forward).

- [ ] **Step 6: Reallocate `risk_grad_buf`**

The `risk_grad_buf` must accommodate the larger `w_risk_fc`. Find where it's allocated and update the size calculation to use `compute_param_sizes` with the new index [64] size.

- [ ] **Step 7: Update `fan_dims` for index [64]**

Change `fan_dims[64]` from `(cfg.adv_h, cfg.shared_h2)` to `(cfg.adv_h, cfg.shared_h2 + 9)`.

- [ ] **Step 8: Verify compilation**

```bash
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
```

- [ ] **Step 9: Commit**

```bash
cd /home/jgrusewski/Work/foxhunt && git add crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs crates/ml/src/cuda_pipeline/experience_kernels.cu && git commit -m "feat(isv): risk branch wider input SH2→SH2+9 — ISV + predicted_error

risk_budget_forward: h_s2 || isv_signals[8] || predicted_error[1].
risk_budget_backward: wider stride j*(SH2+9), grad for ISV+error cols.
w_risk_fc grows from AH*SH2 to AH*(SH2+9). risk_grad_buf reallocated.
risk_aligned_count and all offset callers updated atomically.

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

---

## Task 7: Recursive Confidence Head

Auxiliary head that predicts the model's own TD-error. Supervised by lagged target from `isv_signal_update`.

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/experience_kernels.cu`
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`

- [ ] **Step 1: Write `recursive_confidence_forward` kernel**

In `experience_kernels.cu`:

```cuda
/* ================================================================== */
/* Kernel: recursive_confidence_forward — predict own TD-error        */
/* ================================================================== */
extern "C" __global__ void recursive_confidence_forward(
    const float* __restrict__ h_s2,       /* [B, SH2] */
    const float* __restrict__ w_conf,     /* [SH2] (flat) */
    const float* __restrict__ b_conf,     /* [1] */
    float* __restrict__ predicted_error,  /* [B] */
    int B, int SH2
) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i >= B) return;

    const float* h = h_s2 + (long long)i * SH2;
    float val = b_conf[0];
    for (int k = 0; k < SH2; k++)
        val += w_conf[k] * h[k];
    predicted_error[i] = 1.0f / (1.0f + expf(-val));
}
```

- [ ] **Step 2: Write `recursive_confidence_backward` kernel**

```cuda
/* ================================================================== */
/* Kernel: recursive_confidence_backward — MSE grad into trunk        */
/* ================================================================== */
extern "C" __global__ void recursive_confidence_backward(
    const float* __restrict__ h_s2,             /* [B, SH2] */
    const float* __restrict__ predicted_error,  /* [B] */
    const float* __restrict__ lagged_td_error,  /* [1] pinned — target */
    const float* __restrict__ w_conf,           /* [SH2] */
    float* __restrict__ d_w_conf,               /* [SH2] gradient */
    float* __restrict__ d_b_conf,               /* [1] gradient */
    float* __restrict__ d_h_s2,                 /* [B, SH2] accumulated trunk grad */
    int B, int SH2,
    float loss_weight                            /* 0.01 */
) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i >= B) return;

    float pred = predicted_error[i];
    float target = lagged_td_error[0];
    float d_loss = loss_weight * 2.0f * (pred - target) / (float)B;
    float d_sigmoid = d_loss * pred * (1.0f - pred);

    const float* h = h_s2 + (long long)i * SH2;
    atomicAdd(d_b_conf, d_sigmoid);
    for (int k = 0; k < SH2; k++) {
        atomicAdd(&d_w_conf[k], d_sigmoid * h[k]);
        atomicAdd(&d_h_s2[(long long)i * SH2 + k], d_sigmoid * w_conf[k]);
    }
}
```

- [ ] **Step 3: Load kernels and add launch methods**

Load both kernels from `experience_kernels.cubin`. Add fields for the kernel functions. Add `launch_recursive_confidence_forward()` and `launch_recursive_confidence_backward()` methods.

The forward launch goes in `reduce_current_q_stats()` after `launch_isv_forward()` and before `risk_budget_forward()`:

```rust
        self.launch_recursive_confidence_forward(batch_size)?;
```

The backward launch goes in `step_risk_sgd()` or in the auxiliary gradient injection phase (between `graph_forward` and `graph_adam`).

- [ ] **Step 4: Verify compilation**

```bash
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
```

- [ ] **Step 5: Commit**

```bash
cd /home/jgrusewski/Work/foxhunt && git add crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs crates/ml/src/cuda_pipeline/experience_kernels.cu && git commit -m "feat(isv): recursive confidence head — model predicts own TD-error

h_s2 → sigmoid(w_conf @ h + b_conf) → predicted_error [B].
MSE loss vs lagged_td_error from isv_signal_update (0.01× weight).
Backward accumulates into trunk gradient (d_h_s2) for shared learning.
Creates self-improvement loop: model learns to predict when it's wrong.

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

---

## Task 8: Target Network — ISV Online-Only

Ensure ISV weights are NOT synced to target network. Target forward uses neutral defaults.

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`

- [ ] **Step 1: Verify target network EMA scope**

Find the target network EMA update (search for `target_params_buf` or `ema_kernel`). Verify the EMA only copies param indices 0..67 (or equivalently, the first N parameters excluding ISV). If the EMA copies ALL params, restrict it to indices 0..67.

The key is: the target `params_buf` should be sized for 68 tensors (old count), not 78. Or if it's sized for 78, the ISV weights (68-77) should be initialized to neutral values (zero weights, zero bias for gamma → 0.75 modifier, uniform gate).

- [ ] **Step 2: Ensure target forward uses base_gamma**

In the target network Q-value computation path, verify that the C51 loss kernel's target portion uses `base_gamma` directly (not the ISV-modulated gamma). The C51 loss kernel computes Bellman targets from the target network — these should use the stable base gamma.

Check `launch_c51_loss()` — the gamma_buf is filled for the online forward, but the target Bellman projection inside c51_loss uses a separate gamma path. Read the kernel carefully: the target projection `block_bellman_project_f` receives `gamma_eff` which comes from the online network's `gamma_buf`. This is correct — the Bellman target uses the online model's gamma (the model's own planning horizon choice affects what it learns).

If this causes instability, add a separate `target_gamma_buf` filled with `base_gamma`. For now, using online gamma is the intended design.

- [ ] **Step 3: Verify compilation**

```bash
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
```

- [ ] **Step 4: Commit**

```bash
cd /home/jgrusewski/Work/foxhunt && git add crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs && git commit -m "feat(isv): target network — ISV weights online-only

ISV encoder weights (68-77) not synced to target_params_buf.
Target forward uses neutral defaults: base_gamma, no ISV gating.
Online gamma (drift-conditioned) used for Bellman projection.

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

---

## Task 9: Smoke Test + Compute-Sanitizer Verification

**Files:**
- Modify: `crates/ml/src/trainers/dqn/smoke_tests/generalization.rs`

- [ ] **Step 1: Verify smoke test passes**

```bash
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml --lib -- test_generalization_components_smoke --include-ignored --nocapture 2>&1 | tail -20
```

If it fails, fix the root cause (likely missing kernel arg, buffer size mismatch, or graph recapture issue).

- [ ] **Step 2: Run compute-sanitizer**

```bash
FOXHUNT_TEST_DATA=test_data/futures-baseline compute-sanitizer --tool memcheck --print-limit 5 target/debug/deps/ml-* "test_generalization_components_smoke" --test-threads=1 --include-ignored 2>&1 | grep "ERROR SUMMARY"
```

**Target: 0 errors.**

If errors found: read the OOB report, identify the kernel and buffer, fix the allocation or indexing.

- [ ] **Step 3: Run full test suite**

```bash
SQLX_OFFLINE=true cargo test -p ml --lib 2>&1 | tail -5
```

**Target: 899+ passed, 0 failed.**

- [ ] **Step 4: Commit**

```bash
cd /home/jgrusewski/Work/foxhunt && git add -A && git commit -m "chore(isv): build verification — smoke test + compute-sanitizer 0 errors

ISV implementation complete: 8 signals, encoder MLP, branch confidence
routing, drift-conditioned gamma, risk branch ISV input, recursive
confidence head. All pure GPU, zero CPU. 582 new params, ~39KB VRAM.
compute-sanitizer: 0 errors. Smoke test passes. Full suite passes.

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

---

## Self-Review Checklist

**Spec coverage:**
- ✅ ISV signal vector [8] — Task 1 (scratch scalars) + Task 2 (isv_signal_update)
- ✅ ISV encoder MLP 8→16→8 — Task 3 (isv_forward kernel)
- ✅ Temporal ISV [K=4, 8] with decay — Task 2 (buffer) + Task 3 (isv_forward reads it)
- ✅ Branch gating (softmax [4]) — Task 3 (isv_forward) + Task 5 (branch_confidence_routing)
- ✅ Drift-conditioned gamma — Task 4 (c51_loss change + fill_gamma_buf)
- ✅ Risk branch wider input SH2→SH2+9 — Task 6
- ✅ Recursive confidence head — Task 7
- ✅ Target network ISV online-only — Task 8
- ✅ NUM_WEIGHT_TENSORS 68→78 — Task 3
- ✅ `compute_param_sizes` update — Task 3 (ISV tensors) + Task 6 (w_risk_fc wider)
- ✅ `xavier_init_params_buf` fan_dims — Task 3 + Task 6
- ✅ Compute-sanitizer 0 errors — Task 9
- ✅ Pure GPU, zero CPU — All kernels are GPU-only
- ✅ All pinned device-mapped — All new scalars use cuMemAllocHost + cuMemHostGetDevicePointer

**Placeholder scan:** No TBD, TODO, or "implement later" found.

**Type consistency:** `isv_signals_dev_ptr` used consistently in Tasks 2, 3, 5, 6, 7. `predicted_error_buf` used consistently in Tasks 3, 6, 7. `branch_gate_buf` used consistently in Tasks 3, 5. `gamma_mod_buf` used in Tasks 3, 4. `ISV_K=4`, `ISV_DIM=8` constants used everywhere.
