Files
nvfp4-megamoe-kernel/dsv4/kernels/attention/production.py
biondizzle 4336de9372 attention/: Clean up folder, archive backups, add detailed status headers
What changed:
- Moved fmha_backup_pre_epilog.py, fmha_backup_v2.py, fmha_smem_acc.py to archive/
- Deleted fmha.py.backup (git has history)
- Added detailed heredoc headers to ALL files documenting:
  * WHAT WORKS and WHAT'S BROKEN
  * WHY each limitation exists (CuTeDSL toolchain gaps)
  * KEY INSIGHTS FOR NVIDIA (what CuTeDSL is missing)
  * What each file unblocks if fixed

File status:
  fmha.py                 — CuTeDSL FMHA, cos 0.999998, D1.5 workaround
  fmha_common.cuh         — Raw CUDA shared defs (BF16, TMEM ops)
  fmha_sm100.cuh          — Raw CUDA reference, cos 0.999999
  fmha_epilogue_sm100.cuh — Raw CUDA TMEM epilogue, HANGS (needs debug)
  fmha_sm100_launch.cu    — PyTorch binding (JIT broken, nvcc works)
  production.py           — CuTeDSL production wrapper (partial)
  archive/                — Historical backups with explanation headers
2026-05-28 07:01:33 +00:00

452 lines
18 KiB
Python

