nvfp4_kernel.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. """NVFP4 codebook MoE GEMM (Triton) — reads the checkpoint's NATIVE 2-fp4/byte packing DIRECTLY
  2. (no convert/prepare/compaction). W[e,n,k] = e2m1(nibble) * block_scale[e,n,k//16] * global[e].
  3. Sibling of nf3_kernel: same sorted-token grouped GEMM (reuses vLLM moe_align_block_size) with inline
  4. unpack + dequant. Graph-safe. Importable: nvfp4_moe_layer(hidden, w13,s13,g13, w2,s2,g2,
  5. topk_ids, topk_w, emap, Etot, H, I). nibble/e2m1 convention matches vLLM dequantize_to_dtype(swizzle=False)."""
  6. import torch, triton, triton.language as tl
  7. @triton.jit
  8. def _e2m1_mag(mag):
  9. """3-bit magnitude index -> e2m1 magnitude [0,.5,1,1.5,2,3,4,6] (binary-tree, matches vLLM)."""
  10. b2 = (mag >> 2) & 1
  11. b1 = (mag >> 1) & 1
  12. b0 = mag & 1
  13. low = tl.where(b1 == 1, tl.where(b0 == 1, 1.5, 1.0), tl.where(b0 == 1, 0.5, 0.0))
  14. high = tl.where(b1 == 1, tl.where(b0 == 1, 6.0, 4.0), tl.where(b0 == 1, 3.0, 2.0))
  15. return tl.where(b2 == 1, high, low)
  16. @triton.jit
  17. def nvfp4_moe_kernel(
  18. a_ptr, b_ptr, s_ptr, g_ptr, c_ptr, tw_ptr,
  19. sorted_ids_ptr, expert_ids_ptr, ntpp_ptr,
  20. N, K, EM, num_valid_tokens,
  21. stride_am, stride_ak,
  22. stride_be, stride_bn, stride_bk, # packed weight [E, N, K//2] uint8 (2 fp4/byte)
  23. stride_se, stride_sn, stride_sk, # block scale [E, N, K//GS] fp8_e4m3
  24. stride_cm, stride_cn,
  25. top_k: tl.constexpr, MUL_W: tl.constexpr, GS: tl.constexpr,
  26. BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr,
  27. compute_type: tl.constexpr,
  28. ):
  29. pid = tl.program_id(0)
  30. num_pid_n = tl.cdiv(N, BLOCK_N)
  31. pid_m = pid // num_pid_n
  32. pid_n = pid % num_pid_n
  33. if pid_m * BLOCK_M >= tl.load(ntpp_ptr):
  34. return
  35. offs_tid = pid_m * BLOCK_M + tl.arange(0, BLOCK_M)
  36. offs_token = tl.load(sorted_ids_ptr + offs_tid)
  37. tok_mask = offs_token < num_valid_tokens
  38. e = tl.load(expert_ids_ptr + pid_m)
  39. if e == -1:
  40. return
  41. g = tl.load(g_ptr + e).to(tl.float32) # per-expert global scale
  42. offs_bn = (pid_n * BLOCK_N + tl.arange(0, BLOCK_N)) % N
  43. offs_k = tl.arange(0, BLOCK_K)
  44. a_ptrs = a_ptr + (offs_token[:, None] // top_k * stride_am + offs_k[None, :] * stride_ak)
  45. acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32)
  46. for kb in range(0, tl.cdiv(K, BLOCK_K)):
  47. kk = kb * BLOCK_K + offs_k # [BLOCK_K]
  48. kmask = kk < K
  49. a = tl.load(a_ptrs, mask=tok_mask[:, None] & (kk[None, :] < K), other=0.0)
  50. # nvfp4: 2 fp4/byte. byte at k//2; low nibble = even col, high nibble = odd col.
  51. byte = tl.load(b_ptr + e * stride_be + offs_bn[None, :] * stride_bn
  52. + (kk[:, None] // 2) * stride_bk).to(tl.int32) # [BLOCK_K, BLOCK_N]
  53. sh = (4 * (kk[:, None] % 2)).to(tl.int32)
  54. nib = (byte >> sh) & 0xF
  55. sign = (nib >> 3) & 1
  56. mag = nib & 0x7
  57. v = tl.where(sign == 1, -_e2m1_mag(mag), _e2m1_mag(mag))
  58. sb = e * stride_se + offs_bn[None, :] * stride_sn + (kk[:, None] // GS) * stride_sk
  59. s = tl.load(s_ptr + sb, mask=kmask[:, None], other=0.0).to(tl.float32)
  60. b = (v * s * g).to(compute_type)
  61. acc = tl.dot(a, b, acc=acc)
  62. a_ptrs += BLOCK_K * stride_ak
  63. if MUL_W:
  64. w = tl.load(tw_ptr + offs_token, mask=tok_mask, other=0.0)
  65. acc = acc * w[:, None]
  66. c_ptrs = c_ptr + stride_cm * offs_token[:, None] + stride_cn * offs_bn[None, :]
  67. tl.store(c_ptrs, acc.to(compute_type), mask=tok_mask[:, None])
  68. _E2M1 = [0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0]
  69. def ref_dequant(weight_u8, bscale, gscale, GS):
  70. """[E,N,K//2] u8 + [E,N,K//GS] fp8 + [E] f32 -> [E,N,K] f32 (matches dequantize_to_dtype)."""
  71. E, N, Kp = weight_u8.shape
  72. K = Kp * 2
  73. lut = torch.tensor(_E2M1, device=weight_u8.device)
  74. low = (weight_u8 & 0xF).to(torch.int64)
  75. high = ((weight_u8 >> 4) & 0xF).to(torch.int64)
  76. def deq(nib):
  77. return lut[nib & 0x7] * torch.where((nib >> 3) & 1 == 1, -1.0, 1.0)
  78. w = torch.stack([deq(low), deq(high)], -1).reshape(E, N, K) # interleave low,high
  79. s = bscale.float().repeat_interleave(GS, dim=2)
  80. return w * s * gscale.float().view(E, 1, 1)
  81. def run_kernel(a, b, s, g, sorted_ids, expert_ids, ntpp, N, K, top_k, GS,
  82. mul_w=False, tw=None, out=None, BM=16, BN=64, BK=32, warps=4):
  83. M = a.shape[0]
  84. c = out if out is not None else torch.zeros(M * top_k, N, dtype=torch.bfloat16, device=a.device)
  85. grid = (triton.cdiv(sorted_ids.shape[0], BM) * triton.cdiv(N, BN),)
  86. nvfp4_moe_kernel[grid](
  87. a, b, s, g, c, tw if tw is not None else a,
  88. sorted_ids, expert_ids, ntpp,
  89. N, K, sorted_ids.shape[0], M * top_k,
  90. a.stride(0), a.stride(1),
  91. b.stride(0), b.stride(1), b.stride(2),
  92. s.stride(0), s.stride(1), s.stride(2),
  93. c.stride(0), c.stride(1),
  94. top_k=top_k, MUL_W=mul_w, GS=GS, BLOCK_M=BM, BLOCK_N=BN, BLOCK_K=BK,
  95. compute_type=tl.bfloat16, num_warps=warps,
  96. )
  97. return c
  98. def nvfp4_moe_layer(hidden, w13, s13, g13, w2, s2, g2, topk_ids, topk_w, emap,
  99. Etot, H, I, GS=16, BM=16):
  100. """Full NVFP4 MoE layer -> [M,H]. w13 [Km,2I,H//2] u8, s13 [Km,2I,H//GS] fp8, g13 [Km] f32."""
  101. import torch.nn.functional as F
  102. from vllm.model_executor.layers.fused_moe.moe_align_block_size import moe_align_block_size
  103. M, tk = topk_ids.shape
  104. sids, eids, ntpp = moe_align_block_size(topk_ids.to(torch.int32), BM, Etot, expert_map=emap)
  105. sids, eids, ntpp = sids.to(torch.int32), eids.to(torch.int32), ntpp.to(torch.int32)
  106. c1 = run_kernel(hidden, w13, s13, g13, sids, eids, ntpp, 2 * I, H, tk, GS) # [M*tk, 2I]
  107. gg, uu = c1.chunk(2, -1)
  108. c2 = (F.silu(gg.float()) * uu.float()).to(torch.bfloat16) # [M*tk, I]
  109. c3 = run_kernel(c2, w2, s2, g2, sids, eids, ntpp, H, I, 1, GS,
  110. mul_w=True, tw=topk_w.reshape(-1).float()) # [M*tk, H]
  111. return c3.view(M, tk, H).sum(1)
  112. if __name__ == "__main__":
  113. import torch.nn.functional as F
  114. from vllm.model_executor.layers.fused_moe.moe_align_block_size import moe_align_block_size
  115. torch.manual_seed(0); dev = "cuda"; GS = 16
  116. # TEST1: single expert vs reference dequant
  117. E, N, K = 1, 128, 256
  118. wu8 = torch.randint(0, 256, (E, N, K // 2), device=dev, dtype=torch.uint8)
  119. bs = (torch.rand(E, N, K // GS, device=dev) * 200 + 1).to(torch.float8_e4m3fn)
  120. gs = (torch.rand(E, device=dev) * 1e-4 + 1e-5).to(torch.float32)
  121. a = torch.randn(16, K, device=dev, dtype=torch.bfloat16)
  122. W = ref_dequant(wu8, bs, gs, GS)[0]
  123. ref = (a.float() @ W.t()).to(torch.bfloat16)
  124. sids = torch.arange(16, dtype=torch.int32, device=dev)
  125. eids = torch.zeros(1, dtype=torch.int32, device=dev); ntpp = torch.tensor([16], dtype=torch.int32, device=dev)
  126. out = run_kernel(a, wu8, bs, gs, sids, eids, ntpp, N, K, 1, GS)[:16]
  127. r1 = (out.float() - ref.float()).abs().sum() / (ref.float().abs().sum() + 1e-9)
  128. print(f"TEST1 single-expert: rel={r1:.5f} {'PASS' if r1 < 0.03 else 'FAIL'}")
  129. # TEST4: full MoE layer (kept-tier shape) vs reference
  130. Etot, Kept = 256, 64; Km = Etot - Kept
  131. emap = torch.full((Etot,), -1, dtype=torch.int32, device=dev) # -1 = skip (moe_align convention)
  132. emap[:Kept] = torch.arange(Kept, dtype=torch.int32, device=dev) # kept globals [0,64)->local
  133. H, I, tk, M = 512, 128, 8, 8
  134. Kn = Kept
  135. w13 = torch.randint(0, 256, (Kn, 2 * I, H // 2), device=dev, dtype=torch.uint8)
  136. s13 = (torch.rand(Kn, 2 * I, H // GS, device=dev) * 200 + 1).to(torch.float8_e4m3fn)
  137. g13 = (torch.rand(Kn, device=dev) * 1e-4 + 1e-5).to(torch.float32)
  138. w2 = torch.randint(0, 256, (Kn, H, I // 2), device=dev, dtype=torch.uint8)
  139. s2 = (torch.rand(Kn, H, I // GS, device=dev) * 200 + 1).to(torch.float8_e4m3fn)
  140. g2 = (torch.rand(Kn, device=dev) * 1e-4 + 1e-5).to(torch.float32)
  141. hid = torch.randn(M, H, device=dev, dtype=torch.bfloat16) * 0.5
  142. tids = torch.randint(0, Etot, (M, tk), device=dev, dtype=torch.int32)
  143. tw = torch.softmax(torch.rand(M, tk, device=dev), -1)
  144. out4 = nvfp4_moe_layer(hid, w13, s13, g13, w2, s2, g2, tids, tw, emap, Etot, H, I, GS)
  145. W13, W2 = ref_dequant(w13, s13, g13, GS), ref_dequant(w2, s2, g2, GS)
  146. ref4 = torch.zeros(M, H, device=dev)
  147. for m in range(M):
  148. for slot in range(tk):
  149. e = int(tids[m, slot])
  150. if e >= Kept:
  151. continue
  152. inter = hid[m].float() @ W13[e].t(); ga, ub = inter.chunk(2, -1)
  153. ref4[m] += tw[m, slot].float() * ((F.silu(ga) * ub) @ W2[e].t())
  154. r4 = (out4.float() - ref4).abs().sum() / (ref4.abs().sum() + 1e-9)
  155. print(f"TEST4 full-MoE-layer: rel={r4:.5f} {'PASS' if r4 < 0.03 else 'FAIL'}")