# Custom Kernel Optimization — cuBLAS Projections + Batch-Parallel Scans

## Problem

nsys profiling reveals custom CUDA kernels consume 95% of GPU time. cuBLAS GEMMs
take <1%. The root cause: all custom kernels use a "one thread per weight, loop
over batch" anti-pattern that serializes B=8192 samples into each thread.

## nsys Breakdown (batch=8192, H100 PCIe)

| Kernel | Time% | ms/call | Calls | Pattern |
|--------|-------|---------|-------|---------|
| mamba2_scan_backward | 75.0% | 1567 | 25 | 1 thread/weight, loop B×K×SH2 |
| q_denoise_backward | 7.3% | 152 | 25 | 1 thread/weight, loop B |
| iqn_bias_grad_reduce | 3.9% | 16 | 130 | 1 thread/neuron, loop B×Q |
| kan_grad_reduce | 3.2% | 8 | 208 | 1 thread/param, loop B |
| curiosity_fwd_bwd_per_block | 2.1% | 1085 | 1 | per-block reduction |
| mamba2_temporal_scan | 1.1% | 22 | 26 | 1 thread/sample (OK), inner matmul SH2 |
| risk_budget_forward | 1.0% | 20 | 26 | 1 thread/sample, inner matmul SH2 |

## Design

### 1. Mamba2 Scan — cuBLAS Projections + Lightweight Scan (75% → <0.5%)

**Current:** One thread per weight element (12,288 threads), each loops over all
8192 samples. Each iteration does inner products over SH2=256 dimensions. Total:
12,288 × 8192 × 8 × 256 = 206 billion operations on 12,288 threads.

**Reference:** Official Mamba CUDA kernel (state-spaces/mamba) uses
`grid=(batch, dim)` — one block per (sample, feature). Uses `cub::BlockScan` for
sequential dependency. Our kernel uses the exact opposite pattern.

Source: https://github.com/state-spaces/mamba/blob/main/csrc/selective_scan/selective_scan_bwd_kernel.cuh

**New (Approach C — cuBLAS projections + lightweight scan):**

#### Forward (`mamba2_temporal_scan`):

Step 1: Pre-project all timesteps with cuBLAS (3 GEMMs):
```
A_vals[B×K, STATE_D] = h_history[B×K, SH2] @ W_A[SH2, STATE_D]  // m=16, n=B×K, k=256
B_vals[B×K, STATE_D] = h_history[B×K, SH2] @ W_B[SH2, STATE_D]
C_vals[B, STATE_D]   = h_s2[B, SH2] @ W_C[SH2, STATE_D]
```

Step 2: Lightweight scan kernel — grid=(B, ceil(STATE_D/32)), block=(32):
```c
// Each thread handles one (sample, state_dim) pair
// K=8 sequential steps — trivial, no inner matmul
int b = blockIdx.x;
int s = blockIdx.y * blockDim.x + threadIdx.x;
float x = 0.0f;
for (int t = 0; t < K; t++) {
    float gate = sigmoid(A_vals[b*K*D + t*D + s]);
    x = gate * x + B_vals[b*K*D + t*D + s];
}
// temporal_context = C_vals[b,s] * x
float context_s = C_vals[b*D + s] * x;
// Accumulate into h_enriched via atomicAdd or shared memory reduce
```

Step 3: Apply temporal context to h_enriched (element-wise kernel or cuBLAS).

#### Backward (`mamba2_scan_backward`):

Step 1: Pre-project (same 3 cuBLAS GEMMs as forward — replay activations).

Step 2: Lightweight reverse scan kernel — grid=(B, STATE_D):
```c
// Forward replay + reverse scan for d_x
// Each thread: one (sample, state_dim) pair
// K=8 steps forward, K=8 steps reverse — 16 iterations, no inner matmul
```