"""DSV4 Blackwell Attention — Production kernel wrapper.
====================================================================
STATUS: WORKING for single-tile, Python KV merge for multi-tile
====================================================================
See ROADMAP.md Priority 5 (Stage E) for what's needed to ship.
Key gaps: custom_op registration, kernel cache warmup, batch fusion.
====================================================================
WHAT WORKS
====================================================================
- Per-KV-group head-packed launch (MQA/GQA efficient)
- Python KV merge for multi-KV-tile (cos 0.999998)
- D3/D4/D5c masks
- Batch via Python outer loop
====================================================================
WHAT'S BLOCKED
====================================================================
- In-kernel multi-KV-tile: blocked on D1.5 (TMEM round-trip broken)
- Batch fusion into grid: blocked on D2 (multi-CTA, epilogue_tma_store)
- hd > 256: CuTeDSL MLIR hang (>3hr optimizer time)
====================================================================
Wraps the CuTeDSL FMHA kernel with Python KV merge for multi-KV-tile.
Supports MHA, MQA, and GQA attention patterns with head-packed launches
for efficient MQA/GQA (all Q heads sharing a KV head dispatched in one
kernel call via packed M dimension).
Architecture:
- Per-KV-group head-packed launch: q_group reshaped to (q_per_kv * T, hd, 1)
- Python KV merge for multi-KV-tile (correct, cos 0.999998)
- Kernel cache keyed on (head_dim, s_k, flags...) with warmup support
- Batch dimension via outer loop over batch items
- Custom op registration for torch.compile compatibility
Limitations:
- head_dim > 256: MLIR compilation hang (known CuTeDSL issue)
- In-kernel multi-KV-tile: blocked on TMA layout matching (uses Python KV merge)
- Batch: Python loop (not fused into kernel grid — requires D2 multi-CTA)
"""
import torch
import math
import logging
from typing import Optional
import cutlass.cute as cute
import cutlass.torch as ct
import cuda.bindings.driver as cuda
from dsv4.kernels.attention.fmha import FmhaKernel
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Kernel cache — keyed on compile-time configuration
# ---------------------------------------------------------------------------
_kernel_cache: dict[tuple, tuple] = {}
def _cache_key(
head_dim: int,
s_k: int,
use_smem_p: bool,
normalize: bool,
apply_swa_mask: bool,
is_causal: bool,
n_comp: int,
apply_sink_bias: bool,
) -> tuple:
"""Deterministic cache key for kernel compilation."""
return (head_dim, s_k, use_smem_p, normalize, apply_swa_mask, is_causal, n_comp, apply_sink_bias)
def _get_or_compile_kernel(
head_dim: int,
s_k: int,
use_smem_p: bool = False,
normalize: bool = False,
apply_swa_mask: bool = False,
is_causal: bool = False,
n_comp: int = 0,
apply_sink_bias: bool = False,
) -> tuple:
"""Get or compile a kernel for the given configuration. Cache by config.
Returns:
(compiled_kernel, FmhaKernel instance) — the compiled kernel callable
and the kernel object (needed for pv_n_tile, n_pv_tiles, etc.)
"""
key = _cache_key(head_dim, s_k, use_smem_p, normalize, apply_swa_mask, is_causal, n_comp, apply_sink_bias)
if key in _kernel_cache:
return _kernel_cache[key]
logger.info(f"Compiling FMHA kernel: hd={head_dim} s_k={s_k} smem_p={use_smem_p} "
f"norm={normalize} swa={apply_swa_mask} causal={is_causal} "
f"n_comp={n_comp} sink={apply_sink_bias}")
kernel = FmhaKernel(
head_dim=head_dim,
s_k=s_k,
use_smem_p=use_smem_p,
normalize=normalize,
apply_swa_mask=apply_swa_mask,
is_causal=is_causal,
n_comp=n_comp if n_comp > 0 else None,
apply_sink_bias=apply_sink_bias,
)
pv_n_tile = kernel.pv_n_tile
# Dummy tensors for compilation (single-head, single-segment)
m = 128
q = torch.randn(m, head_dim, 1, dtype=torch.bfloat16, device='cuda')
k = torch.randn(s_k, head_dim, 1, dtype=torch.bfloat16, device='cuda')
v = torch.randn(s_k, pv_n_tile, dtype=torch.bfloat16, device='cuda')
c = torch.zeros(m, pv_n_tile, 1, dtype=torch.bfloat16, device='cuda')
lse = torch.zeros(m, 1, 1, dtype=torch.float32, device='cuda')
row_sums = torch.zeros(m, 1, 1, dtype=torch.float32, device='cuda')
stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
v_tile = v[:, 0:pv_n_tile].contiguous().unsqueeze(-1)
mQ = ct.from_dlpack(q).mark_layout_dynamic(leading_dim=ct.get_leading_dim(q))
mK = ct.from_dlpack(k).mark_layout_dynamic(leading_dim=ct.get_leading_dim(k))
mV = ct.from_dlpack(v_tile).mark_layout_dynamic(leading_dim=ct.get_leading_dim(v_tile))
mC = ct.from_dlpack(c).mark_layout_dynamic(leading_dim=ct.get_leading_dim(c))
mLSE = ct.from_dlpack(lse).mark_layout_dynamic(leading_dim=ct.get_leading_dim(lse))
mRS = ct.from_dlpack(row_sums).mark_layout_dynamic(leading_dim=ct.get_leading_dim(row_sums))
compiled = cute.compile(kernel, mQ, mK, mV, mC, stream, lse=mLSE, row_sums=mRS)
_kernel_cache[key] = (compiled, kernel)
logger.info(f"FMHA kernel compiled and cached (key={key})")
return (compiled, kernel)
# ---------------------------------------------------------------------------
# Warmup — pre-compile all kernels needed for a model config
# ---------------------------------------------------------------------------
def warmup_attention_kernels(
head_dims: list[int],
s_k_values: list[int],
swa_mask: bool = True,
causal: bool = True,
n_comp_values: list[int] = [0, 4, 128],
sink_bias: bool = True,
):
"""Pre-compile all kernel variants needed for model execution.
Call once during model loading to avoid JIT stalls during inference.
Args:
head_dims: list of head dimensions used in the model (e.g. [64, 128])
s_k_values: list of KV segment sizes (e.g. [128])
swa_mask: whether SWA masking is used
causal: whether causal masking is used
n_comp_values: compressed KV lengths (0=no compression, 4=CSA, 128=HCA)
sink_bias: whether sink bias is used
"""
for hd in head_dims:
for s_k in s_k_values:
use_smem_p = hd > 64
for n_comp in n_comp_values:
apply_swa = swa_mask and n_comp > 0 # SWA mask only meaningful with compression
apply_sink = sink_bias
_get_or_compile_kernel(
head_dim=hd, s_k=s_k, use_smem_p=use_smem_p,
normalize=False, apply_swa_mask=apply_swa,
is_causal=causal, n_comp=n_comp,
apply_sink_bias=apply_sink,
)
logger.info("All attention kernels warmed up")
# ---------------------------------------------------------------------------
# Internal: single-head-group FMHA with Python KV merge
# ---------------------------------------------------------------------------
def _run_fmha_segmented(
q_3d: torch.Tensor, # (M, hd, 1) BF16 — M = q_per_kv * T (head-packed)
k_3d: torch.Tensor, # (N, hd, 1) BF16
v_2d: torch.Tensor, # (N, hd) BF16
scale: float,
swa_len: Optional[int] = None,
is_causal: bool = False,
n_comp: int = 0,
sink_bias: Optional[torch.Tensor] = None, # scalar or (1,) FP32
) -> torch.Tensor:
"""Run FMHA with Python KV merge over s_k=128 segments.
This is the core compute routine. It segments K/V into 128-token chunks,
runs the CuTeDSL kernel per segment, and merges results using the correct
LSE-weighted normalized-O formula:
O = Σ exp(lse_i)·O_i_norm / Σ exp(lse_i)
where O_i_norm = O_i_unnorm / row_sum_i.
Args:
q_3d: (M, hd, 1) BF16 query tensor (M may be T or n_h*T for head-packed)
k_3d: (N, hd, 1) BF16 key tensor
v_2d: (N, hd) BF16 value tensor
scale: softmax scale (1/sqrt(hd))
swa_len: sliding window length (None = no SWA mask)
is_causal: apply causal mask on SWA region
n_comp: compressed KV length for sink bias offset
sink_bias: per-head FP32 logit bias (scalar for single-head launch)
Returns:
(M, hd) BF16 attention output
"""
M, hd, _ = q_3d.shape
N = k_3d.shape[0]
s_k_per_seg = 128
n_segments = (N + s_k_per_seg - 1) // s_k_per_seg
apply_swa_mask = swa_len is not None
apply_sink_bias = sink_bias is not None
compiled, kernel = _get_or_compile_kernel(
head_dim=hd, s_k=s_k_per_seg,
use_smem_p=hd > 64,
normalize=False,
apply_swa_mask=apply_swa_mask,
is_causal=is_causal,
n_comp=n_comp if n_comp > 0 else 0,
apply_sink_bias=apply_sink_bias,
)
pv_n_tile = kernel.pv_n_tile
n_pv_tiles = kernel.n_pv_tiles
o_accum = torch.zeros(M, hd, dtype=torch.float32, device='cuda')
lse_accum = torch.full((M, 1), float('-inf'), dtype=torch.float32, device='cuda')
stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
for seg in range(n_segments):
k_start = seg * s_k_per_seg
k_end = min(k_start + s_k_per_seg, N)
k_seg = k_3d[k_start:k_end]
v_seg = v_2d[k_start:k_end]
# Pad partial last segment
if k_end - k_start < s_k_per_seg:
pad_len = s_k_per_seg - (k_end - k_start)
k_seg = torch.cat([k_seg, torch.zeros(pad_len, hd, 1, dtype=k_seg.dtype, device='cuda')], dim=0)
v_seg = torch.cat([v_seg, torch.zeros(pad_len, hd, dtype=v_seg.dtype, device='cuda')], dim=0)
seg_o = torch.zeros(M, hd, dtype=torch.float32, device='cuda')
seg_lse = torch.zeros(M, 1, dtype=torch.float32, device='cuda')
seg_row_sums = torch.zeros(M, 1, dtype=torch.float32, device='cuda')
for nt in range(n_pv_tiles):
v_start = nt * pv_n_tile
v_end = v_start + pv_n_tile
v_tile = v_seg[:, v_start:v_end].contiguous()
v_kernel = v_tile.unsqueeze(-1) # (s_k, pv_n_tile, 1)
c_tile = torch.zeros(M, pv_n_tile, 1, dtype=torch.bfloat16, device='cuda')
lse_tensor = torch.zeros(M, 1, 1, dtype=torch.float32, device='cuda')
row_sums_tensor = torch.zeros(M, 1, 1, dtype=torch.float32, device='cuda')
mQ = ct.from_dlpack(q_3d).mark_layout_dynamic(leading_dim=ct.get_leading_dim(q_3d))
mK = ct.from_dlpack(k_seg).mark_layout_dynamic(leading_dim=ct.get_leading_dim(k_seg))
mV = ct.from_dlpack(v_kernel).mark_layout_dynamic(leading_dim=ct.get_leading_dim(v_kernel))
mC = ct.from_dlpack(c_tile).mark_layout_dynamic(leading_dim=ct.get_leading_dim(c_tile))
mLSE = ct.from_dlpack(lse_tensor).mark_layout_dynamic(leading_dim=ct.get_leading_dim(lse_tensor))
mRS = ct.from_dlpack(row_sums_tensor).mark_layout_dynamic(leading_dim=ct.get_leading_dim(row_sums_tensor))
# Pass sink_bias as CuTe tensor when needed
if apply_sink_bias:
# For head-packed launch, all heads in the group share the same sink_bias
# because they share the same KV head. Use the first head's bias.
# (This is correct for MQA. For GQA with different biases, use per-head launch.)
sb_tensor = sink_bias.flatten()[:1].contiguous().unsqueeze(-1) # (1, 1) or use the scalar
mSB = ct.from_dlpack(sb_tensor).mark_layout_dynamic(leading_dim=ct.get_leading_dim(sb_tensor))
compiled(mQ, mK, mV, mC, stream, lse=mLSE, swa_len=swa_len, sink_bias=mSB, row_sums=mRS)
else:
compiled(mQ, mK, mV, mC, stream, lse=mLSE, swa_len=swa_len, row_sums=mRS)
torch.cuda.synchronize()
seg_o[:, v_start:v_end] = c_tile[:, :, 0].float()
if nt == 0:
seg_lse[:, 0] = lse_tensor[:, 0, 0].float()
seg_row_sums[:, 0] = row_sums_tensor[:, 0, 0].float()
# Python KV merge: O = Σ exp(lse_i)·O_i_norm / Σ exp(lse_i)
seg_row_sums = seg_row_sums.clamp(min=1e-30)
seg_o_norm = seg_o / seg_row_sums
e_old = torch.exp(lse_accum)
e_new = torch.exp(seg_lse)
e_sum = e_old + e_new
o_accum = (e_old * o_accum + e_new * seg_o_norm) / e_sum
lse_accum = torch.log(e_sum)
return o_accum.to(torch.bfloat16)
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def dsv4_attention(
q: torch.Tensor, # (batch, n_q_heads, T, hd) or (n_q_heads, T, hd)
k: torch.Tensor, # (batch, n_kv_heads, N, hd) or (n_kv_heads, N, hd) or (N, hd)
v: torch.Tensor, # same shape as k
scale: Optional[float] = None,
swa_len: Optional[int] = None,
is_causal: bool = False,
n_comp: int = 0,
sink_bias: Optional[torch.Tensor] = None, # (n_q_heads,) or (batch, n_q_heads)
) -> torch.Tensor:
"""Production DSV4 attention: MHA / MQA / GQA with head-packed launches.
For MQA/GQA: all Q heads sharing a KV head are packed into one kernel
launch via M dimension (q_per_kv * T rows). This eliminates redundant
K/V TMA loads — each KV head is loaded once per segment, not per Q head.
Args:
q: (n_q_heads, T, hd) or (batch, n_q_heads, T, hd) BF16
k: (n_kv_heads, N, hd) or (N, hd) for MQA, or with batch dim BF16
v: same shape as k
scale: 1/sqrt(hd) if None
swa_len: sliding window length
is_causal: causal mask
n_comp: compressed KV length for D5c sink bias
sink_bias: per-head FP32 logit bias (n_q_heads,) or (batch, n_q_heads)
Returns:
Same shape as q input (without batch: (n_q_heads, T, hd) BF16)
"""
# Handle batch dimension
has_batch = q.dim() == 4
if has_batch:
batch_size = q.shape[0]
# Process each batch item
outputs = []
for b in range(batch_size):
q_b = q[b] # (n_q_heads, T, hd)
k_b = k[b] if k.dim() == 4 else k # (n_kv_heads, N, hd) or (N, hd)
v_b = v[b] if v.dim() == 4 else v
sb_b = sink_bias[b] if sink_bias is not None and sink_bias.dim() == 2 else sink_bias
out_b = dsv4_attention(
q_b, k_b, v_b, scale=scale, swa_len=swa_len,
is_causal=is_causal, n_comp=n_comp, sink_bias=sb_b,
)
outputs.append(out_b)
return torch.stack(outputs, dim=0)
# 3D case: (n_q_heads, T, hd)
n_q, T, hd = q.shape
scale = scale or (1.0 / math.sqrt(hd))
# Normalize K/V to (n_kv, N, hd)
if k.dim() == 2:
k = k.unsqueeze(0) # (1, N, hd) — MQA
if v.dim() == 2:
v = v.unsqueeze(0)
n_kv, N, _ = k.shape
# GQA ratio: each KV head serves (n_q // n_kv) Q heads
q_per_kv = n_q // n_kv
assert n_q % n_kv == 0, f"n_q_heads ({n_q}) must be divisible by n_kv_heads ({n_kv})"
output = torch.zeros(n_q, T, hd, dtype=torch.bfloat16, device='cuda')
for kv_idx in range(n_kv):
# Head-packed: all Q heads for this KV group in ONE launch
q_start = kv_idx * q_per_kv
q_end = q_start + q_per_kv
q_group = q[q_start:q_end] # (q_per_kv, T, hd)
k_kv = k[kv_idx] # (N, hd)
v_kv = v[kv_idx] # (N, hd)
# Pack Q heads into M dimension: (q_per_kv * T, hd, 1)
q_packed = q_group.transpose(0, 1).reshape(q_per_kv * T, hd).contiguous().unsqueeze(-1)
k_3d = k_kv.unsqueeze(-1) # (N, hd, 1)
v_2d = v_kv # (N, hd)
# Sink bias for this KV group — use first Q head's bias
# (All Q heads sharing a KV head have different biases, but the kernel
# only accepts a scalar sink_bias per launch. For head-packed launch,
# we use the first head's bias. This is an approximation — for exact
# per-head biases, fall back to per-head launch.)
if sink_bias is not None:
sb = sink_bias[q_start:q_start+1].contiguous() # scalar
else:
sb = None
o_packed = _run_fmha_segmented(
q_packed, k_3d, v_2d, scale=scale,
swa_len=swa_len, is_causal=is_causal, n_comp=n_comp,
sink_bias=sb,
) # (q_per_kv * T, hd)
# Unpack: (q_per_kv * T, hd) -> (q_per_kv, T, hd)
o_group = o_packed.reshape(q_per_kv, T, hd)
output[q_start:q_end] = o_group
return output
def dsv4_attention_per_head(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
scale: Optional[float] = None,
swa_len: Optional[int] = None,
is_causal: bool = False,
n_comp: int = 0,
sink_bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
"""Per-head launch variant — exact per-head sink bias support.
Use this when Q heads within a KV group have different sink biases
and exact results matter more than launch overhead. Otherwise prefer
dsv4_attention (head-packed).
"""
n_q, T, hd = q.shape
scale = scale or (1.0 / math.sqrt(hd))
if k.dim() == 2:
k = k.unsqueeze(0)
if v.dim() == 2:
v = v.unsqueeze(0)
n_kv, N, _ = k.shape
q_per_kv = n_q // n_kv
output = torch.zeros(n_q, T, hd, dtype=torch.bfloat16, device='cuda')
for kv_idx in range(n_kv):
k_kv = k[kv_idx:kv_idx+1] # (1, N, hd)
v_kv = v[kv_idx:kv_idx+1]
for qi in range(q_per_kv):
q_idx = kv_idx * q_per_kv + qi
q_h = q[q_idx:q_idx+1] # (1, T, hd)
sb = sink_bias[q_idx:q_idx+1] if sink_bias is not None else None
q_3d = q_h[0].contiguous().unsqueeze(-1) # (T, hd, 1)
k_3d = k_kv[0].contiguous().unsqueeze(-1)
v_2d = v_kv[0].contiguous()
o = _run_fmha_segmented(
q_3d, k_3d, v_2d, scale=scale,
swa_len=swa_len, is_causal=is_causal, n_comp=n_comp,
sink_bias=sb,
)
output[q_idx] = o
return output