2026-05-24 04:32:43 +00:00
# STAGE_D.md — FMHA Kernel Development
2026-05-23 05:52:03 +00:00
## ⚠️ IKEA INSTRUCTIONS — READ EVERY TIME BEFORE CODING
### The Workflow (DO NOT SKIP STEPS)
1. **Edit code in ** `~/dev/nvfp4-megamoe-kernel/dsv4/kernels/attention/fmha.py` — this is the ONLY file for the FMHA kernel.
2. **Commit and push: **
```bash
cd ~/dev/nvfp4-megamoe-kernel
git add -A && git commit -m "description" && git push origin master
```
3. **Pull on B200: **
```bash
sshpass -p '6)Jr)B@dcX [mN?dx' ssh -o StrictHostKeyChecking=no root@45 .76.247.107 \
"cd /root/dsv4-nvfp4-workspace/kernel && git pull origin master"
```
2026-05-24 04:32:43 +00:00
4. **Test on B200 using the test harness scripts ** — see README.md "Test Harness" section.
5. **Regression check: ** After every change, verify hd=64 cos ~0.999998 still matches. If it doesn't, the change is WRONG. Revert.
2026-05-23 05:52:03 +00:00
2026-05-24 04:32:43 +00:00
### The Rules (BURNED INTO THIS FILE)
2026-05-23 05:52:03 +00:00
- **NEVER edit files directly on the B200.** Edit locally, commit, push, pull, test. Every time.
2026-05-24 04:32:43 +00:00
- **NEVER delete or modify the test files in `tests/unit/` ** without explicit approval.
2026-05-23 05:52:03 +00:00
- **NEVER touch drivers, kernels, firmware, or system packages on the B200.**
2026-05-24 04:32:43 +00:00
- **CuTeDSL variables defined in `if` blocks are NOT visible in other `if` blocks.** Define all variables unconditionally before any branching.
2026-05-23 05:52:03 +00:00
- **Always test at hd=64 FIRST.** If the proven path (TMEM-P) regresses, nothing else matters.
- **After every P store to TMEM, call `cute.arch.fence_view_async_tmem_store()` .** Missing this produces NaN.
2026-05-24 04:32:43 +00:00
- **`tOrP0` MUST include the `tmem_p0_offset` column offset.** Use `const_expr` for the conditional.
- **PRINT THE SHAPES. ALWAYS.** Reasoning about layouts without evidence is how we waste days.
2026-05-23 05:52:03 +00:00
---
2026-05-24 04:32:43 +00:00
## Current Status (2026-05-24)
2026-05-23 05:52:03 +00:00
2026-05-24 04:32:43 +00:00
### ✅ WORKING
2026-05-23 05:52:03 +00:00
2026-05-24 04:32:43 +00:00
| hd | n=128 cos | LSE err | Path |
|---:|----------:|--------:|------|
| 64 | 0.999998 | 0.000000 | TMEM-P |
| 128 | 0.999997 | 0.000000 | TMEM-P / SMEM-P |
| 256 | 0.999998 | 0.000000 | TMEM-P |
2026-05-23 05:52:03 +00:00
2026-05-24 04:32:43 +00:00
### ❌ KNOWN ISSUES
2026-05-23 05:52:03 +00:00
2026-05-24 04:32:43 +00:00
- **hd=512:** SMEM overflow (344KB > 232KB). sQ(128KB) + sK(128KB) + sV(64KB) too large. Needs SMEM tiling or buffer overlap.
- **O rescale (kt>0):** Uses hand-constructed TMEM atoms. May corrupt data for n>128 (multi-KV-tile). At n=128 (1 KV tile, kt=0), no rescale needed.
- **Kernel always outputs un-normalized O + LSE.** No in-kernel normalization (eliminates TMEM round-trip error). External normalization: `O_norm = O_unnorm / row_sum` .
2026-05-23 05:52:03 +00:00
2026-05-23 06:31:37 +00:00
---
2026-05-24 04:32:43 +00:00
## Architecture
2026-05-23 06:43:01 +00:00
2026-05-24 04:32:43 +00:00
### 6-Warp Layout
2026-05-23 06:43:01 +00:00
2026-05-24 04:32:43 +00:00
```
Warps 0-3: Softmax + Epilogue (row_max, row_sum, P store, O rescale)
Warp 4: MMA (QK, PV)
Warp 5: TMA (Q/K/V load)
```
2026-05-23 06:43:01 +00:00
2026-05-24 04:32:43 +00:00
### Kernel Output
2026-05-23 06:43:01 +00:00
2026-05-24 04:32:43 +00:00
The kernel outputs **un-normalized O + LSE ** via `epilogue_tma_store` :
- O_unnorm = sum(P * V) where P = exp(S * scale - row_max)
- LSE = ln(row_sum) + row_max * ln(2)
- External normalization: O_norm = O_unnorm / row_sum
- For D5 merge: use exp(LSE) directly in the merge formula
2026-05-23 05:52:03 +00:00
2026-05-24 04:32:43 +00:00
### TMEM Layout
2026-05-23 05:52:03 +00:00
2026-05-24 04:32:43 +00:00
```
Col 0-31: S (QK acc, 128 FP32 via Ld32x32bOp Repetition(32))
Col 32-95: P (64 FP32 via register bridge, BF16 view)
Col 128+: O (PV acc, 64+ FP32)
```
2026-05-23 06:31:37 +00:00
2026-05-24 04:32:43 +00:00
### P Staging Paths
2026-05-23 06:31:37 +00:00
2026-05-24 04:32:43 +00:00
**TMEM-P (hd≤64, also works at hd=128/256):**
- P stored to TMEM via register bridge (FP32 backing + BF16 view)
- PV MMA reads P from TMEM via `tOrP0`
- Works because QK C-fragment and PV A-fragment TMEM layouts agree at tested head dims
2026-05-23 06:31:37 +00:00
2026-05-24 04:32:43 +00:00
**SMEM-P (hd>64):**
- P written to SMEM via coordinate-indexed store
- Uses `tTMEM_LOADcS` identity tensor to get (m, k) coordinates
- Maps to sP's subtile layout: `sP[(m_coord, k_sub), 0, (k_g1, k_g2)]`
- PV MMA reads P from SMEM via `tCrP = pv_mma.make_fragment_A(sP)`
- SMEM-P uses `OperandSource.SMEM` for PV MMA
2026-05-23 06:31:37 +00:00
2026-05-24 04:32:43 +00:00
### Key Configuration
2026-05-23 06:31:37 +00:00
```python
2026-05-24 04:32:43 +00:00
head_dim: constructor arg (64, 128, 256, 512)
pv_n_tile: min(head_dim, 256) # tcgen05 MMA max N=256
n_pv_tiles: head_dim // pv_n_tile
kv_stage: 1 if head_dim > 128 else 2 # Reduce SMEM at large hd
use_smem_p: head_dim > 64 # SMEM-P for hd>64
qk_mma_tiler: (128, 128, head_dim) # K-dim = head_dim (NOT hardcoded!)
2026-05-23 06:31:37 +00:00
```
---
2026-05-24 04:32:43 +00:00
## Critical Bug Fix: qk_mma_tiler K-dim (2026-05-24)
2026-05-23 06:31:37 +00:00
2026-05-24 04:32:43 +00:00
**ROOT CAUSE of hd>64 failure:** `qk_mma_tiler` K-dim was hardcoded to `qk_ik * 4 = 64` instead of `head_dim` .
2026-05-23 06:31:37 +00:00
2026-05-24 04:32:43 +00:00
This caused the QK GEMM to only compute 64 of 128 (or 256, 512) dimensions at hd>64. The QK dot products were half the correct length, producing wrong attention scores.
2026-05-23 05:52:03 +00:00
2026-05-24 04:32:43 +00:00
**Fix:** `self.qk_mma_tiler = (128, 128, self.head_dim)` — one line change.
2026-05-23 05:52:03 +00:00
2026-05-24 04:32:43 +00:00
**Impact:** hd=128 went from cos 0.78 to 0.999997. hd=256 went from broken to 0.999998.
2026-05-23 05:52:03 +00:00
2026-05-24 04:32:43 +00:00
**LESSON:** The MMA tiler's K dimension must match the actual GEMM K dimension (head_dim), not the MMA instruction's K sub-tile size.
2026-05-23 06:31:37 +00:00
---
2026-05-24 04:32:43 +00:00
## SMEM Budget at Various hd
2026-05-23 05:52:03 +00:00
2026-05-24 04:32:43 +00:00
| hd | sQ | sK (kv_stage=1) | sV (kv_stage=1) | sP (SMEM-P) | sC | Total | Limit | Status |
|---:|----:|----:|----:|----:|----:|------:|------:|--------|
| 64 | 32KB | 32KB | 32KB | 32KB | 32KB | 160KB | 232KB | ✅ |
| 128 | 32KB | 32KB | 32KB | 32KB | 32KB | 160KB | 232KB | ✅ |
| 256 | 64KB | 64KB | 64KB | 0* | 32KB | 224KB | 232KB | ✅ |
| 512 | 128KB | 128KB | 64KB | 0* | 32KB | 352KB | 232KB | ❌ |
2026-05-23 05:52:03 +00:00
2026-05-24 04:32:43 +00:00
*TMEM-P path: sP allocation skipped (const_expr conditional)
2026-05-23 18:37:53 +00:00
2026-05-24 04:32:43 +00:00
---
2026-05-23 05:52:03 +00:00
2026-05-24 04:32:43 +00:00
## D1.5: Correction Epilogue (TMEM Round-Trip Error)
2026-05-23 18:37:53 +00:00
2026-05-24 04:32:43 +00:00
**Issue:** Hand-constructed `Ld32x32bOp` /`St32x32bOp` atoms don't preserve the C-fragment layout during TMEM round-trips (load→modify→store). Causes ~3% error per round-trip.
2026-05-23 18:37:53 +00:00
2026-05-24 04:32:43 +00:00
**Current workaround:** Kernel outputs un-normalized O + LSE. No in-kernel normalization needed. External normalization is exact.
2026-05-23 18:37:53 +00:00
2026-05-24 04:32:43 +00:00
**Proper fix (future):** Use CUTLASS `epilogue_tmem_copy_and_partition` + `epilogue_smem_copy_and_partition` pattern with paired atoms. One-way trip: TMEM → registers (normalize) → SMEM → GMEM.
2026-05-23 18:37:53 +00:00
2026-05-24 04:32:43 +00:00
**Priority:** MEDIUM. Not a correctness blocker (external normalization is exact). Would enable in-kernel normalization for D5c/D5d.
2026-05-23 18:37:53 +00:00
2026-05-24 04:32:43 +00:00
---
2026-05-23 18:37:53 +00:00
2026-05-24 04:32:43 +00:00
## Build Order (Remaining)
2026-05-23 18:37:53 +00:00
2026-05-24 04:32:43 +00:00
### D1.4 — hd=512 SMEM Budget ⚡ CURRENT
2026-05-23 18:37:53 +00:00
2026-05-24 04:32:43 +00:00
hd=512 needs sQ(128KB) + sK(128KB) + sV(64KB) = 320KB. Must reduce to fit 232KB.
2026-05-23 18:37:53 +00:00
2026-05-24 04:32:43 +00:00
Options:
1. **Tile Q along head_dim: ** Process Q in chunks of 256. Two Q sub-tiles per kernel.
2. **SMEM buffer overlap: ** sQ and sK/sV used at different times. After Q is consumed by MMA, reuse sQ's SMEM for K/V.
3. **Split the GEMM K dimension: ** Process K in sub-tiles (K=256 then K=256-512). Each sub-tile fits SMEM.
2026-05-23 06:31:37 +00:00
### D2 — Multi-Query Grid with Head Packing
2026-05-23 05:52:03 +00:00
2026-05-24 04:32:43 +00:00
- Grid changes from `(1, 1, 1)` to `(num_q_blocks, 1, batch)`
- DSV4 is MQA: all 128 query heads share same K/V
- Head axis folded into M dimension: `M_tile = 128` covers `M = T * n_h` rows
2026-05-23 05:52:03 +00:00
2026-05-23 06:31:37 +00:00
### D3 — SWA Sequence Length Mask
2026-05-23 05:52:03 +00:00
2026-05-24 04:32:43 +00:00
- Add `swa_lens: [batch] int32` kernel input
- Mask SWA-branch logits to `-inf` where `swa_idx >= swa_lens[b]`
2026-05-23 05:52:03 +00:00
2026-05-23 06:31:37 +00:00
### D4 — Causal Mask on SWA Branch
2026-05-23 05:52:03 +00:00
2026-05-24 04:32:43 +00:00
- Add `is_causal: bool` constructor flag
- Apply `swa_idx > q_pos` masking to `-inf` in SWA pass
2026-05-23 19:22:28 +00:00
2026-05-24 04:32:43 +00:00
### D5 — SWA + Sink Merge
2026-05-23 19:22:28 +00:00
2026-05-24 04:32:43 +00:00
- **D5a ✅:** Kernel outputs un-normalized O + LSE
- **D5b ✅:** Python merge works (cos 0.961 at hd=64)
- **D5c:** Fuse two passes into one kernel launch
- **D5d:** Fuse sink merge into kernel epilogue