megamoe_vllm.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. # SPDX-License-Identifier: Apache-2.0
  2. """megamoe for decode-sized batches on vLLM's NVFP4 MoE layers (GB10).
  3. megamoe_rm.cu reads the FlashInfer CUTLASS backend's own processed tensors, so
  4. nothing is copied: batches of at most VLLM_MEGAMOE_MAX_TOKENS tokens take
  5. megamoe and larger ones keep CUTLASS. megamoe keeps activations in 16-bit
  6. (W4A16, exact in the weights), where CUTLASS quantizes them to FP4.
  7. With VLLM_MOE_PREFILL=1, batches of at least VLLM_MOE_PREFILL_MIN_TOKENS
  8. tokens take moe_prefill.cu instead: the same W4A4 math as CUTLASS on the same
  9. tensors, with the token gather and SwiGLU folded into the fc1 GEMM.
  10. VLLM_MOE_PREFILL_CHECK=N also runs CUTLASS on the first N such batches and
  11. logs the difference.
  12. install() swaps each ModelOptNvFp4FusedMoE method's class for a subclass whose
  13. apply() dispatches by batch size, so every isinstance check still holds. Only
  14. layers whose finalize is synchronous qualify: there the method returns the
  15. weighted top-k sum and the runner owns shared experts and the TP all-reduce,
  16. which is what megamoe produces.
  17. """
  18. import os
  19. import torch
  20. from vllm.logger import init_logger
  21. logger = init_logger(__name__)
  22. _SRC = os.environ.get("VLLM_MEGAMOE_SRC", "/opt/megamoe/megamoe_rm.cu")
  23. _PREFILL_SRC = os.environ.get("VLLM_MOE_PREFILL_SRC", "/opt/megamoe/moe_prefill.cu")
  24. _PREFILL = os.environ.get("VLLM_MOE_PREFILL") == "1"
  25. _PREFILL_MIN = int(os.environ.get("VLLM_MOE_PREFILL_MIN_TOKENS", "1024"))
  26. _check_left = int(os.environ.get("VLLM_MOE_PREFILL_CHECK", "0"))
  27. # fc2's per-expert rows as e4m3 with a scale per 128 columns: half the bytes
  28. # written and read back, for callers that pass the scales on (arxbig's finalize).
  29. _PREFILL_Y8 = os.environ.get("VLLM_MOE_PREFILL_Y8") == "1"
  30. _ext = None
  31. _pext = None
  32. def _jit_load(name, *args, **kwargs):
  33. """torch's cpp_extension.load(), safe after a build was killed midway (#53).
  34. torch marks a build with a `lock` file and waits forever on one it finds, so a
  35. build killed midway hangs every later load with nothing in the log. Builds here
  36. also hold an flock, which the kernel drops when its holder dies: whoever holds
  37. it is the only builder, and any `lock` file it finds is stale."""
  38. import fcntl
  39. from torch.utils.cpp_extension import _get_build_directory, load
  40. build = _get_build_directory(name, False)
  41. with open(os.path.join(build, "lock.flock"), "w") as f:
  42. fcntl.flock(f, fcntl.LOCK_EX)
  43. try:
  44. os.remove(os.path.join(build, "lock"))
  45. except FileNotFoundError:
  46. pass
  47. return load(name, *args, **kwargs)
  48. _scratch: dict = {}
  49. def _load():
  50. global _ext
  51. if _ext is None:
  52. _ext = _jit_load("megamoe_rm", [_SRC], extra_cuda_cflags=["-O3", "-gencode=arch=compute_121a,code=sm_121a"])
  53. return _ext
  54. def _load_prefill():
  55. global _pext
  56. if _pext is None:
  57. _pext = _jit_load("moe_prefill", [_PREFILL_SRC], extra_cuda_cflags=[
  58. "-O3", "-gencode=arch=compute_121a,code=sm_121a", "-DMOE_STAGES=2", "-DMOE_MINB=2", "-DMOE_PREFETCH=5"])
  59. return _pext
  60. def prefill_routed(m, x, topk_weights, topk_ids, y8: bool = False, x4=None):
  61. """moe_prefill.cu's fc1 and fc2 on the tensors CUTLASS would get.
  62. x4 = (xq, xs, block): x already quantized by scaled_fp4_quant in blocks
  63. of `block` rows, each padded to a multiple of 128 rows (the SP gather of
  64. each rank's rows). x then only gives the shape and device.
  65. Returns (y, pos, y8s): y [M * topk, H] holds each (token, k) expert output
  66. in expert-sorted order, pos [M * topk] its row. With y8, y is e4m3 bytes
  67. and y8s [M * topk, H / 128] fp32 its scales; otherwise y is bf16, y8s None.
  68. """
  69. fe = m._moe_prefill_experts
  70. w13, w2 = m.w13_weight, m.w2_weight
  71. E, H, I = w13.shape[0], w2.shape[1], w13.shape[1] // 2
  72. M, topk = topk_ids.shape
  73. from vllm import _custom_ops as ops
  74. if x4 is None:
  75. xq, xs = ops.scaled_fp4_quant(x, m._moe_prefill_a1)
  76. else:
  77. xq, xs, block = x4
  78. ids = topk_ids.to(torch.int32)
  79. flat = ids.flatten().long()
  80. R = flat.numel()
  81. order = torch.argsort(flat, stable=True)
  82. counts = torch.zeros(E, dtype=torch.int64, device=x.device).scatter_add_(0, flat, torch.ones_like(flat))
  83. off = torch.zeros(E + 1, dtype=torch.int32, device=x.device)
  84. off[1:] = torch.cumsum(counts, 0)
  85. mt = (counts + 127) // 128
  86. tcum = torch.cumsum(mt, 0)
  87. tidx = torch.arange(R // 128 + E, device=x.device)
  88. te = torch.searchsorted(tcum, tidx, right=True)
  89. tm = tidx - (tcum - mt)[te.clamp(max=E - 1)]
  90. tiles = torch.stack([te, tm], 1).int().contiguous()
  91. pos = torch.empty(R, dtype=torch.int32, device=x.device)
  92. pos[order] = torch.arange(R, dtype=torch.int32, device=x.device)
  93. rows = order // topk
  94. if x4 is not None and block % 128:
  95. rows = rows // block * ((block + 127) // 128 * 128) + rows % block
  96. rows = rows.int()
  97. hq = torch.empty(R, I // 2, dtype=torch.uint8, device=x.device)
  98. hs = torch.empty(((R + 127) // 128) * 128 * (I // 16), dtype=torch.uint8, device=x.device)
  99. _pext.fc1(xq, xs.view(torch.uint8).flatten(), rows, off, tiles, w13, fe.w1_scale.view(torch.uint8),
  100. m._moe_prefill_g1, m._moe_prefill_a2, hq, hs, m._megamoe_limit)
  101. if y8:
  102. y = torch.empty(R, H, dtype=torch.uint8, device=x.device)
  103. y8s = torch.empty(R, H // 128, dtype=torch.float32, device=x.device)
  104. _pext.fc2(hq, hs, off, tiles, w2, fe.w2_scale.view(torch.uint8), m._moe_prefill_g2, y, y8s)
  105. return y, pos, y8s
  106. y = torch.empty(R, H, dtype=torch.bfloat16, device=x.device)
  107. _pext.fc2(hq, hs, off, tiles, w2, fe.w2_scale.view(torch.uint8), m._moe_prefill_g2, y)
  108. return y, pos, None
  109. def _prefill(m, x, topk_weights, topk_ids):
  110. """moe_prefill.cu on the tensors CUTLASS would get; returns the weighted top-k sum."""
  111. y, pos, _ = prefill_routed(m, x, topk_weights, topk_ids)
  112. out = torch.empty(x.shape[0], m.w2_weight.shape[1], dtype=torch.bfloat16, device=x.device)
  113. _pext.finalize(y, pos, topk_weights.to(torch.float32).contiguous(), out)
  114. return out
  115. def _buffers(M: int, topk: int, H: int, I: int, device) -> dict:
  116. """Scratch shared by every layer (they run one after another), per batch size."""
  117. key = (M, topk, H, I)
  118. if key not in _scratch:
  119. pairs = M * topk
  120. _scratch[key] = dict(
  121. xh=torch.empty(M, H, dtype=torch.half, device=device),
  122. ints=torch.empty(1 + pairs + 8 * pairs, dtype=torch.int32, device=device),
  123. hbuf=torch.empty(pairs, 8, I, dtype=torch.half, device=device),
  124. ypart=torch.empty(pairs, H, dtype=torch.float32, device=device))
  125. return _scratch[key]
  126. def _apply(self, layer, x, topk_weights, topk_ids, shared_experts, shared_experts_input):
  127. global _check_left
  128. M = x.shape[0]
  129. if (_PREFILL and M >= _PREFILL_MIN and x.dtype == torch.bfloat16 and hasattr(layer, "_moe_prefill_experts")
  130. and not torch.cuda.is_current_stream_capturing()):
  131. out = _prefill(layer, x.contiguous(), topk_weights, topk_ids)
  132. if _check_left > 0:
  133. _check_left -= 1
  134. ref = super(type(self), self).apply(layer, x, topk_weights, topk_ids, shared_experts, shared_experts_input)
  135. ref = ref[0] if isinstance(ref, tuple) else ref
  136. rel = ((out.float() - ref.float()).norm() / ref.float().norm()).item()
  137. logger.warning("moe_prefill check: %d tokens, rel diff vs CUTLASS %.4f", M, rel)
  138. return out
  139. if M == 0 or M > self._megamoe_max_tokens or x.dtype != torch.bfloat16:
  140. return super(type(self), self).apply(layer, x, topk_weights, topk_ids, shared_experts,
  141. shared_experts_input)
  142. w13, w2 = layer.w13_weight, layer.w2_weight
  143. H, I, topk = w2.shape[1], w13.shape[1] // 2, topk_ids.shape[1]
  144. s = _buffers(M, topk, H, I, x.device)
  145. out = torch.empty(M, H, dtype=torch.bfloat16, device=x.device)
  146. _ext.forward(x.contiguous(), topk_ids.to(torch.int32).contiguous(), topk_weights.to(torch.float32).contiguous(),
  147. w13, layer.w13_weight_scale, layer._megamoe_alpha1, w2, layer.w2_weight_scale,
  148. layer._megamoe_alpha2, s["xh"], s["ints"], s["hbuf"], s["ypart"], out,
  149. layer._megamoe_limit, self._megamoe_variant)
  150. return out
  151. def install(model: torch.nn.Module) -> None:
  152. """Route decode-sized batches of every eligible NVFP4 MoE layer to megamoe."""
  153. if os.environ.get("VLLM_MEGAMOE") != "1":
  154. return
  155. max_tokens = int(os.environ.get("VLLM_MEGAMOE_MAX_TOKENS", "8"))
  156. variant = int(os.environ.get("VLLM_MEGAMOE_VARIANT", "20"))
  157. classes: dict[type, type] = {}
  158. count = 0
  159. for name, m in model.named_modules():
  160. method = getattr(m, "quant_method", None)
  161. if type(method).__name__ != "ModelOptNvFp4FusedMoE" or not hasattr(m, "w13_weight"):
  162. continue
  163. kernel = getattr(method, "moe_kernel", None)
  164. impl = getattr(kernel, "impl", kernel)
  165. pf = getattr(impl, "prepare_finalize", None)
  166. if method.is_monolithic or pf is None or pf.supports_async() or m.expert_map is not None:
  167. logger.warning("megamoe: %s does not qualify; it keeps CUTLASS", name)
  168. continue
  169. clamp = getattr(getattr(impl, "fused_experts", None), "gemm1_clamp_limit", None)
  170. limit = float(clamp.max()) if isinstance(clamp, torch.Tensor) else float("inf")
  171. m._megamoe_limit = limit
  172. m._megamoe_alpha1 = (m.w13_weight_scale_2 / m.w13_input_scale).float().contiguous()
  173. m._megamoe_alpha2 = (m.w2_weight_scale_2 / m.w2_input_scale).float().contiguous()
  174. fe = getattr(impl, "fused_experts", None)
  175. if _PREFILL and fe is not None and getattr(fe, "g1_alphas", None) is not None:
  176. E = m.w13_weight.shape[0]
  177. m._moe_prefill_experts = fe
  178. m._moe_prefill_a1 = fe.a1_gscale.float().flatten()[:1].contiguous()
  179. m._moe_prefill_g1 = fe.g1_alphas.float().flatten().expand(E).contiguous()
  180. m._moe_prefill_a2 = fe.a2_gscale.float().flatten().expand(E).contiguous()
  181. m._moe_prefill_g2 = fe.g2_alphas.float().flatten().expand(E).contiguous()
  182. cls = type(method)
  183. if cls not in classes:
  184. classes[cls] = type("MegaMoE" + cls.__name__, (cls,), {"apply": _apply})
  185. method.__class__ = classes[cls]
  186. method._megamoe_max_tokens = max_tokens
  187. method._megamoe_variant = variant
  188. count += 1
  189. if count:
  190. _load()
  191. if _PREFILL:
  192. _load_prefill()
  193. logger.info("megamoe: %d MoE layers take batches of <= %d tokens (variant %d)", count, max_tokens, variant)