"""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