_cutlass_nvfp4_moe_realweights_test.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. #!/usr/bin/env python3
  2. """T10: FlashInfer CUTLASS NVFP4 fused MoE VALUES on real GLM-5.3-Flash experts.
  3. Loads 16 experts of layer 5 (extracted read-only from a checkpoint into
  4. /kv/moe-l5-<name>.safetensors), builds the kernel-format weights exactly as
  5. vLLM's FLASHINFER_CUTLASS path does (reorder [w1;w3]->[w3;w1], swizzled block
  6. scales, g1_alphas = weight_scale_2 * input_scale, a1_gscale = 1/input_scale),
  7. routes random tokens among those experts, and compares the kernel against:
  8. ref_a4 : dequantised weights (vLLM's dequantize_to_dtype, linear scales),
  9. activations quantised by vLLM's own scaled_fp4_quant with the
  10. checkpoint's global input scales and dequantised -- what the
  11. kernel is supposed to compute (w4a4)
  12. ref_a16: dequantised weights, bf16 activations -- the w4a16 quantity
  13. (what the marlin backend computes)
  14. Both checkpoints are supported: ModelOpt naming (weight/weight_scale/
  15. weight_scale_2/input_scale) and compressed-tensors naming (weight_packed/
  16. weight_scale/weight_global_scale/input_global_scale, reciprocal convention).
  17. usage: python3 this.py [nvidia|rh]
  18. """
  19. import sys
  20. import torch
  21. from safetensors.torch import load_file
  22. from vllm import _custom_ops as ops
  23. from vllm.model_executor.layers.fused_moe.activation import MoEActivation
  24. from vllm.model_executor.layers.quantization.utils.flashinfer_utils import (
  25. activation_to_flashinfer_type,
  26. )
  27. from vllm.model_executor.layers.quantization.utils.nvfp4_utils import swizzle_blockscale
  28. from vllm.model_executor.layers.quantization.utils.flashinfer_fp4_moe import (
  29. reorder_w1w3_to_w3w1,
  30. )
  31. from vllm.model_executor.layers.quantization.utils.nvfp4_emulation_utils import (
  32. dequantize_to_dtype,
  33. )
  34. from vllm.utils.flashinfer import flashinfer_cutlass_fused_moe
  35. name = sys.argv[1] if len(sys.argv) > 1 else "nvidia"
  36. dev = torch.device("cuda")
  37. torch.manual_seed(0)
  38. E_REAL, E, K, N, TOPK = 16, 288, 4096, 2048, 8
  39. LIMIT = 10.0
  40. fp8 = torch.float8_e4m3fn
  41. sd = load_file(f"/kv/moe-l5-{name}.safetensors", device="cuda")
  42. pre = next(k for k in sd if ".experts.0." in k).split(".experts.0.")[0] + ".experts."
  43. def get(e, proj, kind):
  44. """kind in weight, scale, gscale (weight_scale_2 convention), iscale (input_scale convention)."""
  45. base = f"{pre}{e}.{proj}."
  46. if kind == "weight":
  47. return sd.get(base + "weight", sd.get(base + "weight_packed"))
  48. if kind == "scale":
  49. return sd[base + "weight_scale"]
  50. if kind == "gscale":
  51. return sd[base + "weight_scale_2"].reshape(()) if base + "weight_scale_2" in sd else 1.0 / sd[base + "weight_global_scale"].reshape(())
  52. if kind == "iscale":
  53. return sd[base + "input_scale"].reshape(()) if base + "input_scale" in sd else 1.0 / sd[base + "input_global_scale"].reshape(())
  54. raise KeyError(kind)
  55. # checkpoint-layout tensors, padded to E experts (zeros beyond the real ones)
  56. w13 = torch.zeros(E, 2 * N, K // 2, dtype=torch.uint8, device=dev)
  57. w13_scale = torch.ones(E, 2 * N, K // 16, device=dev).to(fp8)
  58. w2 = torch.zeros(E, K, N // 2, dtype=torch.uint8, device=dev)
  59. w2_scale = torch.ones(E, K, N // 16, device=dev).to(fp8)
  60. w13_gs = torch.ones(E, device=dev)
  61. w2_gs = torch.ones(E, device=dev)
  62. a13 = torch.zeros(E_REAL, device=dev)
  63. a2 = torch.zeros(E_REAL, device=dev)
  64. for e in range(E_REAL):
  65. w13[e, :N] = get(e, "gate_proj", "weight"); w13[e, N:] = get(e, "up_proj", "weight")
  66. w13_scale[e, :N] = get(e, "gate_proj", "scale"); w13_scale[e, N:] = get(e, "up_proj", "scale")
  67. g1, g3 = float(get(e, "gate_proj", "gscale")), float(get(e, "up_proj", "gscale"))
  68. assert abs(g1 - g3) <= 1e-9 * max(abs(g1), 1e-30), f"gate/up weight_scale_2 differ for expert {e}: {g1} vs {g3}"
  69. w13_gs[e] = g1
  70. w2[e] = get(e, "down_proj", "weight"); w2_scale[e] = get(e, "down_proj", "scale"); w2_gs[e] = float(get(e, "down_proj", "gscale"))
  71. a13[e] = max(float(get(e, "gate_proj", "iscale")), float(get(e, "up_proj", "iscale")))
  72. a2[e] = float(get(e, "down_proj", "iscale"))
  73. print(f"[{name}] input_scale gate/up per expert: min {a13.min():.6g} max {a13.max():.6g}; down: min {a2.min():.6g} max {a2.max():.6g}")
  74. print(f"[{name}] weight_scale_2 w13: {w13_gs[:E_REAL].min():.4g}..{w13_gs[:E_REAL].max():.4g}; w2: {w2_gs[:E_REAL].min():.4g}..{w2_gs[:E_REAL].max():.4g}")
  75. # FLASHINFER_CUTLASS uses one global activation scale = max over experts (prepare_nvfp4_moe_layer_for_fi_or_cutlass)
  76. a13_g = a13.max().item(); a2_g = a2.max().item()
  77. w13_input_scale = torch.full((E,), a13_g, device=dev); w2_input_scale = torch.full((E,), a2_g, device=dev)
  78. # kernel format (prepare_nvfp4_moe_layer_for_fi_or_cutlass + FlashInferExperts.process_weights_after_loading)
  79. w13_k, w13_scale_k = reorder_w1w3_to_w3w1(w13.clone(), w13_scale.clone())
  80. w13_scale_k = swizzle_blockscale(w13_scale_k); w2_scale_k = swizzle_blockscale(w2_scale.clone())
  81. g1_alphas = (w13_gs * w13_input_scale).contiguous(); g2_alphas = (w2_gs * w2_input_scale).contiguous()
  82. a1_gscale = (1.0 / w13_input_scale).contiguous(); a2_gscale = (1.0 / w2_input_scale).contiguous()
  83. quant_scales = [a1_gscale, w13_scale_k.view(torch.int32), g1_alphas, a2_gscale, w2_scale_k.view(torch.int32), g2_alphas]
  84. swiglu_limit = torch.full((E,), LIMIT, device=dev)
  85. act = activation_to_flashinfer_type(MoEActivation.SILU)
  86. def kernel(x, ids, w):
  87. M = x.shape[0]
  88. out = torch.empty(M, K, device=dev, dtype=torch.bfloat16)
  89. xq, xsf = ops.scaled_fp4_quant(x, a1_gscale[:1].reshape(()).clone(), is_sf_swizzled_layout=True)
  90. flashinfer_cutlass_fused_moe(
  91. input=xq, token_selected_experts=ids.to(torch.int), token_final_scales=w,
  92. fc1_expert_weights=w13_k.view(torch.long), fc2_expert_weights=w2.view(torch.long),
  93. fc1_expert_biases=None, fc2_expert_biases=None, swiglu_alpha=None, swiglu_beta=None,
  94. swiglu_limit=swiglu_limit, output=out, output_dtype=torch.bfloat16, quant_scales=quant_scales,
  95. input_sf=xsf, tp_size=1, tp_rank=0, ep_size=1, ep_rank=0, activation_type=act,
  96. use_deepseek_fp8_block_scale=False, use_mxfp8_act_scaling=False, use_w4_group_scaling=False)
  97. return out, xq, xsf
  98. # dequantised weights for the real experts
  99. W13 = dequantize_to_dtype(w13[:E_REAL], w13_scale[:E_REAL], w13_gs[:E_REAL], torch.float32, 16, swizzle=False) # [E, 2N, K]
  100. W2 = dequantize_to_dtype(w2[:E_REAL], w2_scale[:E_REAL], w2_gs[:E_REAL], torch.float32, 16, swizzle=False) # [E, K, N]
  101. def act_q(h, gscale_inv):
  102. """quantise like the kernel's a2 path: scaled_fp4_quant with global scale 1/input_scale, then dequantise."""
  103. q, sf = ops.scaled_fp4_quant(h.to(torch.bfloat16), torch.tensor(1.0 / gscale_inv, device=dev), is_sf_swizzled_layout=True)
  104. return dequantize_to_dtype(q, sf, torch.tensor(gscale_inv, device=dev), torch.float32, 16, swizzle=True)
  105. def reference(x, ids, w, xq, xsf, quant_act):
  106. M = x.shape[0]
  107. if quant_act:
  108. xd = dequantize_to_dtype(xq, xsf, torch.tensor(a13_g, device=dev), torch.float32, 16, swizzle=True)
  109. else:
  110. xd = x.float()
  111. out = torch.zeros(M, K, device=dev)
  112. for r in range(M):
  113. for j in range(TOPK):
  114. e = int(ids[r, j])
  115. gate = xd[r] @ W13[e, :N].T
  116. up = xd[r] @ W13[e, N:].T
  117. h = torch.nn.functional.silu(gate.clamp(max=LIMIT)) * up.clamp(-LIMIT, LIMIT)
  118. hd = act_q(h[None], a2_g)[0] if quant_act else h
  119. out[r] += w[r, j] * (hd @ W2[e].T)
  120. return out
  121. gen = torch.Generator(device=dev).manual_seed(1)
  122. worst_a4 = worst_a16 = 0.0
  123. for M, amp in ((1, 0.5), (4, 0.5), (8, 0.5), (8, 1.0), (16, 0.5), (1, 2.0)):
  124. x = (torch.randn(M, K, device=dev, generator=gen) * amp).to(torch.bfloat16)
  125. ids = torch.stack([torch.randperm(E_REAL, device=dev, generator=gen)[:TOPK] for _ in range(M)]).to(torch.int32)
  126. w = torch.rand(M, TOPK, device=dev, generator=gen) + 0.1
  127. w = (w / w.sum(-1, keepdim=True)) * 2.5
  128. out, xq, xsf = kernel(x, ids, w)
  129. torch.cuda.synchronize()
  130. ref4 = reference(x, ids, w, xq, xsf, True)
  131. ref16 = reference(x, ids, w, xq, xsf, False)
  132. e4 = (out.float() - ref4).abs().amax(1) / (ref4.abs().amax(1) + 1e-6)
  133. e16 = (out.float() - ref16).abs().amax(1) / (ref16.abs().amax(1) + 1e-6)
  134. q_loss = (ref4 - ref16).abs().amax(1) / (ref16.abs().amax(1) + 1e-6)
  135. cos = torch.nn.functional.cosine_similarity(out.float(), ref16, dim=1)
  136. print(f"[{name}] M={M} amp={amp}: kernel vs ref_a4 rel err per row {[round(v, 3) for v in e4.tolist()]}; "
  137. f"kernel vs ref_a16 {[round(v, 3) for v in e16.tolist()]}; a4-vs-a16 quant loss {[round(v, 3) for v in q_loss.tolist()]}; "
  138. f"cos(kernel, ref_a16) min {cos.min():.4f}; |out| mean {out.float().abs().mean():.3g} nan={torch.isnan(out).any().item()}")
  139. worst_a4 = max(worst_a4, e4.max().item()); worst_a16 = max(worst_a16, e16.max().item())
  140. print(f"[{name}] T10 worst: kernel vs w4a4 reference {worst_a4:.3f}, kernel vs w4a16 reference {worst_a16:.3f}")
  141. print("T10:", "PASS" if worst_a4 < 0.1 else "FAIL")