_cutlass_nvfp4_moe_rows_test.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. #!/usr/bin/env python3
  2. """T7: FlashInfer CUTLASS NVFP4 fused MoE on GB10 -- row consistency vs M.
  3. Mirrors FlashInferExperts.apply (vllm/model_executor/layers/fused_moe/experts/
  4. flashinfer_cutlass_moe.py) for the nvfp4 path at GLM-5.3-Flash's per-rank
  5. shape (E=288, K=4096, N=512, top-k 8, swiglu_limit 10): pre-quantized
  6. activation via ops.scaled_fp4_quant (swizzled sf), [w3;w1] packed weights,
  7. swizzle_blockscale'd block scales viewed as int32, per-expert global scales,
  8. output= preallocated.
  9. For each M, every batched row is compared with the same row run alone (M=1,
  10. same routing). A row that disagrees is checked against every other row's
  11. solo output to detect row mixing.
  12. """
  13. import sys
  14. import torch
  15. from vllm import _custom_ops as ops
  16. from vllm.model_executor.layers.fused_moe.activation import MoEActivation
  17. from vllm.model_executor.layers.quantization.utils.flashinfer_utils import (
  18. activation_to_flashinfer_type,
  19. )
  20. from vllm.model_executor.layers.quantization.utils.nvfp4_utils import swizzle_blockscale
  21. from vllm.model_executor.layers.quantization.utils.flashinfer_fp4_moe import (
  22. reorder_w1w3_to_w3w1,
  23. )
  24. from vllm.utils.flashinfer import flashinfer_cutlass_fused_moe
  25. torch.manual_seed(0)
  26. dev = torch.device("cuda")
  27. E, K, N, TOPK = 288, 4096, 512, 8
  28. fp8 = torch.float8_e4m3fn
  29. # ---- synthetic weights in checkpoint layout, then kernel layout ----
  30. g = torch.Generator(device=dev).manual_seed(7)
  31. w13 = torch.randint(0, 256, (E, 2 * N, K // 2), device=dev, dtype=torch.uint8, generator=g)
  32. w2 = torch.randint(0, 256, (E, K, N // 2), device=dev, dtype=torch.uint8, generator=g)
  33. w13_scale = (torch.rand(E, 2 * N, K // 16, device=dev, generator=g) * 1.5 + 0.5).to(fp8)
  34. w2_scale = (torch.rand(E, K, N // 16, device=dev, generator=g) * 1.5 + 0.5).to(fp8)
  35. w13_scale_2 = torch.full((E,), 1.0 / 96, device=dev) # weight global scale
  36. w2_scale_2 = torch.full((E,), 1.0 / 96, device=dev)
  37. w13_input_scale = torch.full((E,), 8.0 / (448 * 6), device=dev) # amax 8 -> ModelOpt convention
  38. w2_input_scale = torch.full((E,), 8.0 / (448 * 6), device=dev)
  39. w13_k, w13_scale_k = reorder_w1w3_to_w3w1(w13.clone(), w13_scale.clone())
  40. w13_scale_k = swizzle_blockscale(w13_scale_k)
  41. w2_scale_k = swizzle_blockscale(w2_scale.clone())
  42. g1_alphas = (w13_scale_2 * w13_input_scale).contiguous()
  43. g2_alphas = (w2_scale_2 * w2_input_scale).contiguous()
  44. a1_gscale = (1.0 / w13_input_scale).contiguous()
  45. a2_gscale = (1.0 / w2_input_scale).contiguous()
  46. swiglu_limit = torch.full((E,), 10.0, device=dev, dtype=torch.float32)
  47. act = activation_to_flashinfer_type(MoEActivation.SILU)
  48. quant_scales = [a1_gscale, w13_scale_k.view(torch.int32), g1_alphas,
  49. a2_gscale, w2_scale_k.view(torch.int32), g2_alphas]
  50. fc1 = w13_k.view(torch.long)
  51. fc2 = w2.view(torch.long)
  52. def moe(x_bf16, topk_ids, topk_w, prequant=True, tp_size=1, tp_rank=0):
  53. M = x_bf16.shape[0]
  54. out = torch.empty(M, K, device=dev, dtype=torch.bfloat16)
  55. if prequant:
  56. xq, xsf = ops.scaled_fp4_quant(x_bf16, a1_gscale[:1].reshape(()).clone(), is_sf_swizzled_layout=True)
  57. inp, sf = xq, xsf
  58. else:
  59. inp, sf = x_bf16, None
  60. flashinfer_cutlass_fused_moe(
  61. input=inp,
  62. token_selected_experts=topk_ids.to(torch.int),
  63. token_final_scales=topk_w,
  64. fc1_expert_weights=fc1,
  65. fc2_expert_weights=fc2,
  66. fc1_expert_biases=None,
  67. fc2_expert_biases=None,
  68. swiglu_alpha=None,
  69. swiglu_beta=None,
  70. swiglu_limit=swiglu_limit,
  71. output=out,
  72. output_dtype=torch.bfloat16,
  73. quant_scales=quant_scales,
  74. input_sf=sf,
  75. tp_size=tp_size,
  76. tp_rank=tp_rank,
  77. ep_size=1,
  78. ep_rank=0,
  79. activation_type=act,
  80. use_deepseek_fp8_block_scale=False,
  81. use_mxfp8_act_scaling=False,
  82. use_w4_group_scaling=False,
  83. )
  84. return out
  85. def routing(M, gen):
  86. ids = torch.stack([torch.randperm(E, device=dev, generator=gen)[:TOPK] for _ in range(M)])
  87. w = torch.rand(M, TOPK, device=dev, generator=gen) + 0.1
  88. w = (w / w.sum(-1, keepdim=True)) * 2.5 # routed_scaling_factor
  89. return ids.to(torch.int32), w.to(torch.float32)
  90. def check(prequant, tp_size=1, Ms=(1, 2, 4, 8, 16, 32, 64), trials=12, tag=""):
  91. gen = torch.Generator(device=dev).manual_seed(11)
  92. total_bad = 0
  93. for M in Ms:
  94. worst = 0.0
  95. bad_rows = 0
  96. mixes = []
  97. nondet = 0.0
  98. for t in range(trials):
  99. x = torch.randn(M, K, device=dev, generator=gen).to(torch.bfloat16) * 1.5
  100. ids, w = routing(M, gen)
  101. outB = moe(x, ids, w, prequant, tp_size)
  102. outB2 = moe(x, ids, w, prequant, tp_size)
  103. nondet = max(nondet, (outB.float() - outB2.float()).abs().max().item())
  104. solo = torch.stack([moe(x[r : r + 1], ids[r : r + 1], w[r : r + 1], prequant, tp_size)[0] for r in range(M)])
  105. torch.cuda.synchronize()
  106. for r in range(M):
  107. scale = solo[r].float().abs().max().item() + 1e-6
  108. err = (outB[r].float() - solo[r].float()).abs().max().item() / scale
  109. worst = max(worst, err)
  110. if err > 0.05:
  111. bad_rows += 1
  112. # does the batched row match some other row's solo output?
  113. d = (solo.float() - outB[r].float()[None]).abs().amax(dim=1) / scale
  114. j = int(d.argmin())
  115. mixes.append((t, r, round(err, 3), j, round(float(d[j]), 3)))
  116. total_bad += bad_rows
  117. print(f" [{tag}] M={M:3d}: worst row err vs solo {worst:.4f}, bad rows {bad_rows}/{M * trials}, "
  118. f"repeat-call nondeterminism max|d| {nondet:.4g}" + (f", mixes(trial,row,err,best_row,best_err)={mixes[:6]}" if mixes else ""))
  119. return total_bad
  120. bad = 0
  121. print("== production path: pre-quantized nvfp4 input + swizzled input_sf, tp_size=1 ==")
  122. bad += check(True, 1, tag="prequant")
  123. print("== bf16 input (kernel-internal quant), tp_size=1 ==")
  124. bad += check(False, 1, tag="bf16in")
  125. print("== production tp flags: prequant, tp_size=4 tp_rank=0 ==")
  126. bad += check(True, 4, Ms=(1, 4, 8, 16), trials=8, tag="prequant-tp4")
  127. print("T7:", "PASS" if bad == 0 else f"FAIL ({bad} bad rows)")
  128. sys.exit(0 if bad == 0 else 1)