Files
foxhunt/docs/superpowers/plans/2026-04-19-kernel-optimization.md
2026-04-19 12:15:04 +02:00

356 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Custom Kernel Optimization 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:** Rewrite 7 custom CUDA kernels that consume 95% of GPU time — replace inner matmul loops with cuBLAS GEMMs, replace batch-serial patterns with batch-parallel grids. Target: 1950ms/step → <50ms/step.
**Architecture:** Split each "one-thread-per-weight-loops-over-batch" kernel into: (1) cuBLAS GEMMs for matrix projections (tensor cores), (2) lightweight element-wise/scan kernels for non-GEMM ops. The key insight from NVIDIA's official Mamba implementation: `grid=(batch, dim)` not `grid=(params)`.
**Tech Stack:** CUDA 13, cublasLtMatmul (TF32), cudarc (vendored), existing IqnGemmDesc pattern for cached GEMM descriptors.
**Dimensions:** SH2=256, STATE_D=16, K=8, batch=8192, IQN_Q=64, adv_h=128.
---
### Task 1: Mamba2 Forward — cuBLAS Projections + Lightweight Scan (1.1%)
**Files:**
- Modify: `crates/ml/src/cuda_pipeline/mamba2_temporal_kernel.cu`
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`
The forward scan currently does per-sample inner matmuls: `for j in 0..SH2: a_val += W_A[j,s] * h_t[j]`. This is a GEMM `[STATE_D, SH2] × [SH2, B]` that should use cuBLAS.
- [ ] **Step 1: Allocate scratch buffers for pre-projected values**
In `gpu_dqn_trainer.rs` constructor, allocate:
```rust
// Mamba2 pre-projection scratch: A_vals[B*K, STATE_D], B_vals[B*K, STATE_D]
let mamba2_a_proj = alloc_f32(&stream, batch_size * MAMBA2_HISTORY_K * MAMBA2_STATE_DIM, "mamba2_a_proj")?;
let mamba2_b_proj = alloc_f32(&stream, batch_size * MAMBA2_HISTORY_K * MAMBA2_STATE_DIM, "mamba2_b_proj")?;
let mamba2_c_proj = alloc_f32(&stream, batch_size * MAMBA2_STATE_DIM, "mamba2_c_proj")?;
```
- [ ] **Step 2: Create cuBLAS GEMM descriptors for Mamba2 projections**
Using the existing `create_cached_fwd_gemm_desc` pattern, create 3 GEMM descriptors:
```
// A_proj = W_A^T @ h_history_flat: m=STATE_D(16), n=B*K(65536), k=SH2(256)
// B_proj = W_B^T @ h_history_flat: same dims
// C_proj = W_C^T @ h_s2: m=STATE_D(16), n=B(8192), k=SH2(256)
```
Load these from a new CUmodule (graph-captured, must be isolated).
- [ ] **Step 3: Write lightweight scan kernel `mamba2_scan_projected`**
Replace `mamba2_temporal_scan` with a kernel that reads pre-projected values:
```c
extern "C" __global__ void mamba2_scan_projected(
const float* __restrict__ a_proj, // [B*K, STATE_D] pre-projected
const float* __restrict__ b_proj, // [B*K, STATE_D] pre-projected
const float* __restrict__ c_proj, // [B, STATE_D] pre-projected
const float* __restrict__ h_s2, // [B, SH2] current trunk
float* __restrict__ h_enriched, // [B, SH2] output
int B, int K, int sh2, int state_d,
const float* __restrict__ isv_signals,
const float* __restrict__ temporal_weight)
{
int b = blockIdx.x;
int j = blockIdx.y * blockDim.x + threadIdx.x;
if (b >= B || j >= sh2) return;
// Scan over K=8 timesteps — reads from pre-projected buffers
float context = 0.0f;
for (int s = 0; s < state_d; s++) {
float x = 0.0f;
for (int t = 0; t < K; t++) {
int idx = (b * K + t) * state_d + s;
float gate = 1.0f / (1.0f + expf(-a_proj[idx]));
if (isv_signals) gate *= isv_signals[11];
x = gate * x + b_proj[idx];
}
context += c_proj[b * state_d + s] * x;
}
// Apply temporal routing and residual
float h_val = h_s2[b * sh2 + j];
float weight = temporal_weight ? temporal_weight[j] : 1.0f;
float blended = h_val + weight * context * (1.0f / (float)state_d);
h_enriched[b * sh2 + j] = blended;
}
```
Grid: `(B, ceil(SH2/256))`, Block: `(256)`. Each thread handles one (sample, feature). K=8 inner loop — trivial. STATE_D=16 inner loop — 16 iterations, no memory bottleneck.
- [ ] **Step 4: Update `mamba2_forward` in gpu_dqn_trainer.rs**
Replace the old kernel launch with:
1. cuBLAS GEMM: `W_A^T @ h_history_flat → mamba2_a_proj`
2. cuBLAS GEMM: `W_B^T @ h_history_flat → mamba2_b_proj`
3. cuBLAS GEMM: `W_C^T @ h_s2 → mamba2_c_proj`
4. Launch `mamba2_scan_projected` kernel
Note: `h_history` is `[B, K, SH2]` — reshape to `[B*K, SH2]` for the GEMM by treating it as a flat contiguous buffer (it already is in memory).
- [ ] **Step 5: Build and test**
```bash
SQLX_OFFLINE=true cargo check -p ml
SQLX_OFFLINE=true cargo test -p ml --lib
```
- [ ] **Step 6: Commit**
```bash
git commit -m "perf: mamba2 forward — cuBLAS projections + lightweight scan (22ms → <1ms)"
```
---
### Task 2: Mamba2 Backward — cuBLAS Projections + Lightweight Reverse Scan (75%)
**Files:**
- Modify: `crates/ml/src/cuda_pipeline/experience_kernels.cu`
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`
This is THE bottleneck — 1567ms/call, 75% of total GPU time. Same approach as forward but with reverse scan + weight gradient accumulation via cuBLAS.
- [ ] **Step 1: Allocate backward scratch buffers**
```rust
// Backward needs: d_gate[B*K, STATE_D], d_x[B*K, STATE_D], d_context[B, STATE_D]
let mamba2_d_gate = alloc_f32(&stream, batch_size * MAMBA2_HISTORY_K * MAMBA2_STATE_DIM, "mamba2_d_gate")?;
let mamba2_d_x = alloc_f32(&stream, batch_size * MAMBA2_HISTORY_K * MAMBA2_STATE_DIM, "mamba2_d_x")?;
let mamba2_d_context = alloc_f32(&stream, batch_size * MAMBA2_STATE_DIM, "mamba2_d_context")?;
```
- [ ] **Step 2: Write `mamba2_scan_backward_projected` kernel**
This kernel does the reverse scan using pre-projected values. No inner matmuls.
Grid: `(B, STATE_D)`, each thread handles one (sample, state_dim) pair.
```c
extern "C" __global__ void mamba2_scan_backward_projected(
const float* __restrict__ a_proj, // [B*K, STATE_D] forward A projections
const float* __restrict__ b_proj, // [B*K, STATE_D] forward B projections
const float* __restrict__ c_proj, // [B, STATE_D] forward C projections
const float* __restrict__ d_h_enriched,// [B, SH2] upstream gradient
const float* __restrict__ w_c, // [SH2, STATE_D]
float* __restrict__ d_gate, // [B*K, STATE_D] output: sigmoid_deriv * d_x * x_prev
float* __restrict__ d_x_out, // [B*K, STATE_D] output: d_x per timestep
float* __restrict__ d_context, // [B, STATE_D] output: d_context = d_out @ W_C
int B, int K, int sh2, int state_d,
const float* __restrict__ isv_signals)
{
int b = blockIdx.x;
int s = blockIdx.y * blockDim.x + threadIdx.x;
if (b >= B || s >= state_d) return;
// Forward replay: recompute x_states[0..K] for this (b, s)
float x_states[9]; // K+1 max
x_states[0] = 0.0f;
float a_raw[8];
for (int t = 0; t < K; t++) {
int idx = (b * K + t) * state_d + s;
a_raw[t] = a_proj[idx];
float gate = 1.0f / (1.0f + expf(-a_raw[t]));
if (isv_signals) gate *= isv_signals[11];
x_states[t + 1] = gate * x_states[t] + b_proj[idx];
}
// d_context[b, s] = sum_j(d_h_enriched[b, j] * w_c[j, s])
float d_ctx = 0.0f;
for (int j = 0; j < sh2; j++) {
d_ctx += d_h_enriched[b * sh2 + j] * w_c[j * state_d + s];
}
d_context[b * state_d + s] = d_ctx;
// d_x[K] = d_context * c_proj was the last step contribution
float d_x_s = d_ctx;
// Reverse scan
for (int t = K - 1; t >= 0; t--) {
int idx = (b * K + t) * state_d + s;
float a_val = a_raw[t];
float gate = 1.0f / (1.0f + expf(-a_val));
float sigmoid_deriv = gate * (1.0f - gate);
float stability = isv_signals ? isv_signals[11] : 1.0f;
// d_gate for W_A gradient: d_x * x_prev * sigmoid'
d_gate[idx] = d_x_s * x_states[t] * sigmoid_deriv * stability;
// d_x for W_B gradient
d_x_out[idx] = d_x_s;
// Propagate d_x backward
d_x_s = d_x_s * gate * stability;
}
}
```
- [ ] **Step 3: Create cuBLAS GEMM descriptors for weight gradient accumulation**
After the scan kernel produces `d_gate[B*K, STATE_D]` and `d_x[B*K, STATE_D]`:
```
d_W_A[SH2, STATE_D] = h_history^T[SH2, B*K] @ d_gate[B*K, STATE_D]
GEMM: transa=N, transb=N, m=STATE_D(16), n=SH2(256), k=B*K(65536)
d_W_B[SH2, STATE_D] = h_history^T[SH2, B*K] @ d_x[B*K, STATE_D]
same dims
d_W_C[SH2, STATE_D] = h_s2^T[SH2, B] @ d_context[B, STATE_D]
GEMM: m=STATE_D(16), n=SH2(256), k=B(8192)
```
- [ ] **Step 4: Update `mamba2_backward` in gpu_dqn_trainer.rs**
Replace old kernel launch with:
1. Reuse forward A/B projections (already computed in forward, save them)
2. Launch `mamba2_scan_backward_projected` kernel
3. cuBLAS GEMM: `h_history^T @ d_gate → d_W_A`
4. cuBLAS GEMM: `h_history^T @ d_x → d_W_B`
5. cuBLAS GEMM: `h_s2^T @ d_context → d_W_C`
- [ ] **Step 5: Build and test**
```bash
SQLX_OFFLINE=true cargo check -p ml
SQLX_OFFLINE=true cargo test -p ml --lib
```
- [ ] **Step 6: Run local smoke test**
```bash
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml --lib --release --features ml/cuda -- smoke_tests --ignored --nocapture
```
- [ ] **Step 7: Commit**
```bash
git commit -m "perf: mamba2 backward — cuBLAS projections + reverse scan (1567ms → <5ms)"
```
---
### Task 3: Q-Denoise Backward — cuBLAS GEMMs (7.3%)
**Files:**
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`
Replace the custom `q_denoise_backward` kernel (2-layer MLP backward, one thread per weight, loops over batch) with 2× cuBLAS backward GEMMs + bias_grad_reduce. The MLP is 12→24→12 with batch=8192.
- [ ] **Step 1: Create cuBLAS GEMM descriptors for denoise backward**
```rust
// Layer 2 backward dX: m=24, n=8192, k=12 (dX = W2^T @ dY2)
// Layer 2 backward dW: m=12, n=24, k=8192 (dW = dY2^T @ h1)
// Layer 1 backward dX: m=24, n=8192, k=24 (dX = W1^T @ dY1) -- not needed (no upstream)
// Layer 1 backward dW: m=24, n=24, k=8192 (dW = dY1^T @ Q_input)
```
- [ ] **Step 2: Rewrite `launch_q_denoise_backward` to use cuBLAS**
Replace single kernel with: loss gradient → cuBLAS dW2 → ReLU mask → cuBLAS dW1 → bias_grad_reduce × 2.
- [ ] **Step 3: Build, test, commit**
```bash
git commit -m "perf: q_denoise_backward — cuBLAS GEMMs (152ms → <1ms)"
```
---
### Task 4: IQN Bias Grad Reduce — Warp-Parallel Reduction (3.9%)
**Files:**
- Modify: `crates/ml/src/cuda_pipeline/iqn_dual_head_kernel.cu`
Current: one thread per output neuron (out_dim=3), each loops over B×Q=524,288. 3 threads doing 524K iterations.
- [ ] **Step 1: Rewrite `iqn_bias_grad_reduce` with batch-parallel grid**
```c
// Phase 1: each block reduces 256 elements
// Grid: (ceil(B*Q/256), out_dim), Block: 256
// Each thread reads one element, warp shuffle reduces to partial sum
// Block-level reduction via shared memory → one partial per block
// Phase 2: reduce block partials → final db[out_dim]
// Grid: (out_dim), Block: 256
// Each thread reads one block partial, warp reduce to final sum
```
- [ ] **Step 2: Build, test, commit**
```bash
git commit -m "perf: iqn_bias_grad_reduce — warp-parallel (16ms×130 → 0.1ms×130)"
```
---
### Task 5: KAN Grad Reduce — Batch-Parallel Reduction (3.2%)
**Files:**
- Modify: `crates/ml/src/cuda_pipeline/experience_kernels.cu`
Same anti-pattern as IQN — one thread per param, loops over batch. Fix: batch-parallel 2-phase reduction.
- [ ] **Step 1: Rewrite `kan_grad_reduce` with batch-parallel grid**
Same pattern as Task 4 — phase 1 block reduce + phase 2 final reduce.
- [ ] **Step 2: Build, test, commit**
```bash
git commit -m "perf: kan_grad_reduce — batch-parallel (8ms×208 → 0.1ms×208)"
```
---
### Task 6: Curiosity Fwd+Bwd — cuBLAS GEMMs (2.1%)
**Files:**
- Modify: `crates/ml/src/cuda_pipeline/curiosity_training_kernel.cu`
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`
The training kernel is a single launch doing forward + backward for the entire batch. Replace with cuBLAS GEMMs for the curiosity MLP.
- [ ] **Step 1: Replace `curiosity_fwd_bwd_per_block` with cuBLAS pipeline**
Forward: 2 cuBLAS GEMMs (state→hidden→pred). Backward: 2 cuBLAS GEMMs (dW). Loss: element-wise kernel.
- [ ] **Step 2: Build, test, commit**
```bash
git commit -m "perf: curiosity training — cuBLAS GEMMs (1085ms → <5ms)"
```
---
### Task 7: Risk Budget Forward — cuBLAS GEMMs (1.0%)
**Files:**
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`
- Modify: `crates/ml/src/cuda_pipeline/experience_kernels.cu`
Per-sample inner matmul over SH2=256. Replace with cuBLAS.
- [ ] **Step 1: Replace `risk_budget_forward` with cuBLAS pipeline**
Concat [h_s2; ISV; pred_error] → cuBLAS GEMM1 → ReLU → cuBLAS GEMM2 → sigmoid. Same pattern as main forward.
- [ ] **Step 2: Build, test, commit**
```bash
git commit -m "perf: risk_budget_forward — cuBLAS GEMMs (20ms → <0.1ms)"
```
---
### Task 8: Deploy + nsys Validation
- [ ] **Step 1: Push and deploy with nsys**
```bash
git push origin main
./scripts/argo-train.sh dqn --baseline --epochs 1 --sanitizer nsys
```
- [ ] **Step 2: Verify step timing**
Expected: `STEP_TIMING step=50 step_ms=<50` (was 1950ms)
- [ ] **Step 3: Compare nsys kernel breakdown**
Verify all 7 kernels are now <5ms each. cuBLAS GEMMs should dominate.
- [ ] **Step 4: Run smoke tests on H100**
All 20 smoke tests pass. Training metrics within 5% of baseline.
- [ ] **Step 5: Commit validation results**
```bash
git commit -m "docs: kernel optimization validation — step time Xms (was 1950ms)"
```