@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / d1481ba783
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 8, 12:30 UTC

[MoE Refactor] Introduce MoERunner abstraction and move execution logic from FusedMoE to DefaultMoERunner (#32344)

Signed-off-by: Bill Nell <bnell@redhat.com>

bnellnm committed Feb 10, 2026 at 19:51 UTC d1481ba78323bcba5937f5ff74f3a8d27ab54f88
25 files changed +913 -753
docs/design/moe_kernel_features.md
+1 -1
@@ -32,7 +32,7 @@ th {
32
33 | Backend | Output act. format | Quant. types | Quant. format | Async | Apply Weight On Input | Subclass |
34 |---------|--------------------|--------------|---------------|-------|-----------------------|-----------|
35 -| naive | standard | all<sup>1</sup> | G,A,T | N | <sup>6</sup> | [layer.py][vllm.model_executor.layers.fused_moe.layer.FusedMoE.forward_impl] |
35 +| naive | standard | all<sup>1</sup> | G,A,T | N | <sup>6</sup> | [layer.py][vllm.model_executor.layers.fused_moe.layer.FusedMoE |
36 | pplx | batched | fp8,int8 | G,A,T | Y | Y | [`PplxPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.pplx_prepare_finalize.PplxPrepareAndFinalize] |
37 | deepep_high_throughput | standard | fp8 | G(128),A,T<sup>2</sup> | Y | Y | [`DeepEPLLPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.deepep_ll_prepare_finalize.DeepEPLLPrepareAndFinalize] |
38 | deepep_low_latency | batched | fp8 | G(128),A,T<sup>3</sup> | Y | Y | [`DeepEPHTPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.deepep_ht_prepare_finalize.DeepEPHTPrepareAndFinalize] |
tests/kernels/moe/modular_kernel_tools/common.py
+2
@@ -585,6 +585,7 @@ def make_modular_kernel(
585 tp_size_=get_tensor_model_parallel_world_size(),
586 pcp_size_=get_pcp_group().world_size,
587 dp_size_=get_dp_group().world_size,
588 + sp_size_=1,
589 vllm_parallel_config=vllm_config.parallel_config,
590 )
591
@@ -594,6 +595,7 @@ def make_modular_kernel(
595 hidden_dim=config.K,
596 intermediate_size_per_partition=config.N,
597 num_local_experts=config.num_local_experts,
598 + num_logical_experts=config.E,
599 moe_parallel_config=moe_parallel_config,
600 in_dtype=config.dtype,
601 max_num_tokens=next_power_of_2(config.M),
tests/kernels/moe/utils.py
+1
@@ -52,6 +52,7 @@ def make_dummy_moe_config(
52 hidden_dim=hidden_dim,
53 intermediate_size_per_partition=intermediate_size_per_partition,
54 num_local_experts=num_experts,
55 + num_logical_experts=num_experts,
56 moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
57 activation="silu",
58 in_dtype=in_dtype,
vllm/model_executor/layers/fused_moe/config.py
+18 -4
@@ -913,12 +913,16 @@ class FusedMoEParallelConfig:
913 pcp_rank: int
914 dp_rank: int
915 ep_rank: int
916 + sp_size: int
917
918 use_ep: bool # whether to use EP or not
919 all2all_backend: str # all2all backend for MoE communication
919 - is_sequence_parallel: bool # whether sequence parallelism is used
920 enable_eplb: bool # whether to enable expert load balancing
921
922 + @property
923 + def is_sequence_parallel(self) -> bool:
924 + return self.sp_size > 1
925 +
926 @property
927 def use_all2all_kernels(self):
928 return self.dp_size > 1 and self.use_ep
@@ -974,6 +978,7 @@ class FusedMoEParallelConfig:
978 tp_size_: int,
979 pcp_size_: int,
980 dp_size_: int,
981 + sp_size_: int,
982 vllm_parallel_config: ParallelConfig,
983 ) -> "FusedMoEParallelConfig":
984 """
@@ -1073,9 +1078,9 @@ class FusedMoEParallelConfig:
1078 dp_rank=dp_rank,
1079 ep_size=1,
1080 ep_rank=0,
1081 + sp_size=sp_size_,
1082 use_ep=False,
1083 all2all_backend=vllm_parallel_config.all2all_backend,
1078 - is_sequence_parallel=vllm_parallel_config.use_sequence_parallel_moe,
1084 enable_eplb=vllm_parallel_config.enable_eplb,
1085 )
1086 # DP + EP / TP + EP / DP + TP + EP
@@ -1093,9 +1098,9 @@ class FusedMoEParallelConfig:
1098 dp_rank=dp_rank,
1099 ep_size=ep_size,
1100 ep_rank=ep_rank,
1101 + sp_size=sp_size_,
1102 use_ep=True,
1103 all2all_backend=vllm_parallel_config.all2all_backend,
1098 - is_sequence_parallel=vllm_parallel_config.use_sequence_parallel_moe,
1104 enable_eplb=vllm_parallel_config.enable_eplb,
1105 )
1106
@@ -1111,10 +1116,10 @@ class FusedMoEParallelConfig:
1116 dp_rank=0,
1117 ep_size=1,
1118 ep_rank=0,
1119 + sp_size=1,
1120 use_ep=False,
1121 all2all_backend="naive",
1122 enable_eplb=False,
1117 - is_sequence_parallel=False,
1123 )
1124
1125
@@ -1126,6 +1131,7 @@ class FusedMoEConfig:
1131 hidden_dim: int
1132 intermediate_size_per_partition: int
1133 num_local_experts: int
1134 + num_logical_experts: int
1135 activation: str
1136 device: torch.device | str
1137 routing_method: RoutingMethodType
@@ -1175,6 +1181,14 @@ class FusedMoEConfig:
1181 def ep_size(self):
1182 return self.moe_parallel_config.ep_size
1183
1184 + @property
1185 + def sp_size(self):
1186 + return self.moe_parallel_config.sp_size
1187 +
1188 + @property
1189 + def is_sequence_parallel(self):
1190 + return self.moe_parallel_config.is_sequence_parallel
1191 +
1192 @property
1193 def tp_rank(self):
1194 return self.moe_parallel_config.tp_rank
vllm/model_executor/layers/fused_moe/fused_moe_method_base.py
+1 -2
@@ -121,17 +121,16 @@ class FusedMoEMethodBase(QuantizeMethodBase):
121 def is_monolithic(self) -> bool:
122 return False
123
124 - # @abstractmethod
124 def apply(
125 self,
126 layer: "FusedMoE", # type: ignore[name-defined] # noqa: F821
127 x: torch.Tensor,
128 topk_weights: torch.Tensor,
129 topk_ids: torch.Tensor,
130 + shared_experts_input: torch.Tensor | None,
131 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
132 raise NotImplementedError
133
134 - # @abstractmethod
134 def apply_monolithic(
135 self,
136 layer: "FusedMoE", # type: ignore[name-defined] # noqa: F821
vllm/model_executor/layers/fused_moe/fused_moe_modular_method.py
+2 -1
@@ -89,6 +89,7 @@ class FusedMoEModularMethod(FusedMoEMethodBase, CustomOp):
89 x: torch.Tensor,
90 topk_weights: torch.Tensor,
91 topk_ids: torch.Tensor,
92 + shared_experts_input: torch.Tensor | None,
93 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
94 assert self.moe_mk is not None
95 return self.moe_mk(
@@ -101,5 +102,5 @@ class FusedMoEModularMethod(FusedMoEMethodBase, CustomOp):
102 global_num_experts=layer.global_num_experts,
103 apply_router_weight_on_input=layer.apply_router_weight_on_input,
104 expert_map=None if self.disable_expert_map else layer.expert_map,
104 - shared_experts_input=layer._get_shared_experts_input(x),
105 + shared_experts_input=shared_experts_input,
106 )
vllm/model_executor/layers/fused_moe/layer.py
+69 -672
@@ -1,13 +1,11 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 -from collections.abc import Callable, Generator, Iterable
5 -from contextlib import contextmanager, nullcontext
4 +from collections.abc import Callable, Iterable
5 from enum import Enum
6 from typing import Literal, cast, get_args, overload
7
8 import torch
10 -import torch.nn.functional as F
9 from torch.nn.parameter import UninitializedParameter
10
11 import vllm.envs as envs
@@ -16,17 +14,10 @@ from vllm.config import VllmConfig, get_current_vllm_config
14 from vllm.config.parallel import ExpertPlacementStrategy
15 from vllm.distributed import (
16 get_dp_group,
19 - get_ep_group,
17 get_pcp_group,
18 get_tensor_model_parallel_world_size,
22 - tensor_model_parallel_all_reduce,
19 )
20 from vllm.distributed.eplb.eplb_state import EplbLayerState, EplbState
25 -from vllm.forward_context import (
26 - ForwardContext,
27 - get_forward_context,
28 - is_forward_context_available,
29 -)
21 from vllm.logger import init_logger
22 from vllm.model_executor.custom_op import CustomOp
23 from vllm.model_executor.layers.fused_moe.config import (
@@ -47,6 +38,9 @@ from vllm.model_executor.layers.fused_moe.rocm_aiter_fused_moe import (
38 from vllm.model_executor.layers.fused_moe.router.router_factory import (
39 create_fused_moe_router,
40 )
41 +from vllm.model_executor.layers.fused_moe.runner.default_moe_runner import (
42 + DefaultMoERunner,
43 +)
44 from vllm.model_executor.layers.fused_moe.unquantized_fused_moe_method import (
45 UnquantizedFusedMoEMethod,
46 )
@@ -57,13 +51,7 @@ from vllm.model_executor.layers.quantization.base_config import (
51 QuantizationConfig,
52 )
53 from vllm.platforms import current_platform
60 -from vllm.utils.math_utils import cdiv, round_up
61 -from vllm.utils.torch_utils import (
62 - aux_stream,
63 - current_stream,
64 - direct_register_custom_op,
65 -)
66 -from vllm.v1.worker.ubatching import dbo_current_ubatch_id
54 +from vllm.utils.math_utils import round_up
55
56 logger = init_logger(__name__)
57
@@ -264,6 +252,7 @@ def maybe_roundup_hidden_size(
252 )
253
254 current_mxfp4_backend = get_mxfp4_backend(is_lora_enabled)
255 +
256 if (
257 current_mxfp4_backend == Mxfp4Backend.SM90_FI_MXFP4_BF16
258 or current_mxfp4_backend == Mxfp4Backend.SM100_FI_MXFP4_MXFP8_CUTLASS
@@ -273,6 +262,7 @@ def maybe_roundup_hidden_size(
262 current_platform.is_rocm()
263 or current_mxfp4_backend == Mxfp4Backend.SM100_FI_MXFP4_MXFP8_TRTLLM
264 or current_mxfp4_backend == Mxfp4Backend.SM100_FI_MXFP4_BF16
265 + or current_mxfp4_backend == Mxfp4Backend.MARLIN
266 ):
267 hidden_size = round_up(hidden_size, 256)
268
@@ -338,29 +328,15 @@ class FusedMoE(CustomOp):
328 expert_mapping: list[tuple[str, str, int, str]] | None = None,
329 n_shared_experts: int | None = None,
330 router_logits_dtype: torch.dtype | None = None,
341 - has_shared_experts: bool = False,
331 + gate: torch.nn.Module | None = None,
332 + shared_experts: torch.nn.Module | None = None,
333 + routed_input_transform: torch.nn.Module | None = None,
334 ):
335 super().__init__()
336
345 - # Allow disabling of the separate shared experts stream for
346 - # debug purposes.
347 - # TODO: Remove this after more extensive testings with TP/DP
348 - # and other execution modes
349 - if envs.VLLM_DISABLE_SHARED_EXPERTS_STREAM:
350 - logger.debug_once("Disabling MoE shared_experts cuda stream", scope="local")
351 - self.shared_experts_stream = None
352 - else:
353 - # TODO(rob): enable shared expert overlap with non-cuda-alike.
354 - # aux_stream() returns None on non-cuda-alike platforms.
355 - self.shared_experts_stream = aux_stream()
356 - if self.shared_experts_stream is not None:
357 - logger.debug_once(
358 - "Enabled separate cuda stream for MoE shared_experts", scope="local"
359 - )
360 -
361 - # For latent MoE: stores original hidden_states before routed_input_transform
362 - # so shared_experts can use it for cloning (they need original dimension)
363 - self._shared_experts_input: torch.Tensor | None = None
337 + self._gate = gate
338 + self._shared_experts = shared_experts
339 + self._routed_input_transform = routed_input_transform
340
341 if params_dtype is None:
342 params_dtype = torch.get_default_dtype()
@@ -392,9 +368,12 @@ class FusedMoE(CustomOp):
368 tp_size_=tp_size_,
369 pcp_size_=pcp_size_,
370 dp_size_=dp_size_,
371 + sp_size_=self.sp_size,
372 vllm_parallel_config=vllm_config.parallel_config,
373 )
374
375 + assert self.moe_parallel_config.is_sequence_parallel == is_sequence_parallel
376 +
377 self.global_num_experts = num_experts + num_redundant_experts
378 self.logical_num_experts = num_experts
379
@@ -410,6 +389,7 @@ class FusedMoE(CustomOp):
389 self.layer_name = prefix
390
391 self.enable_eplb = enable_eplb
392 + # TODO(bnell): should this be owned by router?
393 self.eplb_state = EplbLayerState()
394 self.expert_placement_strategy: ExpertPlacementStrategy = (
395 vllm_config.parallel_config.expert_placement_strategy
@@ -506,7 +486,8 @@ class FusedMoE(CustomOp):
486 self.reduce_results = reduce_results
487 self.renormalize = renormalize
488
509 - # TODO(bnell): these attributes are only used by cpu/xpu/mxfp4
489 + # TODO(bnell): these attributes are only used by monolithic kernels.
490 + # Put them in a MoERouterConfig dataclass?
491 self.use_grouped_topk = use_grouped_topk
492 if self.use_grouped_topk:
493 assert num_expert_group is not None and topk_group is not None
@@ -565,6 +546,7 @@ class FusedMoE(CustomOp):
546 hidden_dim=hidden_size,
547 intermediate_size_per_partition=self.intermediate_size_per_partition,
548 num_local_experts=self.local_num_experts,
549 + num_logical_experts=self.logical_num_experts,
550 moe_parallel_config=self.moe_parallel_config,
551 in_dtype=moe_in_dtype,
552 router_logits_dtype=router_logits_dtype,
@@ -576,9 +558,9 @@ class FusedMoE(CustomOp):
558 device=vllm_config.device_config.device,
559 routing_method=self.routing_method_type,
560 # TODO: in_dtype == out_dtype?
579 - disable_inplace=disable_inplace() or has_shared_experts,
561 + disable_inplace=disable_inplace() or self._shared_experts is not None,
562 )
581 - if self.use_mori_kernels:
563 + if self.moe_config.use_mori_kernels:
564 assert self.rocm_aiter_fmoe_enabled, (
565 "Mori needs to be used with aiter fused_moe for now."
566 )
@@ -641,9 +623,36 @@ class FusedMoE(CustomOp):
623
624 self.quant_method.create_weights(layer=self, **moe_quant_params)
625
644 - # Chunked all2all staging tensor
645 - self.batched_hidden_states: torch.Tensor | None = None
646 - self.batched_router_logits: torch.Tensor | None = None
626 + # Disable shared expert overlap if:
627 + # - we are using eplb with non-default backend, because of correctness issues
628 + # - we are using flashinfer with DP, since there nothing to gain
629 + # - we are using marlin kernels
630 + backend = self.moe_parallel_config.all2all_backend
631 + self.use_overlapped = (
632 + not (
633 + (self.enable_eplb and backend != "allgather_reducescatter")
634 + or self.moe_parallel_config.use_fi_all2allv_kernels
635 + )
636 + and self._shared_experts is not None
637 + )
638 +
639 + self.runner = self._init_runner()
640 +
641 + def _init_runner(self):
642 + # Storing the runner in the FusedMoE is an intermediate state, eventually
643 + # the runner will own the FusedMoE layer and provide the execution interface
644 + # for MoE ops.
645 + return DefaultMoERunner(
646 + layer=self,
647 + moe_config=self.moe_config,
648 + router=self.router,
649 + routed_input_transform=self._routed_input_transform,
650 + gate=self.gate,
651 + shared_experts=self.shared_experts,
652 + quant_method=self.quant_method,
653 + reduce_results=self.reduce_results,
654 + enable_dbo=self.vllm_config.parallel_config.enable_dbo,
655 + )
656
657 # Note: maybe_init_modular_kernel should only be called by
658 # prepare_communication_buffer_for_model.
@@ -673,10 +682,14 @@ class FusedMoE(CustomOp):
682 self.shared_experts,
683 inplace=not self.moe_config.disable_inplace,
684 )
685 + # We need to force reconstruction of runner because we're swapping out
686 + # the quant_method with a FusedMoEModularMethod. This logic can go
687 + # away once the FusedMoEModularMethod is eliminated.
688 + self.runner = self._init_runner()
689
690 @property
691 def shared_experts(self) -> torch.nn.Module | None:
679 - return None
692 + return self._shared_experts if self.use_overlapped else None
693
694 @property
695 def layer_id(self):
@@ -687,53 +700,12 @@ class FusedMoE(CustomOp):
700
701 @property
702 def gate(self) -> torch.nn.Module | None:
690 - return None
691 -
692 - def apply_routed_input_transform(self, hidden_states: torch.Tensor) -> torch.Tensor:
693 - """Hook to transform hidden_states before passing to routed experts.
694 - For latent MoE: transforms [S, hidden_size] → [S, moe_latent_size].
695 - The original hidden_states is saved in _shared_experts_input so
696 - shared_experts still receive the original [S, hidden_size].
697 -
698 - Override in subclasses (e.g., SharedFusedMoE) for latent MoE.
699 - """
700 - return hidden_states
701 -
702 - @contextmanager
703 - def _set_shared_experts_input(
704 - self, value: torch.Tensor | None
705 - ) -> Generator[None, None, None]:
706 - """Context manager to safely set/clear _shared_experts_input."""
707 - self._shared_experts_input = value
708 - try:
709 - yield
710 - finally:
711 - self._shared_experts_input = None
712 -
713 - def _get_shared_experts_input(self, hidden_states: torch.Tensor) -> torch.Tensor:
714 - """Get input for shared experts.
715 -
716 - For latent MoE: shared_experts need original [S, hidden_size],
717 - not the transformed [S, latent_size] used by routed experts.
718 - """
719 - return (
720 - self._shared_experts_input
721 - if self._shared_experts_input is not None
722 - else hidden_states
723 - )
703 + return self._gate
704
705 @property
706 def tp_size(self):
707 return self.moe_parallel_config.tp_size
708
729 - @property
730 - def dp_size(self):
731 - return self.moe_parallel_config.dp_size
732 -
733 - @property
734 - def pcp_size(self):
735 - return self.moe_parallel_config.pcp_size
736 -
709 @property
710 def ep_size(self):
711 return self.moe_parallel_config.ep_size
@@ -742,14 +714,6 @@ class FusedMoE(CustomOp):
714 def tp_rank(self):
715 return self.moe_parallel_config.tp_rank
716
745 - @property
746 - def dp_rank(self):
747 - return self.moe_parallel_config.dp_rank
748 -
749 - @property
750 - def pcp_rank(self):
751 - return self.moe_parallel_config.pcp_rank
752 -
717 @property
718 def ep_rank(self):
719 return self.moe_parallel_config.ep_rank
@@ -758,39 +722,10 @@ class FusedMoE(CustomOp):
722 def use_ep(self):
723 return self.moe_parallel_config.use_ep
724
761 - @property
762 - def use_pplx_kernels(self):
763 - return self.moe_parallel_config.use_pplx_kernels
764 -
765 - @property
766 - def use_deepep_ht_kernels(self):
767 - return self.moe_parallel_config.use_deepep_ht_kernels
768 -
769 - @property
770 - def use_deepep_ll_kernels(self):
771 - return self.moe_parallel_config.use_deepep_ll_kernels
772 -
773 - @property
774 - def use_mori_kernels(self):
775 - return self.moe_parallel_config.use_mori_kernels
776 -
777 - @property
778 - def use_marlin_kernels(self):
779 - return getattr(self.quant_method, "use_marlin", False)
780 -
781 - @property
782 - def use_dp_chunking(self) -> bool:
783 - return (
784 - self.moe_parallel_config.use_pplx_kernels
785 - or self.moe_parallel_config.use_deepep_ll_kernels
786 - or self.moe_parallel_config.use_mori_kernels
787 - or self.moe_parallel_config.use_fi_all2allv_kernels
788 - ) and envs.VLLM_ENABLE_MOE_DP_CHUNK
789 -
725 @property
726 def is_internal_router(self) -> bool:
727 # By default, router/gate is called before FusedMoE forward pass
793 - return False
728 + return self._gate is not None
729
730 def _maybe_init_expert_routing_tables(
731 self,
@@ -799,7 +734,7 @@ class FusedMoE(CustomOp):
734 # with DeepEP-ll all2all backend.
735 if (
736 self.expert_placement_strategy != "round_robin"
802 - or not self.use_deepep_ll_kernels
737 + or not self.moe_parallel_config.use_deepep_ll_kernels
738 ):
739 return None
740
@@ -892,48 +827,6 @@ class FusedMoE(CustomOp):
827 dp_size=get_dp_group().world_size,
828 )
829
895 - def _maybe_setup_shared_experts_stream(
896 - self,
897 - hidden_states: torch.Tensor,
898 - has_separate_shared_experts: bool,
899 - use_chunked_impl: bool,
900 - ) -> tuple[bool, torch.Tensor | None]:
901 - use_shared_experts_stream = (
902 - current_platform.is_cuda()
903 - and has_separate_shared_experts
904 - and not use_chunked_impl
905 - and self.shared_experts_stream is not None
906 - and (
907 - hidden_states.shape[0]
908 - <= envs.VLLM_SHARED_EXPERTS_STREAM_TOKEN_THRESHOLD
909 - )
910 - )
911 -
912 - hidden_states_clone: torch.Tensor | None = None
913 - if use_shared_experts_stream:
914 - assert self.shared_experts_stream is not None
915 -
916 - shared_experts_input = self._get_shared_experts_input(hidden_states)
917 -
918 - # Clone BEFORE switching streams to avoid race condition
919 - # where routed_expert kernel may mutate hidden_states.
920 - hidden_states_clone = shared_experts_input.clone()
921 -
922 - # Record that the clone will be used by shared_experts_stream
923 - # to avoid gc issue from deallocation of hidden_states_clone
924 - # For more details: https://docs.pytorch.org/docs/stable/generated/torch.Tensor.record_stream.html # noqa: E501
925 - # NOTE: We don't need shared_output.record_stream(current_stream())
926 - # because we synch the streams before using shared_output.
927 - hidden_states_clone.record_stream(self.shared_experts_stream)
928 -
929 - # Mark sync start point for the separate shared experts
930 - # stream here since we want to run in parallel with the
931 - # router/gate (next op below)
932 - assert self.shared_experts_stream is not None
933 - self.shared_experts_stream.wait_stream(current_stream())
934 -
935 - return use_shared_experts_stream, hidden_states_clone
936 -
830 def _load_per_tensor_weight_scale(
831 self,
832 shard_id: str,
@@ -1191,7 +1084,7 @@ class FusedMoE(CustomOp):
1084 # compressed-tensors checkpoints with packed weights are stored flipped
1085 # TODO (mgoin): check self.quant_method.quant_config.quant_format
1086 # against known CompressionFormat enum values that have this quality
1194 - if self.quant_method.__class__.__name__ in (
1087 + if quant_method_name in (
1088 "CompressedTensorsWNA16MarlinMoEMethod",
1089 "CompressedTensorsWNA16MoEMethod",
1090 ):
@@ -1488,7 +1381,7 @@ class FusedMoE(CustomOp):
1381 assert all(
1382 weight.is_contiguous()
1383 for name, weight in weights
1491 - if not name.startswith("_shared_experts.")
1384 + if not (name.startswith("_shared_experts.") or name.startswith("_gate."))
1385 )
1386
1387 # Filter out the non-expert weights.
@@ -1538,32 +1431,6 @@ class FusedMoE(CustomOp):
1431 self.ensure_moe_quant_config_init()
1432 return self.quant_method.moe_quant_config
1433
1541 - def ensure_dp_chunking_init(self):
1542 - if not self.use_dp_chunking or self.batched_hidden_states is not None:
1543 - return
1544 -
1545 - states_shape: tuple[int, ...]
1546 - logits_shape: tuple[int, ...]
1547 -
1548 - moe = self.moe_config
1549 -
1550 - if self.vllm_config.parallel_config.enable_dbo:
1551 - states_shape = (2, moe.max_num_tokens, self.hidden_size)
1552 - logits_shape = (2, moe.max_num_tokens, self.logical_num_experts)
1553 - else:
1554 - states_shape = (moe.max_num_tokens, self.hidden_size)
1555 - logits_shape = (moe.max_num_tokens, self.logical_num_experts)
1556 -
1557 - self.batched_hidden_states = torch.zeros(
1558 - states_shape, dtype=moe.in_dtype, device=torch.cuda.current_device()
1559 - )
1560 -
1561 - self.batched_router_logits = torch.zeros(
1562 - logits_shape,
1563 - dtype=moe.router_logits_dtype,
1564 - device=torch.cuda.current_device(),
1565 - )
1566 -
1434 def must_reduce_shared_expert_outputs(self) -> bool:
1435 """
1436 The shared_experts are typically computed using the RowParallelLinear
@@ -1577,100 +1444,24 @@ class FusedMoE(CustomOp):
1444 Therefore it is required that we reduce the shared_experts output
1445 early.
1446 """
1580 - assert self.quant_method is not None
1581 - return (
1582 - isinstance(self.quant_method, FusedMoEModularMethod)
1583 - and self.quant_method.moe_mk.output_is_reduced() # type: ignore[union-attr]
1584 - )
1447 + return self.runner.must_reduce_shared_expert_outputs()
1448
1449 def maybe_all_reduce_tensor_model_parallel(self, final_hidden_states: torch.Tensor):
1450 """
1451 Some combine kernels reduce across GPU ranks by default.
1452 """
1590 - if self.must_reduce_shared_expert_outputs():
1591 - return final_hidden_states
1592 - else:
1593 - return tensor_model_parallel_all_reduce(final_hidden_states)
1453 + return self.runner.maybe_all_reduce_tensor_model_parallel(final_hidden_states)
1454
1455 def forward_native(
1456 self,
1457 hidden_states: torch.Tensor,
1458 router_logits: torch.Tensor,
1459 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
1600 - # For latent MoE: save ORIGINAL hidden_states before transform
1601 - # (shared_experts need original dimension, routed experts use transformed)
1602 - original_hidden_states = hidden_states
1603 - original_hidden_dim = hidden_states.shape[-1]
1604 -
1605 - # Apply transform for routed experts (e.g., latent projection for latent MoE)
1606 - hidden_states = self.apply_routed_input_transform(hidden_states)
1607 -
1608 - # This is the dimension after transform (for routed expert output slicing)
1609 - transformed_hidden_dim = hidden_states.shape[-1]
1610 - if self.hidden_size != transformed_hidden_dim:
1611 - hidden_states = F.pad(
1612 - hidden_states,
1613 - (0, self.hidden_size - transformed_hidden_dim),
1614 - mode="constant",
1615 - value=0.0,
1616 - )
1617 -
1618 - def reduce_output(states: torch.Tensor) -> torch.Tensor:
1619 - if (
1620 - not self.is_sequence_parallel
1621 - and not self.use_dp_chunking
1622 - and self.reduce_results
1623 - and (self.tp_size > 1 or self.ep_size > 1)
1624 - ):
1625 - states = self.maybe_all_reduce_tensor_model_parallel(states)
1626 - return states
1627 -
1628 - def encode_layer_name() -> str:
1629 - # Can be unavailable or None in unittests
1630 - if (
1631 - is_forward_context_available()
1632 - and get_forward_context().all_moe_layers is not None
1633 - ):
1634 - return "from_forward_context"
1635 - return self.layer_name
1636 -
1637 - if self.shared_experts is None:
1638 - if current_platform.is_tpu() or current_platform.is_cpu():
1639 - # TODO: Once the OOM issue for the TPU backend is resolved, we
1640 - # will switch to using the moe_forward custom op.
1641 - # Note: CPU doesn't require wrapped forward_impl.
1642 - fused_output = self.forward_impl(hidden_states, router_logits)
1643 - assert not isinstance(fused_output, tuple)
1644 - else:
1645 - fused_output = torch.ops.vllm.moe_forward(
1646 - hidden_states, router_logits, encode_layer_name()
1647 - )
1648 - return reduce_output(fused_output)[..., :transformed_hidden_dim]
1649 - else:
1650 - if current_platform.is_tpu() or current_platform.is_cpu():
1651 - # TODO: Once the OOM issue for the TPU backend is resolved, we
1652 - # will switch to using the moe_forward custom op.
1653 - # Note: CPU doesn't require wrapped forward_impl.
1654 - with self._set_shared_experts_input(original_hidden_states):
1655 - shared_output, fused_output = self.forward_impl(
1656 - hidden_states, router_logits
1657 - )
1658 - else:
1659 - # Custom op handles setting/clearing _shared_experts_input internally
1660 - # We pass original tensor for shared experts (not transformed)
1661 - shared_output, fused_output = torch.ops.vllm.moe_forward_shared(
1662 - hidden_states,
1663 - router_logits,
1664 - encode_layer_name(),
1665 - original_hidden_states,
1666 - )
1667 -
1668 - # shared_output uses original dimension (before transform)
1669 - # fused_output uses transformed dimension (after transform)
1670 - return (
1671 - reduce_output(shared_output)[..., :original_hidden_dim],
1672 - reduce_output(fused_output)[..., :transformed_hidden_dim],
1673 - )
1460 + self.ensure_moe_quant_config_init()
1461 + return self.runner.forward(
1462 + hidden_states,
1463 + router_logits,
1464 + )
1465
1466 @property
1467 def expert_map(self) -> torch.Tensor | None:
@@ -1685,312 +1476,6 @@ class FusedMoE(CustomOp):
1476 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
1477 return self.forward_native(hidden_states, router_logits)
1478
1688 - def forward_impl_chunked(
1689 - self,
1690 - full_hidden_states: torch.Tensor,
1691 - full_router_logits: torch.Tensor,
1692 - has_separate_shared_experts: bool,
1693 - ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
1694 - assert self.batched_hidden_states is not None
1695 - assert self.batched_router_logits is not None
1696 - assert self.batched_hidden_states.dtype == full_hidden_states.dtype, (
1697 - f"{self.batched_hidden_states.dtype} == {full_hidden_states.dtype}"
1698 - )
1699 - assert self.batched_router_logits.dtype == full_router_logits.dtype, (
1700 - f"{self.batched_router_logits.dtype} == {full_router_logits.dtype}"
1701 - )
1702 - # Check size compatibility.
1703 - assert self.batched_hidden_states.size(-1) == full_hidden_states.size(-1)
1704 - assert self.batched_router_logits.size(-1) == full_router_logits.size(-1)
1705 -
1706 - full_fused_final_hidden_states = torch.empty_like(full_hidden_states)
1707 - if self.shared_experts is not None:
1708 - full_shared_final_hidden_states = torch.empty_like(full_hidden_states)
1709 -
1710 - def process_chunk(chunk_start, chunk_end, skip_result_store=False):
1711 - chunk_size = chunk_end - chunk_start
1712 - hidden_states = full_hidden_states[chunk_start:chunk_end, :]
1713 - router_logits = full_router_logits[chunk_start:chunk_end, :]
1714 -
1715 - assert self.batched_hidden_states is not None
1716 - assert self.batched_router_logits is not None
1717 - # This is only true when DBO has been enabled in the config.
1718 - # Both tensors will have an outer dimension for the ubatch id
1719 - if self.batched_hidden_states.dim() == 3:
1720 - assert self.batched_router_logits.dim() == 3
1721 - batch_buffer_idx = dbo_current_ubatch_id()
1722 - batched_hidden_states = self.batched_hidden_states[batch_buffer_idx, :]
1723 - batched_router_logits = self.batched_router_logits[batch_buffer_idx, :]
1724 - else:
1725 - batched_hidden_states = self.batched_hidden_states
1726 - batched_router_logits = self.batched_router_logits
1727 -
1728 - assert (
1729 - batched_hidden_states.size(0) # type: ignore
1730 - >= chunk_size
1731 - )
1732 - assert (
1733 - batched_router_logits.size(0) # type: ignore
1734 - >= chunk_size
1735 - )
1736 - staged_hidden_states = batched_hidden_states[:chunk_size, :] # type: ignore
1737 - staged_router_logits = batched_router_logits[:chunk_size, :] # type: ignore
1738 - staged_hidden_states.copy_(hidden_states, non_blocking=True)
1739 - staged_router_logits.copy_(router_logits, non_blocking=True)
1740 -
1741 - # Matrix multiply.
1742 - if self.quant_method.is_monolithic:
1743 - final_hidden_states = self.quant_method.apply_monolithic(
1744 - layer=self,
1745 - x=staged_hidden_states,
1746 - router_logits=staged_router_logits,
1747 - )
1748 - else:
1749 - topk_weights, topk_ids = self.router.select_experts(
1750 - hidden_states=staged_hidden_states,
1751 - router_logits=staged_router_logits,
1752 - )
1753 -
1754 - final_hidden_states = self.quant_method.apply(
1755 - layer=self,
1756 - x=staged_hidden_states,
1757 - topk_weights=topk_weights,
1758 - topk_ids=topk_ids,
1759 - )
1760 -
1761 - if has_separate_shared_experts:
1762 - assert not isinstance(final_hidden_states, tuple)
1763 - assert self.shared_experts is not None
1764 -
1765 - shared_output = self.shared_experts(staged_hidden_states)
1766 -
1767 - final_hidden_states = (
1768 - shared_output,
1769 - final_hidden_states,
1770 - )
1771 -
1772 - if not skip_result_store:
1773 - if self.shared_experts is None:
1774 - full_fused_final_hidden_states[chunk_start:chunk_end, :].copy_(
1775 - final_hidden_states, non_blocking=True
1776 - )
1777 - else:
1778 - full_shared_final_hidden_states[chunk_start:chunk_end, :].copy_(
1779 - final_hidden_states[0], non_blocking=True
1780 - )
1781 - full_fused_final_hidden_states[chunk_start:chunk_end, :].copy_(
1782 - final_hidden_states[1], non_blocking=True
1783 - )
1784 -
1785 - ctx = get_forward_context()
1786 - # flashinfer_cutlass_kernels can handle: optional DP + TP/EP
1787 - max_tokens_across_dispatchers = ctx.dp_metadata.max_tokens_across_dp_cpu
1788 - moe_dp_chunk_size_per_rank = self.moe_config.max_num_tokens
1789 -
1790 - # If the input to the MoE is sequence parallel then divide by sp_size
1791 - # to find the maximum number of tokens for any individual dispatcher.
1792 - if self.is_sequence_parallel:
1793 - max_tokens_across_dispatchers = cdiv(
1794 - max_tokens_across_dispatchers, self.sp_size
1795 - )
1796 -
1797 - num_tokens = full_hidden_states.size(0)
1798 - for chunk_idx, chunk_start_ in enumerate(
1799 - range(0, max_tokens_across_dispatchers, moe_dp_chunk_size_per_rank)
1800 - ):
1801 - chunk_start = chunk_start_
1802 - chunk_end = min(
1803 - chunk_start + moe_dp_chunk_size_per_rank, max_tokens_across_dispatchers
1804 - )
1805 - # clamp start and end
1806 - chunk_start = min(chunk_start, num_tokens - 1)
1807 - chunk_end = min(chunk_end, num_tokens)
1808 - with ctx.dp_metadata.chunked_sizes(
1809 - self.sp_size, moe_dp_chunk_size_per_rank, chunk_idx
1810 - ):
1811 - process_chunk(
1812 - chunk_start, chunk_end, skip_result_store=chunk_start_ >= num_tokens
1813 - )
1814 -
1815 - if self.shared_experts is None:
1816 - return full_fused_final_hidden_states
1817 - else:
1818 - return (full_shared_final_hidden_states, full_fused_final_hidden_states)
1819 -
1820 - def forward_impl(
1821 - self,
1822 - hidden_states: torch.Tensor,
1823 - router_logits: torch.Tensor,
1824 - ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
1825 - assert self.quant_method is not None
1826 -
1827 - self.ensure_moe_quant_config_init()
1828 - self.ensure_dp_chunking_init()
1829 -
1830 - has_separate_shared_experts = (
1831 - not self.quant_method.mk_owns_shared_expert
1832 - and self.shared_experts is not None
1833 - )
1834 -
1835 - use_chunked_impl = self.use_dp_chunking
1836 -
1837 - use_shared_experts_stream, hidden_states_clone = (
1838 - self._maybe_setup_shared_experts_stream(
1839 - hidden_states, has_separate_shared_experts, use_chunked_impl
1840 - )
1841 - )
1842 -
1843 - # If router/gate provided, then apply it here.
1844 - # (Note: This code runs only when "overlapped mode" is on to allow
1845 - # parallel execution of shared experts with the FusedMoE via
1846 - # separate cuda stream)
1847 - if self.gate is not None:
1848 - router_logits, _ = self.gate(hidden_states)
1849 -
1850 - if use_chunked_impl:
1851 - return self.forward_impl_chunked(
1852 - hidden_states, router_logits, has_separate_shared_experts
1853 - )
1854 -
1855 - # NOTE(rob): once we finish migrating all the quant methods to use
1856 - # MKs, we can remove the naive dispatch/combine path from here.
1857 - do_naive_dispatch_combine = (
1858 - self.dp_size > 1 and not self.quant_method.supports_internal_mk
1859 - )
1860 -
1861 - ctx = get_forward_context()
1862 - sp_ctx = (
1863 - ctx.dp_metadata.sp_local_sizes(self.sp_size)
1864 - if ctx.dp_metadata
1865 - else nullcontext()
1866 - )
1867 -
1868 - with sp_ctx:
1869 - extra_tensors = None
1870 - if do_naive_dispatch_combine:
1871 - post_quant_allgather = (
1872 - self.quant_method is not None
1873 - and self.dp_size > 1
1874 - and self.use_ep
1875 - and getattr(self.quant_method, "do_post_quant_allgather", False)
1876 - )
1877 - if post_quant_allgather:
1878 - hidden_states_to_dispatch, extra_tensors = (
1879 - self.quant_method.prepare_dp_allgather_tensor(
1880 - self, hidden_states, router_logits
1881 - )
1882 - )
1883 - else:
1884 - hidden_states_to_dispatch = hidden_states
1885 -
1886 - dispatch_res = get_ep_group().dispatch_router_logits(
1887 - hidden_states_to_dispatch,
1888 - router_logits,
1889 - self.is_sequence_parallel,
1890 - extra_tensors=extra_tensors,
1891 - )
1892 - if extra_tensors is not None:
1893 - (
1894 - orig_hidden_states,
1895 - router_logits,
1896 - extra_tensors_combined,
1897 - ) = dispatch_res
1898 - hidden_states_combined = (
1899 - orig_hidden_states,
1900 - extra_tensors_combined[0],
1901 - )
1902 - else:
1903 - hidden_states_combined, router_logits = dispatch_res
1904 - orig_hidden_states = hidden_states_combined
1905 - else:
1906 - orig_hidden_states = hidden_states
1907 -
1908 - # Run shared experts before matrix multiply.
1909 - # because matrix multiply maybe modify the hidden_states.
1910 - if has_separate_shared_experts and not use_shared_experts_stream:
1911 - assert self.shared_experts is not None
1912 - shared_input = self._get_shared_experts_input(hidden_states)
1913 - shared_output = self.shared_experts(shared_input)
1914 -
1915 - # NOTE: Similar with DP, PCP also needs dispatch and combine. For
1916 - # simplicity, AgRsAll2All was added separately for PCP here. Maybe
1917 - # we should modify All2AllManager abstract to better support PCP.
1918 - if self.pcp_size > 1:
1919 - hidden_states = get_pcp_group().all_gather(
1920 - hidden_states,
1921 - dim=0,
1922 - )
1923 - router_logits = get_pcp_group().all_gather(
1924 - router_logits,
1925 - dim=0,
1926 - )
1927 -
1928 - # Matrix multiply.
1929 - x = hidden_states_combined if do_naive_dispatch_combine else hidden_states
1930 -
1931 - # TODO(bnell): deal with fp4 flashinfer tuple hidden states hack (#30014).
1932 - # Figure out nicer way to do this.
1933 - x_orig = orig_hidden_states if do_naive_dispatch_combine else hidden_states
1934 -
1935 - if self.quant_method.is_monolithic:
1936 - final_hidden_states = self.quant_method.apply_monolithic(
1937 - layer=self,
1938 - x=x,
1939 - router_logits=router_logits,
1940 - )
1941 - else:
1942 - topk_weights, topk_ids = self.router.select_experts(
1943 - hidden_states=x_orig,
1944 - router_logits=router_logits,
1945 - )
1946 -
1947 - final_hidden_states = self.quant_method.apply(
1948 - layer=self,
1949 - x=x, # The type signture of this is wrong due to the hack.
1950 - topk_weights=topk_weights,
1951 - topk_ids=topk_ids,
1952 - )
1953 -
1954 - if has_separate_shared_experts:
1955 - assert self.shared_experts is not None
1956 -
1957 - if use_shared_experts_stream:
1958 - # Run shared experts in parallel on a separate stream
1959 - # NOTE: We start the separate stream here and mark the
1960 - # sync end point immediately after it is done. This is
1961 - # important to avoid excessive stream allocations by the cuda
1962 - # graph replay later.
1963 - with torch.cuda.stream(self.shared_experts_stream):
1964 - # Note that hidden_states clone() is necessary here to avoid
1965 - # conflict with the main stream
1966 - shared_output = self.shared_experts(hidden_states_clone)
1967 - current_stream().wait_stream(self.shared_experts_stream)
1968 -
1969 - final_hidden_states = (
1970 - shared_output,
1971 - final_hidden_states,
1972 - )
1973 -
1974 - def combine_output(states: torch.Tensor) -> torch.Tensor:
1975 - if do_naive_dispatch_combine:
1976 - states = get_ep_group().combine(states, self.is_sequence_parallel)
1977 -
1978 - if self.pcp_size > 1:
1979 - states = get_pcp_group().reduce_scatter(
1980 - states,
1981 - dim=0,
1982 - )
1983 -
1984 - return states
1985 -
1986 - if self.shared_experts is not None:
1987 - return (
1988 - final_hidden_states[0],
1989 - combine_output(final_hidden_states[1]),
1990 - )
1991 - else:
1992 - return combine_output(final_hidden_states)
1993 -
1479 @classmethod
1480 def make_expert_params_mapping(
1481 cls,
@@ -2051,94 +1536,6 @@ class FusedMoE(CustomOp):
1536 return s
1537
1538
2054 -def get_layer_from_name(layer_name: str) -> FusedMoE:
2055 - forward_context: ForwardContext = get_forward_context()
2056 - if layer_name == "from_forward_context":
2057 - all_moe_layers = forward_context.all_moe_layers
2058 - assert all_moe_layers is not None
2059 - moe_layer_index = forward_context.moe_layer_index
2060 - if moe_layer_index >= len(all_moe_layers):
2061 - raise AssertionError(
2062 - "We expected the number of MOE layers in `all_moe_layers` "
2063 - "to be equal to the number of "
2064 - "{vllm.moe_forward, vllm.moe_forward_shared} calls."
2065 - )
2066 - layer_name = all_moe_layers[moe_layer_index]
2067 - forward_context.moe_layer_index += 1
2068 - self = cast(FusedMoE, forward_context.no_compile_layers[layer_name])
2069 - return self
2070 -
2071 -
2072 -def moe_forward(
2073 - hidden_states: torch.Tensor,
2074 - router_logits: torch.Tensor,
2075 - layer_name: str,
2076 -) -> torch.Tensor:
2077 - self = get_layer_from_name(layer_name)
2078 - assert self.shared_experts is None
2079 - return self.forward_impl(hidden_states, router_logits)
2080 -
2081 -
2082 -def moe_forward_fake(
2083 - hidden_states: torch.Tensor,
2084 - router_logits: torch.Tensor,
2085 - layer_name: str,
2086 -) -> torch.Tensor:
2087 - return torch.empty_like(hidden_states)
2088 -
2089 -
2090 -direct_register_custom_op(
2091 - op_name="moe_forward",
2092 - op_func=moe_forward,
2093 - mutates_args=["hidden_states"],
2094 - fake_impl=moe_forward_fake,
2095 - tags=(torch.Tag.needs_fixed_stride_order,),
2096 -)
2097 -
2098 -
2099 -def moe_forward_shared(
2100 - hidden_states: torch.Tensor,
2101 - router_logits: torch.Tensor,
2102 - layer_name: str,
2103 - shared_experts_input: torch.Tensor | None = None,
2104 -) -> tuple[torch.Tensor, torch.Tensor]:
2105 - self = get_layer_from_name(layer_name)
2106 - assert self.shared_experts is not None
2107 -
2108 - # Set here because torch.compile skips forward_native() setup code
2109 - # and calls this op directly. forward_impl() reads from this var.
2110 - with self._set_shared_experts_input(shared_experts_input):
2111 - return self.forward_impl(hidden_states, router_logits)
2112 -
2113 -
2114 -def moe_forward_shared_fake(
2115 - hidden_states: torch.Tensor,
2116 - router_logits: torch.Tensor,
2117 - layer_name: str,
2118 - shared_experts_input: torch.Tensor | None = None,
2119 -) -> tuple[torch.Tensor, torch.Tensor]:
2120 - # Output shapes:
2121 - # - fused_out: same as hidden_states (routed experts use transformed size)
2122 - # - shared_out: same as shared_experts_input if provided, else same as hidden_states
2123 - # (For latent MoE: shared experts use original hidden_size, not latent size)
2124 - fused_out = torch.empty_like(hidden_states)
2125 -
2126 - if shared_experts_input is not None:
2127 - shared_out = torch.empty_like(shared_experts_input)
2128 - else:
2129 - shared_out = torch.empty_like(hidden_states)
2130 -
2131 - return shared_out, fused_out
2132 -
2133 -
2134 -direct_register_custom_op(
2135 - op_name="moe_forward_shared",
2136 - op_func=moe_forward_shared,
2137 - mutates_args=["hidden_states"],
2138 - fake_impl=moe_forward_shared_fake,
2139 - tags=(torch.Tag.needs_fixed_stride_order,),
2140 -)
2141 -
1539 # Mark the FusedMoE weight_loader as supporting MoE-specific parameters
1540 # to avoid expensive runtime reflection in model loading code
1541 FusedMoE.weight_loader.supports_moe_loading = True # type: ignore[attr-defined]
vllm/model_executor/layers/fused_moe/modular_kernel.py
+1 -1
@@ -1228,7 +1228,7 @@ class FusedMoEModularKernel(torch.nn.Module):
1228 topk_weights: torch.Tensor,
1229 topk_ids: torch.Tensor,
1230 apply_router_weight_on_input: bool,
1231 - shared_experts_input: torch.Tensor | None = None,
1231 + shared_experts_input: torch.Tensor | None,
1232 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
1233 """
1234 The _finalize method is a wrapper around self.prepare_finalize.finalize
vllm/model_executor/layers/fused_moe/runner/__init__.py new
+2
@@ -0,0 +1,2 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
vllm/model_executor/layers/fused_moe/runner/default_moe_runner.py new
+743
@@ -0,0 +1,743 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +from contextlib import nullcontext
4 +
5 +import torch
6 +import torch.nn.functional as F
7 +
8 +import vllm.envs as envs
9 +from vllm.distributed import (
10 + get_ep_group,
11 + get_pcp_group,
12 + tensor_model_parallel_all_reduce,
13 +)
14 +from vllm.forward_context import (
15 + ForwardContext,
16 + get_forward_context,
17 + is_forward_context_available,
18 +)
19 +from vllm.logger import init_logger
20 +from vllm.model_executor.layers.fused_moe.config import (
21 + FusedMoEConfig,
22 +)
23 +from vllm.model_executor.layers.fused_moe.fused_moe_method_base import (
24 + FusedMoEMethodBase,
25 +)
26 +from vllm.model_executor.layers.fused_moe.router.fused_moe_router import (
27 + FusedMoERouter,
28 +)
29 +from vllm.model_executor.layers.fused_moe.runner.moe_runner import MoERunner
30 +from vllm.platforms import current_platform
31 +from vllm.utils.math_utils import cdiv
32 +from vllm.utils.torch_utils import (
33 + aux_stream,
34 + current_stream,
35 + direct_register_custom_op,
36 +)
37 +from vllm.v1.worker.ubatching import dbo_current_ubatch_id
38 +
39 +logger = init_logger(__name__)
40 +
41 +
42 +def get_layer_from_name(layer_name: str) -> torch.nn.Module:
43 + forward_context: ForwardContext = get_forward_context()
44 + if layer_name == "from_forward_context":
45 + all_moe_layers = forward_context.all_moe_layers
46 + assert all_moe_layers is not None
47 + moe_layer_index = forward_context.moe_layer_index
48 + if moe_layer_index >= len(all_moe_layers):
49 + raise AssertionError(
50 + "We expected the number of MOE layers in `all_moe_layers` "
51 + "to be equal to the number of "
52 + "{vllm.moe_forward, vllm.moe_forward_shared} calls."
53 + )
54 + layer_name = all_moe_layers[moe_layer_index]
55 + forward_context.moe_layer_index += 1
56 + return forward_context.no_compile_layers[layer_name]
57 +
58 +
59 +def _moe_forward(
60 + hidden_states: torch.Tensor,
61 + router_logits: torch.Tensor,
62 + shared_experts_input: torch.Tensor | None,
63 + layer_name: str,
64 +) -> torch.Tensor:
65 + layer = get_layer_from_name(layer_name)
66 + return layer.runner.forward_impl(
67 + layer, hidden_states, router_logits, shared_experts_input
68 + )
69 +
70 +
71 +def _moe_forward_fake(
72 + hidden_states: torch.Tensor,
73 + router_logits: torch.Tensor,
74 + shared_experts_input: torch.Tensor | None,
75 + layer_name: str,
76 +) -> torch.Tensor:
77 + return torch.empty_like(hidden_states)
78 +
79 +
80 +def _moe_forward_shared(
81 + hidden_states: torch.Tensor,
82 + router_logits: torch.Tensor,
83 + shared_experts_input: torch.Tensor | None,
84 + layer_name: str,
85 +) -> tuple[torch.Tensor, torch.Tensor]:
86 + layer = get_layer_from_name(layer_name)
87 + return layer.runner.forward_impl(
88 + layer, hidden_states, router_logits, shared_experts_input
89 + )
90 +
91 +
92 +def _moe_forward_shared_fake(
93 + hidden_states: torch.Tensor,
94 + router_logits: torch.Tensor,
95 + shared_experts_input: torch.Tensor | None,
96 + layer_name: str,
97 +) -> tuple[torch.Tensor, torch.Tensor]:
98 + # Output shapes:
99 + # - fused_out: same as hidden_states (routed experts use transformed size)
100 + # - shared_out: same as shared_experts_input if provided, else same as
101 + # hidden_states
102 + # (For latent MoE: shared experts use original hidden_size, not latent size)
103 + fused_out = torch.empty_like(hidden_states)
104 +
105 + if shared_experts_input is not None:
106 + shared_out = torch.empty_like(shared_experts_input)
107 + else:
108 + shared_out = torch.empty_like(hidden_states)
109 +
110 + return shared_out, fused_out
111 +
112 +
113 +direct_register_custom_op(
114 + op_name="moe_forward",
115 + op_func=_moe_forward,
116 + mutates_args=["hidden_states"],
117 + fake_impl=_moe_forward_fake,
118 + tags=(torch.Tag.needs_fixed_stride_order,),
119 +)
120 +
121 +
122 +direct_register_custom_op(
123 + op_name="moe_forward_shared",
124 + op_func=_moe_forward_shared,
125 + mutates_args=["hidden_states"],
126 + fake_impl=_moe_forward_shared_fake,
127 + tags=(torch.Tag.needs_fixed_stride_order,),
128 +)
129 +
130 +
131 +class DefaultMoERunner(MoERunner):
132 + """
133 + Default implementation of the MoE runner for executing Mixture of Experts layers.
134 +
135 + This class provides a comprehensive implementation for running MoE computations
136 + with support for:
137 + - Expert routing and token dispatching
138 + - Shared experts computation with optional parallel execution using CUDA streams
139 + - Data parallel (DP) chunking for large batch processing
140 + - Tensor model parallel and expert parallel operations
141 + - Various quantization methods and custom operators
142 + - Both monolithic and decomposed expert execution paths
143 +
144 + The runner handles the complete MoE forward pass including routing tokens to
145 + experts, executing expert computations, and combining results. It supports
146 + advanced features like overlapped execution of shared experts and optimized
147 + kernels for different parallel execution modes.
148 +
149 + Eventually, this class will be split up and specialized for different
150 + configurations, e.g. the presense or absence of shared experts, a gate, etc.
151 + """
152 +
153 + def __init__(
154 + self,
155 + layer: torch.nn.Module,
156 + moe_config: FusedMoEConfig,
157 + router: FusedMoERouter,
158 + routed_input_transform: torch.nn.Module | None,
159 + gate: torch.nn.Module | None,
160 + shared_experts: torch.nn.Module | None,
161 + quant_method: FusedMoEMethodBase,
162 + reduce_results: bool,
163 + enable_dbo: bool,
164 + ):
165 + super().__init__()
166 + self.moe_config = moe_config
167 + self.router = router
168 + self.routed_input_transform = routed_input_transform
169 + self.gate = gate
170 + self.shared_experts = shared_experts
171 + self.quant_method = quant_method
172 + self.reduce_results = reduce_results
173 + self.enable_dbo = enable_dbo
174 +
175 + # Allow disabling of the separate shared experts stream for
176 + # debug purposes.
177 + # TODO: Remove this after more extensive testings with TP/DP
178 + # and other execution modes
179 + if envs.VLLM_DISABLE_SHARED_EXPERTS_STREAM:
180 + logger.debug_once("Disabling MoE shared_experts cuda stream", scope="local")
181 + self.shared_experts_stream = None
182 + else:
183 + # TODO(rob): enable shared expert overlap with non-cuda-alike.
184 + # aux_stream() returns None on non-cuda-alike platforms.
185 + self.shared_experts_stream = aux_stream()
186 + if self.shared_experts_stream is not None:
187 + logger.debug_once(
188 + "Enabled separate cuda stream for MoE shared_experts", scope="local"
189 + )
190 +
191 + # Needed for string -> FusedMoE layer lookup in custom ops.
192 + self.layer_name = layer.layer_name
193 +
194 + if current_platform.is_tpu() or current_platform.is_cpu():
195 + # TODO: Once the OOM issue for the TPU backend is resolved, we
196 + # will switch to using the moe_forward custom op.
197 + # Note: CPU doesn't require wrapped forward_impl.
198 + if self.shared_experts is None:
199 + self.moe_forward = _moe_forward
200 + else:
201 + self.moe_forward = _moe_forward_shared
202 + else:
203 + if self.shared_experts is None:
204 + self.moe_forward = torch.ops.vllm.moe_forward
205 + else:
206 + self.moe_forward = torch.ops.vllm.moe_forward_shared
207 +
208 + # Chunked all2all staging tensor
209 + self.batched_hidden_states: torch.Tensor | None = None
210 + self.batched_router_logits: torch.Tensor | None = None
211 +
212 + @property
213 + def use_dp_chunking(self) -> bool:
214 + return (
215 + self.moe_config.moe_parallel_config.use_pplx_kernels
216 + or self.moe_config.moe_parallel_config.use_deepep_ll_kernels
217 + or self.moe_config.moe_parallel_config.use_mori_kernels
218 + or self.moe_config.moe_parallel_config.use_fi_all2allv_kernels
219 + ) and envs.VLLM_ENABLE_MOE_DP_CHUNK
220 +
221 + def _maybe_setup_shared_experts_stream(
222 + self,
223 + hidden_states: torch.Tensor,
224 + shared_input: torch.Tensor | None,
225 + has_separate_shared_experts: bool,
226 + use_chunked_impl: bool,
227 + ) -> tuple[bool, torch.Tensor | None]:
228 + use_shared_experts_stream = (
229 + current_platform.is_cuda()
230 + and has_separate_shared_experts
231 + and not use_chunked_impl
232 + and self.shared_experts_stream is not None
233 + and (
234 + hidden_states.shape[0]
235 + <= envs.VLLM_SHARED_EXPERTS_STREAM_TOKEN_THRESHOLD
236 + )
237 + )
238 +
239 + hidden_states_clone: torch.Tensor | None = None
240 + if use_shared_experts_stream:
241 + assert self.shared_experts_stream is not None
242 +
243 + shared_experts_input = (
244 + shared_input if shared_input is not None else hidden_states
245 + )
246 +
247 + # Clone BEFORE switching streams to avoid race condition
248 + # where routed_expert kernel may mutate hidden_states.
249 + hidden_states_clone = shared_experts_input.clone()
250 +
251 + # Record that the clone will be used by shared_experts_stream
252 + # to avoid gc issue from deallocation of hidden_states_clone
253 + # For more details: https://docs.pytorch.org/docs/stable/generated/torch.Tensor.record_stream.html # noqa: E501
254 + # NOTE: We don't need shared_output.record_stream(current_stream())
255 + # because we synch the streams before using shared_output.
256 + hidden_states_clone.record_stream(self.shared_experts_stream)
257 +
258 + # Mark sync start point for the separate shared experts
259 + # stream here since we want to run in parallel with the
260 + # router/gate (next op below)
261 + assert self.shared_experts_stream is not None
262 + self.shared_experts_stream.wait_stream(current_stream())
263 +
264 + return use_shared_experts_stream, hidden_states_clone
265 +
266 + def ensure_dp_chunking_init(self):
267 + if not self.use_dp_chunking or self.batched_hidden_states is not None:
268 + return
269 +
270 + states_shape: tuple[int, ...]
271 + logits_shape: tuple[int, ...]
272 +
273 + moe = self.moe_config
274 +
275 + if self.enable_dbo:
276 + states_shape = (2, moe.max_num_tokens, self.moe_config.hidden_dim)
277 + logits_shape = (2, moe.max_num_tokens, self.moe_config.num_logical_experts)
278 + else:
279 + states_shape = (moe.max_num_tokens, self.moe_config.hidden_dim)
280 + logits_shape = (moe.max_num_tokens, self.moe_config.num_logical_experts)
281 +
282 + self.batched_hidden_states = torch.zeros(
283 + states_shape, dtype=moe.in_dtype, device=torch.cuda.current_device()
284 + )
285 +
286 + self.batched_router_logits = torch.zeros(
287 + logits_shape,
288 + dtype=moe.router_logits_dtype,
289 + device=torch.cuda.current_device(),
290 + )
291 +
292 + def must_reduce_shared_expert_outputs(self) -> bool:
293 + """
294 + The shared_experts are typically computed using the RowParallelLinear
295 + layer. The result of this function is typically used as
296 + the reduce_results argument to the module.
297 + When just tensor-parallel is used, it is not required to reduce
298 + the shared_experts results immediately. Instead we reduce at the
299 + once at the end of the MoE op. (Refer to DeepSeekV2MoE module)
300 + With EP and all2all kernels - this is no longer viable as all
301 + GPU ranks in DP, produce the complete set of hidden_states.
302 + Therefore it is required that we reduce the shared_experts output
303 + early.
304 + """
305 + assert self.quant_method is not None
306 + return (
307 + self.quant_method.moe_mk is not None
308 + and self.quant_method.moe_mk.output_is_reduced()
309 + )
310 +
311 + def maybe_all_reduce_tensor_model_parallel(self, final_hidden_states: torch.Tensor):
312 + """
313 + Some combine kernels reduce across GPU ranks by default.
314 + """
315 + if self.must_reduce_shared_expert_outputs():
316 + return final_hidden_states
317 + else:
318 + return tensor_model_parallel_all_reduce(final_hidden_states)
319 +
320 + def apply_routed_input_transform(self, hidden_states: torch.Tensor) -> torch.Tensor:
321 + """Apply transform for routed experts (e.g., latent projection).
322 +
323 + This is called by FusedMoE.forward_native. The original hidden_states
324 + is saved separately so shared experts get [S, hidden_size] while
325 + routed experts get the transformed [S, moe_latent_size].
326 +
327 + TODO: For latent MoE bandwidth optimization, fc2_latent_proj could be
328 + moved inside SharedFusedMoE to all-reduce on the smaller latent
329 + dimension.
330 + """
331 + if self.routed_input_transform is not None:
332 + result = self.routed_input_transform(hidden_states)
333 + # ReplicatedLinear returns (output, extra_bias) tuple.
334 + # We only need the output tensor; extra_bias is not used here.
335 + if isinstance(result, tuple):
336 + return result[0]
337 + return result
338 + return hidden_states
339 +
340 + def _reduce_output(
341 + self,
342 + states: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
343 + trunc_sizes: list[int],
344 + ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
345 + def trunc(x: torch.Tensor, trunc_size: int) -> torch.Tensor:
346 + return x[..., :trunc_size]
347 +
348 + def reduce_and_trunc(x: torch.Tensor, trunc_size: int) -> torch.Tensor:
349 + return trunc(self.maybe_all_reduce_tensor_model_parallel(x), trunc_size)
350 +
351 + if (
352 + not self.moe_config.is_sequence_parallel
353 + and not self.use_dp_chunking
354 + and self.reduce_results
355 + and (self.moe_config.tp_size > 1 or self.moe_config.ep_size > 1)
356 + ):
357 + func = reduce_and_trunc
358 + else:
359 + func = trunc
360 +
361 + if isinstance(states, tuple):
362 + return tuple(
363 + [func(s, trunc_size) for s, trunc_size in zip(states, trunc_sizes)]
364 + )
365 + else:
366 + assert len(trunc_sizes) == 1
367 + return func(states, trunc_sizes[0])
368 +
369 + def _encode_layer_name(self) -> str:
370 + # Can be unavailable or None in unittests
371 + if (
372 + is_forward_context_available()
373 + and get_forward_context().all_moe_layers is not None
374 + ):
375 + return "from_forward_context"
376 + return self.layer_name
377 +
378 + def forward(
379 + self,
380 + hidden_states: torch.Tensor,
381 + router_logits: torch.Tensor,
382 + ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
383 + # For latent MoE: save ORIGINAL hidden_states before transform
384 + # (shared_experts need original dimension, routed experts use transformed)
385 + original_hidden_states = hidden_states
386 + original_hidden_dim = hidden_states.shape[-1]
387 +
388 + # Apply transform for routed experts (e.g., latent projection for latent MoE)
389 + hidden_states = self.apply_routed_input_transform(hidden_states)
390 +
391 + # This is the dimension after transform (for routed expert output slicing)
392 + transformed_hidden_dim = hidden_states.shape[-1]
393 + if self.moe_config.hidden_dim != transformed_hidden_dim:
394 + hidden_states = F.pad(
395 + hidden_states,
396 + (0, self.moe_config.hidden_dim - transformed_hidden_dim),
397 + mode="constant",
398 + value=0.0,
399 + )
400 +
401 + fused_output = self.moe_forward(
402 + hidden_states,
403 + router_logits,
404 + original_hidden_states,
405 + self._encode_layer_name(),
406 + )
407 +
408 + if isinstance(fused_output, tuple):
409 + orig_hidden_dims = [original_hidden_dim, transformed_hidden_dim]
410 + else:
411 + orig_hidden_dims = [transformed_hidden_dim]
412 +
413 + return self._reduce_output(fused_output, orig_hidden_dims)
414 +
415 + def forward_impl_chunked(
416 + self,
417 + layer: torch.nn.Module,
418 + full_hidden_states: torch.Tensor,
419 + full_router_logits: torch.Tensor,
420 + shared_input: torch.Tensor | None,
421 + has_separate_shared_experts: bool,
422 + ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
423 + assert self.batched_hidden_states is not None
424 + assert self.batched_router_logits is not None
425 + assert self.batched_hidden_states.dtype == full_hidden_states.dtype, (
426 + f"{self.batched_hidden_states.dtype} == {full_hidden_states.dtype}"
427 + )
428 + assert self.batched_router_logits.dtype == full_router_logits.dtype, (
429 + f"{self.batched_router_logits.dtype} == {full_router_logits.dtype}"
430 + )
431 + # Check size compatibility.
432 + assert self.batched_hidden_states.size(-1) == full_hidden_states.size(-1)
433 + assert self.batched_router_logits.size(-1) == full_router_logits.size(-1)
434 +
435 + # TODO(bnell): Fix shared_expert_inputs w/chunking.
436 + # assert shared_input is None, (
437 + # "Routed input transform is not currently supported with DP chunking."
438 + # )
439 +
440 + full_fused_final_hidden_states = torch.empty_like(full_hidden_states)
441 + if self.shared_experts is not None:
442 + full_shared_final_hidden_states = torch.empty_like(full_hidden_states)
443 +
444 + def process_chunk(chunk_start, chunk_end, skip_result_store=False):
445 + chunk_size = chunk_end - chunk_start
446 + hidden_states = full_hidden_states[chunk_start:chunk_end, :]
447 + router_logits = full_router_logits[chunk_start:chunk_end, :]
448 +
449 + assert self.batched_hidden_states is not None
450 + assert self.batched_router_logits is not None
451 + # This is only true when DBO has been enabled in the config.
452 + # Both tensors will have an outer dimension for the ubatch id
453 + if self.batched_hidden_states.dim() == 3:
454 + assert self.batched_router_logits.dim() == 3
455 + batch_buffer_idx = dbo_current_ubatch_id()
456 + batched_hidden_states = self.batched_hidden_states[batch_buffer_idx, :]
457 + batched_router_logits = self.batched_router_logits[batch_buffer_idx, :]
458 + else:
459 + batched_hidden_states = self.batched_hidden_states
460 + batched_router_logits = self.batched_router_logits
461 +
462 + assert (
463 + batched_hidden_states.size(0) # type: ignore
464 + >= chunk_size
465 + )
466 + assert (
467 + batched_router_logits.size(0) # type: ignore
468 + >= chunk_size
469 + )
470 + staged_hidden_states = batched_hidden_states[:chunk_size, :] # type: ignore
471 + staged_router_logits = batched_router_logits[:chunk_size, :] # type: ignore
472 + staged_hidden_states.copy_(hidden_states, non_blocking=True)
473 + staged_router_logits.copy_(router_logits, non_blocking=True)
474 +
475 + # Matrix multiply.
476 + if self.quant_method.is_monolithic:
477 + final_hidden_states = self.quant_method.apply_monolithic(
478 + layer=layer,
479 + x=staged_hidden_states,
480 + router_logits=staged_router_logits,
481 + )
482 + else:
483 + topk_weights, topk_ids = self.router.select_experts(
484 + hidden_states=staged_hidden_states,
485 + router_logits=staged_router_logits,
486 + )
487 +
488 + final_hidden_states = self.quant_method.apply(
489 + layer=layer,
490 + x=staged_hidden_states,
491 + topk_weights=topk_weights,
492 + topk_ids=topk_ids,
493 + shared_experts_input=shared_input,
494 + )
495 +
496 + if has_separate_shared_experts:
497 + assert not isinstance(final_hidden_states, tuple)
498 + assert self.shared_experts is not None
499 +
500 + shared_output = self.shared_experts(staged_hidden_states)
501 +
502 + final_hidden_states = (
503 + shared_output,
504 + final_hidden_states,
505 + )
506 +
507 + if not skip_result_store:
508 + if self.shared_experts is None:
509 + full_fused_final_hidden_states[chunk_start:chunk_end, :].copy_(
510 + final_hidden_states, non_blocking=True
511 + )
512 + else:
513 + full_shared_final_hidden_states[chunk_start:chunk_end, :].copy_(
514 + final_hidden_states[0], non_blocking=True
515 + )
516 + full_fused_final_hidden_states[chunk_start:chunk_end, :].copy_(
517 + final_hidden_states[1], non_blocking=True
518 + )
519 +
520 + ctx = get_forward_context()
521 + # flashinfer_cutlass_kernels can handle: optional DP + TP/EP
522 + max_tokens_across_dispatchers = ctx.dp_metadata.max_tokens_across_dp_cpu
523 + moe_dp_chunk_size_per_rank = self.moe_config.max_num_tokens
524 +
525 + # If the input to the MoE is sequence parallel then divide by sp_size
526 + # to find the maximum number of tokens for any individual dispatcher.
527 + if self.moe_config.is_sequence_parallel:
528 + max_tokens_across_dispatchers = cdiv(
529 + max_tokens_across_dispatchers, self.moe_config.sp_size
530 + )
531 +
532 + num_tokens = full_hidden_states.size(0)
533 + for chunk_idx, chunk_start_ in enumerate(
534 + range(0, max_tokens_across_dispatchers, moe_dp_chunk_size_per_rank)
535 + ):
536 + chunk_start = chunk_start_
537 + chunk_end = min(
538 + chunk_start + moe_dp_chunk_size_per_rank, max_tokens_across_dispatchers
539 + )
540 + # clamp start and end
541 + chunk_start = min(chunk_start, num_tokens - 1)
542 + chunk_end = min(chunk_end, num_tokens)
543 + with ctx.dp_metadata.chunked_sizes(
544 + self.moe_config.sp_size, moe_dp_chunk_size_per_rank, chunk_idx
545 + ):
546 + process_chunk(
547 + chunk_start, chunk_end, skip_result_store=chunk_start_ >= num_tokens
548 + )
549 +
550 + if self.shared_experts is None:
551 + return full_fused_final_hidden_states
552 + else:
553 + return (full_shared_final_hidden_states, full_fused_final_hidden_states)
554 +
555 + def forward_impl(
556 + self,
557 + layer: torch.nn.Module,
558 + hidden_states: torch.Tensor,
559 + router_logits: torch.Tensor,
560 + shared_input: torch.Tensor | None,
561 + ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
562 + assert self.quant_method is not None
563 +
564 + self.ensure_dp_chunking_init()
565 +
566 + has_separate_shared_experts = (
567 + not self.quant_method.mk_owns_shared_expert
568 + and self.shared_experts is not None
569 + )
570 +
571 + use_chunked_impl = self.use_dp_chunking
572 +
573 + use_shared_experts_stream, hidden_states_clone = (
574 + self._maybe_setup_shared_experts_stream(
575 + hidden_states,
576 + shared_input,
577 + has_separate_shared_experts,
578 + use_chunked_impl,
579 + )
580 + )
581 +
582 + # If router/gate provided, then apply it here.
583 + # (Note: This code runs only when "overlapped mode" is on to allow
584 + # parallel execution of shared experts with the FusedMoE via
585 + # separate cuda stream)
586 + if self.gate is not None:
587 + router_logits, _ = self.gate(hidden_states)
588 +
589 + if use_chunked_impl:
590 + return self.forward_impl_chunked(
591 + layer,
592 + hidden_states,
593 + router_logits,
594 + shared_input,
595 + has_separate_shared_experts,
596 + )
597 +
598 + # NOTE(rob): once we finish migrating all the quant methods to use
599 + # MKs, we can remove the naive dispatch/combine path from here.
600 + do_naive_dispatch_combine = (
601 + self.moe_config.dp_size > 1 and not self.quant_method.supports_internal_mk
602 + )
603 +
604 + ctx = get_forward_context()
605 + sp_ctx = (
606 + ctx.dp_metadata.sp_local_sizes(self.moe_config.sp_size)
607 + if ctx.dp_metadata
608 + else nullcontext()
609 + )
610 +
611 + with sp_ctx:
612 + extra_tensors = None
613 + if do_naive_dispatch_combine:
614 + post_quant_allgather = (
615 + self.quant_method is not None
616 + and self.moe_config.dp_size > 1
617 + and self.moe_config.use_ep
618 + and getattr(self.quant_method, "do_post_quant_allgather", False)
619 + )
620 + if post_quant_allgather:
621 + hidden_states_to_dispatch, extra_tensors = (
622 + self.quant_method.prepare_dp_allgather_tensor(
623 + layer, hidden_states, router_logits
624 + )
625 + )
626 + else:
627 + hidden_states_to_dispatch = hidden_states
628 +
629 + dispatch_res = get_ep_group().dispatch_router_logits(
630 + hidden_states_to_dispatch,
631 + router_logits,
632 + self.moe_config.is_sequence_parallel,
633 + extra_tensors=extra_tensors,
634 + )
635 + if extra_tensors is not None:
636 + (
637 + orig_hidden_states,
638 + router_logits,
639 + extra_tensors_combined,
640 + ) = dispatch_res
641 + hidden_states_combined = (
642 + orig_hidden_states,
643 + extra_tensors_combined[0],
644 + )
645 + else:
646 + hidden_states_combined, router_logits = dispatch_res
647 + orig_hidden_states = hidden_states_combined
648 + else:
649 + orig_hidden_states = hidden_states
650 +
651 + # Run shared experts before matrix multiply.
652 + # because matrix multiply maybe modify the hidden_states.
653 + if has_separate_shared_experts and not use_shared_experts_stream:
654 + assert self.shared_experts is not None
655 + shared_input = (
656 + shared_input if shared_input is not None else hidden_states
657 + )
658 + shared_output = self.shared_experts(shared_input)
659 +
660 + # NOTE: Similar with DP, PCP also needs dispatch and combine. For
661 + # simplicity, AgRsAll2All was added separately for PCP here. Maybe
662 + # we should modify All2AllManager abstract to better support PCP.
663 + if self.moe_config.pcp_size > 1:
664 + hidden_states = get_pcp_group().all_gather(
665 + hidden_states,
666 + dim=0,
667 + )
668 + router_logits = get_pcp_group().all_gather(
669 + router_logits,
670 + dim=0,
671 + )
672 +
673 + # TODO(bnell): deal with fp4 flashinfer tuple hidden states hack (#30014).
674 + # Figure out nicer way to do this.
675 + if do_naive_dispatch_combine:
676 + x = hidden_states_combined
677 + x_orig = orig_hidden_states
678 + else:
679 + x = hidden_states
680 + x_orig = hidden_states
681 +
682 + # Matrix multiply.
683 + if self.quant_method.is_monolithic:
684 + final_hidden_states = self.quant_method.apply_monolithic(
685 + layer=layer,
686 + x=x,
687 + router_logits=router_logits,
688 + )
689 + else:
690 + topk_weights, topk_ids = self.router.select_experts(
691 + hidden_states=x_orig,
692 + router_logits=router_logits,
693 + )
694 +
695 + final_hidden_states = self.quant_method.apply(
696 + layer=layer,
697 + x=x, # The type signture of this is wrong due to the hack.
698 + topk_weights=topk_weights,
699 + topk_ids=topk_ids,
700 + shared_experts_input=shared_input,
701 + )
702 +
703 + if has_separate_shared_experts:
704 + assert self.shared_experts is not None
705 +
706 + if use_shared_experts_stream:
707 + # Run shared experts in parallel on a separate stream
708 + # NOTE: We start the separate stream here and mark the
709 + # sync end point immediately after it is done. This is
710 + # important to avoid excessive stream allocations by the cuda
711 + # graph replay later.
712 + with torch.cuda.stream(self.shared_experts_stream):
713 + # Note that hidden_states clone() is necessary here to avoid
714 + # conflict with the main stream
715 + shared_output = self.shared_experts(hidden_states_clone)
716 + current_stream().wait_stream(self.shared_experts_stream)
717 +
718 + final_hidden_states = (
719 + shared_output,
720 + final_hidden_states,
721 + )
722 +
723 + def combine_output(states: torch.Tensor) -> torch.Tensor:
724 + if do_naive_dispatch_combine:
725 + states = get_ep_group().combine(
726 + states, self.moe_config.is_sequence_parallel
727 + )
728 +
729 + if self.moe_config.pcp_size > 1:
730 + states = get_pcp_group().reduce_scatter(
731 + states,
732 + dim=0,
733 + )
734 +
735 + return states
736 +
737 + if self.shared_experts is not None:
738 + return (
739 + final_hidden_states[0],
740 + combine_output(final_hidden_states[1]),
741 + )
742 + else:
743 + return combine_output(final_hidden_states)
vllm/model_executor/layers/fused_moe/runner/moe_runner.py new
+34
@@ -0,0 +1,34 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +from abc import ABC, abstractmethod
4 +
5 +import torch
6 +
7 +
8 +class MoERunner(ABC):
9 + """
10 + Abstract base class for Mixture of Experts (MoE) runners.
11 +
12 + This class defines the interface that all MoE runner implementations must follow.
13 + MoE runners are responsible for executing the forward pass of MoE layers, handling
14 + expert routing, and managing tensor parallel operations.
15 + """
16 +
17 + @abstractmethod
18 + def forward(
19 + self,
20 + hidden_states: torch.Tensor,
21 + router_logits: torch.Tensor,
22 + ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
23 + raise NotImplementedError
24 +
25 + @abstractmethod
26 + def must_reduce_shared_expert_outputs(self) -> bool:
27 + raise NotImplementedError
28 +
29 + @abstractmethod
30 + def maybe_all_reduce_tensor_model_parallel(
31 + self,
32 + final_hidden_states: torch.Tensor,
33 + ):
34 + raise NotImplementedError
vllm/model_executor/layers/fused_moe/shared_fused_moe.py
-64
@@ -18,70 +18,6 @@ class SharedFusedMoE(FusedMoE):
18 can be interleaved with the fused all2all dispatch communication step.
19 """
20
21 - def __init__(
22 - self,
23 - shared_experts: torch.nn.Module | None,
24 - gate: torch.nn.Module | None = None,
25 - use_overlapped: bool = True,
26 - routed_input_transform: torch.nn.Module | None = None,
27 - **kwargs,
28 - ):
29 - # Pass has_shared_experts so FusedMoE.__init__ can set disable_inplace
30 - # without accessing self.shared_experts (submodules cannot be set before
31 - # Module.__init__()).
32 - kwargs["has_shared_experts"] = shared_experts is not None
33 - super().__init__(**kwargs)
34 - self._shared_experts = shared_experts
35 - self._routed_input_transform = routed_input_transform
36 -
37 - # Disable shared expert overlap if:
38 - # - we are using eplb with non-default backend, because of correctness issues
39 - # - we are using flashinfer with DP, since there nothing to gain
40 - # - we are using marlin kernels
41 - backend = self.moe_parallel_config.all2all_backend
42 - self.use_overlapped = (
43 - use_overlapped
44 - and not (
45 - (self.enable_eplb and backend != "allgather_reducescatter")
46 - or self.moe_parallel_config.use_fi_all2allv_kernels
47 - )
48 - and self._shared_experts is not None
49 - )
50 -
51 - self._gate = gate
52 -
53 - @property
54 - def shared_experts(self) -> torch.nn.Module | None:
55 - return self._shared_experts if self.use_overlapped else None
56 -
57 - @property
58 - def gate(self) -> torch.nn.Module | None:
59 - return self._gate if self.use_overlapped else None
60 -
61 - @property
62 - def is_internal_router(self) -> bool:
63 - return self.gate is not None
64 -
65 - def apply_routed_input_transform(self, hidden_states: torch.Tensor) -> torch.Tensor:
66 - """Apply transform for routed experts (e.g., latent projection).
67 -
68 - This is called by FusedMoE.forward_native. The original hidden_states
69 - is saved separately so shared experts get [S, hidden_size] while
70 - routed experts get the transformed [S, moe_latent_size].
71 -
72 - TODO: For latent MoE bandwidth optimization, fc2_latent_proj could be
73 - moved inside SharedFusedMoE to all-reduce on the smaller latent
74 - dimension.
75 - """
76 - if self._routed_input_transform is not None:
77 - result = self._routed_input_transform(hidden_states)
78 - # ReplicatedLinear returns (output, extra_bias) tuple.
79 - # We only need the output tensor; extra_bias is not used here.
80 - if isinstance(result, tuple):
81 - return result[0]
82 - return result
83 - return hidden_states
84 -
21 def forward(
22 self,
23 hidden_states: torch.Tensor,
vllm/model_executor/layers/fused_moe/unquantized_fused_moe_method.py
+8 -1
@@ -55,6 +55,8 @@ logger = init_logger(__name__)
55 class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
56 """MoE method without quantization."""
57
58 + # --8<-- [end:unquantized_fused_moe]
59 +
60 def __init__(self, moe: FusedMoEConfig):
61 super().__init__(moe)
62 self.unquantized_backend = select_unquantized_moe_backend(
@@ -90,8 +92,9 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
92 x: torch.Tensor,
93 topk_weights: torch.Tensor,
94 topk_ids: torch.Tensor,
95 + shared_experts_input: torch.Tensor | None,
96 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
94 - return self.forward_cuda(layer, x, topk_weights, topk_ids)
97 + return self.forward_cuda(layer, x, topk_weights, topk_ids, shared_experts_input)
98
99 @property
100 def is_monolithic(self) -> bool:
@@ -293,12 +296,14 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
296 x: torch.Tensor,
297 topk_weights: torch.Tensor,
298 topk_ids: torch.Tensor,
299 + shared_experts_input: torch.Tensor | None,
300 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
301 return self.forward(
302 layer=layer,
303 x=x,
304 topk_weights=topk_weights,
305 topk_ids=topk_ids,
306 + shared_experts_input=shared_experts_input,
307 )
308
309 def get_fused_moe_quant_config(self, layer: torch.nn.Module) -> FusedMoEQuantConfig:
@@ -316,6 +321,7 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
321 x: torch.Tensor,
322 topk_weights: torch.Tensor,
323 topk_ids: torch.Tensor,
324 + shared_experts_input: torch.Tensor | None,
325 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
326 assert self.kernel is not None
327
@@ -329,6 +335,7 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
335 apply_router_weight_on_input=layer.apply_router_weight_on_input,
336 global_num_experts=layer.global_num_experts,
337 expert_map=layer.expert_map,
338 + shared_experts_input=shared_experts_input,
339 )
340
341 def forward_monolithic_cuda(
vllm/model_executor/layers/quantization/awq_marlin.py
+1
@@ -764,6 +764,7 @@ class AWQMarlinMoEMethod(FusedMoEMethodBase):
764 x: torch.Tensor,
765 topk_weights: torch.Tensor,
766 topk_ids: torch.Tensor,
767 + shared_experts_input: torch.Tensor | None,
768 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
769 return fused_marlin_moe(
770 x,
vllm/model_executor/layers/quantization/bitsandbytes.py
+1
@@ -501,6 +501,7 @@ class BitsAndBytesMoEMethod(FusedMoEMethodBase):
501 x: torch.Tensor,
502 topk_weights: torch.Tensor,
503 topk_ids: torch.Tensor,
504 + shared_experts_input: torch.Tensor | None,
505 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
506 from vllm.model_executor.layers.fused_moe import fused_experts
507
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe.py
+10 -3
@@ -349,6 +349,7 @@ class CompressedTensorsW4A4Mxfp4MoEMethod(CompressedTensorsMoEMethod):
349 x: torch.Tensor,
350 topk_weights: torch.Tensor,
351 topk_ids: torch.Tensor,
352 + shared_experts_input: torch.Tensor | None,
353 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
354 assert self.moe_mk is not None
355 return self.moe_mk(
@@ -361,7 +362,7 @@ class CompressedTensorsW4A4Mxfp4MoEMethod(CompressedTensorsMoEMethod):
362 global_num_experts=layer.global_num_experts,
363 expert_map=layer.expert_map,
364 apply_router_weight_on_input=layer.apply_router_weight_on_input,
364 - shared_experts_input=layer._get_shared_experts_input(x),
365 + shared_experts_input=shared_experts_input,
366 )
367
368
@@ -645,6 +646,7 @@ class CompressedTensorsW4A4Nvfp4MoEMethod(CompressedTensorsMoEMethod):
646 x: torch.Tensor,
647 topk_weights: torch.Tensor,
648 topk_ids: torch.Tensor,
649 + shared_experts_input: torch.Tensor | None,
650 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
651 assert not self.is_monolithic
652 assert layer.activation == "silu", "Only SiLU activation is supported."
@@ -673,7 +675,7 @@ class CompressedTensorsW4A4Nvfp4MoEMethod(CompressedTensorsMoEMethod):
675 global_num_experts=layer.global_num_experts,
676 expert_map=layer.expert_map,
677 apply_router_weight_on_input=layer.apply_router_weight_on_input,
676 - shared_experts_input=layer._get_shared_experts_input(x),
678 + shared_experts_input=shared_experts_input,
679 )
680
681
@@ -1064,6 +1066,7 @@ class CompressedTensorsW8A8Fp8MoEMethod(CompressedTensorsMoEMethod):
1066 x: torch.Tensor,
1067 topk_weights: torch.Tensor,
1068 topk_ids: torch.Tensor,
1069 + shared_experts_input: torch.Tensor | None,
1070 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
1071 assert not self.is_monolithic
1072 assert self.moe_mk is not None
@@ -1079,7 +1082,7 @@ class CompressedTensorsW8A8Fp8MoEMethod(CompressedTensorsMoEMethod):
1082 # https://github.com/vllm-project/vllm/commit/84166fee9770e6fba71a96978b3e7d149392fb28 # noqa: E501
1083 expert_map=layer.expert_map,
1084 apply_router_weight_on_input=layer.apply_router_weight_on_input,
1082 - shared_experts_input=layer._get_shared_experts_input(x),
1085 + shared_experts_input=shared_experts_input,
1086 )
1087
1088 @property
@@ -1203,6 +1206,7 @@ class CompressedTensorsW8A8Int8MoEMethod(CompressedTensorsMoEMethod):
1206 x: torch.Tensor,
1207 topk_weights: torch.Tensor,
1208 topk_ids: torch.Tensor,
1209 + shared_experts_input: torch.Tensor | None,
1210 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
1211 from vllm.model_executor.layers.fused_moe import fused_experts
1212
@@ -1713,6 +1717,7 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
1717 x: torch.Tensor,
1718 topk_weights: torch.Tensor,
1719 topk_ids: torch.Tensor,
1720 + shared_experts_input: torch.Tensor | None,
1721 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
1722 assert self.kernel_backend == "Marlin"
1723 return fused_marlin_moe(
@@ -1961,6 +1966,7 @@ class CompressedTensorsWNA16MoEMethod(CompressedTensorsMoEMethod):
1966 x: torch.Tensor,
1967 topk_weights: torch.Tensor,
1968 topk_ids: torch.Tensor,
1969 + shared_experts_input: torch.Tensor | None,
1970 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
1971 from vllm.model_executor.layers.fused_moe import fused_experts
1972
@@ -2575,6 +2581,7 @@ class CompressedTensorsW4A8Fp8MoEMethod(CompressedTensorsMoEMethod):
2581 x: torch.Tensor,
2582 topk_weights: torch.Tensor,
2583 topk_ids: torch.Tensor,
2584 + shared_experts_input: torch.Tensor | None,
2585 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
2586 if layer.enable_eplb:
2587 raise NotImplementedError(
vllm/model_executor/layers/quantization/experts_int8.py
+1
@@ -140,6 +140,7 @@ class ExpertsInt8MoEMethod(FusedMoEMethodBase):
140 x: torch.Tensor,
141 topk_weights: torch.Tensor,
142 topk_ids: torch.Tensor,
143 + shared_experts_input: torch.Tensor | None,
144 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
145 from vllm.model_executor.layers.fused_moe import fused_experts
146
vllm/model_executor/layers/quantization/fp8.py
+2 -1
@@ -1010,6 +1010,7 @@ class Fp8MoEMethod(FusedMoEMethodBase):
1010 x: torch.Tensor,
1011 topk_weights: torch.Tensor,
1012 topk_ids: torch.Tensor,
1013 + shared_experts_input: torch.Tensor | None,
1014 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
1015 assert self.moe_mk is not None
1016 assert not self.is_monolithic
@@ -1023,7 +1024,7 @@ class Fp8MoEMethod(FusedMoEMethodBase):
1024 global_num_experts=layer.global_num_experts,
1025 expert_map=layer.expert_map,
1026 apply_router_weight_on_input=layer.apply_router_weight_on_input,
1026 - shared_experts_input=layer._get_shared_experts_input(x),
1027 + shared_experts_input=shared_experts_input,
1028 )
1029
1030
vllm/model_executor/layers/quantization/gguf.py
+1
@@ -635,6 +635,7 @@ class GGUFMoEMethod(FusedMoEMethodBase):
635 x: torch.Tensor,
636 topk_weights: torch.Tensor,
637 topk_ids: torch.Tensor,
638 + shared_experts_input: torch.Tensor | None,
639 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
640 assert layer.activation == "silu", "Only SiLU activation is supported."
641 if layer.apply_router_weight_on_input:
vllm/model_executor/layers/quantization/gptq_marlin.py
+1
@@ -900,6 +900,7 @@ class GPTQMarlinMoEMethod(FusedMoEMethodBase):
900 x: torch.Tensor,
901 topk_weights: torch.Tensor,
902 topk_ids: torch.Tensor,
903 + shared_experts_input: torch.Tensor | None,
904 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
905 return fused_marlin_moe(
906 x,
vllm/model_executor/layers/quantization/modelopt.py
+4 -2
@@ -958,6 +958,7 @@ class ModelOptFp8MoEMethod(FusedMoEMethodBase):
958 x: torch.Tensor,
959 topk_weights: torch.Tensor,
960 topk_ids: torch.Tensor,
961 + shared_experts_input: torch.Tensor | None,
962 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
963 assert not self.is_monolithic
964
@@ -980,7 +981,7 @@ class ModelOptFp8MoEMethod(FusedMoEMethodBase):
981 global_num_experts=layer.global_num_experts,
982 expert_map=layer.expert_map,
983 apply_router_weight_on_input=layer.apply_router_weight_on_input,
983 - shared_experts_input=layer._get_shared_experts_input(x),
984 + shared_experts_input=shared_experts_input,
985 )
986
987
@@ -1524,6 +1525,7 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1525 x: torch.Tensor,
1526 topk_weights: torch.Tensor,
1527 topk_ids: torch.Tensor,
1528 + shared_experts_input: torch.Tensor | None,
1529 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
1530 assert not self.is_monolithic
1531
@@ -1551,7 +1553,7 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1553 global_num_experts=layer.global_num_experts,
1554 expert_map=layer.expert_map,
1555 apply_router_weight_on_input=layer.apply_router_weight_on_input,
1554 - shared_experts_input=layer._get_shared_experts_input(x),
1556 + shared_experts_input=shared_experts_input,
1557 )
1558
1559
vllm/model_executor/layers/quantization/moe_wna16.py
+1
@@ -367,6 +367,7 @@ class MoeWNA16Method(FusedMoEMethodBase):
367 x: torch.Tensor,
368 topk_weights: torch.Tensor,
369 topk_ids: torch.Tensor,
370 + shared_experts_input: torch.Tensor | None,
371 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
372 from vllm.model_executor.layers.fused_moe import fused_experts
373
vllm/model_executor/layers/quantization/mxfp4.py
+1
@@ -900,6 +900,7 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
900 x: torch.Tensor,
901 topk_weights: torch.Tensor,
902 topk_ids: torch.Tensor,
903 + shared_experts_input: torch.Tensor | None,
904 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
905 assert not self.is_monolithic
906 if layer.enable_eplb:
vllm/model_executor/layers/quantization/quark/quark_moe.py
+3
@@ -419,6 +419,7 @@ class QuarkW8A8Fp8MoEMethod(QuarkMoEMethod):
419 x: torch.Tensor,
420 topk_weights: torch.Tensor,
421 topk_ids: torch.Tensor,
422 + shared_experts_input: torch.Tensor | None,
423 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
424 if self.rocm_aiter_moe_enabled:
425 from vllm.model_executor.layers.fused_moe.rocm_aiter_fused_moe import (
@@ -607,6 +608,7 @@ class QuarkW4A8Fp8MoEMethod(QuarkMoEMethod):
608 x: torch.Tensor,
609 topk_weights: torch.Tensor,
610 topk_ids: torch.Tensor,
611 + shared_experts_input: torch.Tensor | None,
612 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
613 from vllm.model_executor.layers.fused_moe.rocm_aiter_fused_moe import (
614 rocm_aiter_fused_experts,
@@ -977,6 +979,7 @@ class QuarkOCP_MX_MoEMethod(QuarkMoEMethod):
979 x: torch.Tensor,
980 topk_weights: torch.Tensor,
981 topk_ids: torch.Tensor,
982 + shared_experts_input: torch.Tensor | None,
983 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
984 if not self.emulate:
985 if (
vllm/v1/worker/gpu_worker.py
+5 -1
@@ -816,10 +816,14 @@ class Worker(WorkerBase):
816 for module in moe_modules:
817 module.moe_config.num_experts = num_local_experts * new_ep_size
818 module.global_num_experts = module.moe_config.num_experts
819 + tp_size = get_tp_group().world_size
820 + is_sequence_parallel = parallel_config.use_sequence_parallel_moe
821 + sp_size = tp_size if is_sequence_parallel else 1
822 module.moe_parallel_config = FusedMoEParallelConfig.make(
820 - tp_size_=get_tp_group().world_size,
823 + tp_size_=tp_size,
824 pcp_size_=get_pcp_group().world_size,
825 dp_size_=get_dp_group().world_size,
826 + sp_size_=sp_size,
827 vllm_parallel_config=parallel_config,
828 )
829 module.moe_config.moe_parallel_config = module.moe_parallel_config