Task 1: ISV scratch scalars (piggyback on existing kernels) Task 2: isv_signal_update GPU kernel + temporal history Task 3: ISV encoder MLP + weight tensors 68→78 Task 4: Drift-conditioned gamma (C51 scalar→per-sample buffer) Task 5: Branch confidence routing (replaces regime_branch_gate) Task 6: Risk branch wider input SH2→SH2+9 Task 7: Recursive confidence head (predict own TD-error) Task 8: Target network ISV online-only Task 9: Smoke test + compute-sanitizer verification (0 errors) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
51 KiB
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:
// ── 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:
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):
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:
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:
/* 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):
.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:
// 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):
// 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:
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
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
- Step 8: Commit
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_updatekernel
In crates/ml/src/cuda_pipeline/experience_kernels.cu, at the end (before the final closing comment), add:
/* ================================================================== */
/* 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:
// ── 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:
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):
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
/// 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:
// 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
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
- Step 9: Commit
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_forwardkernel
In experience_kernels.cu, add:
/* ================================================================== */
/* 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_sizesto 78 tensors
In gpu_dqn_trainer.rs, change:
pub(crate) const NUM_WEIGHT_TENSORS: usize = 78;
In compute_param_sizes(), after the risk branch entries (indices 64-67), add:
// ── 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_dimsinxavier_init_params_buf
Find the fan_dims array (around line 8709). Extend it with 10 new entries:
// 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:
// ── 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:
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_forwardkernel + add launch method
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:
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(¶m_sizes, 68);
let b_fc1 = self.ptrs.params_ptr + padded_byte_offset(¶m_sizes, 69);
let w_fc2 = self.ptrs.params_ptr + padded_byte_offset(¶m_sizes, 70);
let b_fc2 = self.ptrs.params_ptr + padded_byte_offset(¶m_sizes, 71);
let w_gate = self.ptrs.params_ptr + padded_byte_offset(¶m_sizes, 72);
let b_gate = self.ptrs.params_ptr + padded_byte_offset(¶m_sizes, 73);
let w_gamma = self.ptrs.params_ptr + padded_byte_offset(¶m_sizes, 74);
let b_gamma = self.ptrs.params_ptr + padded_byte_offset(¶m_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):
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
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
- Step 8: Commit
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_bufkernel
In experience_kernels.cu:
/* ================================================================== */
/* 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:
float gamma,
To:
const float* __restrict__ gamma_buf, /* [B] per-sample effective gamma */
Change line 334 from:
float gamma_eff = gamma;
To:
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_lossin Rust
In gpu_dqn_trainer.rs, in launch_c51_loss() (line 7850), change:
From:
let gamma = self.adaptive_gamma.powi(self.config.n_steps as i32);
.arg(&gamma)
To:
let gamma_buf_ptr = self.gamma_buf.raw_ptr();
.arg(&gamma_buf_ptr)
- Step 4: Add
fill_gamma_buflaunch before C51 loss
Add method:
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):
self.fill_gamma_buf()?;
- Step 5: Load fill_gamma_buf kernel
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
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
- Step 7: Commit
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_routingkernel
In experience_kernels.cu:
/* ================================================================== */
/* 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_gatewithapply_branch_confidence_routing
In gpu_dqn_trainer.rs, replace the apply_regime_gate method (line 1974-2013) with:
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_gatecall sites
In reduce_current_q_stats() (line 6626), change:
self.apply_regime_gate(batch_size)?;
to:
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:
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_forwardintoreduce_current_q_stats
Before the branch confidence routing call, add ISV forward:
// 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
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
- Step 7: Commit
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_forwardkernel
In experience_kernels.cu, replace the risk_budget_forward kernel (line 4692-4721):
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_backwardkernel
Replace risk_budget_backward (line 4750-4792):
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_sizesfor widerw_risk_fc
Change index [64] from:
cfg.adv_h * cfg.shared_h2, // [64] w_risk_fc [AH, SH2]
To:
cfg.adv_h * (cfg.shared_h2 + 9), // [64] w_risk_fc [AH, SH2+9]
Update the doc comment.
- Step 4: Update
risk_budget_forwardRust launch
In risk_budget_forward() (line 2017-2037), add the new ISV and predicted_error args:
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(¶m_sizes, 64);
let b_fc_ptr = self.ptrs.params_ptr + padded_byte_offset(¶m_sizes, 65);
let w_out_ptr = self.ptrs.params_ptr + padded_byte_offset(¶m_sizes, 66);
let b_out_ptr = self.ptrs.params_ptr + padded_byte_offset(¶m_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_sgdbackward 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_dimsfor 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
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
- Step 9: Commit
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_forwardkernel
In experience_kernels.cu:
/* ================================================================== */
/* 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_backwardkernel
/* ================================================================== */
/* 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():
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
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
- Step 5: Commit
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
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
- Step 4: Commit
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
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
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
SQLX_OFFLINE=true cargo test -p ml --lib 2>&1 | tail -5
Target: 899+ passed, 0 failed.
- Step 4: Commit
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_sizesupdate — Task 3 (ISV tensors) + Task 6 (w_risk_fc wider) - ✅
xavier_init_params_buffan_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.