Step 3: Weight gradient accumulation via cuBLAS:
```
d_W_A[SH2, STATE_D] = h_history^T[SH2, B×K] @ d_gate[B×K, STATE_D]  // one GEMM
d_W_B[SH2, STATE_D] = h_history^T[SH2, B×K] @ d_x[B×K, STATE_D]    // one GEMM
d_W_C[SH2, STATE_D] = h_s2^T[SH2, B] @ d_context[B, STATE_D]       // one GEMM
```

Total: 6 cuBLAS GEMMs + 2 lightweight scan kernels.
Expected: 1567ms + 22ms → ~5ms total.

### 2. Q-Denoise Backward (7.3% → <0.1%)

**Current:** 2-layer MLP backward (12→24→12), one thread per weight (1800 params),
loops over B=8192.

**New:** Replace with 2× cuBLAS backward GEMMs + 2× bias_grad_reduce.
Same as the main DQN backward pass. Dimensions are tiny (24×24, 12×24) but
batch=8192 makes it a proper GEMM.

### 3. IQN Bias Grad Reduce (3.9% → <0.2%)

**Current:** 130 calls × 16ms. Each reduces [B×Q, out_dim] over batch. One thread
per output neuron, loops over B×Q=524,288. With out_dim=3, that's 3 threads doing
524K iterations each.

**New:** 2-phase warp reduction:
- Phase 1: grid=(ceil(B×Q/256), out_dim), block=256. Each block sums 256 elements.
- Phase 2: reduce block partials → final result.
Or use `cub::DeviceSegmentedReduce` for segmented reduction.

### 4. KAN Grad Reduce (3.2% → <0.1%)

Same pattern as IQN — one thread per param, loop over batch. Fix: batch-parallel
reduction with warp shuffle.

### 5. Curiosity Fwd+Bwd (2.1% → <0.1%)

**Current:** 1 instance × 1085ms. Check grid dimensions — likely too few blocks.

**New:** 2× cuBLAS GEMMs for the curiosity MLP (already cuBLAS for inference, 
but training kernel is custom). Replace with cuBLAS forward + backward + 
element-wise loss.

### 6. Risk Budget Forward (1.0% → <0.01%)

**Current:** One thread per sample, inner matmul loop over SH2=256.

**New:** 2× cuBLAS GEMMs. Concat [h_s2; ISV; pred_error] → GEMM1 → ReLU → GEMM2 → sigmoid.
Same pattern as the main DQN forward pass.

## Scratch Buffer Requirements

Mamba2 pre-projection needs:
- `A_vals[B×K, STATE_D]` = 8192 × 8 × 16 × 4 = 4MB
- `B_vals[B×K, STATE_D]` = 4MB
- `C_vals[B, STATE_D]` = 0.5MB
- `d_gate[B×K, STATE_D]` = 4MB (backward only)
- `d_x[B×K, STATE_D]` = 4MB (backward only)

Total: ~17MB additional VRAM. Negligible on H100.

## Success Criteria

- Step time: 1950ms → <50ms
- mamba2_scan_backward: 1567ms → <3ms
- All custom kernels use batch-parallel patterns (no per-weight batch loops)
- 899 unit tests + 20 smoke tests pass
- Training loss and grad_norm match within 5% of current values

## Files Modified

- `crates/ml/src/cuda_pipeline/experience_kernels.cu` — mamba2_scan_backward, q_denoise_backward, kan_grad_reduce, risk_budget_forward
- `crates/ml/src/cuda_pipeline/mamba2_temporal_kernel.cu` — mamba2_temporal_scan
- `crates/ml/src/cuda_pipeline/iqn_dual_head_kernel.cu` — iqn_bias_grad_reduce
- `crates/ml/src/cuda_pipeline/curiosity_training_kernel.cu` — curiosity_fwd_bwd_per_block
- `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs` — new cuBLAS GEMM descriptors for Mamba2 projections, denoise, risk_budget
- `crates/ml/src/trainers/dqn/fused_training.rs` — updated capture for new kernel topology
