Files
nvfp4-megamoe-kernel/tests/unit/test_d1_tmem_trip.py

103 lines
4.2 KiB
Python

"""
D1: Test TMEM round-trip on O in isolation.
Runs the kernel with s_k=128 (1 KV tile, no rescale needed).
Then manually does a load-modify-store round-trip on O in TMEM
using the correction_rescale atoms.
If the round-trip corrupts data, we know the atoms are broken.
If it preserves data, the bug is elsewhere.
"""
import torch, math
import cutlass.cute as cute
import cutlass.torch as ct
import cuda.bindings.driver as cuda
from dsv4.kernels.attention.fmha import FmhaKernel
def test():
hd = 64
s_k = 128 # 1 KV tile, no rescale needed
m = 128
torch.manual_seed(42)
q = torch.randn(m, hd, 1, dtype=torch.bfloat16, device='cuda')
k = torch.randn(s_k, hd, 1, dtype=torch.bfloat16, device='cuda')
v = torch.randn(s_k, hd, dtype=torch.bfloat16, device='cuda')
c = torch.zeros(m, hd, 1, dtype=torch.bfloat16, device='cuda')
# FP32 reference
qf = q[:, :, 0].float()
kf = k[:, :, 0].float()
scale = 1.0 / math.sqrt(hd)
attn_max = (qf @ kf.T * scale).max(dim=-1, keepdim=True)[0]
attn_exp = torch.exp(qf @ kf.T * scale - attn_max)
attn_sum = attn_exp.sum(dim=-1, keepdim=True)
ref_unnorm = attn_exp @ v.float()
lse_tensor = torch.zeros(m, 1, 1, dtype=torch.float32, device='cuda')
# Test 1: s_k=128 baseline (no rescale) — should be PASS
kernel = FmhaKernel(head_dim=hd, s_k=s_k, use_smem_p=False, normalize=False)
pv_n_tile = kernel.pv_n_tile
stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
v_tile = v[:, 0:pv_n_tile].contiguous()
v_kernel = v_tile.unsqueeze(-1)
c_tile = torch.zeros(m, pv_n_tile, 1, dtype=torch.bfloat16, device='cuda')
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_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))
print(f'Test 1: s_k=128 baseline (no rescale)', flush=True)
compiled = cute.compile(kernel, mQ, mK, mV, mC, stream, mLSE)
compiled(mQ, mK, mV, mC, stream, mLSE)
torch.cuda.synchronize()
out1 = c_tile[:, :, 0].float()
cos1 = torch.nn.functional.cosine_similarity(
out1.flatten().unsqueeze(0), ref_unnorm.flatten().unsqueeze(0)
).item()
print(f' cos_unnorm={cos1:.6f} {"PASS" if cos1 >= 0.99 else "FAIL"}')
# Test 2: s_k=256 with rescale — this is the failing test
s_k2 = 256
k2 = torch.randn(s_k2, hd, 1, dtype=torch.bfloat16, device='cuda')
v2 = torch.randn(s_k2, hd, dtype=torch.bfloat16, device='cuda')
c2 = torch.zeros(m, hd, 1, dtype=torch.bfloat16, device='cuda')
kf2 = k2[:, :, 0].float()
attn_max2 = (qf @ kf2.T * scale).max(dim=-1, keepdim=True)[0]
attn_exp2 = torch.exp(qf @ kf2.T * scale - attn_max2)
attn_sum2 = attn_exp2.sum(dim=-1, keepdim=True)
ref_unnorm2 = attn_exp2 @ v2.float()
kernel2 = FmhaKernel(head_dim=hd, s_k=s_k2, use_smem_p=False, normalize=False)
lse_tensor2 = torch.zeros(m, 1, 1, dtype=torch.float32, device='cuda')
v_tile2 = v2[:, 0:pv_n_tile].contiguous()
v_kernel2 = v_tile2.unsqueeze(-1)
c_tile2 = torch.zeros(m, pv_n_tile, 1, dtype=torch.bfloat16, device='cuda')
mK2 = ct.from_dlpack(k2).mark_layout_dynamic(leading_dim=ct.get_leading_dim(k2))
mV2 = ct.from_dlpack(v_kernel2).mark_layout_dynamic(leading_dim=ct.get_leading_dim(v_kernel2))
mC2 = ct.from_dlpack(c_tile2).mark_layout_dynamic(leading_dim=ct.get_leading_dim(c_tile2))
mLSE2 = ct.from_dlpack(lse_tensor2).mark_layout_dynamic(leading_dim=ct.get_leading_dim(lse_tensor2))
print(f'Test 2: s_k=256 with O rescale', flush=True)
compiled2 = cute.compile(kernel2, mQ, mK2, mV2, mC2, stream, mLSE2)
compiled2(mQ, mK2, mV2, mC2, stream, mLSE2)
torch.cuda.synchronize()
out2 = c_tile2[:, :, 0].float()
cos2 = torch.nn.functional.cosine_similarity(
out2.flatten().unsqueeze(0), ref_unnorm2.flatten().unsqueeze(0)
).item()
print(f' cos_unnorm={cos2:.6f} {"PASS" if cos2 >= 0.99 else "FAIL"}')
if __name__ == '__main__':
test()