megamoe_vllm.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  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. # A row with no routed experts carries id -1 (weight 0). Every expert id is used as an
  74. # index below (scatter_add_, argsort/searchsorted, the fc1 gather), so point it at expert 0;
  75. # its weight is 0, so finalize adds nothing for it, the same as CUTLASS skipping it.
  76. topk_ids = topk_ids.masked_fill(topk_ids < 0, 0)
  77. from vllm import _custom_ops as ops
  78. if x4 is None:
  79. xq, xs = ops.scaled_fp4_quant(x, m._moe_prefill_a1)
  80. else:
  81. xq, xs, block = x4
  82. ids = topk_ids.to(torch.int32)
  83. flat = ids.flatten().long()
  84. R = flat.numel()
  85. order = torch.argsort(flat, stable=True)
  86. counts = torch.zeros(E, dtype=torch.int64, device=x.device).scatter_add_(0, flat, torch.ones_like(flat))
  87. off = torch.zeros(E + 1, dtype=torch.int32, device=x.device)
  88. off[1:] = torch.cumsum(counts, 0)
  89. mt = (counts + 127) // 128
  90. tcum = torch.cumsum(mt, 0)
  91. tidx = torch.arange(R // 128 + E, device=x.device)
  92. te = torch.searchsorted(tcum, tidx, right=True)
  93. tm = tidx - (tcum - mt)[te.clamp(max=E - 1)]
  94. tiles = torch.stack([te, tm], 1).int().contiguous()
  95. pos = torch.empty(R, dtype=torch.int32, device=x.device)
  96. pos[order] = torch.arange(R, dtype=torch.int32, device=x.device)
  97. rows = order // topk
  98. if x4 is not None and block % 128:
  99. rows = rows // block * ((block + 127) // 128 * 128) + rows % block
  100. rows = rows.int()
  101. hq = torch.empty(R, I // 2, dtype=torch.uint8, device=x.device)
  102. hs = torch.empty(((R + 127) // 128) * 128 * (I // 16), dtype=torch.uint8, device=x.device)
  103. _pext.fc1(xq, xs.view(torch.uint8).flatten(), rows, off, tiles, w13, fe.w1_scale.view(torch.uint8),
  104. m._moe_prefill_g1, m._moe_prefill_a2, hq, hs, m._megamoe_limit)
  105. if y8:
  106. y = torch.empty(R, H, dtype=torch.uint8, device=x.device)
  107. y8s = torch.empty(R, H // 128, dtype=torch.float32, device=x.device)
  108. _pext.fc2(hq, hs, off, tiles, w2, fe.w2_scale.view(torch.uint8), m._moe_prefill_g2, y, y8s)
  109. return y, pos, y8s
  110. y = torch.empty(R, H, dtype=torch.bfloat16, device=x.device)
  111. _pext.fc2(hq, hs, off, tiles, w2, fe.w2_scale.view(torch.uint8), m._moe_prefill_g2, y)
  112. return y, pos, None
  113. def _prefill(m, x, topk_weights, topk_ids):
  114. """moe_prefill.cu on the tensors CUTLASS would get; returns the weighted top-k sum."""
  115. y, pos, _ = prefill_routed(m, x, topk_weights, topk_ids)
  116. out = torch.empty(x.shape[0], m.w2_weight.shape[1], dtype=torch.bfloat16, device=x.device)
  117. _pext.finalize(y, pos, topk_weights.to(torch.float32).contiguous(), out)
  118. return out
  119. def _buffers(M: int, topk: int, H: int, I: int, device) -> dict:
  120. """Scratch shared by every layer (they run one after another), per batch size."""
  121. key = (M, topk, H, I)
  122. if key not in _scratch:
  123. pairs = M * topk
  124. _scratch[key] = dict(
  125. xh=torch.empty(M, H, dtype=torch.half, device=device),
  126. ints=torch.empty(1 + pairs + 8 * pairs, dtype=torch.int32, device=device),
  127. hbuf=torch.empty(pairs, 8, I, dtype=torch.half, device=device),
  128. ypart=torch.empty(pairs, H, dtype=torch.float32, device=device))
  129. return _scratch[key]
  130. def _apply(self, layer, x, topk_weights, topk_ids, shared_experts, shared_experts_input):
  131. global _check_left
  132. M = x.shape[0]
  133. # Rows with no routed experts arrive as id -1 / weight 0 (seen in batches that mix a long
  134. # prefill chunk with concurrent decode rows). CUTLASS skips them; megamoe and moe_prefill
  135. # index by id, so send them to expert 0 at weight 0, which adds exactly nothing.
  136. # Elementwise, no host sync.
  137. neg = topk_ids < 0
  138. topk_ids = topk_ids.masked_fill(neg, 0)
  139. topk_weights = topk_weights.masked_fill(neg, 0)
  140. if (_PREFILL and M >= _PREFILL_MIN and x.dtype == torch.bfloat16 and hasattr(layer, "_moe_prefill_experts")
  141. and not torch.cuda.is_current_stream_capturing()):
  142. out = _prefill(layer, x.contiguous(), topk_weights, topk_ids)
  143. if _check_left > 0:
  144. _check_left -= 1
  145. ref = super(type(self), self).apply(layer, x, topk_weights, topk_ids, shared_experts, shared_experts_input)
  146. ref = ref[0] if isinstance(ref, tuple) else ref
  147. rel = ((out.float() - ref.float()).norm() / ref.float().norm()).item()
  148. logger.warning("moe_prefill check: %d tokens, rel diff vs CUTLASS %.4f", M, rel)
  149. return out
  150. if M == 0 or M > self._megamoe_max_tokens or x.dtype != torch.bfloat16:
  151. return super(type(self), self).apply(layer, x, topk_weights, topk_ids, shared_experts,
  152. shared_experts_input)
  153. w13, w2 = layer.w13_weight, layer.w2_weight
  154. H, I, topk = w2.shape[1], w13.shape[1] // 2, topk_ids.shape[1]
  155. s = _buffers(M, topk, H, I, x.device)
  156. out = torch.empty(M, H, dtype=torch.bfloat16, device=x.device)
  157. _ext.forward(x.contiguous(), topk_ids.to(torch.int32).contiguous(), topk_weights.to(torch.float32).contiguous(),
  158. w13, layer.w13_weight_scale, layer._megamoe_alpha1, w2, layer.w2_weight_scale,
  159. layer._megamoe_alpha2, s["xh"], s["ints"], s["hbuf"], s["ypart"], out,
  160. layer._megamoe_limit, self._megamoe_variant)
  161. return out
  162. def install(model: torch.nn.Module) -> None:
  163. """Route decode-sized batches of every eligible NVFP4 MoE layer to megamoe."""
  164. if os.environ.get("VLLM_MEGAMOE") != "1":
  165. return
  166. max_tokens = int(os.environ.get("VLLM_MEGAMOE_MAX_TOKENS", "8"))
  167. variant = int(os.environ.get("VLLM_MEGAMOE_VARIANT", "20"))
  168. classes: dict[type, type] = {}
  169. count = 0
  170. for name, m in model.named_modules():
  171. method = getattr(m, "quant_method", None)
  172. if type(method).__name__ != "ModelOptNvFp4FusedMoE" or not hasattr(m, "w13_weight"):
  173. continue
  174. kernel = getattr(method, "moe_kernel", None)
  175. impl = getattr(kernel, "impl", kernel)
  176. pf = getattr(impl, "prepare_finalize", None)
  177. if method.is_monolithic or pf is None or pf.supports_async() or m.expert_map is not None:
  178. logger.warning("megamoe: %s does not qualify; it keeps CUTLASS", name)
  179. continue
  180. clamp = getattr(getattr(impl, "fused_experts", None), "gemm1_clamp_limit", None)
  181. limit = float(clamp.max()) if isinstance(clamp, torch.Tensor) else float("inf")
  182. m._megamoe_limit = limit
  183. m._megamoe_alpha1 = (m.w13_weight_scale_2 / m.w13_input_scale).float().contiguous()
  184. m._megamoe_alpha2 = (m.w2_weight_scale_2 / m.w2_input_scale).float().contiguous()
  185. fe = getattr(impl, "fused_experts", None)
  186. if _PREFILL and fe is not None and getattr(fe, "g1_alphas", None) is not None:
  187. E = m.w13_weight.shape[0]
  188. m._moe_prefill_experts = fe
  189. m._moe_prefill_a1 = fe.a1_gscale.float().flatten()[:1].contiguous()
  190. m._moe_prefill_g1 = fe.g1_alphas.float().flatten().expand(E).contiguous()
  191. m._moe_prefill_a2 = fe.a2_gscale.float().flatten().expand(E).contiguous()
  192. m._moe_prefill_g2 = fe.g2_alphas.float().flatten().expand(E).contiguous()
  193. cls = type(method)
  194. if cls not in classes:
  195. classes[cls] = type("MegaMoE" + cls.__name__, (cls,), {"apply": _apply})
  196. method.__class__ = classes[cls]
  197. method._megamoe_max_tokens = max_tokens
  198. method._megamoe_variant = variant
  199. count += 1
  200. if count:
  201. _load()
  202. if _PREFILL:
  203. _load_prefill()
  204. logger.info("megamoe: %d MoE layers take batches of <= %d tokens (variant %d)", count, max_tokens, variant)