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
452 lines
18 KiB
Python
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
|