Files
nvfp4-megamoe-kernel/tests/unit/test_fmha_sink_bias.py
biondizzle 13be3ad443 FMHA sink bias in kernel + single_shot production rewrite
FMHA kernel (fmha_6warp_tma_multirow_multitile.cuh):
- Added sink_bias field to FmhaTmaMultiRowMultiTileParams
- After KV tile loop, sink logit is included in online softmax rescale:
  new_max = max(running_max, sink_bias * scale)
  rescale existing O_unnorm and running_sum
  running_sum += exp(sink_bias * scale - new_max)
  No PV contribution from sink (D5c: single softmax)
- C API: fmha_multitile_decode_launch now takes sink_bias_ptr
- Python: fmha_multitile_decode_raw accepts attn_sink tensor

single_shot_inference.py:
- Full rewrite to use production kernel stack
- mHC: uses dsv4.layers.mhc.mHCLayer (proper Sinkhorn-Knopp)
- Projections: uses Nvfp4Linear (CuTeDSL GEMM) for q_a, q_b, kv, o_b
- FMHA: 6-warp TMA multi-tile with sink bias (no SDPA fallback)
- MoE: Nvfp4MoE + Nvfp4SharedExpert (no reference fallback)
- Router: production dense/hash dispatch
- Compressor/Indexer: reference dequant (not yet on tensor cores)
- NO try/except fallbacks on production paths
2026-05-31 23:10:13 +00:00

89 lines
3.2 KiB
Python

#!/usr/bin/env python3
"""Test FMHA kernel with attention sink bias.
Validates that the kernel's sink bias correction matches PyTorch reference:
softmax([QK^T * scale, sink_bias])[:N] @ V
Tests HD=64,128,256,512 with and without sinks.
"""
import torch
import math
import sys
def reference_fmha_with_sink(q, k, v, scale, sink_bias=None):
"""PyTorch reference: softmax([QK^T * scale, sink_bias]) @ V.
q: (n_h, T, hd), k: (1, N, hd), v: (1, N, hd)
sink_bias: (n_h,) FP32 or None
Returns: (n_h, T, hd) BF16
"""
n_h, T, hd = q.shape
N = k.shape[1]
# QK^T: (n_h, T, N)
scores = torch.matmul(q, k.transpose(-1, -2)) * scale # (n_h, T, N)
if sink_bias is not None:
# Concatenate sink as extra column: (n_h, T, N+1)
sb = sink_bias.reshape(n_h, 1, 1).expand(-1, T, 1)
combined = torch.cat([scores, sb], dim=-1)
attn = torch.softmax(combined.float(), dim=-1)[:, :, :N] # drop sink column
else:
attn = torch.softmax(scores.float(), dim=-1)
out = torch.matmul(attn.bfloat16(), v) # (n_h, T, hd)
return out
def test_fmha_sink():
from dsv4.kernels.attention.production import dsv4_attention
torch.manual_seed(42)
device = 'cuda'
passed = 0
failed = 0
for hd in [64, 128, 256, 512]:
for N in [9, 32, 128, 256]:
for use_sink in [False, True]:
n_h = 4 # small for speed
T = 1
scale = 1.0 / math.sqrt(hd)
q = torch.randn(n_h, T, hd, dtype=torch.bfloat16, device=device)
k = torch.randn(1, N, hd, dtype=torch.bfloat16, device=device)
v = torch.randn(1, N, hd, dtype=torch.bfloat16, device=device)
sink = torch.randn(n_h, dtype=torch.float32, device=device) * 2 if use_sink else None
# Production kernel
try:
o_kernel = dsv4_attention(q, k, v, scale=scale, sink_bias=sink)
except Exception as e:
print(f" FAIL hd={hd} N={N} sink={use_sink}: kernel error: {e}")
failed += 1
continue
# PyTorch reference
o_ref = reference_fmha_with_sink(q, k, v, scale, sink)
# Compare
o_kf = o_kernel.float()
o_rf = o_ref.float()
cos = torch.nn.functional.cosine_similarity(o_kf.flatten().unsqueeze(0),
o_rf.flatten().unsqueeze(0)).item()
max_diff = (o_kf - o_rf).abs().max().item()
status = "PASS" if cos > 0.999 else "FAIL"
if status == "PASS":
passed += 1
else:
failed += 1
print(f" {status} hd={hd} N={N} sink={use_sink} cos={cos:.6f} max_diff={max_diff:.6f}")
print(f"\n{'='*60}")
print(f"Results: {passed} PASSED, {failed} FAILED")
print(f"{'='*60}")
return failed == 0
if __name__ == "__main__":
success = test_fmha_sink()
sys.exit(0 if success else 1)