From 1b77f498a0f7c422782eaccd018fdd850b421cfd Mon Sep 17 00:00:00 2001 From: Shu Wang Date: Thu, 10 Sep 2026 02:22:47 -0500 Subject: [PATCH] [NVIDIA] Support flashinfer Mega Moe (#31470) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: djns99 <40156487+djns99@users.noreply.github.com> Co-authored-by: δΊ‘ζŒš Co-authored-by: Yangmin Li Co-authored-by: Po-Han Huang (NVIDIA) <53919306+nvpohanh@users.noreply.github.com> --- .../advanced_features/server_arguments.mdx | 8 +- .../docs/references/environment_variables.mdx | 15 + python/pyproject.toml | 1 + python/sglang/srt/arg_groups/choices.py | 2 + python/sglang/srt/arg_groups/fields/exec_.py | 6 + python/sglang/srt/arg_groups/moe_hook.py | 190 ++++- python/sglang/srt/arg_groups/overrides.py | 13 +- python/sglang/srt/environ.py | 15 + .../srt/layers/moe/flashinfer_megamoe.py | 672 ++++++++++++++++++ .../srt/layers/moe/fused_moe_triton/layer.py | 3 + .../moe/moe_runner/flashinfer_trtllm.py | 40 +- .../srt/layers/moe/moe_runner/runner.py | 7 + .../layers/moe/token_dispatcher/flashinfer.py | 107 ++- .../layers/moe/token_dispatcher/standard.py | 2 + python/sglang/srt/layers/moe/utils.py | 46 +- python/sglang/srt/layers/quantization/fp8.py | 49 +- .../srt/layers/quantization/modelopt_quant.py | 88 ++- .../sglang/srt/model_executor/model_runner.py | 13 + python/sglang/srt/models/deepseek_v2.py | 1 + python/sglang/srt/models/glm4_moe.py | 1 + python/sglang/srt/models/nemotron_h.py | 1 + python/sglang/srt/models/qwen2_moe.py | 1 + python/sglang/srt/models/qwen3_moe.py | 9 +- python/sglang/srt/runtime_context.py | 1 + test/manual/ep/test_flashinfer_dispatcher.py | 186 ++++- .../test_flashinfer_trtllm_gen_moe_backend.py | 56 ++ .../layers/moe/test_flashinfer_dispatcher.py | 62 ++ .../layers/moe/test_flashinfer_megamoe.py | 220 ++++++ .../unit/lora/test_mem_pool_ep_unit.py | 10 +- .../unit/server_args/test_server_args.py | 262 +++++++ test/registered/unit/test_model_overrides.py | 6 +- 31 files changed, 2021 insertions(+), 72 deletions(-) create mode 100644 python/sglang/srt/layers/moe/flashinfer_megamoe.py create mode 100644 test/registered/unit/layers/moe/test_flashinfer_dispatcher.py create mode 100644 test/registered/unit/layers/moe/test_flashinfer_megamoe.py diff --git a/docs/docs/advanced_features/server_arguments.mdx b/docs/docs/advanced_features/server_arguments.mdx index a10536533..224b8fa52 100644 --- a/docs/docs/advanced_features/server_arguments.mdx +++ b/docs/docs/advanced_features/server_arguments.mdx @@ -1818,7 +1818,13 @@ Please consult the documentation below and [server_args.py](https://github.com/s `auto` auto, bf16, fp8, int8, nvfp4 - + + --flashinfer-a2a-dispatch-type + Select FlashInfer A2A dispatcher activation dtype. When omitted, it falls back to the SGLANG_MOE_NVFP4_DISPATCH environment variable. Explicit auto selects mxfp8 for --quantization mxfp8, nvfp4 for modelopt FP4 or hybrid NVFP4 MoE checkpoints, and bf16 otherwise. The SGLANG_MOE_NVFP4_DISPATCH environment variable cannot be set with this argument. + None + auto, bf16, nvfp4, mxfp8 + + `--ep-num-redundant-experts` Allocate this number of redundant experts in expert parallel. `0` diff --git a/docs/docs/references/environment_variables.mdx b/docs/docs/references/environment_variables.mdx index 99bf8c935..b08398520 100644 --- a/docs/docs/references/environment_variables.mdx +++ b/docs/docs/references/environment_variables.mdx @@ -326,6 +326,21 @@ SGLang supports various environment variables that can be used to configure its The maximum number of dispatched tokens on each GPU for --moe-a2a-backend=flashinfer `"1024"` + + SGLANG_FLASHINFER_MEGAMOE_COMBINE_DTYPE + FlashInfer NVFP4 MegaMOE cross-rank combine wire format. Supported values are bf16, mxfp8, and nvfp4. Quantized formats are incompatible with SGLANG_FLASHINFER_MEGAMOE_IN_KERNEL_FC2_REDUCE=1. + "bf16" + + + SGLANG_FLASHINFER_MEGAMOE_MAX_TOKENS_PER_RANK + Per-rank FlashInfer MegaMOE symmetric-workspace token capacity. A value of 0 derives the capacity from the runtime token limits. + 0 + + + SGLANG_FLASHINFER_MEGAMOE_IN_KERNEL_FC2_REDUCE + Use the CuTe DSL MegaMOE in-kernel FC2 reduction. This can reduce workspace size and improve large-batch performance, but BF16 atomic accumulation is nondeterministic. It is incompatible with quantized combine dtypes. + "false" + `SGLANG_DEEPEP_LL_COMBINE_SEND_NUM_SMS` Number of SMs used for DeepEP combine when single batch overlap is enabled diff --git a/python/pyproject.toml b/python/pyproject.toml index ac73d329f..c4a78fb02 100755 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -55,6 +55,7 @@ dependencies = [ "nvidia-cutlass-dsl[cu13]==4.6.2", "nvidia-mathdx==25.6.0", "nvidia-ml-py", + "nvshmem4py-cu13", "openai==2.6.1", "openai-harmony==0.0.4", "orjson", diff --git a/python/sglang/srt/arg_groups/choices.py b/python/sglang/srt/arg_groups/choices.py index b5a663806..1ec9c21ba 100644 --- a/python/sglang/srt/arg_groups/choices.py +++ b/python/sglang/srt/arg_groups/choices.py @@ -146,6 +146,7 @@ MOE_RUNNER_BACKEND_CHOICES = [ "flashinfer_cutlass", "flashinfer_mxfp4", "flashinfer_cutedsl", + "flashinfer_megamoe", "cutlass", "aiter", "marlin", @@ -159,6 +160,7 @@ MOE_RUNNER_BACKEND_CHOICES = [ MXFP8_MOE_RUNNER_BACKEND_CHOICES = [ "cutlass", "deep_gemm", + "flashinfer_megamoe", "flashinfer_trtllm", "flashinfer_trtllm_routed", ] diff --git a/python/sglang/srt/arg_groups/fields/exec_.py b/python/sglang/srt/arg_groups/fields/exec_.py index f501d490e..a403df395 100644 --- a/python/sglang/srt/arg_groups/fields/exec_.py +++ b/python/sglang/srt/arg_groups/fields/exec_.py @@ -636,6 +636,7 @@ class ExecMoe(msgspec.Struct): "deepep_v2", "ascend_tp", "pplx", + "flashinfer_megamoe", ], Arg( help="Choose the backend for MoE A2A.", @@ -651,6 +652,7 @@ class ExecMoe(msgspec.Struct): "deepep_v2", "pplx", "ascend_tp", + "flashinfer_megamoe", ], resolvable=True, ), @@ -695,6 +697,10 @@ class ExecMoe(msgspec.Struct): Literal["auto", "bf16", "fp8", "int8", "nvfp4"], "Select DeepEP dispatcher output dtype", ] = "auto" + flashinfer_a2a_dispatch_type: A[ + Optional[Literal["auto", "bf16", "nvfp4", "mxfp8"]], + "Select FlashInfer A2A dispatcher activation dtype.", + ] = None ep_num_redundant_experts: A[ int, "Allocate this number of redundant experts in expert parallel." ] = 0 diff --git a/python/sglang/srt/arg_groups/moe_hook.py b/python/sglang/srt/arg_groups/moe_hook.py index 945bf328e..bb96534e3 100644 --- a/python/sglang/srt/arg_groups/moe_hook.py +++ b/python/sglang/srt/arg_groups/moe_hook.py @@ -26,7 +26,7 @@ from sglang.srt.connector import ConnectorType from sglang.srt.environ import envs from sglang.srt.model_executor.cuda_graph_config import Backend, Phase, with_phase from sglang.srt.runtime_context import get_platform -from sglang.srt.utils.common import parse_connector_type +from sglang.srt.utils.common import is_sm100_supported, parse_connector_type logger = logging.getLogger(__name__) @@ -126,6 +126,110 @@ def handle_moe_kernel_config(server_args: Any): ) +def handle_flashinfer_a2a_dispatch_type(server_args: Any): + cfg = resolving_view(server_args) + cli_dispatch_type = cfg.flashinfer_a2a_dispatch_type + nvfp4_dispatch_env_is_set = envs.SGLANG_MOE_NVFP4_DISPATCH.is_set() + + if nvfp4_dispatch_env_is_set: + raise ValueError( + "SGLANG_MOE_NVFP4_DISPATCH cannot be set together with " + "--flashinfer-a2a-dispatch-type." + ) + + dispatch_type = cli_dispatch_type or "auto" + + supports_nvfp4_dispatch = ( + cfg.quantization == "modelopt_fp4" + or model_config_of(server_args).nvfp4_moe_meta is not None + ) + if dispatch_type == "auto": + if cfg.quantization == "mxfp8": + dispatch_type = "mxfp8" + elif supports_nvfp4_dispatch: + dispatch_type = "nvfp4" + else: + dispatch_type = "bf16" + + if dispatch_type == "mxfp8": + if cfg.quantization != "mxfp8": + raise ValueError( + "--flashinfer-a2a-dispatch-type mxfp8 requires --quantization mxfp8." + ) + if cfg.moe_runner_backend != "flashinfer_trtllm_routed": + raise ValueError( + "--flashinfer-a2a-dispatch-type mxfp8 requires " + "--moe-runner-backend flashinfer_trtllm_routed." + ) + elif dispatch_type == "nvfp4" and not supports_nvfp4_dispatch: + raise ValueError( + "--flashinfer-a2a-dispatch-type nvfp4 requires NVFP4/" + "modelopt-FP4 quantization or hybrid NVFP4 MoE metadata." + ) + + declare_resolution( + server_args, + "_handle_flashinfer_a2a_dispatch_type", + flashinfer_a2a_dispatch_type=dispatch_type, + ) + + +def validate_flashinfer_megamoe_envs() -> None: + combine_dtype = envs.SGLANG_FLASHINFER_MEGAMOE_COMBINE_DTYPE.get().strip().lower() + if combine_dtype not in ("bf16", "mxfp8", "nvfp4"): + raise ValueError( + "SGLANG_FLASHINFER_MEGAMOE_COMBINE_DTYPE must be one of " + f"'bf16', 'mxfp8', or 'nvfp4', got {combine_dtype!r}." + ) + if ( + combine_dtype != "bf16" + and envs.SGLANG_FLASHINFER_MEGAMOE_IN_KERNEL_FC2_REDUCE.get() + ): + raise ValueError( + "SGLANG_FLASHINFER_MEGAMOE_COMBINE_DTYPE=" + f"{combine_dtype!r} is incompatible with " + "SGLANG_FLASHINFER_MEGAMOE_IN_KERNEL_FC2_REDUCE=1." + ) + + +def validate_flashinfer_megamoe_model(server_args: Any) -> None: + model_config = model_config_of(server_args) + architectures = model_config.hf_config.architectures or [] + validated_architectures = ( + "DeepseekV2ForCausalLM", + "DeepseekV3ForCausalLM", + "DeepseekV32ForCausalLM", + "DeepseekV4ForCausalLM", + "Glm4MoeForCausalLM", + "NemotronHForCausalLM", + "NemotronHPuzzleForCausalLM", + "Qwen2MoeForCausalLM", + "Qwen3MoeForCausalLM", + ) + if not any( + architecture in validated_architectures for architecture in architectures + ): + raise ValueError( + "FlashInfer MegaMOE is not validated for model architectures " + f"{architectures}. Supported architectures: " + f"{sorted(validated_architectures)}." + ) + + quantization = resolved_view(server_args).quantization + supports_megamoe_quantization = ( + quantization in ("mxfp8", "modelopt_fp4") + or model_config.is_fp4_experts + or model_config.nvfp4_moe_meta is not None + ) + if not supports_megamoe_quantization: + raise ValueError( + "FlashInfer MegaMOE currently supports only MXFP8, ModelOpt " + "NVFP4, FP4-expert, or hybrid NVFP4 MoE checkpoints; got " + f"quantization={quantization!r}. Standard FP8 MoE checkpoints " + "are not supported." + ) + + def handle_a2a_moe(server_args: Any): # The backend overrides and the ep_size=tp_size adjustments moved to # the resolution pipeline (arg_groups/overrides.py: @@ -149,6 +253,38 @@ def handle_a2a_moe(server_args: Any): ) logger.info(f"Waterfill is enabled with moe_a2a_backend='{a2a_backend}'.") + if a2a_backend != "flashinfer" and cfg.flashinfer_a2a_dispatch_type not in ( + None, + "auto", + ): + raise ValueError( + "--flashinfer-a2a-dispatch-type requires --moe-a2a-backend flashinfer." + ) + + if a2a_backend == "flashinfer_megamoe": + validate_flashinfer_megamoe_model(server_args) + validate_flashinfer_megamoe_envs() + assert cfg.enable_dp_attention and cfg.dp_size == cfg.tp_size, ( + "FlashInfer MegaMOE is only supported with dp_size == tp_size and --enable-dp-attention" + ) + if resolved_view(server_args).moe_runner_backend == "auto": + declare_resolution( + server_args, "_handle_a2a_moe", moe_runner_backend="flashinfer_megamoe" + ) + assert resolved_view(server_args).moe_runner_backend == "flashinfer_megamoe", ( + "FlashInfer MegaMOE a2a backend requires --moe-runner-backend flashinfer_megamoe" + ) + if not is_sm100_supported(): + raise ValueError( + "FlashInfer MegaMOE currently requires an SM100-family " + "CUDA device for all supported quantization formats." + ) + logger.info( + "FlashInfer MegaMOE is enabled. The expert parallel size is " + "adjusted to be the same as the tensor parallel size[%s].", + cfg.tp_size, + ) + if a2a_backend == "deepep": if cfg.moe_runner_backend == "flashinfer_cutedsl": if cfg.deepep_mode == "auto": @@ -260,7 +396,7 @@ def handle_a2a_moe(server_args: Any): moe_a2a_backend="none", ) - if cfg.moe_a2a_backend == "flashinfer": + if a2a_now == "flashinfer": assert ( resolved_view(server_args).enable_dp_attention and cfg.dp_size == cfg.tp_size @@ -273,20 +409,44 @@ def handle_a2a_moe(server_args: Any): resolved_view(server_args).moe_runner_backend == "flashinfer_cutedsl" and envs.SGLANG_FLASHINFER_CUTEDSL_NVFP4_W4A16.get() ) - if use_cutedsl_w4a16: - if envs.SGLANG_MOE_NVFP4_DISPATCH.get(): - raise ValueError( - "CuTe DSL NVFP4 W4A16 requires BF16 FlashInfer MoE " - "dispatch; unset SGLANG_MOE_NVFP4_DISPATCH." - ) - elif not envs.SGLANG_MOE_NVFP4_DISPATCH.is_set() and ( - resolved_view(server_args).quantization == "modelopt_fp4" - or model_config_of(server_args).nvfp4_moe_meta is not None - ): - envs.SGLANG_MOE_NVFP4_DISPATCH.set(True) - logger.warning( - "SGLANG_MOE_NVFP4_DISPATCH is set to True for Flashinfer MoE A2A" + if use_cutedsl_w4a16 and envs.SGLANG_MOE_NVFP4_DISPATCH.get(): + raise ValueError( + "CuTe DSL NVFP4 W4A16 requires BF16 FlashInfer MoE " + "dispatch; unset SGLANG_MOE_NVFP4_DISPATCH." ) + if cfg.flashinfer_a2a_dispatch_type is None: + if ( + not use_cutedsl_w4a16 + and not envs.SGLANG_MOE_NVFP4_DISPATCH.is_set() + and ( + resolved_view(server_args).quantization == "modelopt_fp4" + or model_config_of(server_args).nvfp4_moe_meta is not None + ) + ): + envs.SGLANG_MOE_NVFP4_DISPATCH.set(True) + logger.warning( + "SGLANG_MOE_NVFP4_DISPATCH is set to True for Flashinfer MoE A2A" + ) + else: + if resolved_view(server_args).moe_runner_backend == "flashinfer_trtllm": + declare_resolution( + server_args, + "_handle_a2a_moe", + moe_runner_backend="flashinfer_trtllm_routed", + ) + logger.warning( + "Flashinfer MoE A2A is enabled with flashinfer_trtllm. " + "Using flashinfer_trtllm_routed because A2A dispatch " + "provides top-k ids and weights." + ) + if use_cutedsl_w4a16 and cfg.flashinfer_a2a_dispatch_type in ( + "auto", + "nvfp4", + ): + raise ValueError( + "CuTe DSL NVFP4 W4A16 requires --flashinfer-a2a-dispatch-type bf16." + ) + handle_flashinfer_a2a_dispatch_type(server_args) assert resolved_view(server_args).moe_runner_backend in [ "flashinfer_cutlass", "flashinfer_cutedsl", diff --git a/python/sglang/srt/arg_groups/overrides.py b/python/sglang/srt/arg_groups/overrides.py index b73d49583..d4245c66d 100644 --- a/python/sglang/srt/arg_groups/overrides.py +++ b/python/sglang/srt/arg_groups/overrides.py @@ -1483,7 +1483,12 @@ def _moe_runner_backend_quant_constraints(view: Any) -> dict: allowed = list(MXFP8_MOE_RUNNER_BACKEND_CHOICES) if is_gfx95_mxfp8: allowed.append("triton") - mxfp8_default = "triton" if is_gfx95_mxfp8 else "flashinfer_trtllm" + + if view.moe_a2a_backend == "flashinfer_megamoe": + mxfp8_default = "flashinfer_megamoe" + else: + mxfp8_default = "triton" if is_gfx95_mxfp8 else "flashinfer_trtllm" + if moe_runner_backend == "auto": moe_runner_backend = mxfp8_default elif moe_runner_backend not in allowed: @@ -1537,7 +1542,8 @@ def _moe_runner_fusion_disable(view: Any) -> dict: def _a2a_fusion_adjustments(view: Any) -> dict: """A2A-backend-driven shared-experts fusion adjustments, declared at the legacy write slots in _handle_a2a_moe: Waterfill requires the - fusion enabled; FlashInfer and DeepEP v2 A2A require it disabled.""" + fusion enabled; FlashInfer, FlashInfer MegaMOE, and DeepEP v2 A2A require it disabled. + """ if view.moe_a2a_backend in ("deepep", "megamoe") and view.enable_waterfill: if view.disable_shared_experts_fusion: logger.warning( @@ -1545,7 +1551,7 @@ def _a2a_fusion_adjustments(view: Any) -> dict: ) return {"disable_shared_experts_fusion": False} return {} - if view.moe_a2a_backend == "flashinfer": + if view.moe_a2a_backend in ("flashinfer", "flashinfer_megamoe"): logger.warning( "Flashinfer MoE A2A is enabled. --disable-shared-experts-fusion is automatically set." ) @@ -1566,6 +1572,7 @@ _A2A_EP_SPANNING_BACKENDS = frozenset( "nixl", "ascend_fuseep", "flashinfer", + "flashinfer_megamoe", "mori", "pplx", "deepep_v2", diff --git a/python/sglang/srt/environ.py b/python/sglang/srt/environ.py index 19f1f8dd7..5e7d4086a 100644 --- a/python/sglang/srt/environ.py +++ b/python/sglang/srt/environ.py @@ -989,6 +989,21 @@ class Envs: # Per-rank dispatch capacity of the FlashInfer MoE A2A dispatcher. Unset # means each call site keeps its own default. SGLANG_FLASHINFER_NUM_MAX_DISPATCH_TOKENS_PER_RANK = EnvInt(None) + # FlashInfer MegaMOE (generic moe_ep.MoEEpMegaLayer backend). Sizes the + # per-rank symmetric workspace; must be >= the largest padded per-rank batch + # (derived from cuda_graph_max_bs / chunked_prefill_size when unset). + SGLANG_FLASHINFER_MEGAMOE_MAX_TOKENS_PER_RANK = EnvInt(0) + # Opt-in in-kernel FC2 top-k reduce (cross-rank REDG atomic-add) for the + # cutedsl mega kernels (NVFP4 / MXFP8). Deletes the multi-GB combine staging + # region and can win at large batch, but makes the output accumulation order + # nondeterministic (bf16 unordered sum) -- keep off for bit-reproducibility. + # No effect on the DeepGEMM (block-FP8) mega path, which lacks the knob. + SGLANG_FLASHINFER_MEGAMOE_IN_KERNEL_FC2_REDUCE = EnvBool(False) + # Cross-rank combine wire format for the FlashInfer NVFP4 cutedsl MegaMOE + # kernel. "bf16" is exact/default; "mxfp8" and "nvfp4" reduce combine + # traffic with a small accuracy tradeoff and require FC2 reduce outside the + # kernel. + SGLANG_FLASHINFER_MEGAMOE_COMBINE_DTYPE = EnvStr("bf16") # Enable per-token FP32 activation scaling for serialized ModelOpt FP4 with # FlashInfer TRT-LLM or CuTe DSL v2 MoE. SGLANG_FLASHINFER_NVFP4_PER_TOKEN_ACTIVATION = EnvBool(False) diff --git a/python/sglang/srt/layers/moe/flashinfer_megamoe.py b/python/sglang/srt/layers/moe/flashinfer_megamoe.py new file mode 100644 index 000000000..1e336f576 --- /dev/null +++ b/python/sglang/srt/layers/moe/flashinfer_megamoe.py @@ -0,0 +1,672 @@ +# Copyright 2023-2024 SGLang Team +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Generic FlashInfer MegaMOE backend (moe_ep.MoEEpMegaLayer). + +Wraps FlashInfer's fused EP all-to-all + expert-compute mega kernel so it can +be selected as a model-agnostic MoE runner backend through the standard +FusedMoE dispatch -> run_moe_core -> combine flow. The mega kernel does its EP +communication internally via the deep_gemm symmetric buffer, so the dispatcher +and combine stay pure no-ops; this module owns the layer build + forward. +""" + +from __future__ import annotations + +import logging +from collections.abc import Callable, Generator +from contextlib import contextmanager +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +import torch + +from sglang.srt.environ import envs +from sglang.srt.layers.moe.moe_runner.base import ( + MoeQuantInfo, + MoeRunnerConfig, + register_fused_func, +) + +logger = logging.getLogger(__name__) + + +def _format_megakernel_config(config: Any) -> str: + """Readable one-line repr of a mega kernel config. + + The config dataclasses carry per-expert tensor fields (e.g. fc1_alpha / + fc2_alpha / fc1_norm_const); their default repr dumps every element, so + abbreviate tensors to shape/dtype/device instead. + """ + import dataclasses + + if not dataclasses.is_dataclass(config): + return repr(config) + + parts = [] + for field, value in zip(dataclasses.fields(config), dataclasses.astuple(config)): + if isinstance(value, torch.Tensor): + value = ( + f"Tensor(shape={tuple(value.shape)}, dtype={value.dtype}, " + f"device={value.device})" + ) + else: + value = repr(value) + parts.append(f"{field.name}={value}") + return f"{type(config).__name__}({', '.join(parts)})" + + +if TYPE_CHECKING: + from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE + from sglang.srt.layers.moe.token_dispatcher import ( + DispatchOutput, + StandardCombineInput, + ) + + +@contextmanager +def _capture_safe_ue8m0_pack() -> Generator[None, None, None]: + """Make deep_gemm's UE8M0 scale packing safe under CUDA graph capture. + + The deep_gemm mega staging path (block-FP8 models such as DeepSeek-V4-Flash) + runs ``per_token_cast_to_fp8(..., use_packed_ue8m0=True)`` on every forward, + which calls ``deep_gemm.utils.math.pack_ue8m0_to_int``. Its upstream + implementation carries two debug assertions:: + + assert (x_int >= 0).all() and (x_int & 0x7fffff == 0).all() + + ``.all()`` forces a device->host sync, which is illegal while a CUDA graph is + capturing. Replace the helper only around the mega forward that runs during + capture, then restore the exact upstream function. + + TODO(deepseek-ai/DeepGEMM#414): remove once upstream provides a capture-safe + helper: https://github.com/deepseek-ai/DeepGEMM/issues/414 + """ + if not torch.cuda.is_available() or not torch.cuda.is_current_stream_capturing(): + yield + return + + try: + import deep_gemm.utils.math as _dgm + except ImportError: + # deep_gemm is only needed by the block-FP8 mega path; NVFP4/MXFP8 mega + # runs on cutedsl and does not import it. Nothing to patch here. + yield + return + + def _pack_ue8m0_to_int(x: torch.Tensor) -> torch.Tensor: + x_int = x.view(torch.int) + return (x_int >> 23).to(torch.uint8).view(torch.int) + + original = _dgm.pack_ue8m0_to_int + _dgm.pack_ue8m0_to_int = _pack_ue8m0_to_int + try: + yield + finally: + _dgm.pack_ue8m0_to_int = original + + +@dataclass +class FlashInferMegaMoeQuantInfo(MoeQuantInfo): + mega: Any + mega_forward: Callable[[Any, Any], torch.Tensor] | None = None + fc1_alpha: torch.Tensor | None = None + fc2_alpha: torch.Tensor | None = None + fc1_norm_const: torch.Tensor | None = None + apply_routed_scaling_factor: bool = False + + def __post_init__(self) -> None: + if self.mega_forward is None: + self.mega_forward = _select_megamoe_forward(self.mega) + + +def _forward_megamoe_with_workspace_view(mega: Any, tensors: Any) -> torch.Tensor: + return mega.forward(tensors, return_workspace_view=True) + + +def _forward_megamoe_legacy(mega: Any, tensors: Any) -> torch.Tensor: + return mega.forward(tensors) + + +def _select_megamoe_forward(mega: Any) -> Callable[[Any, Any], torch.Tensor]: + import inspect + + if "return_workspace_view" in inspect.signature(mega.forward).parameters: + return _forward_megamoe_with_workspace_view + return _forward_megamoe_legacy + + +def _resolve_max_tokens_per_rank() -> int: + """Per-rank symmetric-buffer sizing for the mega kernel. + + Honors the explicit env override; otherwise derives the largest per-(DP)rank + token count a single MoE forward can route (same bound the cutedsl A2A path + uses), falling back to 1024 if it cannot be determined. + """ + configured = envs.SGLANG_FLASHINFER_MEGAMOE_MAX_TOKENS_PER_RANK.get() + if configured > 0: + return configured + + from sglang.srt.runtime_context import cutedsl_moe_max_num_tokens + + derived = cutedsl_moe_max_num_tokens() + return derived if derived > 0 else 1024 + + +def resolve_flashinfer_megamoe_combine_dtype() -> str: + combine_dtype = envs.SGLANG_FLASHINFER_MEGAMOE_COMBINE_DTYPE.get().strip().lower() + if combine_dtype not in ("bf16", "mxfp8", "nvfp4"): + raise ValueError( + "SGLANG_FLASHINFER_MEGAMOE_COMBINE_DTYPE must be one of " + f"'bf16', 'mxfp8', or 'nvfp4', got {combine_dtype!r}." + ) + if ( + combine_dtype != "bf16" + and envs.SGLANG_FLASHINFER_MEGAMOE_IN_KERNEL_FC2_REDUCE.get() + ): + raise ValueError( + "SGLANG_FLASHINFER_MEGAMOE_COMBINE_DTYPE=" + f"{combine_dtype!r} is incompatible with " + "SGLANG_FLASHINFER_MEGAMOE_IN_KERNEL_FC2_REDUCE=1." + ) + return combine_dtype + + +def _layer_ep_world_rank(layer: FusedMoE) -> tuple[int, int]: + world_size = int(layer.moe_ep_size) + rank = int(layer.moe_ep_rank) + if world_size <= 0: + raise ValueError(f"moe_ep_size must be positive, got {world_size}.") + if rank < 0 or rank >= world_size: + raise ValueError(f"moe_ep_rank must be in [0, {world_size}), got {rank}.") + return world_size, rank + + +def _scalar_float(value: Any) -> float: + if isinstance(value, torch.Tensor): + return float(value.detach().to(torch.float32).max()) + return float(value) + + +def _local_expert_vector(value: torch.Tensor, num_local_experts: int) -> torch.Tensor: + value = value.detach().to(torch.float32) + if value.dim() == 0: + return value.expand(num_local_experts).contiguous() + if value.shape != (num_local_experts,): + raise ValueError( + f"expected per-local-expert vector of shape ({num_local_experts},), " + f"got {tuple(value.shape)}" + ) + return value.contiguous() + + +def _validate_nvfp4_fc1_alpha(layer: FusedMoE) -> None: + """MegaMOE reuses ``g1_alphas`` as fc1_alpha; the kernel takes one alpha per + expert, so the gate and up FC1 alphas must agree.""" + if not layer.moe_runner_config.is_gated: + return + gate_alpha = _local_expert_vector(layer.g1_alphas, layer.num_local_experts) + up_alpha = _local_expert_vector(layer.g1_alphas_up, layer.num_local_experts) + if not torch.allclose(gate_alpha, up_alpha): + raise ValueError( + "FlashInfer NVFP4 MegaMOE requires matching gate/up FC1 alpha " + "values because the kernel accepts one alpha per expert." + ) + + +def _bind_transformed_weights( + layer: FusedMoE, + transformed_weights: Any, + *, + w13_scale_name: str, + w2_scale_name: str, +) -> None: + from sglang.srt.layers.utils.common import copy_or_rebind_param + + (w13_weight, w13_scale), (w2_weight, w2_scale) = transformed_weights + copy_or_rebind_param(layer, "w13_weight", w13_weight) + copy_or_rebind_param(layer, w13_scale_name, w13_scale) + copy_or_rebind_param(layer, "w2_weight", w2_weight) + copy_or_rebind_param(layer, w2_scale_name, w2_scale) + + +def _init_flashinfer_megamoe_layer_state(layer: FusedMoE) -> None: + layer._flashinfer_megamoe_layer = None + layer._flashinfer_megamoe_forward = None + layer._flashinfer_megamoe_input_norm_const = None + + +def _get_or_init_flashinfer_megamoe_layer_state(layer: FusedMoE) -> Any: + if not hasattr(layer, "_flashinfer_megamoe_layer"): + _init_flashinfer_megamoe_layer_state(layer) + return layer._flashinfer_megamoe_layer + + +def _ensure_flashinfer_megamoe_layer( + layer: FusedMoE, + *, + megakernel_config: Any, + w13_scale: torch.Tensor, + w2_scale: torch.Tensor, +) -> Any: + mega = _get_or_init_flashinfer_megamoe_layer_state(layer) + if mega is not None: + return mega + + from flashinfer.moe_ep import ( + BootstrapConfig, + FleetParams, + MegaConfig, + MoEEpMegaLayer, + ) + + transformed_weights = ( + (layer.w13_weight.data, w13_scale.data), + (layer.w2_weight.data, w2_scale.data), + ) + world_size, rank = _layer_ep_world_rank(layer) + + max_tokens_per_rank = _resolve_max_tokens_per_rank() + logger.debug( + "FlashInfer MegaMOE layer[%s] build: megakernel_config=%s " + "(world_size=%d, num_experts=%d, max_tokens_per_rank=%d, hidden_size=%d)", + layer.layer_id, + _format_megakernel_config(megakernel_config), + world_size, + layer.num_experts, + max_tokens_per_rank, + layer.hidden_size, + ) + + mega = MoEEpMegaLayer( + bootstrap=BootstrapConfig( + world_size=world_size, rank=rank, device=torch.cuda.current_device() + ), + fleet_params=FleetParams( + num_experts=layer.num_experts, + max_tokens_per_rank=max_tokens_per_rank, + token_hidden_size=layer.hidden_size, + ), + # weights already preprocessed in prepare_*; with transformed_weights set + # the kernel never reads `weights` (see MoEEpMegaLayer), so pass None. + weights=None, + backend=MegaConfig( + megakernel=megakernel_config, + preprocess_weights=False, + transformed_weights=transformed_weights, + ), + ) + layer._flashinfer_megamoe_layer = mega + layer._flashinfer_megamoe_forward = _select_megamoe_forward(mega) + return mega + + +def ensure_fp4_moe_layer_for_flashinfer_megamoe(layer: FusedMoE) -> Any: + mega = _get_or_init_flashinfer_megamoe_layer_state(layer) + if mega is not None: + return mega + + from flashinfer.moe_ep import DeepGemmMegaMoeConfig + + return _ensure_flashinfer_megamoe_layer( + layer, + megakernel_config=DeepGemmMegaMoeConfig( + intermediate_size=layer.intermediate_size_per_partition, + top_k=layer.top_k, + activation_clamp=layer.moe_runner_config.swiglu_limit, + ), + w13_scale=layer.w13_weight_scale_inv, + w2_scale=layer.w2_weight_scale_inv, + ) + + +def ensure_nvfp4_moe_layer_for_flashinfer_megamoe(layer: FusedMoE) -> Any: + mega = _get_or_init_flashinfer_megamoe_layer_state(layer) + if mega is not None: + return mega + + from flashinfer.moe_ep import Nvfp4CutedslMegaMoeConfig + + input_norm_const = layer._flashinfer_megamoe_input_norm_const + if input_norm_const is None: + if torch.cuda.is_available() and torch.cuda.is_current_stream_capturing(): + raise RuntimeError( + "FlashInfer NVFP4 MegaMOE layer must be initialized before " + "CUDA graph capture." + ) + logger.warning( + "FlashInfer NVFP4 MegaMOE layer[%s]: input_norm_const was not " + "precomputed at weight-load time; computing it lazily now via a " + "blocking device sync. This should only happen once per layer, " + "but if it happens during warmup it will look like a stall.", + layer.layer_id, + ) + input_norm_const = _scalar_float(layer.w13_input_scale_quant) + layer._flashinfer_megamoe_input_norm_const = input_norm_const + + return _ensure_flashinfer_megamoe_layer( + layer, + megakernel_config=Nvfp4CutedslMegaMoeConfig( + intermediate_size=layer.intermediate_size_per_partition, + top_k=layer.top_k, + gate_up_clamp=layer.moe_runner_config.swiglu_limit, + apply_topk_in_fc1=True, + in_kernel_fc2_reduce=envs.SGLANG_FLASHINFER_MEGAMOE_IN_KERNEL_FC2_REDUCE.get(), + combine_dtype=resolve_flashinfer_megamoe_combine_dtype(), + input_norm_const=input_norm_const, + fc1_alpha=layer.g1_alphas, + fc2_alpha=layer.g2_alphas, + fc1_norm_const=layer.w2_input_scale_quant, + ), + w13_scale=layer.w13_weight_scale, + w2_scale=layer.w2_weight_scale, + ) + + +def ensure_mxfp8_moe_layer_for_flashinfer_megamoe(layer: FusedMoE) -> Any: + mega = _get_or_init_flashinfer_megamoe_layer_state(layer) + if mega is not None: + return mega + + from flashinfer.moe_ep import Mxfp8CutedslMegaMoeConfig + + return _ensure_flashinfer_megamoe_layer( + layer, + megakernel_config=Mxfp8CutedslMegaMoeConfig( + intermediate_size=layer.intermediate_size_per_partition, + top_k=layer.top_k, + kind="mxfp8_e4m3", + gate_up_clamp=layer.moe_runner_config.swiglu_limit, + in_kernel_fc2_reduce=envs.SGLANG_FLASHINFER_MEGAMOE_IN_KERNEL_FC2_REDUCE.get(), + ), + w13_scale=layer.w13_weight_scale_inv, + w2_scale=layer.w2_weight_scale_inv, + ) + + +def prepare_fp4_moe_weights_for_flashinfer_megamoe( + layer: FusedMoE, +) -> None: + """Prepare loaded FP4 weights for MegaMOE. + + SGLang loads FP4-packed expert weights plus raw block scales. FlashInfer's + current moe_ep API owns backend-specific weight preprocessing, including + DeepGEMM scale layout transforms. + """ + _init_flashinfer_megamoe_layer_state(layer) + + from flashinfer.moe_ep import ( + MoEWeightPack, + preprocess_mega_weights, + ) + + weights = MoEWeightPack( + w13=layer.w13_weight.data, + w2=layer.w2_weight.data, + w13_scale=layer.w13_weight_scale_inv.data, + w2_scale=layer.w2_weight_scale_inv.data, + ) + transformed_weights = preprocess_mega_weights( + weights, + intermediate_size=layer.intermediate_size_per_partition, + hidden_size=layer.hidden_size, + ) + _bind_transformed_weights( + layer, + transformed_weights, + w13_scale_name="w13_weight_scale_inv", + w2_scale_name="w2_weight_scale_inv", + ) + + +def prepare_nvfp4_moe_weights_for_flashinfer_megamoe( + layer: FusedMoE, +) -> None: + _init_flashinfer_megamoe_layer_state(layer) + + from flashinfer.moe_ep import ( + MoEWeightPack, + preprocess_nvfp4_cutedsl_mega_weights, + ) + + if layer.hidden_size % 128 != 0: + raise ValueError( + "FlashInfer NVFP4 MegaMOE requires hidden_size to be a multiple " + f"of 128, got {layer.hidden_size}." + ) + if layer.quant_config.use_per_token_activation: + raise ValueError( + "FlashInfer NVFP4 MegaMOE does not support per-token activation " + "scaling. Use flashinfer_trtllm/flashinfer_trtllm_routed for " + "ModelOpt NVFP4 per-token activation." + ) + if layer.intermediate_size_per_partition % 128 != 0: + raise ValueError( + "FlashInfer NVFP4 MegaMOE requires intermediate_size_per_partition " + f"to be a multiple of 128, got {layer.intermediate_size_per_partition}." + ) + if layer.num_experts % layer.moe_ep_size != 0: + raise ValueError( + "FlashInfer NVFP4 MegaMOE requires num_experts to be divisible by " + f"ep_size, got {layer.num_experts=} and {layer.moe_ep_size=}." + ) + + _validate_nvfp4_fc1_alpha(layer) + layer._flashinfer_megamoe_input_norm_const = _scalar_float( + layer.w13_input_scale_quant + ) + + gate_up_clamp = layer.moe_runner_config.swiglu_limit + + weights = MoEWeightPack( + w13=layer.w13_weight.data, + w2=layer.w2_weight.data, + w13_scale=layer.w13_weight_scale.data, + w2_scale=layer.w2_weight_scale.data, + ) + transformed_weights = preprocess_nvfp4_cutedsl_mega_weights( + weights, + intermediate_size=layer.intermediate_size_per_partition, + hidden_size=layer.hidden_size, + gate_up_clamp=gate_up_clamp, + activation_clamp=None, + ) + _bind_transformed_weights( + layer, + transformed_weights, + w13_scale_name="w13_weight_scale", + w2_scale_name="w2_weight_scale", + ) + + +def prepare_mxfp8_moe_weights_for_flashinfer_megamoe( + layer: FusedMoE, +) -> None: + _init_flashinfer_megamoe_layer_state(layer) + + from flashinfer.moe_ep import ( + MoEWeightPack, + preprocess_mxfp8_cutedsl_mega_weights, + ) + + if layer.hidden_size % 128 != 0: + raise ValueError( + "FlashInfer MXFP8 MegaMOE requires hidden_size to be a multiple " + f"of 128, got {layer.hidden_size}." + ) + if layer.intermediate_size_per_partition % 128 != 0: + raise ValueError( + "FlashInfer MXFP8 MegaMOE requires intermediate_size_per_partition " + f"to be a multiple of 128, got {layer.intermediate_size_per_partition}." + ) + if layer.num_experts % layer.moe_ep_size != 0: + raise ValueError( + "FlashInfer MXFP8 MegaMOE requires num_experts to be divisible by " + f"ep_size, got {layer.num_experts=} and {layer.moe_ep_size=}." + ) + + weights = MoEWeightPack( + w13=layer.w13_weight.data, + w2=layer.w2_weight.data, + w13_scale=layer.w13_weight_scale_inv.data, + w2_scale=layer.w2_weight_scale_inv.data, + ) + transformed_weights = preprocess_mxfp8_cutedsl_mega_weights( + weights, + intermediate_size=layer.intermediate_size_per_partition, + hidden_size=layer.hidden_size, + kind="mxfp8_e4m3", + gate_up_clamp=layer.moe_runner_config.swiglu_limit, + activation_clamp=None, + ) + _bind_transformed_weights( + layer, + transformed_weights, + w13_scale_name="w13_weight_scale_inv", + w2_scale_name="w2_weight_scale_inv", + ) + + +def _ensure_shared_workspace(mega: Any) -> None: + """Share this layer's workspace across MegaMOE layers with identical + fleet/kernel geometry. + FlashInfer's own workspace pool keys by fc1_alpha/fc2_alpha/fc1_norm_const + tensor identity, but sglang binds distinct tensor objects per layer, so + its pool never hits across layers; key on geometry instead, since those + values are re-staged into the workspace per forward rather than baked + into the compiled kernel. Creation is collective, so it must only happen + on a layer's first forward, under warmup's cross-rank lockstep. + """ + if mega._workspace is not None: + return + fp = mega._fleet_params + kc = mega._megakernel_config + mc = mega._mega_config + from sglang.srt.runtime_context import get_resources + + key = ( + getattr(kc, "kernel_name", kc.__class__.__name__), + mega._bootstrap.world_size, + fp.num_experts, + fp.max_tokens_per_rank, + fp.token_hidden_size, + kc.top_k, + kc.intermediate_size, + getattr(kc, "gate_up_clamp", None), + getattr(kc, "activation_clamp", None), + getattr(kc, "apply_topk_in_fc1", None), + getattr(kc, "kind", None), + getattr(kc, "in_kernel_fc2_reduce", None), + getattr(kc, "combine_dtype", None), + getattr(kc, "token_back_by_dispatch", None), + getattr(kc, "fast_math", None), + mc.quantize_input, + ) + workspaces = get_resources().flashinfer_megamoe_workspaces + shared = workspaces.get(key) + if shared is None: + workspaces[key] = mega._ensure_workspace() + else: + mega._workspace = shared + + +@register_fused_func("flashinfer_megamoe", "flashinfer_megamoe") +def run_flashinfer_megamoe( + dispatch_output: DispatchOutput, + quant_info: MoeQuantInfo, + runner_config: MoeRunnerConfig, +) -> StandardCombineInput: + """Run the fused mega kernel and return per-rank outputs (no combine).""" + from flashinfer.moe_ep import MoEEpTensors + + from sglang.srt.layers.moe.token_dispatcher import StandardCombineInput + + assert isinstance(quant_info, FlashInferMegaMoeQuantInfo), ( + f"Unexpected quant_info type for flashinfer_megamoe: {type(quant_info)}" + ) + + x = dispatch_output.hidden_states + topk_output = dispatch_output.topk_output + topk_weights = topk_output.topk_weights + topk_ids = topk_output.topk_ids + mega = quant_info.mega + _ensure_shared_workspace(mega) + + t = MoEEpTensors( + hidden_states=x.to(torch.bfloat16), + # FlashInfer's fused staging accepts the int32 router output and widens + # directly into its final int64 workspace buffer. Keep this path copy-free. + topk_ids=topk_ids, + topk_weights=topk_weights.to(torch.float32), + fc1_alpha=quant_info.fc1_alpha, + fc2_alpha=quant_info.fc2_alpha, + fc1_norm_const=quant_info.fc1_norm_const, + ) + with _capture_safe_ue8m0_pack(): + assert quant_info.mega_forward is not None + y = quant_info.mega_forward(mega, t) + + if quant_info.apply_routed_scaling_factor: + rsf = runner_config.routed_scaling_factor + if rsf is not None and rsf != 1.0: + y.mul_(rsf) + + return StandardCombineInput(hidden_states=y) + + +def warmup_all_flashinfer_megamoe_layers(model: torch.nn.Module) -> None: + """Force every FlashInfer MegaMOE layer to build before CUDA graph capture. + ``ensure_*_moe_layer_for_flashinfer_megamoe`` builds a layer's state + lazily on its first forward; if that first forward instead happens inside + a CUDA graph capture, the lazy build's blocking device sync isn't allowed + and it raises rather than silently recovering. Call this once, explicitly, + right before capture begins (see ``ModelRunner.init_cuda_graphs``) so + every layer is built eagerly outside any graph. + Only the nvfp4 path is wired up below; extend the dispatch if fp4/mxfp8 + MegaMOE hits the same gap. + """ + from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE + + n_built = 0 + for module in model.modules(): + if not isinstance(module, FusedMoE): + continue + if not hasattr(module, "_flashinfer_megamoe_layer"): + # This layer's quant method never went through one of the + # prepare_*_moe_weights_for_flashinfer_megamoe hooks -- not a + # MegaMOE layer (or not on the flashinfer_megamoe backend). + continue + if getattr(module, "_flashinfer_megamoe_layer", None) is not None: + continue # already built (e.g. warmup's dummy batch hit it) + + # Dispatch mirrors modelopt_quant.py's apply(): only the nvfp4 + # method is wired up here today. + if type(module.quant_method).__name__ == "ModelOptNvFp4FusedMoEMethod": + ensure_nvfp4_moe_layer_for_flashinfer_megamoe(module) + n_built += 1 + else: + logger.warning( + "warmup_all_flashinfer_megamoe_layers: layer[%s] uses " + "quant_method=%s, which this eager pre-capture warmup does " + "not know how to build. If capture then fails with " + "'must be initialized before CUDA graph capture', add a " + "branch here for that quant method.", + getattr(module, "layer_id", "?"), + type(module.quant_method).__name__, + ) + + if n_built: + logger.info( + "warmup_all_flashinfer_megamoe_layers: eagerly built %d " + "FlashInfer MegaMOE layer(s) before CUDA graph capture.", + n_built, + ) diff --git a/python/sglang/srt/layers/moe/fused_moe_triton/layer.py b/python/sglang/srt/layers/moe/fused_moe_triton/layer.py index 06c8560aa..3b5de8c67 100644 --- a/python/sglang/srt/layers/moe/fused_moe_triton/layer.py +++ b/python/sglang/srt/layers/moe/fused_moe_triton/layer.py @@ -164,11 +164,14 @@ def create_moe_dispatcher(moe_runner_config: MoeRunnerConfig) -> BaseDispatcher: elif ( a2a_backend.is_none() or a2a_backend.is_megamoe() + or a2a_backend.is_flashinfer_megamoe() or a2a_backend.is_ascend_fuseep() ): # ascend_fuseep bypasses the dispatcher abstraction (see # forward_fuseep in hardware_backend/npu/moe/fuseep.py); a # StandardDispatcher is created but never invoked. + # flashinfer_megamoe does its EP all-to-all inside the kernel, so the + # dispatcher stays a pure noop passthrough. return StandardDispatcher(moe_runner_config) elif ( a2a_backend.is_deepep() diff --git a/python/sglang/srt/layers/moe/moe_runner/flashinfer_trtllm.py b/python/sglang/srt/layers/moe/moe_runner/flashinfer_trtllm.py index 5b3311445..2eb19936d 100644 --- a/python/sglang/srt/layers/moe/moe_runner/flashinfer_trtllm.py +++ b/python/sglang/srt/layers/moe/moe_runner/flashinfer_trtllm.py @@ -133,6 +133,8 @@ def round_up_to_multiple(x: int, m: int) -> int: if TYPE_CHECKING: from sglang.srt.layers.moe.token_dispatcher import ( + FlashinferCombineInput, + FlashinferDispatchOutput, StandardCombineInput, StandardDispatchOutput, ) @@ -721,13 +723,16 @@ class FlashInferTrtllmFp8MoeQuantInfo(MoeQuantInfo): def fused_experts_none_to_flashinfer_trtllm_fp8( - dispatch_output: StandardDispatchOutput, + dispatch_output: StandardDispatchOutput | FlashinferDispatchOutput, quant_info: FlashInferTrtllmFp8MoeQuantInfo, runner_config: MoeRunnerConfig, use_routed_topk: bool = False, ) -> StandardCombineInput: from flashinfer.fused_moe import Fp8QuantizationType + from sglang.srt.layers.moe.token_dispatcher.flashinfer import ( + FlashinferDispatchOutput, + ) from sglang.srt.layers.moe.token_dispatcher.standard import StandardCombineInput from sglang.srt.layers.moe.topk import TopKOutputChecker from sglang.srt.layers.moe.utils import RoutingMethodType @@ -740,6 +745,11 @@ def fused_experts_none_to_flashinfer_trtllm_fp8( assert not runner_config.no_combine, "no_combine is not supported for flashinfer." hidden_states = dispatch_output.hidden_states + output_dtype = ( + dispatch_output.output_dtype + if isinstance(dispatch_output, FlashinferDispatchOutput) + else hidden_states.dtype + ) topk_output = dispatch_output.topk_output if TopKOutputChecker.format_is_bypassed(topk_output): router_logits = topk_output.router_logits @@ -775,14 +785,26 @@ def fused_experts_none_to_flashinfer_trtllm_fp8( if quant_info.use_mxfp8: assert quant_info.weight_block_k == 32 - from sglang.srt.layers.quantization.fp8_utils import ( - flashinfer_mxfp8_quantize, - ) + if dispatch_output.hidden_states_scale is not None: + a_q = hidden_states + a_sf_t = dispatch_output.hidden_states_scale + a_sf_t = a_sf_t.reshape(hidden_states.shape[0], -1).contiguous() + else: + from sglang.srt.layers.quantization.fp8_utils import ( + flashinfer_mxfp8_quantize, + ) - a_q, a_sf = flashinfer_mxfp8_quantize(hidden_states, False) - # FlashInfer TRT-LLM MxFP8 expects token-major activation scales: - # [num_tokens, hidden_size // 32] (no transpose). - a_sf_t = a_sf.view(torch.uint8).reshape(hidden_states.shape[0], -1) + a_q, a_sf = flashinfer_mxfp8_quantize(hidden_states, False) + # FlashInfer TRT-LLM MxFP8 expects token-major activation scales: + # [num_tokens, hidden_size // 32] (no transpose). + a_sf_t = ( + a_sf.view(torch.uint8) + .reshape(hidden_states.shape[0], -1) + .contiguous() + ) + assert a_q.dtype == torch.float8_e4m3fn + assert a_sf_t.dtype == torch.uint8 + assert a_sf_t.shape[1] == hidden_states.shape[1] // 32 else: a_q, a_sf = per_token_group_quant_fp8( hidden_states, quant_info.weight_block_k, column_major_scales=True @@ -799,7 +821,7 @@ def fused_experts_none_to_flashinfer_trtllm_fp8( symm_output = torch.empty( hidden_states.shape[0], hidden_states.shape[1], - dtype=hidden_states.dtype, + dtype=output_dtype, device=hidden_states.device, ) diff --git a/python/sglang/srt/layers/moe/moe_runner/runner.py b/python/sglang/srt/layers/moe/moe_runner/runner.py index 9933775d9..ec684bbd1 100644 --- a/python/sglang/srt/layers/moe/moe_runner/runner.py +++ b/python/sglang/srt/layers/moe/moe_runner/runner.py @@ -130,6 +130,13 @@ class MoeRunner: from sglang.srt.layers.moe.moe_runner import ( # noqa: F401 flashinfer_cutlass, ) + elif runner_backend.is_flashinfer_megamoe(): + if lora_enabled: + raise NotImplementedError( + "FlashInfer MegaMOE does not support LoRA because it requires a fused path." + ) + self.runner_core = None # FlashInfer MegaMOE only supports fused path + import sglang.srt.layers.moe.flashinfer_megamoe # noqa: F401 elif runner_backend.is_cutlass(): self.runner_core = None # CUTLASS uses the direct cutlass_moe_fp4 path elif runner_backend.is_hpc_ops(): diff --git a/python/sglang/srt/layers/moe/token_dispatcher/flashinfer.py b/python/sglang/srt/layers/moe/token_dispatcher/flashinfer.py index 7df874f03..f248be4d7 100644 --- a/python/sglang/srt/layers/moe/token_dispatcher/flashinfer.py +++ b/python/sglang/srt/layers/moe/token_dispatcher/flashinfer.py @@ -32,7 +32,11 @@ from sglang.srt.layers.moe.topk import ( TopKOutput, TopKOutputChecker, ) -from sglang.srt.layers.moe.utils import get_moe_runner_backend +from sglang.srt.layers.moe.utils import ( + FlashinferA2ADispatchType, + get_flashinfer_a2a_dispatch_type, + get_moe_runner_backend, +) from sglang.srt.runtime_context import get_flags, get_parallel, get_schedule, get_spec from sglang.srt.speculative.spec_info import SpeculativeAlgorithm @@ -50,8 +54,6 @@ except ImportError: logger = logging.getLogger(__name__) -MOE_NVFP4_DISPATCH = envs.SGLANG_MOE_NVFP4_DISPATCH.get() - # FlashInfer keys MNNVL allocations by workspace size; aligned tail padding gives # concurrently live paths distinct persistent workspaces without extra token work. _WORKSPACE_NAMESPACE_ALIGNMENT = 128 @@ -91,6 +93,7 @@ class FlashinferDispatchOutput(NamedTuple): topk_output: StandardTopKOutput # Provide an output tensor to fused_moe so it writes directly to our buffer moe_output: Optional[torch.Tensor] = None + output_dtype: Optional[torch.dtype] = None @property def format(self) -> DispatchOutputFormat: @@ -151,6 +154,7 @@ class FlashinferDispatcher(BaseDispatcher): ) # TODO: Can other moe runners use payload_in_workspace too? self.payload_in_workspace = get_moe_runner_backend().is_flashinfer_cutlass() + self.dispatch_type = get_flashinfer_a2a_dispatch_type() if moe_runner_config is None: from sglang.srt.layers.moe.moe_runner.base import MoeRunnerConfig @@ -184,24 +188,42 @@ class FlashinferDispatcher(BaseDispatcher): if configured_max_tokens is not None else default_max_tokens ) - - # Calculate workspace size. For eagle mode, use the larger workspace size since nextn layer will be unquantized. speculative_algo = SpeculativeAlgorithm.from_string( get_spec().speculative_algorithm ) - if MOE_NVFP4_DISPATCH and not speculative_algo.is_eagle(): - total_dispatch_payload_size_per_token = ( + can_use_quantized_dispatch = not speculative_algo.is_eagle() + topk_id_and_weight_bytes = self.router_topk * 4 + self.router_topk * 4 + bf16_dispatch_payload_size_per_token = ( + hidden_size * 2 + topk_id_and_weight_bytes # bf16 hidden states + ) + if ( + self.dispatch_type == FlashinferA2ADispatchType.NVFP4 + and can_use_quantized_dispatch + ): + quantized_dispatch_payload_size_per_token = ( hidden_size // 2 # nvfp4 hidden states - + hidden_size // 16 # fp8 scaling factors - + self.router_topk * 4 # int32 topks ids - + self.router_topk * 4 # float32 topk weights + + hidden_size // 16 # uint8 scaling factors + + topk_id_and_weight_bytes + ) + total_dispatch_payload_size_per_token = max( + quantized_dispatch_payload_size_per_token, + bf16_dispatch_payload_size_per_token, + ) + elif ( + self.dispatch_type == FlashinferA2ADispatchType.MXFP8 + and can_use_quantized_dispatch + ): + quantized_dispatch_payload_size_per_token = ( + hidden_size # fp8 hidden states + + hidden_size // 32 # ue8m0 scaling factors + + topk_id_and_weight_bytes + ) + total_dispatch_payload_size_per_token = max( + quantized_dispatch_payload_size_per_token, + bf16_dispatch_payload_size_per_token, ) else: - total_dispatch_payload_size_per_token = ( - hidden_size * 2 # bf16 hidden states - + self.router_topk * 4 # int32 topks ids - + self.router_topk * 4 # float32 topk weights - ) + total_dispatch_payload_size_per_token = bf16_dispatch_payload_size_per_token combine_payload_size_per_token = hidden_size * 2 # bf16 hidden states self.workspace_size = moe_a2a_get_workspace_size_per_rank( ep_size=self.ep_size, @@ -293,6 +315,18 @@ class FlashinferDispatcher(BaseDispatcher): StandardTopKOutput(topk_weights, topk_ids, topk_output.router_logits), ) + def _effective_dispatch_type(self) -> FlashinferA2ADispatchType: + if self.dispatch_type == FlashinferA2ADispatchType.NVFP4: + global_scale = (self.quant_config or {}).get("input_global_scale", None) + if global_scale is None: + return FlashinferA2ADispatchType.BF16 + elif self.dispatch_type == FlashinferA2ADispatchType.MXFP8: + # Draft/NextN or mixed layers may not be MXFP8 even when the + # process-wide default is MXFP8. + if not (self.quant_config or {}).get("use_mxfp8", False): + return FlashinferA2ADispatchType.BF16 + return self.dispatch_type + @debug_kernel_api def dispatch( self, hidden_states: torch.Tensor, topk_output: TopKOutput @@ -320,6 +354,7 @@ class FlashinferDispatcher(BaseDispatcher): ) output_dtype = hidden_states.dtype + dispatch_type = self._effective_dispatch_type() x = hidden_states x_sf = None # FlashInfer dispatch requires materialized top-k IDs and weights. @@ -330,14 +365,36 @@ class FlashinferDispatcher(BaseDispatcher): topk_ids = topk_output.topk_ids.to(torch.int32) topk_weights = topk_output.topk_weights - global_scale = self.quant_config.get("input_global_scale", None) - if global_scale is not None: + if dispatch_type == FlashinferA2ADispatchType.NVFP4: + global_scale = (self.quant_config or {}).get("input_global_scale", None) + assert global_scale is not None if x.shape[0] > 0: x, x_sf = fp4_quantize(x, global_scale, is_sf_swizzled_layout=False) else: - x_col = x.shape[1] - x = torch.zeros(0, x_col // 2, dtype=torch.uint8, device=x.device) - x_sf = torch.zeros(0, x_col // 16, dtype=torch.uint8, device=x.device) + x = torch.zeros( + 0, self.hidden_size // 2, dtype=torch.uint8, device=x.device + ) + x_sf = torch.zeros( + 0, self.hidden_size // 16, dtype=torch.uint8, device=x.device + ) + elif dispatch_type == FlashinferA2ADispatchType.MXFP8: + if x.shape[0] > 0: + from flashinfer import mxfp8_quantize + + x, x_sf = mxfp8_quantize(x, False) + x_sf = x_sf.view(torch.uint8).reshape( + x.shape[0], self.hidden_size // 32 + ) + else: + x = torch.zeros( + 0, + self.hidden_size, + dtype=torch.float8_e4m3fn, + device=x.device, + ) + x_sf = torch.zeros( + 0, self.hidden_size // 32, dtype=torch.uint8, device=x.device + ) payloads = [] payloads.append(x) @@ -423,12 +480,17 @@ class FlashinferDispatcher(BaseDispatcher): if x_sf is not None: x_recv, x_sf_recv, topk_ids_recv, topk_weights_recv = recv_tensors x_sf = x_sf_recv.view(-1, x_sf_recv.shape[-1]) - # TODO: fuse interleave into cutlass moe - if get_moe_runner_backend().is_flashinfer_cutlass(): + # TODO: Fuse interleave into cutlass moe when FlashInfer supports it. + if ( + dispatch_type == FlashinferA2ADispatchType.NVFP4 + and get_moe_runner_backend().is_flashinfer_cutlass() + ): x_sf = nvfp4_block_scale_interleave(x_sf) else: x_recv, topk_ids_recv, topk_weights_recv = recv_tensors x = x_recv.view(-1, x_recv.shape[-1]) + if dispatch_type == FlashinferA2ADispatchType.MXFP8: + x = x.view(torch.float8_e4m3fn) topk_ids = topk_ids_recv.view(-1, topk_ids_recv.shape[-1]) topk_weights = topk_weights_recv.view(-1, topk_weights_recv.shape[-1]) @@ -443,6 +505,7 @@ class FlashinferDispatcher(BaseDispatcher): x_sf, StandardTopKOutput(topk_weights, topk_ids, topk_output.router_logits), moe_output, + output_dtype, ) @debug_kernel_api diff --git a/python/sglang/srt/layers/moe/token_dispatcher/standard.py b/python/sglang/srt/layers/moe/token_dispatcher/standard.py index 7bd27ab3f..89a4bd0a1 100644 --- a/python/sglang/srt/layers/moe/token_dispatcher/standard.py +++ b/python/sglang/srt/layers/moe/token_dispatcher/standard.py @@ -112,6 +112,7 @@ class StandardDispatcher(BaseDispatcher): # - cutlass / cutedsl / trtllm_routed handle EP internally # - mxfp4 dispatcher mapping is already global # - hpc_ops consumes global ids together with rank_ep / num_expert_total + # - flashinfer_megamoe routes by global expert ID inside the mega kernel self.skip_local_expert_mapping = ( backend.is_flashinfer_cutlass() or backend.is_flashinfer_cutedsl() @@ -119,6 +120,7 @@ class StandardDispatcher(BaseDispatcher): or backend.is_experimental_sgl_trtllm() or backend.is_flashinfer_trtllm_routed() or backend.is_hpc_ops() + or backend.is_flashinfer_megamoe() or self.enable_flashinfer_mxfp4_moe ) self.num_experts = moe_runner_config.num_experts diff --git a/python/sglang/srt/layers/moe/utils.py b/python/sglang/srt/layers/moe/utils.py index 88c07c1e3..16091ad96 100644 --- a/python/sglang/srt/layers/moe/utils.py +++ b/python/sglang/srt/layers/moe/utils.py @@ -43,6 +43,7 @@ class MoeA2ABackend(Enum): MEGAMOE = "megamoe" DEEPEP_V2 = "deepep_v2" PPLX = "pplx" + FLASHINFER_MEGAMOE = "flashinfer_megamoe" CUSTOMIZED = "customized" @classmethod @@ -87,6 +88,9 @@ class MoeA2ABackend(Enum): def is_pplx(self): return self == MoeA2ABackend.PPLX + def is_flashinfer_megamoe(self): + return self == MoeA2ABackend.FLASHINFER_MEGAMOE + def is_customized(self): return self == MoeA2ABackend.CUSTOMIZED @@ -141,6 +145,9 @@ class _MoeRunnerBackendPredicates: def is_flashinfer_cutedsl(self): return self.value == MoeRunnerBackend.FLASHINFER_CUTEDSL.value + def is_flashinfer_megamoe(self): + return self.value == MoeRunnerBackend.FLASHINFER_MEGAMOE.value + def is_flashinfer_mxfp4(self): return self.value == MoeRunnerBackend.FLASHINFER_MXFP4.value @@ -165,6 +172,9 @@ class _MoeRunnerBackendPredicates: def is_aiter(self): return self.value == MoeRunnerBackend.AITER.value + def is_intel_xpu(self): + return self.value == MoeRunnerBackend.INTEL_XPU.value + class MoeRunnerBackend(_MoeRunnerBackendPredicates, Enum): AUTO = "auto" @@ -178,6 +188,7 @@ class MoeRunnerBackend(_MoeRunnerBackendPredicates, Enum): FLASHINFER_CUTLASS = "flashinfer_cutlass" FLASHINFER_MXFP4 = "flashinfer_mxfp4" FLASHINFER_CUTEDSL = "flashinfer_cutedsl" + FLASHINFER_MEGAMOE = "flashinfer_megamoe" CUTLASS = "cutlass" MARLIN = "marlin" HUMMING = "humming" @@ -227,9 +238,6 @@ def resolve_moe_runner_backend( f"MoE runner backend {backend!r} is neither built in nor registered" ) from None - def is_intel_xpu(self): - return self == MoeRunnerBackend.INTEL_XPU - class DeepEPv2Fp8ScaleFormat(NamedTuple): """DeepGEMM FP8 activation-scale layout expected from DeepEP v2.""" @@ -286,6 +294,33 @@ class DispatcherOutputDtype(Enum): MXFP8 = "mxfp8" +class FlashinferA2ADispatchType(Enum): + BF16 = "bf16" + NVFP4 = "nvfp4" + MXFP8 = "mxfp8" + + +def get_flashinfer_a2a_dispatch_type() -> FlashinferA2ADispatchType: + dispatch_type = get_exec().moe.flashinfer_a2a_dispatch_type + + if dispatch_type is None: + if envs.SGLANG_MOE_NVFP4_DISPATCH.is_set(): + return ( + FlashinferA2ADispatchType.NVFP4 + if envs.SGLANG_MOE_NVFP4_DISPATCH.get() + else FlashinferA2ADispatchType.BF16 + ) + return FlashinferA2ADispatchType.BF16 + + if dispatch_type != "auto": + return FlashinferA2ADispatchType(dispatch_type) + + raise RuntimeError( + "flashinfer_a2a_dispatch_type='auto' reached the published runtime " + "configuration; ServerArgs must resolve it before publication" + ) + + def get_deepep_output_dtype(self) -> DispatcherOutputDtype: """ Automatically choose the dispatch output dtype for DeepEP. @@ -716,6 +751,11 @@ def should_skip_post_experts_all_reduce(*, is_tp_path: bool) -> bool: # pplx's AllToAll.combine already sums each token's expert outputs back # to the source rank return True + if get_moe_a2a_backend().is_flashinfer_megamoe(): + # The mega kernel does its EP all-to-all + combine internally and + # returns per-rank outputs, so any further EP/TP all-reduce would + # double-count. Same opt-in as the flashinfer a2a dispatcher. + return True return False diff --git a/python/sglang/srt/layers/quantization/fp8.py b/python/sglang/srt/layers/quantization/fp8.py index 711397bba..eed096de1 100644 --- a/python/sglang/srt/layers/quantization/fp8.py +++ b/python/sglang/srt/layers/quantization/fp8.py @@ -1667,6 +1667,13 @@ class Fp8MoEMethod(FusedMoEMethodBase): layer.w2_weight.contiguous(), (16, 16) ) return + elif self.use_mxfp8 and get_moe_a2a_backend().is_flashinfer_megamoe(): + from sglang.srt.layers.moe.flashinfer_megamoe import ( + prepare_mxfp8_moe_weights_for_flashinfer_megamoe, + ) + + prepare_mxfp8_moe_weights_for_flashinfer_megamoe(layer) + return elif self.use_mxfp8: self._process_mxfp8_moe_weights( layer, quantize=not self.quant_config.is_checkpoint_fp8_serialized @@ -1762,6 +1769,14 @@ class Fp8MoEMethod(FusedMoEMethodBase): build_mega_moe_experts_weights(layer) return + if get_moe_a2a_backend().is_flashinfer_megamoe(): + from sglang.srt.layers.moe.flashinfer_megamoe import ( + prepare_fp4_moe_weights_for_flashinfer_megamoe, + ) + + prepare_fp4_moe_weights_for_flashinfer_megamoe(layer) + return + if deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0 and will_use_deepgemm: from deep_gemm import transform_sf_into_required_layout @@ -2246,7 +2261,12 @@ class Fp8MoEMethod(FusedMoEMethodBase): self._prepare_hpc_ops_weights(layer) if hasattr(layer, "dispatcher"): - layer.dispatcher.set_quant_config({"weight_dtype": layer.w13_weight.dtype}) + layer.dispatcher.set_quant_config( + { + "weight_dtype": layer.w13_weight.dtype, + "use_mxfp8": self.use_mxfp8, + } + ) def _prepare_flashinfer_trtllm_activation_params(self, layer: Module) -> None: """Materialize optional TRT-LLM SwiGLU parameters once per expert.""" @@ -2440,6 +2460,7 @@ class Fp8MoEMethod(FusedMoEMethodBase): or moe_runner_backend.is_flashinfer_trtllm() or moe_runner_backend.is_flashinfer_trtllm_routed() or moe_runner_backend.is_hpc_ops() + or moe_runner_backend.is_flashinfer_megamoe() ): self.runner = MoeRunner(moe_runner_backend, moe_runner_config) self._owns_moe_runner = True @@ -2598,7 +2619,31 @@ class Fp8MoEMethod(FusedMoEMethodBase): ) return StandardCombineInput(hidden_states=output) - if self.runner.runner_backend.is_deep_gemm(): + if self.runner.runner_backend.is_flashinfer_megamoe(): + from sglang.srt.layers.moe.flashinfer_megamoe import ( + FlashInferMegaMoeQuantInfo, + ensure_fp4_moe_layer_for_flashinfer_megamoe, + ensure_mxfp8_moe_layer_for_flashinfer_megamoe, + ) + + if self.use_mxfp8: + ensure_megamoe_layer = ensure_mxfp8_moe_layer_for_flashinfer_megamoe + elif self.is_fp4_expert: + ensure_megamoe_layer = ensure_fp4_moe_layer_for_flashinfer_megamoe + else: + raise ValueError( + "FlashInfer MegaMOE does not support standard FP8 MoE " + "weights; use MXFP8, NVFP4, or an FP4-expert checkpoint." + ) + mega = ensure_megamoe_layer(layer) + quant_info = FlashInferMegaMoeQuantInfo( + mega=mega, + mega_forward=layer._flashinfer_megamoe_forward, + apply_routed_scaling_factor=( + not layer.should_fuse_routed_scaling_factor_in_topk + ), + ) + elif self.runner.runner_backend.is_deep_gemm(): w13_weight = layer.w13_weight w2_weight = layer.w2_weight diff --git a/python/sglang/srt/layers/quantization/modelopt_quant.py b/python/sglang/srt/layers/quantization/modelopt_quant.py index ec7a6bf4f..bcad61e6f 100755 --- a/python/sglang/srt/layers/quantization/modelopt_quant.py +++ b/python/sglang/srt/layers/quantization/modelopt_quant.py @@ -16,10 +16,13 @@ from sglang.srt.layers.moe import ( MoeRunner, MoeRunnerBackend, MoeRunnerConfig, + get_moe_a2a_backend, get_moe_runner_backend, ) from sglang.srt.layers.moe.moe_runner.triton import TritonMoeQuantInfo from sglang.srt.layers.moe.utils import ( + FlashinferA2ADispatchType, + get_flashinfer_a2a_dispatch_type, is_flashinfer_cutedsl_v1_path, should_use_flashinfer_cutlass_moe_fp4_allgather, ) @@ -291,6 +294,12 @@ MOE_NVFP4_DISPATCH = envs.SGLANG_MOE_NVFP4_DISPATCH.get() ACTIVATION_SCHEMES = ["static"] +def _use_nvfp4_dispatch() -> bool: + if not get_moe_a2a_backend().is_flashinfer(): + return MOE_NVFP4_DISPATCH + return get_flashinfer_a2a_dispatch_type() == FlashinferA2ADispatchType.NVFP4 + + _SUPPORTED_ACT_STRS = ("silu", "relu2", "gelu") @@ -2210,6 +2219,31 @@ class ModelOptNvFp4A16LinearMethod(LinearMethodBase): ) +def _input_scale_to_local_experts( + input_scale: torch.Tensor, + num_local_experts: int, + num_experts: int, + moe_ep_rank: int, +) -> torch.Tensor: + """Normalize a checkpoint input scale to this rank's local experts. + + Checkpoints may store the activation scale as a scalar, a per-local-expert + vector, or a global per-expert vector; return a (num_local_experts,) vector. + """ + input_scale = input_scale.detach().to(torch.float32) + if input_scale.dim() == 0: + return input_scale.expand(num_local_experts).contiguous() + if input_scale.shape == (num_local_experts,): + return input_scale.contiguous() + if input_scale.shape == (num_experts,): + start = moe_ep_rank * num_local_experts + return input_scale[start : start + num_local_experts].contiguous() + raise ValueError( + f"input scale must be scalar, ({num_local_experts},), or " + f"({num_experts},); got {tuple(input_scale.shape)}" + ) + + def _compute_gemm1_alphas( w13_weight_scale_2: torch.Tensor, w13_input_scale: torch.Tensor, @@ -2383,7 +2417,10 @@ class ModelOptNvFp4FusedMoEMethod(FusedMoEMethodBase): # TRTLLM replaces blockscale_swizzled with an alias to weight_scale # during process_weights_after_loading, so skip the expensive # swizzle+allocate here to avoid GPU memory fragmentation - if self.enable_flashinfer_trtllm_moe: + if ( + self.enable_flashinfer_trtllm_moe + or get_moe_runner_backend().is_flashinfer_megamoe() + ): layer.w13_blockscale_swizzled = None else: layer.w13_blockscale_swizzled = Parameter( @@ -2403,7 +2440,10 @@ class ModelOptNvFp4FusedMoEMethod(FusedMoEMethodBase): ) layer.register_parameter("w2_weight_scale", w2_weight_scale) - if self.enable_flashinfer_trtllm_moe: + if ( + self.enable_flashinfer_trtllm_moe + or get_moe_runner_backend().is_flashinfer_megamoe() + ): layer.w2_blockscale_swizzled = None else: layer.w2_blockscale_swizzled = Parameter( @@ -2493,6 +2533,7 @@ class ModelOptNvFp4FusedMoEMethod(FusedMoEMethodBase): moe_runner_backend = getattr( self, "_moe_runner_backend", get_moe_runner_backend() ) + use_nvfp4_dispatch = _use_nvfp4_dispatch() if moe_runner_backend.is_marlin(): # Marlin supports only a single shared w1/w3 weight scale, so collapse # the gate/up columns to the gate scale here. Other backends keep the @@ -2526,6 +2567,18 @@ class ModelOptNvFp4FusedMoEMethod(FusedMoEMethodBase): if self.enable_flashinfer_cutlass_moe or self.enable_flashinfer_trtllm_moe: w13_input_scale = layer.w13_input_scale.max().to(torch.float32) w2_input_scale = layer.w2_input_scale.max().to(torch.float32) + elif moe_runner_backend.is_flashinfer_megamoe(): + # MegaMOE folds a scalar w13 input scale into input_norm_const but keeps + # per-expert w2 scales, so g2_alphas / w2_input_scale_quant stay + # per-expert to feed the mega kernel's fc2_alpha / fc1_norm_const (keeps + # FC1-output renorm and FC2 dequant on the same per-expert scale). + w13_input_scale = layer.w13_input_scale.max().to(torch.float32) + w2_input_scale = _input_scale_to_local_experts( + layer.w2_input_scale, + layer.num_local_experts, + layer.num_experts, + layer.moe_ep_rank, + ) elif self.enable_flashinfer_cutedsl_moe: # CuteDSL standard path uses a single scalar input scale (all experts). w13_input_scale = ( @@ -2548,7 +2601,7 @@ class ModelOptNvFp4FusedMoEMethod(FusedMoEMethodBase): w13_input_scale = _slice_scale(w13_input_scale) w2_input_scale = _slice_scale(w2_input_scale) - if MOE_NVFP4_DISPATCH: + if use_nvfp4_dispatch: assert torch.all(w13_input_scale == w13_input_scale[0]) w13_input_scale = w13_input_scale[0] else: @@ -2633,7 +2686,7 @@ class ModelOptNvFp4FusedMoEMethod(FusedMoEMethodBase): not self.quant_config.use_per_token_activation and not use_cutedsl_w4a16 and ( - MOE_NVFP4_DISPATCH or should_use_flashinfer_cutlass_moe_fp4_allgather() + use_nvfp4_dispatch or should_use_flashinfer_cutlass_moe_fp4_allgather() ) ) @@ -2672,6 +2725,14 @@ class ModelOptNvFp4FusedMoEMethod(FusedMoEMethodBase): f"{name} Weight Blockscale must be represented as FP8-E4M3" ) + if moe_runner_backend.is_flashinfer_megamoe(): + from sglang.srt.layers.moe.flashinfer_megamoe import ( + prepare_nvfp4_moe_weights_for_flashinfer_megamoe, + ) + + prepare_nvfp4_moe_weights_for_flashinfer_megamoe(layer) + return + # Weight processing based on strategy if ( self.enable_flashinfer_trtllm_moe @@ -2900,6 +2961,25 @@ class ModelOptNvFp4FusedMoEMethod(FusedMoEMethodBase): ), f"{activation=} is unsupported by {moe_runner_backend}" moe_runner_config = self.moe_runner_config + if moe_runner_backend.is_flashinfer_megamoe(): + from sglang.srt.layers.moe.flashinfer_megamoe import ( + FlashInferMegaMoeQuantInfo, + ensure_nvfp4_moe_layer_for_flashinfer_megamoe, + ) + + mega = ensure_nvfp4_moe_layer_for_flashinfer_megamoe(layer) + quant_info = FlashInferMegaMoeQuantInfo( + mega=mega, + mega_forward=layer._flashinfer_megamoe_forward, + fc1_alpha=layer.g1_alphas, + fc2_alpha=layer.g2_alphas, + fc1_norm_const=layer.w2_input_scale_quant, + apply_routed_scaling_factor=( + not layer.should_fuse_routed_scaling_factor_in_topk + ), + ) + return self.runner.run(dispatch_output, quant_info) + if moe_runner_backend.is_marlin(): quant_info = self.get_marlin_quant_info(layer) return self.runner.run(dispatch_output, quant_info) diff --git a/python/sglang/srt/model_executor/model_runner.py b/python/sglang/srt/model_executor/model_runner.py index 763f6bb3b..aa20efd8c 100644 --- a/python/sglang/srt/model_executor/model_runner.py +++ b/python/sglang/srt/model_executor/model_runner.py @@ -1081,6 +1081,19 @@ class ModelRunner: return self.sampling_prewarm_result def init_cuda_graphs(self, capture_decode_cuda_graph: bool = True): + # from sglang.srt.layers.moe.utils import get_moe_runner_backend + + # if get_moe_runner_backend().is_flashinfer_megamoe(): + # # Warmup's dummy batches aren't guaranteed to route through every + # # MoE layer; a layer that first builds mid-capture instead of + # # during warmup hits a hard RuntimeError (capture forbids the + # # lazy build's blocking device sync). Force every layer to build + # # here, eagerly, outside any graph. + # from sglang.srt.layers.moe.flashinfer_megamoe import ( + # warmup_all_flashinfer_megamoe_layers, + # ) + + # warmup_all_flashinfer_megamoe_layers(self.model) capture = capture_cuda_graphs( model_runner=self, capture_decode_cuda_graph=capture_decode_cuda_graph ) diff --git a/python/sglang/srt/models/deepseek_v2.py b/python/sglang/srt/models/deepseek_v2.py index 03c9b9c99..41148eb40 100644 --- a/python/sglang/srt/models/deepseek_v2.py +++ b/python/sglang/srt/models/deepseek_v2.py @@ -717,6 +717,7 @@ class DeepseekV2MoE(nn.Module): or get_moe_a2a_backend().is_ascend_fuseep() or get_moe_a2a_backend().is_flashinfer() or get_moe_a2a_backend().is_megamoe() + or get_moe_a2a_backend().is_flashinfer_megamoe() or get_moe_a2a_backend().is_deepep_v2() or should_use_flashinfer_cutlass_moe_fp4_allgather() or envs.SGLANG_SHARED_EXPERT_TP1.get() diff --git a/python/sglang/srt/models/glm4_moe.py b/python/sglang/srt/models/glm4_moe.py index 8f2791be4..fbda6fdc0 100644 --- a/python/sglang/srt/models/glm4_moe.py +++ b/python/sglang/srt/models/glm4_moe.py @@ -468,6 +468,7 @@ class Glm4MoeSparseMoeBlock(nn.Module): or get_moe_a2a_backend().is_mori() or get_moe_a2a_backend().is_ascend_fuseep() or get_moe_a2a_backend().is_flashinfer() + or get_moe_a2a_backend().is_flashinfer_megamoe() or should_use_flashinfer_cutlass_moe_fp4_allgather() else {} ), diff --git a/python/sglang/srt/models/nemotron_h.py b/python/sglang/srt/models/nemotron_h.py index 8621c7d4a..fd85df6bc 100644 --- a/python/sglang/srt/models/nemotron_h.py +++ b/python/sglang/srt/models/nemotron_h.py @@ -247,6 +247,7 @@ class NemotronHMoE(nn.Module): dict(tp_rank=0, tp_size=1) if get_moe_a2a_backend().is_deepep() or get_moe_a2a_backend().is_flashinfer() + or get_moe_a2a_backend().is_flashinfer_megamoe() else {} ), prefix=f"{prefix}.shared_experts", diff --git a/python/sglang/srt/models/qwen2_moe.py b/python/sglang/srt/models/qwen2_moe.py index 60020c7c7..83aad13eb 100644 --- a/python/sglang/srt/models/qwen2_moe.py +++ b/python/sglang/srt/models/qwen2_moe.py @@ -376,6 +376,7 @@ class Qwen2MoeSparseMoeBlock(nn.Module): or get_moe_a2a_backend().is_mori() or get_moe_a2a_backend().is_deepep_v2() or get_moe_a2a_backend().is_flashinfer() + or get_moe_a2a_backend().is_flashinfer_megamoe() ) else {} ), diff --git a/python/sglang/srt/models/qwen3_moe.py b/python/sglang/srt/models/qwen3_moe.py index 7371bd344..a2a71be21 100644 --- a/python/sglang/srt/models/qwen3_moe.py +++ b/python/sglang/srt/models/qwen3_moe.py @@ -318,9 +318,12 @@ class Qwen3MoeSparseMoeBlock(nn.Module): num_tokens, hidden_dim = hidden_states.shape hidden_states = hidden_states.view(-1, hidden_dim) - # router_logits: (num_tokens, n_experts) - router_logits, _ = self.gate(hidden_states) - topk_output = self.topk(hidden_states, router_logits) + if hidden_states.shape[0] > 0: + # router_logits: (num_tokens, n_experts) + router_logits, _ = self.gate(hidden_states) + topk_output = self.topk(hidden_states, router_logits) + else: + topk_output = self.topk.empty_topk_output(hidden_states.device) final_hidden_states = self.experts(hidden_states, topk_output) if self.ep_size > 1 and not should_skip_post_experts_all_reduce( diff --git a/python/sglang/srt/runtime_context.py b/python/sglang/srt/runtime_context.py index 567f7f654..92c7889f5 100644 --- a/python/sglang/srt/runtime_context.py +++ b/python/sglang/srt/runtime_context.py @@ -622,6 +622,7 @@ class Resources(_FlagGroupBase): # Persistent reusable CUDA events for non-EP DP TBO, keyed by # (kind, subbatch) β€” see dp_attention._tbo_event for why reuse matters. tbo_event_pool: dict = msgspec.field(default_factory=dict) + flashinfer_megamoe_workspaces: dict = msgspec.field(default_factory=dict) # State capturers (installed by their subsystems when capture is on). indexer_capturer: Any = None experts_capturer: Any = None diff --git a/test/manual/ep/test_flashinfer_dispatcher.py b/test/manual/ep/test_flashinfer_dispatcher.py index cadf605cc..4847b5c81 100644 --- a/test/manual/ep/test_flashinfer_dispatcher.py +++ b/test/manual/ep/test_flashinfer_dispatcher.py @@ -4,13 +4,15 @@ import torch from sglang.srt.distributed import init_distributed_environment from sglang.srt.distributed.parallel_state import ( + destroy_distributed_environment, + destroy_model_parallel, get_tp_group, initialize_model_parallel, ) from sglang.srt.layers.dp_attention import set_dp_buffer_len from sglang.srt.layers.moe.token_dispatcher.flashinfer import FlashinferDispatcher from sglang.srt.layers.moe.utils import initialize_moe_config -from sglang.srt.runtime_context import publish +from sglang.srt.runtime_context import get_context, publish from sglang.srt.server_args import ServerArgs, set_global_server_args_for_scheduler from sglang.test.test_utils import CustomTestCase @@ -21,6 +23,7 @@ class TestFlashinferDispatcher(CustomTestCase): server_args = ServerArgs(model_path="dummy") server_args.moe_runner_backend = "flashinfer_cutlass" server_args.moe_a2a_backend = "flashinfer" + cls.server_args = server_args set_global_server_args_for_scheduler(server_args) publish(server_args, role="scheduler") initialize_moe_config() @@ -41,9 +44,18 @@ class TestFlashinferDispatcher(CustomTestCase): @classmethod def tearDownClass(cls): - # Clean up distributed environment - if torch.distributed.is_initialized(): - torch.distributed.destroy_process_group() + try: + from flashinfer.comm.trtllm_moe_alltoall import MoeAlltoAll + + for workspace in MoeAlltoAll._WORKSPACE_CACHE.values(): + mnnvl_mem = workspace.get("mnnvl_mem") + if mnnvl_mem is not None and "ptr" in vars(mnnvl_mem): + del mnnvl_mem.ptr + MoeAlltoAll._WORKSPACE_CACHE.clear() + except ImportError: + pass + destroy_model_parallel() + destroy_distributed_environment() def create_dispatcher( self, router_topk=2, num_experts=8, num_local_experts=4, hidden_size=128 @@ -58,8 +70,40 @@ class TestFlashinferDispatcher(CustomTestCase): params_dtype=torch.bfloat16, ) + def set_dispatch_type(self, dispatch_type): + get_context().override( + "test_flashinfer_dispatcher", + flashinfer_a2a_dispatch_type=dispatch_type, + ) + + def _zero_moe_a2a_dispatch_payloads(self): + # Shared MoeAlltoAll workspaces keep stale recv payloads across tests. + # Zero only the payload region so unused-source == 0 asserts stay valid. + try: + from flashinfer.comm.trtllm_moe_alltoall import ( + MoeAlltoAll, + get_moe_alltoall_module, + ) + except ImportError: + return + + module = get_moe_alltoall_module() + for ws in MoeAlltoAll._WORKSPACE_CACHE.values(): + workspace = ws["workspace"] + aux = int( + module.moe_a2a_get_aux_data_size( + ws["ep_size"], + ws["max_num_tokens"], + ws["eplb_stats_num_experts"], + ) + ) + aux = ((aux + 127) // 128) * 128 + if aux < workspace.shape[1]: + workspace[:, aux:].zero_() + def test_dispatch_basic(self): """Test basic dispatch functionality""" + self.set_dispatch_type("bf16") num_tokens = 16 hidden_size = 128 router_topk = 1 # Single expert per token for simplicity @@ -143,9 +187,10 @@ class TestFlashinferDispatcher(CustomTestCase): def test_dispatch_with_empty_tokens(self): """Test dispatch when there are no tokens (edge case)""" + self.set_dispatch_type("bf16") # This tests the dummy token handling num_tokens = 16 - hidden_size = 1 + hidden_size = 128 router_topk = 1 # Single expert per token for simplicity world_size = torch.distributed.get_world_size() rank = torch.distributed.get_rank() @@ -195,6 +240,9 @@ class TestFlashinferDispatcher(CustomTestCase): topk_weights=topk_weights, topk_ids=topk_ids, router_logits=None ) + self._zero_moe_a2a_dispatch_payloads() + torch.distributed.barrier() + dispatcher = self.create_dispatcher( router_topk=router_topk, num_experts=num_experts, @@ -250,6 +298,7 @@ class TestFlashinferDispatcher(CustomTestCase): def test_dispatch_with_fp4_quantization(self): """Test dispatch with FP4 quantization enabled""" + self.set_dispatch_type("nvfp4") num_tokens = 128 hidden_size = 128 router_topk = 1 # Single expert per token for simplicity @@ -312,6 +361,133 @@ class TestFlashinferDispatcher(CustomTestCase): ) self.assertEqual(dispatch_output.hidden_states_scale.dtype, torch.uint8) + def test_dispatch_with_mxfp8_quantization(self): + """Test dispatch with MXFP8 quantization enabled""" + self.set_dispatch_type("mxfp8") + num_tokens = 128 + hidden_size = 128 + router_topk = 1 + world_size = torch.distributed.get_world_size() + rank = torch.distributed.get_rank() + num_experts = world_size + num_local_experts = 1 + + set_dp_buffer_len( + global_dp_buffer_len=num_tokens * world_size, + local_dp_buffer_len=num_tokens, + dp_max_padding=True, + global_num_tokens=None, + ) + + hidden_states = torch.randn( + (num_tokens, hidden_size), dtype=torch.bfloat16, device="cuda" + ) + + target_rank = (rank + 1) % world_size + target_expert = target_rank + topk_ids = torch.full( + (num_tokens, router_topk), target_expert, dtype=torch.int32, device="cuda" + ) + topk_weights = torch.ones( + (num_tokens, router_topk), dtype=torch.float32, device="cuda" + ) + + from sglang.srt.layers.moe.topk import StandardTopKOutput + + topk_output = StandardTopKOutput( + topk_weights=topk_weights, topk_ids=topk_ids, router_logits=None + ) + + dispatcher = self.create_dispatcher( + router_topk=router_topk, + num_experts=num_experts, + num_local_experts=num_local_experts, + hidden_size=hidden_size, + ) + dispatcher.set_quant_config({"input_global_scale": None, "use_mxfp8": True}) + + dispatch_output = dispatcher.dispatch(hidden_states, topk_output) + + self.assertEqual( + dispatch_output.hidden_states.shape, + (num_tokens * world_size, hidden_size), + ) + self.assertEqual(dispatch_output.hidden_states.dtype, torch.float8_e4m3fn) + self.assertEqual(dispatch_output.output_dtype, torch.bfloat16) + + self.assertIsNotNone(dispatch_output.hidden_states_scale) + self.assertEqual( + dispatch_output.hidden_states_scale.shape, + (num_tokens * world_size, hidden_size // 32), + ) + self.assertEqual(dispatch_output.hidden_states_scale.dtype, torch.uint8) + self.assertEqual( + dispatch_output.topk_output.topk_ids.shape, + (num_tokens * world_size, router_topk), + ) + self.assertEqual(dispatch_output.topk_output.topk_ids.dtype, torch.int32) + + def test_dispatch_with_mxfp8_quantization_and_empty_rank(self): + """All ranks must contribute the same payload dtypes, including empty ranks.""" + self.set_dispatch_type("mxfp8") + num_tokens = 16 + hidden_size = 128 + router_topk = 1 + world_size = torch.distributed.get_world_size() + rank = torch.distributed.get_rank() + empty_rank = 1 + + global_num_tokens = [num_tokens] * world_size + global_num_tokens[empty_rank] = 0 + set_dp_buffer_len( + global_dp_buffer_len=num_tokens * world_size, + local_dp_buffer_len=num_tokens, + dp_max_padding=False, + global_num_tokens=global_num_tokens, + ) + + local_tokens = 0 if rank == empty_rank else num_tokens + hidden_states = torch.randn( + (local_tokens, hidden_size), dtype=torch.bfloat16, device="cuda" + ) + target_expert = (rank + 1) % world_size + topk_ids = torch.full( + (local_tokens, router_topk), + target_expert, + dtype=torch.int32, + device="cuda", + ) + topk_weights = torch.ones( + (local_tokens, router_topk), dtype=torch.float32, device="cuda" + ) + + from sglang.srt.layers.moe.topk import StandardTopKOutput + + dispatcher = self.create_dispatcher( + router_topk=router_topk, + num_experts=world_size, + num_local_experts=1, + hidden_size=hidden_size, + ) + dispatcher.set_quant_config({"input_global_scale": None, "use_mxfp8": True}) + self._zero_moe_a2a_dispatch_payloads() + torch.distributed.barrier() + dispatch_output = dispatcher.dispatch( + hidden_states, + StandardTopKOutput( + topk_weights=topk_weights, + topk_ids=topk_ids, + router_logits=None, + ), + ) + + self.assertEqual(dispatch_output.hidden_states.dtype, torch.float8_e4m3fn) + self.assertEqual(dispatch_output.hidden_states_scale.dtype, torch.uint8) + self.assertEqual( + dispatch_output.hidden_states.shape, + (num_tokens * world_size, hidden_size), + ) + if __name__ == "__main__": """ diff --git a/test/registered/backends/test_flashinfer_trtllm_gen_moe_backend.py b/test/registered/backends/test_flashinfer_trtllm_gen_moe_backend.py index fa9a3f862..13a10ed3c 100644 --- a/test/registered/backends/test_flashinfer_trtllm_gen_moe_backend.py +++ b/test/registered/backends/test_flashinfer_trtllm_gen_moe_backend.py @@ -155,6 +155,56 @@ class FlashinferTrtllmGenMoeBackendMXFP8Base: self.assertGreater(metrics["score"], 0.93) +class FlashinferTrtllmGenMoeBackendMXFP8A2ABase: + backend = "flashinfer_trtllm_routed" + + @classmethod + def setUpClass(cls): + cls.model = "zianglih/Qwen3-30B-A3B-Instruct-2507-MXFP8" + cls.base_url = DEFAULT_URL_FOR_TEST + cls.process = popen_launch_server( + cls.model, + cls.base_url, + timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH, + env={**os.environ, "SGLANG_ENABLE_JIT_DEEPGEMM": "False"}, + other_args=[ + "--quantization", + "mxfp8", + "--enable-dp-attention", + "--dp-size", + "4", + "--tp-size", + "4", + "--moe-a2a-backend", + "flashinfer", + "--moe-runner-backend", + cls.backend, + "--flashinfer-a2a-dispatch-type", + "mxfp8", + "--mem-fraction-static", + "0.7", + ], + ) + + @classmethod + def tearDownClass(cls): + kill_process_tree(cls.process.pid) + + def test_gsm8k(self): + args = SimpleNamespace( + base_url=self.base_url, + model=self.model, + eval_name="gsm8k", + api="completion", + max_tokens=512, + num_examples=200, + num_threads=128, + ) + metrics = run_eval(args) + print(f"{metrics=}") + self.assertGreater(metrics["score"], 0.93) + + class FlashinferTrtllmGenMoeBackendMXFP8MixedBF16Base: backend = None @@ -261,6 +311,12 @@ class TestFlashinferTrtllmGenMoeBackendMXFP8Routed( backend = "flashinfer_trtllm_routed" +class TestFlashinferTrtllmGenMoeBackendMXFP8A2A( + FlashinferTrtllmGenMoeBackendMXFP8A2ABase, CustomTestCase +): + pass + + class TestFlashinferTrtllmRoutedMxfp8MixedBF16( FlashinferTrtllmGenMoeBackendMXFP8MixedBF16Base, CustomTestCase ): diff --git a/test/registered/unit/layers/moe/test_flashinfer_dispatcher.py b/test/registered/unit/layers/moe/test_flashinfer_dispatcher.py new file mode 100644 index 000000000..e45b00b32 --- /dev/null +++ b/test/registered/unit/layers/moe/test_flashinfer_dispatcher.py @@ -0,0 +1,62 @@ +import sys +from unittest.mock import patch + +import torch + +from sglang.srt.layers.moe.token_dispatcher.flashinfer import FlashinferDispatcher +from sglang.srt.layers.moe.topk import StandardTopKOutput +from sglang.srt.layers.moe.utils import FlashinferA2ADispatchType +from sglang.test.ci.ci_register import register_cpu_ci + +register_cpu_ci(est_time=1, suite="base-a-test-cpu") + + +def test_empty_mxfp8_dispatch_uses_same_payload_dtype_as_nonempty_rank(): + class FakeMoeAlltoAll: + def dispatch(self, _topk_ids, payloads, *_args, **_kwargs): + self.payload_dtypes = [payload.dtype for payload in payloads] + return payloads + + dispatcher = object.__new__(FlashinferDispatcher) + dispatcher.dispatch_type = FlashinferA2ADispatchType.MXFP8 + dispatcher.hidden_size = 128 + dispatcher.max_num_tokens = 0 + dispatcher.ep_size = 1 + dispatcher.invalid_token_expert_id = 8 + dispatcher.payload_in_workspace = False + dispatcher.quant_config = {"use_mxfp8": True} + dispatcher.moe_a2a = FakeMoeAlltoAll() + + hidden_states = torch.empty((0, 128), dtype=torch.bfloat16) + topk_output = StandardTopKOutput( + topk_weights=torch.empty((0, 1), dtype=torch.float32), + topk_ids=torch.empty((0, 1), dtype=torch.int32), + router_logits=None, + ) + + with ( + patch( + "sglang.srt.layers.moe.token_dispatcher.flashinfer.get_dp_global_num_tokens", + return_value=None, + ), + patch( + "sglang.srt.layers.moe.token_dispatcher.flashinfer.is_dp_attention_enabled", + return_value=False, + ), + ): + output = dispatcher.dispatch(hidden_states, topk_output) + + assert dispatcher.moe_a2a.payload_dtypes == [ + torch.float8_e4m3fn, + torch.uint8, + torch.int32, + torch.float32, + ] + assert output.hidden_states.dtype == torch.float8_e4m3fn + assert output.hidden_states_scale.dtype == torch.uint8 + + +if __name__ == "__main__": + import pytest + + sys.exit(pytest.main([__file__, "-v"])) diff --git a/test/registered/unit/layers/moe/test_flashinfer_megamoe.py b/test/registered/unit/layers/moe/test_flashinfer_megamoe.py new file mode 100644 index 000000000..cd73104ed --- /dev/null +++ b/test/registered/unit/layers/moe/test_flashinfer_megamoe.py @@ -0,0 +1,220 @@ +import importlib.util +import sys +import types +from pathlib import Path + +import torch + +from sglang.test.ci.ci_register import register_cpu_ci + +register_cpu_ci(est_time=5, suite="base-a-test-cpu") + + +def _load_megamoe_module(monkeypatch): + """Load the adapter with only its small import-time dependencies stubbed.""" + + class MoeQuantInfo: + pass + + class MoeRunnerConfig: + pass + + def register_fused_func(*_args, **_kwargs): + return lambda fn: fn + + fake_modules = { + "sglang": types.ModuleType("sglang"), + "sglang.srt": types.ModuleType("sglang.srt"), + "sglang.srt.environ": types.ModuleType("sglang.srt.environ"), + "sglang.srt.layers": types.ModuleType("sglang.srt.layers"), + "sglang.srt.layers.moe": types.ModuleType("sglang.srt.layers.moe"), + "sglang.srt.layers.moe.moe_runner": types.ModuleType( + "sglang.srt.layers.moe.moe_runner" + ), + "sglang.srt.layers.moe.moe_runner.base": types.ModuleType( + "sglang.srt.layers.moe.moe_runner.base" + ), + "sglang.srt.layers.moe.token_dispatcher": types.ModuleType( + "sglang.srt.layers.moe.token_dispatcher" + ), + "sglang.srt.runtime_context": types.ModuleType("sglang.srt.runtime_context"), + "deep_gemm": types.ModuleType("deep_gemm"), + "deep_gemm.utils": types.ModuleType("deep_gemm.utils"), + "deep_gemm.utils.math": types.ModuleType("deep_gemm.utils.math"), + } + fake_modules["sglang.srt.environ"].envs = types.SimpleNamespace( + SGLANG_FLASHINFER_MEGAMOE_MAX_TOKENS_PER_RANK=types.SimpleNamespace( + get=lambda: 0 + ), + SGLANG_FLASHINFER_MEGAMOE_COMBINE_DTYPE=types.SimpleNamespace( + get=lambda: "bf16" + ), + SGLANG_FLASHINFER_MEGAMOE_IN_KERNEL_FC2_REDUCE=types.SimpleNamespace( + get=lambda: False + ), + ) + runtime_context = fake_modules["sglang.srt.runtime_context"] + runtime_context.cutedsl_moe_max_num_tokens = lambda: 2048 + base = fake_modules["sglang.srt.layers.moe.moe_runner.base"] + base.MoeQuantInfo = MoeQuantInfo + base.MoeRunnerConfig = MoeRunnerConfig + base.register_fused_func = register_fused_func + token_dispatcher = fake_modules["sglang.srt.layers.moe.token_dispatcher"] + + class StandardCombineInput: + def __init__(self, *, hidden_states): + self.hidden_states = hidden_states + + token_dispatcher.StandardCombineInput = StandardCombineInput + for name, module in fake_modules.items(): + monkeypatch.setitem(sys.modules, name, module) + + module_path = ( + Path(__file__).resolve().parents[5] + / "python/sglang/srt/layers/moe/flashinfer_megamoe.py" + ) + module_name = "sglang_flashinfer_megamoe_adapter_test" + spec = importlib.util.spec_from_file_location(module_name, module_path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + monkeypatch.setitem(sys.modules, module_name, module) + spec.loader.exec_module(module) + return module + + +def test_max_tokens_uses_runtime_context_accessor(monkeypatch): + module = _load_megamoe_module(monkeypatch) + + assert module._resolve_max_tokens_per_rank() == 2048 + + runtime_context = sys.modules["sglang.srt.runtime_context"] + runtime_context.cutedsl_moe_max_num_tokens = lambda: 0 + assert module._resolve_max_tokens_per_rank() == 1024 + + +def test_adapter_keeps_router_ids_int32(monkeypatch): + module = _load_megamoe_module(monkeypatch) + + class FakeMoEEpTensors: + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + fake_moe_ep = types.ModuleType("flashinfer.moe_ep") + fake_moe_ep.MoEEpTensors = FakeMoEEpTensors + fake_flashinfer = types.ModuleType("flashinfer") + fake_flashinfer.moe_ep = fake_moe_ep + monkeypatch.setitem(sys.modules, "flashinfer", fake_flashinfer) + monkeypatch.setitem(sys.modules, "flashinfer.moe_ep", fake_moe_ep) + + hidden_states = torch.randn((3, 4), dtype=torch.bfloat16) + topk_ids = torch.tensor([[0, 1], [1, 0], [0, 1]], dtype=torch.int32) + topk_weights = torch.randn((3, 2), dtype=torch.float32) + output = torch.randn_like(hidden_states) + + class Mega: + _workspace = object() + + def forward(self, tensors): + self.tensors = tensors + return output + + mega = Mega() + dispatch_output = types.SimpleNamespace( + hidden_states=hidden_states, + topk_output=types.SimpleNamespace( + topk_ids=topk_ids, + topk_weights=topk_weights, + ), + ) + quant_info = module.FlashInferMegaMoeQuantInfo(mega=mega) + runner_config = types.SimpleNamespace(routed_scaling_factor=1.0) + + result = module.run_flashinfer_megamoe( + dispatch_output, + quant_info, + runner_config, + ) + + assert mega.tensors.topk_ids.data_ptr() == topk_ids.data_ptr() + assert mega.tensors.topk_ids.dtype == torch.int32 + assert result.hidden_states is output + + +def test_adapter_requests_workspace_output_view(monkeypatch): + module = _load_megamoe_module(monkeypatch) + + class FakeMoEEpTensors: + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + fake_moe_ep = types.ModuleType("flashinfer.moe_ep") + fake_moe_ep.MoEEpTensors = FakeMoEEpTensors + fake_flashinfer = types.ModuleType("flashinfer") + fake_flashinfer.moe_ep = fake_moe_ep + monkeypatch.setitem(sys.modules, "flashinfer", fake_flashinfer) + monkeypatch.setitem(sys.modules, "flashinfer.moe_ep", fake_moe_ep) + + hidden_states = torch.randn((2, 4), dtype=torch.bfloat16) + topk_ids = torch.tensor([[0, 1], [1, 0]], dtype=torch.int32) + topk_weights = torch.ones((2, 2), dtype=torch.float32) + output = torch.randn_like(hidden_states) + + class Mega: + supports_output_view = True + _workspace = object() + + def forward(self, tensors, *, return_workspace_view=False): + self.tensors = tensors + self.return_workspace_view = return_workspace_view + return output + + mega = Mega() + dispatch_output = types.SimpleNamespace( + hidden_states=hidden_states, + topk_output=types.SimpleNamespace( + topk_ids=topk_ids, + topk_weights=topk_weights, + ), + ) + + result = module.run_flashinfer_megamoe( + dispatch_output, + module.FlashInferMegaMoeQuantInfo(mega=mega), + types.SimpleNamespace(routed_scaling_factor=1.0), + ) + + assert result.hidden_states is output + assert mega.tensors.topk_ids.data_ptr() == topk_ids.data_ptr() + assert mega.tensors.topk_ids.dtype == torch.int32 + assert mega.return_workspace_view is True + + +def test_capture_safe_ue8m0_pack_is_scoped(monkeypatch): + module = _load_megamoe_module(monkeypatch) + + dgm = sys.modules["deep_gemm.utils.math"] + + def original(value): + return value + + dgm.pack_ue8m0_to_int = original + monkeypatch.setattr(torch.cuda, "is_available", lambda: True) + monkeypatch.setattr(torch.cuda, "is_current_stream_capturing", lambda: False) + + with module._capture_safe_ue8m0_pack(): + assert dgm.pack_ue8m0_to_int is original + + monkeypatch.setattr(torch.cuda, "is_current_stream_capturing", lambda: True) + + with module._capture_safe_ue8m0_pack(): + assert dgm.pack_ue8m0_to_int is not original + packed = dgm.pack_ue8m0_to_int(torch.ones(4, dtype=torch.float32)) + assert packed.dtype == torch.int32 + + assert dgm.pack_ue8m0_to_int is original + + +if __name__ == "__main__": + import pytest + + sys.exit(pytest.main([__file__, "-v"])) diff --git a/test/registered/unit/lora/test_mem_pool_ep_unit.py b/test/registered/unit/lora/test_mem_pool_ep_unit.py index d33fc4414..4a3ad357e 100644 --- a/test/registered/unit/lora/test_mem_pool_ep_unit.py +++ b/test/registered/unit/lora/test_mem_pool_ep_unit.py @@ -906,17 +906,19 @@ class TestModuleLevelHelpers(unittest.TestCase): # Without a specific flashinfer backend selected, default is False. self.assertFalse(_moe_runner_keeps_global_expert_ids()) - def test_real_backend_predicate_matches_dispatcher_and_pool(self): + def test_real_backend_predicates_match_supported_id_contracts(self): backends = _load_moe_backend_enum() - expected_global = { + dispatcher_global_ids = { backends.FLASHINFER_TRTLLM, backends.EXPERIMENTAL_SGL_TRTLLM, backends.FLASHINFER_TRTLLM_ROUTED, backends.FLASHINFER_CUTLASS, backends.FLASHINFER_MXFP4, backends.FLASHINFER_CUTEDSL, + backends.FLASHINFER_MEGAMOE, backends.HPC_OPS, } + lora_global_ids = dispatcher_global_ids - {backends.FLASHINFER_MEGAMOE} config = types.SimpleNamespace( num_experts=8, num_local_experts=2, @@ -938,11 +940,11 @@ class TestModuleLevelHelpers(unittest.TestCase): dispatcher = standard_dispatcher(config) self.assertEqual( dispatcher.skip_local_expert_mapping, - backend in expected_global, + backend in dispatcher_global_ids, ) self.assertEqual( _moe_runner_keeps_global_expert_ids(), - backend in expected_global, + backend in lora_global_ids, ) diff --git a/test/registered/unit/server_args/test_server_args.py b/test/registered/unit/server_args/test_server_args.py index 2fa31fe7f..5e9f91bc6 100644 --- a/test/registered/unit/server_args/test_server_args.py +++ b/test/registered/unit/server_args/test_server_args.py @@ -76,6 +76,10 @@ from sglang.srt.entrypoints.sidecar import ( ) from sglang.srt.environ import envs from sglang.srt.layers.cp.base import is_cp_enabled, is_interleave +from sglang.srt.layers.moe.utils import ( + FlashinferA2ADispatchType, + get_flashinfer_a2a_dispatch_type, +) from sglang.srt.model_executor.cuda_graph_config import ( Backend, CudaGraphConfig, @@ -1123,6 +1127,264 @@ class TestContextParallelServerArgs(CustomTestCase): self.assertTrue(is_interleave()) +class TestFlashinferA2ADispatchType(CustomTestCase): + def setUp(self): + self._nvfp4_env_backup = os.environ.get("SGLANG_MOE_NVFP4_DISPATCH") + envs.SGLANG_MOE_NVFP4_DISPATCH.clear() + + def tearDown(self): + if self._nvfp4_env_backup is None: + envs.SGLANG_MOE_NVFP4_DISPATCH.clear() + else: + os.environ["SGLANG_MOE_NVFP4_DISPATCH"] = self._nvfp4_env_backup + + def _make_args( + self, + quantization=None, + dispatch_type=None, + runner_backend="flashinfer_trtllm_routed", + ): + server_args = ServerArgs( + model_path="dummy", + quantization=quantization, + moe_a2a_backend="flashinfer", + moe_runner_backend=runner_backend, + flashinfer_a2a_dispatch_type=dispatch_type, + enable_dp_attention=True, + dp_size=4, + tp_size=4, + ) + server_args._model_config = SimpleNamespace(nvfp4_moe_meta=None) + return server_args + + def test_auto_resolves_mxfp8_and_normalizes_trtllm(self): + server_args = self._make_args( + quantization="mxfp8", + dispatch_type="auto", + runner_backend="flashinfer_trtllm", + ) + handle_a2a_moe(server_args) + + self.assertEqual( + resolution_result(server_args, "moe_runner_backend"), + "flashinfer_trtllm_routed", + ) + self.assertEqual( + resolution_result(server_args, "flashinfer_a2a_dispatch_type"), "mxfp8" + ) + + def test_auto_resolves_modelopt_fp4_to_nvfp4(self): + server_args = self._make_args(quantization="modelopt_fp4", dispatch_type="auto") + handle_a2a_moe(server_args) + + self.assertEqual( + resolution_result(server_args, "flashinfer_a2a_dispatch_type"), "nvfp4" + ) + + def test_auto_resolves_hybrid_nvfp4_metadata_to_nvfp4(self): + server_args = self._make_args(quantization="fp8", dispatch_type="auto") + server_args._model_config = SimpleNamespace(nvfp4_moe_meta={}) + handle_a2a_moe(server_args) + + self.assertEqual( + resolution_result(server_args, "flashinfer_a2a_dispatch_type"), "nvfp4" + ) + + def test_unspecified_preserves_legacy_nvfp4_auto_enable(self): + server_args = self._make_args(quantization="modelopt_fp4") + handle_a2a_moe(server_args) + + self.assertIsNone(server_args.flashinfer_a2a_dispatch_type) + self.assertTrue(envs.SGLANG_MOE_NVFP4_DISPATCH.get()) + + def test_unspecified_getter_preserves_legacy_bf16_fallback(self): + with get_context().override_server_args( + flashinfer_a2a_dispatch_type=None, + quantization="mxfp8", + ): + self.assertEqual( + get_flashinfer_a2a_dispatch_type(), + FlashinferA2ADispatchType.BF16, + ) + + def test_runtime_getter_rejects_unresolved_auto(self): + with get_context().override_server_args( + flashinfer_a2a_dispatch_type="auto", + ): + with self.assertRaisesRegex(RuntimeError, "must resolve it"): + get_flashinfer_a2a_dispatch_type() + + def test_explicit_nvfp4_checks_hybrid_metadata_for_mxfp8_quantization(self): + server_args = self._make_args(quantization="mxfp8", dispatch_type="nvfp4") + server_args._model_config = SimpleNamespace(nvfp4_moe_meta={}) + handle_a2a_moe(server_args) + + self.assertEqual( + resolution_result(server_args, "flashinfer_a2a_dispatch_type"), "nvfp4" + ) + + def test_explicit_bf16_overrides_auto(self): + server_args = self._make_args(quantization="modelopt_fp4", dispatch_type="bf16") + handle_a2a_moe(server_args) + + self.assertEqual( + resolution_result(server_args, "flashinfer_a2a_dispatch_type"), "bf16" + ) + + def test_legacy_env_maps_to_dispatch_type(self): + with envs.SGLANG_MOE_NVFP4_DISPATCH.override("1"): + server_args = self._make_args(quantization="modelopt_fp4") + handle_a2a_moe(server_args) + self.assertIsNone(server_args.flashinfer_a2a_dispatch_type) + + with envs.SGLANG_MOE_NVFP4_DISPATCH.override("0"): + server_args = self._make_args(quantization="modelopt_fp4") + handle_a2a_moe(server_args) + self.assertIsNone(server_args.flashinfer_a2a_dispatch_type) + + def test_legacy_env_conflicts_with_explicit_cli(self): + with envs.SGLANG_MOE_NVFP4_DISPATCH.override("1"): + server_args = self._make_args( + quantization="modelopt_fp4", dispatch_type="bf16" + ) + with self.assertRaisesRegex( + ValueError, "SGLANG_MOE_NVFP4_DISPATCH cannot be set" + ): + handle_a2a_moe(server_args) + + def test_mxfp8_dispatch_requires_mxfp8_quantization(self): + server_args = self._make_args(quantization="fp8", dispatch_type="mxfp8") + with self.assertRaisesRegex(ValueError, "requires --quantization mxfp8"): + handle_a2a_moe(server_args) + + def test_explicit_dispatch_type_requires_flashinfer_a2a(self): + server_args = ServerArgs( + model_path="dummy", + moe_a2a_backend="none", + flashinfer_a2a_dispatch_type="bf16", + ) + with self.assertRaisesRegex(ValueError, "requires --moe-a2a-backend"): + handle_a2a_moe(server_args) + + +class TestFlashinferMegaMoeConfig(CustomTestCase): + def setUp(self): + self._combine_dtype_backup = os.environ.get( + "SGLANG_FLASHINFER_MEGAMOE_COMBINE_DTYPE" + ) + self._ikr_backup = os.environ.get( + "SGLANG_FLASHINFER_MEGAMOE_IN_KERNEL_FC2_REDUCE" + ) + envs.SGLANG_FLASHINFER_MEGAMOE_COMBINE_DTYPE.clear() + envs.SGLANG_FLASHINFER_MEGAMOE_IN_KERNEL_FC2_REDUCE.clear() + + def tearDown(self): + if self._combine_dtype_backup is None: + envs.SGLANG_FLASHINFER_MEGAMOE_COMBINE_DTYPE.clear() + else: + os.environ["SGLANG_FLASHINFER_MEGAMOE_COMBINE_DTYPE"] = ( + self._combine_dtype_backup + ) + if self._ikr_backup is None: + envs.SGLANG_FLASHINFER_MEGAMOE_IN_KERNEL_FC2_REDUCE.clear() + else: + os.environ["SGLANG_FLASHINFER_MEGAMOE_IN_KERNEL_FC2_REDUCE"] = ( + self._ikr_backup + ) + + def _make_args( + self, + architecture="DeepseekV4ForCausalLM", + quantization="modelopt_fp4", + *, + is_fp4_experts=False, + nvfp4_moe_meta=None, + ): + server_args = ServerArgs( + model_path="dummy", + quantization=quantization, + moe_a2a_backend="flashinfer_megamoe", + moe_runner_backend="flashinfer_megamoe", + enable_dp_attention=True, + dp_size=4, + tp_size=4, + ) + server_args._model_config = SimpleNamespace( + hf_config=SimpleNamespace(architectures=[architecture]), + is_fp4_experts=is_fp4_experts, + nvfp4_moe_meta=nvfp4_moe_meta, + ) + return server_args + + @patch("sglang.srt.arg_groups.moe_hook.is_sm100_supported", return_value=True) + def test_megamoe_accepts_audited_model_architectures(self, _): + supported = ( + "DeepseekV2ForCausalLM", + "DeepseekV3ForCausalLM", + "DeepseekV32ForCausalLM", + "DeepseekV4ForCausalLM", + "Glm4MoeForCausalLM", + "NemotronHForCausalLM", + "NemotronHPuzzleForCausalLM", + "Qwen2MoeForCausalLM", + "Qwen3MoeForCausalLM", + ) + for architecture in supported: + with self.subTest(architecture=architecture): + handle_a2a_moe(self._make_args(architecture)) + + def test_megamoe_rejects_unaudited_model_architecture(self): + with self.assertRaisesRegex( + ValueError, + "not validated for model architectures.*UnsupportedMoeForCausalLM", + ): + handle_a2a_moe(self._make_args("UnsupportedMoeForCausalLM")) + + @patch("sglang.srt.arg_groups.moe_hook.is_sm100_supported", return_value=True) + def test_megamoe_accepts_supported_quantization_formats(self, _): + supported = ( + {"quantization": "modelopt_fp4"}, + {"quantization": "mxfp8"}, + {"quantization": "fp8", "is_fp4_experts": True}, + {"quantization": "modelopt_mixed", "nvfp4_moe_meta": {}}, + ) + for config in supported: + with self.subTest(config=config): + handle_a2a_moe(self._make_args(**config)) + + def test_megamoe_rejects_standard_fp8(self): + with self.assertRaisesRegex(ValueError, "Standard FP8 MoE checkpoints"): + handle_a2a_moe(self._make_args(quantization="fp8")) + + @patch("sglang.srt.arg_groups.moe_hook.is_sm100_supported", return_value=False) + def test_megamoe_requires_sm100_for_all_quantization_formats(self, _): + with self.assertRaisesRegex(ValueError, "requires an SM100-family"): + handle_a2a_moe(self._make_args(quantization="fp8", is_fp4_experts=True)) + + @patch("sglang.srt.arg_groups.moe_hook.is_sm100_supported", return_value=True) + def test_megamoe_combine_dtype_accepts_quantized_values(self, _): + with envs.SGLANG_FLASHINFER_MEGAMOE_COMBINE_DTYPE.override("nvfp4"): + handle_a2a_moe(self._make_args()) + + with envs.SGLANG_FLASHINFER_MEGAMOE_COMBINE_DTYPE.override("mxfp8"): + handle_a2a_moe(self._make_args()) + + @patch("sglang.srt.arg_groups.moe_hook.is_sm100_supported", return_value=True) + def test_megamoe_combine_dtype_rejects_invalid_value(self, _): + with envs.SGLANG_FLASHINFER_MEGAMOE_COMBINE_DTYPE.override("fp8"): + with self.assertRaisesRegex( + ValueError, "SGLANG_FLASHINFER_MEGAMOE_COMBINE_DTYPE" + ): + handle_a2a_moe(self._make_args()) + + @patch("sglang.srt.arg_groups.moe_hook.is_sm100_supported", return_value=True) + def test_megamoe_combine_dtype_conflicts_with_ikr(self, _): + with envs.SGLANG_FLASHINFER_MEGAMOE_COMBINE_DTYPE.override("nvfp4"): + with envs.SGLANG_FLASHINFER_MEGAMOE_IN_KERNEL_FC2_REDUCE.override("1"): + with self.assertRaisesRegex(ValueError, "incompatible"): + handle_a2a_moe(self._make_args()) + + class TestPortArgs(unittest.TestCase): @patch("sglang.srt.server_args.tempfile.NamedTemporaryFile") def test_init_new_standard_case(self, mock_temp_file): diff --git a/test/registered/unit/test_model_overrides.py b/test/registered/unit/test_model_overrides.py index 0387dfb6b..d1ca6f6f4 100644 --- a/test/registered/unit/test_model_overrides.py +++ b/test/registered/unit/test_model_overrides.py @@ -2466,7 +2466,11 @@ class TestGoldenModelOverrides(_IsolatedPublish): ) def _view(**kw): - defaults = dict(quantization=None, moe_runner_backend="auto") + defaults = dict( + quantization=None, + moe_runner_backend="auto", + moe_a2a_backend="none", + ) defaults.update(kw) return ResolvedView(SimpleNamespace(**defaults))