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

186 lines
6.8 KiB
Python

"""
SMEM-P Coordinate Verification Test.
Writes a known pattern to sP using the coordinate-indexed approach
(identical to FmhaKernel's SMEM-P path), then reads sP back
via a simple SMEM→GMEM copy and verifies on the host.
Pattern: sP[m, k] = float(m*128 + k) (unique value per position)
Expected: output[m, k] = float(m*128 + k) after round-trip
If coordinates are correct, all values match.
If coordinates are wrong, values will be at different positions.
"""
import torch, math
import cutlass, cutlass.cute as cute
import cutlass.utils as utils
from cutlass.cute.nvgpu import tcgen05
from cutlass import Float32, BFloat16, Int32, const_expr
from cutlass.utils import LayoutEnum
import cutlass.torch as ct
import cuda.bindings.driver as cuda
import cutlass.pipeline as pipeline
@cute.jit
def smem_p_coord_test(
mOut, qk_mma, pv_mma, qk_mma_tiler, pv_mma_tiler, p_smem_s
):
"""Write known pattern to sP using coordinate-indexed approach, read back."""
tidx, _, _ = cute.arch.thread_idx()
warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
# 5 warps: 4 softmax + 1 for TMEM alloc
if warp_idx >= 5:
return
# SMEM allocation
smem = utils.SmemAllocator()
tmem_bar = pipeline.NamedBarrier(barrier_id=2, num_threads=32 * 5)
sP = smem.allocate_tensor(element_type=BFloat16, layout=p_smem_s.outer, byte_alignment=128, swizzle=p_smem_s.inner)
sP_nostage = sP[(None, None, None, 0)]
# TMEM allocation
tmem = utils.TmemAllocator(None, barrier_for_retrieve=tmem_bar, allocator_warp_id=4, is_two_cta=False)
if warp_idx == 4:
tmem.allocate(128)
tmem.wait_for_alloc()
tmem_ptr = tmem.retrieve_ptr(Float32)
# QK C-fragment (for TMEM layout)
qk_thr = qk_mma.get_slice(0)
qk_as = qk_thr.partition_shape_C(qk_mma_tiler[:2])
tStS = qk_thr.make_fragment_C(qk_as)
tStS0 = cute.make_tensor(tStS.iterator, tStS.layout)
# TMEM-load copy (same as FmhaKernel)
tmem_load_atom = cute.make_copy_atom(tcgen05.copy.Ld32x32bOp(tcgen05.copy.Repetition(32)), Float32)
tiled_tmem_load = tcgen05.make_tmem_copy(tmem_load_atom, tStS0)
# Softmax warps: write known pattern to sP
if warp_idx < 4:
sfw_idx = tidx % 128 # 4 softmax warps
thr_load = tiled_tmem_load.get_slice(sfw_idx)
# Coordinate identity tensor
cS = cute.make_identity_tensor((128, 128))
tScS = qk_thr.partition_C(cS)
tTMEM_LOADcS = thr_load.partition_D(tScS)
# Write known pattern to sP using coordinate-indexed approach
for j0 in range(32):
for j1 in range(4):
coord = tTMEM_LOADcS[(j0, 0), j1, 0, 0]
m_coord = coord[0]
k_coord = coord[1]
k0 = k_coord % 16
k1 = (k_coord // 16) % 4
k2 = k_coord // 64
# Pattern: value = (m * 128 + k) as BF16
val = BFloat16(m_coord * 128 + k_coord)
sP_nostage[(m_coord, k0), 0, (k1, k2)] = val
cute.arch.fence_proxy("async.shared", space="cta")
# Barrier between softmax writes and read-back
sync_bar = pipeline.NamedBarrier(barrier_id=3, num_threads=32 * 5)
sync_bar.arrive_and_wait()
# Read sP back to global memory
# Thread 0 of warp 0 does the read (simple sequential access)
if tidx == 0:
gOut = mOut # (128, 128) output
for m in range(128):
for k in range(128):
k0 = k % 16
k1 = (k // 16) % 4
k2 = k // 64
val = sP_nostage[(m, k0), 0, (k1, k2)]
gOut[m, k] = val
if warp_idx < 4:
tmem.relinquish_alloc_permit()
tmem.free(tmem_ptr)
def main():
head_dim = 256
s_k = 128
m = 128
pv_n_tile = min(head_dim, 256)
# Create tensors for layout derivation
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, head_dim, 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))
v_tile = v[:, 0:pv_n_tile].contiguous().unsqueeze(-1)
mV = ct.from_dlpack(v_tile).mark_layout_dynamic(leading_dim=ct.get_leading_dim(v_tile))
a_major = LayoutEnum.from_tensor(mQ).mma_major_mode()
b_major = LayoutEnum.from_tensor(mK).mma_major_mode()
v_major = LayoutEnum.from_tensor(mV).mma_major_mode()
qk_mma = utils.sm100.make_trivial_tiled_mma(
BFloat16, BFloat16, a_major, b_major, Float32,
tcgen05.CtaGroup.ONE, (128, 128), tcgen05.OperandSource.SMEM
)
pv_mma = utils.sm100.make_trivial_tiled_mma(
BFloat16, BFloat16, a_major, v_major, Float32,
tcgen05.CtaGroup.ONE, (128, pv_n_tile), tcgen05.OperandSource.SMEM
)
qk_ik = cute.size(qk_mma.shape_mnk, mode=[2])
qk_mma_tiler = (128, 128, qk_ik * 4)
pv_ik = cute.size(pv_mma.shape_mnk, mode=[2])
pv_mma_tiler = (128, pv_n_tile, pv_ik * (128 // pv_ik))
p_smem_s = utils.sm100.make_smem_layout_a(pv_mma, pv_mma_tiler, BFloat16, 1)
# Output tensor
out = torch.zeros(128, 128, dtype=torch.bfloat16, device='cuda')
mOut = ct.from_dlpack(out).mark_layout_dynamic(leading_dim=ct.get_leading_dim(out))
stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
print("Compiling...")
compiled = cute.compile(smem_p_coord_test, mOut, qk_mma, pv_mma, qk_mma_tiler, pv_mma_tiler, p_smem_s)
print("Running...")
compiled(mOut, qk_mma, pv_mma, qk_mma_tiler, pv_mma_tiler, p_smem_s, stream)
torch.cuda.synchronize()
# Verify: out[m, k] should be m*128 + k
out_float = out.float()
expected = torch.arange(128, dtype=torch.float32, device='cuda').unsqueeze(0) * 128 + torch.arange(128, dtype=torch.float32, device='cuda').unsqueeze(1)
# Check a few values
print(f"\n=== Verification ===")
n_correct = 0
n_total = 128 * 128
for m in range(128):
for k in range(128):
exp = float(m * 128 + k)
got = out_float[m, k].item()
if abs(got - exp) < 1.0: # BF16 precision
n_correct += 1
elif n_correct > 0 and n_correct < 5:
print(f" MISMATCH at ({m},{k}): expected {exp}, got {got}")
accuracy = n_correct / n_total * 100
print(f" Accuracy: {n_correct}/{n_total} = {accuracy:.1f}%")
# Print a few sample values
print(f"\n Sample values:")
for m in [0, 1, 2, 64, 127]:
for k in [0, 1, 16, 32, 64, 127]:
exp = float(m * 128 + k)
got = out_float[m, k].item()
status = "" if abs(got - exp) < 1.0 else ""
print(f" [{m},{k}] expected={exp:.0f} got={got:.1f} {status}")
if __name__ == '__main__':
main()