_cutlass_nvfp4_moe_sanity_test.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. #!/usr/bin/env python3
  2. """T7b: sanity for T7 -- are the CUTLASS NVFP4 MoE outputs real (finite,
  3. non-zero, input-dependent) and do they agree with a torch dequantised
  4. reference? Same synthetic layer as kt7.py."""
  5. import sys
  6. import torch
  7. sys.argv = [sys.argv[0]]
  8. exec(open("/kv/kt7.py").read().split("def routing(")[0]) # reuse weights + moe()
  9. FP4_LUT = torch.tensor([0, 0.5, 1, 1.5, 2, 3, 4, 6], device=dev)
  10. def deq_fp4(packed: torch.Tensor, scale_fp8: torch.Tensor, gscale: torch.Tensor) -> torch.Tensor:
  11. """packed uint8 [..., K/2], scale [..., K/16] fp8, gscale scalar -> float [..., K]."""
  12. lo = packed & 0xF
  13. hi = packed >> 4
  14. nib = torch.stack([lo, hi], dim=-1).reshape(*packed.shape[:-1], packed.shape[-1] * 2)
  15. mag = FP4_LUT[(nib & 7).long()]
  16. sign = torch.where((nib & 8) != 0, -1.0, 1.0)
  17. vals = mag * sign
  18. sc = scale_fp8.float().repeat_interleave(16, dim=-1)
  19. return vals * sc * gscale
  20. def quant_deq_act(x: torch.Tensor, gscale_inv: float) -> torch.Tensor:
  21. """Emulate nvfp4 activation quant: per-16 block fp8 scale, global scale."""
  22. M, Kd = x.shape
  23. xb = x.float().view(M, Kd // 16, 16)
  24. amax = xb.abs().amax(-1, keepdim=True)
  25. # block scale (fp8) = amax / 6 * global; ModelOpt: sf = amax/6 / input_scale
  26. sf = (amax / 6.0 * gscale_inv).to(fp8).float()
  27. sf = torch.where(sf == 0, torch.ones_like(sf), sf)
  28. q = (xb / (sf / gscale_inv)).clamp(-6, 6)
  29. # round to e2m1 grid
  30. grid = torch.tensor([0, 0.5, 1, 1.5, 2, 3, 4, 6], device=x.device)
  31. idx = (q.abs().unsqueeze(-1) - grid).abs().argmin(-1)
  32. qv = grid[idx] * torch.sign(q)
  33. return (qv * (sf / gscale_inv)).view(M, Kd)
  34. gen = torch.Generator(device=dev).manual_seed(3)
  35. M = 8
  36. x = torch.randn(M, K, device=dev, generator=gen).to(torch.bfloat16) * 1.5
  37. ids = torch.stack([torch.randperm(E, device=dev, generator=gen)[:TOPK] for _ in range(M)]).to(torch.int32)
  38. w = torch.rand(M, TOPK, device=dev, generator=gen) + 0.1
  39. w = (w / w.sum(-1, keepdim=True)) * 2.5
  40. out = moe(x, ids, w, True, 1)
  41. torch.cuda.synchronize()
  42. print("out finite:", torch.isfinite(out).all().item(), "abs mean:", out.float().abs().mean().item(),
  43. "abs max:", out.float().abs().max().item(), "zero rows:", int((out.float().abs().amax(1) == 0).sum()))
  44. out_b = moe(x, ids, w, False, 1)
  45. print("bf16-input variant vs prequant: max |d| =", (out.float() - out_b.float()).abs().max().item())
  46. x2 = x.clone(); x2[0] = torch.randn(K, device=dev, generator=gen).to(torch.bfloat16)
  47. out2 = moe(x2, ids, w, True, 1)
  48. print("changing row 0 input changes row 0 output:", (out2[0].float() - out[0].float()).abs().max().item() > 1e-3,
  49. "and leaves other rows unchanged:", (out2[1:].float() - out[1:].float()).abs().max().item())
  50. # torch reference for a few rows
  51. a1_inv = float(1.0 / a1_gscale[0]) # = w13_input_scale
  52. xd = quant_deq_act(x, a1_inv)
  53. ref = torch.zeros(M, K, device=dev)
  54. for r in range(M):
  55. for j in range(TOPK):
  56. e = int(ids[r, j])
  57. W13 = deq_fp4(w13[e], w13_scale[e], float(w13_scale_2[e])) # [2N, K] rows: [w1(gate); w3(up)]
  58. W2 = deq_fp4(w2[e], w2_scale[e], float(w2_scale_2[e])) # [K, N]
  59. gate = xd[r] @ W13[:N].T
  60. up = xd[r] @ W13[N:].T
  61. gate = gate.clamp(max=10.0)
  62. up = up.clamp(-10.0, 10.0)
  63. h = torch.nn.functional.silu(gate) * up
  64. hd = quant_deq_act(h[None], float(w2_input_scale[e]))[0]
  65. ref[r] += w[r, j] * (hd @ W2.T)
  66. err = (out.float() - ref).abs().amax(1) / (ref.abs().amax(1) + 1e-6)
  67. print("per-row rel err vs torch reference:", [round(v, 3) for v in err.tolist()])
  68. print("ref abs mean:", ref.abs().mean().item())