| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- # SPDX-License-Identifier: Apache-2.0
- """megamoe for decode-sized batches on vLLM's NVFP4 MoE layers (GB10).
- megamoe_rm.cu reads the FlashInfer CUTLASS backend's own processed tensors, so
- nothing is copied: batches of at most VLLM_MEGAMOE_MAX_TOKENS tokens take
- megamoe and larger ones keep CUTLASS. megamoe keeps activations in 16-bit
- (W4A16, exact in the weights), where CUTLASS quantizes them to FP4.
- With VLLM_MOE_PREFILL=1, batches of at least VLLM_MOE_PREFILL_MIN_TOKENS
- tokens take moe_prefill.cu instead: the same W4A4 math as CUTLASS on the same
- tensors, with the token gather and SwiGLU folded into the fc1 GEMM.
- VLLM_MOE_PREFILL_CHECK=N also runs CUTLASS on the first N such batches and
- logs the difference.
- install() swaps each ModelOptNvFp4FusedMoE method's class for a subclass whose
- apply() dispatches by batch size, so every isinstance check still holds. Only
- layers whose finalize is synchronous qualify: there the method returns the
- weighted top-k sum and the runner owns shared experts and the TP all-reduce,
- which is what megamoe produces.
- """
- import os
- import torch
- from vllm.logger import init_logger
- logger = init_logger(__name__)
- _SRC = os.environ.get("VLLM_MEGAMOE_SRC", "/opt/megamoe/megamoe_rm.cu")
- _PREFILL_SRC = os.environ.get("VLLM_MOE_PREFILL_SRC", "/opt/megamoe/moe_prefill.cu")
- _PREFILL = os.environ.get("VLLM_MOE_PREFILL") == "1"
- _PREFILL_MIN = int(os.environ.get("VLLM_MOE_PREFILL_MIN_TOKENS", "1024"))
- _check_left = int(os.environ.get("VLLM_MOE_PREFILL_CHECK", "0"))
- # fc2's per-expert rows as e4m3 with a scale per 128 columns: half the bytes
- # written and read back, for callers that pass the scales on (arxbig's finalize).
- _PREFILL_Y8 = os.environ.get("VLLM_MOE_PREFILL_Y8") == "1"
- _ext = None
- _pext = None
- def _jit_load(name, *args, **kwargs):
- """torch's cpp_extension.load(), safe after a build was killed midway (#53).
- torch marks a build with a `lock` file and waits forever on one it finds, so a
- build killed midway hangs every later load with nothing in the log. Builds here
- also hold an flock, which the kernel drops when its holder dies: whoever holds
- it is the only builder, and any `lock` file it finds is stale."""
- import fcntl
- from torch.utils.cpp_extension import _get_build_directory, load
- build = _get_build_directory(name, False)
- with open(os.path.join(build, "lock.flock"), "w") as f:
- fcntl.flock(f, fcntl.LOCK_EX)
- try:
- os.remove(os.path.join(build, "lock"))
- except FileNotFoundError:
- pass
- return load(name, *args, **kwargs)
- _scratch: dict = {}
- def _load():
- global _ext
- if _ext is None:
- _ext = _jit_load("megamoe_rm", [_SRC], extra_cuda_cflags=["-O3", "-gencode=arch=compute_121a,code=sm_121a"])
- return _ext
- def _load_prefill():
- global _pext
- if _pext is None:
- _pext = _jit_load("moe_prefill", [_PREFILL_SRC], extra_cuda_cflags=[
- "-O3", "-gencode=arch=compute_121a,code=sm_121a", "-DMOE_STAGES=2", "-DMOE_MINB=2", "-DMOE_PREFETCH=5"])
- return _pext
- def prefill_routed(m, x, topk_weights, topk_ids, y8: bool = False, x4=None):
- """moe_prefill.cu's fc1 and fc2 on the tensors CUTLASS would get.
- x4 = (xq, xs, block): x already quantized by scaled_fp4_quant in blocks
- of `block` rows, each padded to a multiple of 128 rows (the SP gather of
- each rank's rows). x then only gives the shape and device.
- Returns (y, pos, y8s): y [M * topk, H] holds each (token, k) expert output
- in expert-sorted order, pos [M * topk] its row. With y8, y is e4m3 bytes
- and y8s [M * topk, H / 128] fp32 its scales; otherwise y is bf16, y8s None.
- """
- fe = m._moe_prefill_experts
- w13, w2 = m.w13_weight, m.w2_weight
- E, H, I = w13.shape[0], w2.shape[1], w13.shape[1] // 2
- M, topk = topk_ids.shape
- # A row with no routed experts carries id -1 (weight 0). Every expert id is used as an
- # index below (scatter_add_, argsort/searchsorted, the fc1 gather), so point it at expert 0;
- # its weight is 0, so finalize adds nothing for it, the same as CUTLASS skipping it.
- topk_ids = topk_ids.masked_fill(topk_ids < 0, 0)
- from vllm import _custom_ops as ops
- if x4 is None:
- xq, xs = ops.scaled_fp4_quant(x, m._moe_prefill_a1)
- else:
- xq, xs, block = x4
- ids = topk_ids.to(torch.int32)
- flat = ids.flatten().long()
- R = flat.numel()
- order = torch.argsort(flat, stable=True)
- counts = torch.zeros(E, dtype=torch.int64, device=x.device).scatter_add_(0, flat, torch.ones_like(flat))
- off = torch.zeros(E + 1, dtype=torch.int32, device=x.device)
- off[1:] = torch.cumsum(counts, 0)
- mt = (counts + 127) // 128
- tcum = torch.cumsum(mt, 0)
- tidx = torch.arange(R // 128 + E, device=x.device)
- te = torch.searchsorted(tcum, tidx, right=True)
- tm = tidx - (tcum - mt)[te.clamp(max=E - 1)]
- tiles = torch.stack([te, tm], 1).int().contiguous()
- pos = torch.empty(R, dtype=torch.int32, device=x.device)
- pos[order] = torch.arange(R, dtype=torch.int32, device=x.device)
- rows = order // topk
- if x4 is not None and block % 128:
- rows = rows // block * ((block + 127) // 128 * 128) + rows % block
- rows = rows.int()
- hq = torch.empty(R, I // 2, dtype=torch.uint8, device=x.device)
- hs = torch.empty(((R + 127) // 128) * 128 * (I // 16), dtype=torch.uint8, device=x.device)
- _pext.fc1(xq, xs.view(torch.uint8).flatten(), rows, off, tiles, w13, fe.w1_scale.view(torch.uint8),
- m._moe_prefill_g1, m._moe_prefill_a2, hq, hs, m._megamoe_limit)
- if y8:
- y = torch.empty(R, H, dtype=torch.uint8, device=x.device)
- y8s = torch.empty(R, H // 128, dtype=torch.float32, device=x.device)
- _pext.fc2(hq, hs, off, tiles, w2, fe.w2_scale.view(torch.uint8), m._moe_prefill_g2, y, y8s)
- return y, pos, y8s
- y = torch.empty(R, H, dtype=torch.bfloat16, device=x.device)
- _pext.fc2(hq, hs, off, tiles, w2, fe.w2_scale.view(torch.uint8), m._moe_prefill_g2, y)
- return y, pos, None
- def _prefill(m, x, topk_weights, topk_ids):
- """moe_prefill.cu on the tensors CUTLASS would get; returns the weighted top-k sum."""
- y, pos, _ = prefill_routed(m, x, topk_weights, topk_ids)
- out = torch.empty(x.shape[0], m.w2_weight.shape[1], dtype=torch.bfloat16, device=x.device)
- _pext.finalize(y, pos, topk_weights.to(torch.float32).contiguous(), out)
- return out
- def _buffers(M: int, topk: int, H: int, I: int, device) -> dict:
- """Scratch shared by every layer (they run one after another), per batch size."""
- key = (M, topk, H, I)
- if key not in _scratch:
- pairs = M * topk
- _scratch[key] = dict(
- xh=torch.empty(M, H, dtype=torch.half, device=device),
- ints=torch.empty(1 + pairs + 8 * pairs, dtype=torch.int32, device=device),
- hbuf=torch.empty(pairs, 8, I, dtype=torch.half, device=device),
- ypart=torch.empty(pairs, H, dtype=torch.float32, device=device))
- return _scratch[key]
- def _apply(self, layer, x, topk_weights, topk_ids, shared_experts, shared_experts_input):
- global _check_left
- M = x.shape[0]
- # Rows with no routed experts arrive as id -1 / weight 0 (seen in batches that mix a long
- # prefill chunk with concurrent decode rows). CUTLASS skips them; megamoe and moe_prefill
- # index by id, so send them to expert 0 at weight 0, which adds exactly nothing.
- # Elementwise, no host sync.
- neg = topk_ids < 0
- topk_ids = topk_ids.masked_fill(neg, 0)
- topk_weights = topk_weights.masked_fill(neg, 0)
- if (_PREFILL and M >= _PREFILL_MIN and x.dtype == torch.bfloat16 and hasattr(layer, "_moe_prefill_experts")
- and not torch.cuda.is_current_stream_capturing()):
- out = _prefill(layer, x.contiguous(), topk_weights, topk_ids)
- if _check_left > 0:
- _check_left -= 1
- ref = super(type(self), self).apply(layer, x, topk_weights, topk_ids, shared_experts, shared_experts_input)
- ref = ref[0] if isinstance(ref, tuple) else ref
- rel = ((out.float() - ref.float()).norm() / ref.float().norm()).item()
- logger.warning("moe_prefill check: %d tokens, rel diff vs CUTLASS %.4f", M, rel)
- return out
- if M == 0 or M > self._megamoe_max_tokens or x.dtype != torch.bfloat16:
- return super(type(self), self).apply(layer, x, topk_weights, topk_ids, shared_experts,
- shared_experts_input)
- w13, w2 = layer.w13_weight, layer.w2_weight
- H, I, topk = w2.shape[1], w13.shape[1] // 2, topk_ids.shape[1]
- s = _buffers(M, topk, H, I, x.device)
- out = torch.empty(M, H, dtype=torch.bfloat16, device=x.device)
- _ext.forward(x.contiguous(), topk_ids.to(torch.int32).contiguous(), topk_weights.to(torch.float32).contiguous(),
- w13, layer.w13_weight_scale, layer._megamoe_alpha1, w2, layer.w2_weight_scale,
- layer._megamoe_alpha2, s["xh"], s["ints"], s["hbuf"], s["ypart"], out,
- layer._megamoe_limit, self._megamoe_variant)
- return out
- def install(model: torch.nn.Module) -> None:
- """Route decode-sized batches of every eligible NVFP4 MoE layer to megamoe."""
- if os.environ.get("VLLM_MEGAMOE") != "1":
- return
- max_tokens = int(os.environ.get("VLLM_MEGAMOE_MAX_TOKENS", "8"))
- variant = int(os.environ.get("VLLM_MEGAMOE_VARIANT", "20"))
- classes: dict[type, type] = {}
- count = 0
- for name, m in model.named_modules():
- method = getattr(m, "quant_method", None)
- if type(method).__name__ != "ModelOptNvFp4FusedMoE" or not hasattr(m, "w13_weight"):
- continue
- kernel = getattr(method, "moe_kernel", None)
- impl = getattr(kernel, "impl", kernel)
- pf = getattr(impl, "prepare_finalize", None)
- if method.is_monolithic or pf is None or pf.supports_async() or m.expert_map is not None:
- logger.warning("megamoe: %s does not qualify; it keeps CUTLASS", name)
- continue
- clamp = getattr(getattr(impl, "fused_experts", None), "gemm1_clamp_limit", None)
- limit = float(clamp.max()) if isinstance(clamp, torch.Tensor) else float("inf")
- m._megamoe_limit = limit
- m._megamoe_alpha1 = (m.w13_weight_scale_2 / m.w13_input_scale).float().contiguous()
- m._megamoe_alpha2 = (m.w2_weight_scale_2 / m.w2_input_scale).float().contiguous()
- fe = getattr(impl, "fused_experts", None)
- if _PREFILL and fe is not None and getattr(fe, "g1_alphas", None) is not None:
- E = m.w13_weight.shape[0]
- m._moe_prefill_experts = fe
- m._moe_prefill_a1 = fe.a1_gscale.float().flatten()[:1].contiguous()
- m._moe_prefill_g1 = fe.g1_alphas.float().flatten().expand(E).contiguous()
- m._moe_prefill_a2 = fe.a2_gscale.float().flatten().expand(E).contiguous()
- m._moe_prefill_g2 = fe.g2_alphas.float().flatten().expand(E).contiguous()
- cls = type(method)
- if cls not in classes:
- classes[cls] = type("MegaMoE" + cls.__name__, (cls,), {"apply": _apply})
- method.__class__ = classes[cls]
- method._megamoe_max_tokens = max_tokens
- method._megamoe_variant = variant
- count += 1
- if count:
- _load()
- if _PREFILL:
- _load_prefill()
- logger.info("megamoe: %d MoE layers take batches of <= %d tokens (variant %d)", count, max_tokens, variant)
|