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 12, 23:30 UTC
[Feat][Perf] Enable deepep-low-latency with round-robin expert placement. (#28449)
Signed-off-by: bruceszchen <bruceszchen@tencent.com> Co-authored-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
Chen Bruce committed
Nov 19, 2025 at 20:46 UTC
da2f6800e0d6ac768c6f63b95f7c0755407f4263
8 files changed
+208
-37
vllm/model_executor/layers/fused_moe/all2all_utils.py
+11
@@ -67,6 +67,7 @@ def maybe_roundup_layer_hidden_size(
67
def maybe_make_prepare_finalize(
68
moe: FusedMoEConfig,
69
quant_config: FusedMoEQuantConfig | None,
70
+ routing_tables: tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None = None,
71
) -> FusedMoEPrepareAndFinalize | None:
72
if not moe.moe_parallel_config.use_all2all_kernels:
73
return None
@@ -134,6 +135,13 @@ def maybe_make_prepare_finalize(
135
136
elif moe.use_deepep_ll_kernels:
137
assert quant_config is not None
138
+ global_to_physical = physical_to_global = local_expert_global_ids = None
139
+ if routing_tables is not None:
140
+ (
141
+ global_to_physical,
142
+ physical_to_global,
143
+ local_expert_global_ids,
144
+ ) = routing_tables
145
all_to_all_args = dict(
146
max_num_tokens_per_dp_rank=moe.max_num_tokens,
147
token_hidden_size=moe.hidden_dim,
@@ -155,6 +163,9 @@ def maybe_make_prepare_finalize(
163
max_tokens_per_rank=moe.max_num_tokens,
164
num_dispatchers=all2all_manager.world_size,
165
use_fp8_dispatch=use_fp8_dispatch,
166
+ global_to_physical=global_to_physical,
167
+ physical_to_global=physical_to_global,
168
+ local_expert_global_ids=local_expert_global_ids,
169
)
170
171
return prepare_finalize
vllm/model_executor/layers/fused_moe/deepep_ll_prepare_finalize.py
+28
-2
@@ -85,6 +85,9 @@ class DeepEPLLPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
85
max_tokens_per_rank: int,
86
num_dispatchers: int,
87
use_fp8_dispatch: bool = False,
88
+ global_to_physical: torch.Tensor | None = None,
89
+ physical_to_global: torch.Tensor | None = None,
90
+ local_expert_global_ids: torch.Tensor | None = None,
91
):
92
super().__init__()
93
@@ -97,6 +100,17 @@ class DeepEPLLPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
100
self.handles: list[tuple | None] = [None, None]
101
self.num_dispatchers_ = num_dispatchers
102
103
+ topk_indices_dtype = self.topk_indices_dtype()
104
+
105
+ def _maybe_cast(tensor: torch.Tensor | None) -> torch.Tensor | None:
106
+ if tensor is None or topk_indices_dtype is None:
107
+ return tensor
108
+ return tensor.to(dtype=topk_indices_dtype)
109
+
110
+ self.global_to_physical = _maybe_cast(global_to_physical)
111
+ self.physical_to_global = _maybe_cast(physical_to_global)
112
+ self.local_expert_global_ids = _maybe_cast(local_expert_global_ids)
113
+
114
# We don't have enough information to determine if we should dispatch
115
# activation scales in a packed ue8m0 format during object construction
116
# time. This setting is handled by post_init_setup.
@@ -136,6 +150,16 @@ class DeepEPLLPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
150
def topk_indices_dtype(self) -> torch.dtype | None:
151
return torch.int64
152
153
+ def _map_global_to_physical_ids(self, topk_ids: torch.Tensor) -> torch.Tensor:
154
+ if self.global_to_physical is None:
155
+ return topk_ids
156
+ return self.global_to_physical[topk_ids]
157
+
158
+ def _map_local_to_global_ids(self, expert_topk_ids: torch.Tensor) -> torch.Tensor:
159
+ if self.local_expert_global_ids is None:
160
+ return expert_topk_ids
161
+ return self.local_expert_global_ids[expert_topk_ids]
162
+
163
def _do_quant(
164
self,
165
x: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
@@ -226,9 +250,10 @@ class DeepEPLLPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
250
a1 = a1 * topk_weights.to(a1.dtype)
251
252
# Dispatch
253
+ dispatch_topk_ids = self._map_global_to_physical_ids(topk_ids)
254
expert_x, expert_num_tokens, handle, _, hook = self.buffer.low_latency_dispatch(
255
a1,
231
- topk_ids,
256
+ dispatch_topk_ids,
257
self.max_tokens_per_rank,
258
num_experts,
259
use_fp8=self.use_fp8_dispatch,
@@ -313,11 +338,12 @@ class DeepEPLLPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
338
# weights have already been applied.
339
combine_topk_weights = torch.ones_like(topk_weights)
340
341
+ combine_topk_ids = self._map_global_to_physical_ids(topk_ids)
342
# TODO (varun) : Enable zero copy mode
343
dbo_maybe_run_recv_hook()
344
_, _, recv_hook = self.buffer.low_latency_combine(
345
fused_expert_output,
320
- topk_ids,
346
+ combine_topk_ids,
347
combine_topk_weights,
348
handle,
349
async_finish=False,
vllm/model_executor/layers/fused_moe/fused_moe_method_base.py
+7
-2
@@ -50,10 +50,15 @@ class FusedMoEMethodBase(QuantizeMethodBase):
50
"""
51
return False
52
53
- def maybe_make_prepare_finalize(self) -> FusedMoEPrepareAndFinalize | None:
53
+ def maybe_make_prepare_finalize(
54
+ self,
55
+ routing_tables: tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None = None,
56
+ ) -> FusedMoEPrepareAndFinalize | None:
57
from .all2all_utils import maybe_make_prepare_finalize
58
56
- return maybe_make_prepare_finalize(self.moe, self.moe_quant_config)
59
+ return maybe_make_prepare_finalize(
60
+ self.moe, self.moe_quant_config, routing_tables
61
+ )
62
63
def select_gemm_impl(
64
self,
vllm/model_executor/layers/fused_moe/layer.py
+135
-22
@@ -5,7 +5,7 @@ from collections.abc import Callable, Iterable
5
from contextlib import nullcontext
6
from enum import Enum
7
from functools import partial
8
-from typing import Literal, get_args, overload
8
+from typing import Literal, cast, get_args, overload
9
10
import torch
11
import torch.nn.functional as F
@@ -192,6 +192,42 @@ def determine_expert_map(
192
return (local_num_experts, expert_map, expert_mask)
193
194
195
+def determine_expert_placement_strategy(
196
+ expert_placement_strategy: ExpertPlacementStrategy,
197
+ moe_parallel_config: FusedMoEParallelConfig,
198
+ num_expert_group: int | None,
199
+ num_redundant_experts: int,
200
+ enable_eplb: bool,
201
+) -> ExpertPlacementStrategy:
202
+ if expert_placement_strategy == "round_robin":
203
+ round_robin_supported = (
204
+ (num_expert_group is not None and num_expert_group > 1)
205
+ and num_redundant_experts == 0
206
+ and not enable_eplb
207
+ )
208
+
209
+ if not round_robin_supported:
210
+ logger.warning(
211
+ "Round-robin expert placement is only supported for "
212
+ "models with multiple expert groups and no redundant "
213
+ "experts. Falling back to linear expert placement."
214
+ )
215
+ return "linear"
216
+ if (
217
+ moe_parallel_config.use_all2all_kernels
218
+ and not moe_parallel_config.use_deepep_ll_kernels
219
+ ):
220
+ logger.warning(
221
+ "Round-robin expert placement currently only supports "
222
+ "the DeepEP low-latency backend, but '%s' was configured. "
223
+ "Falling back to linear expert placement.",
224
+ moe_parallel_config.all2all_backend,
225
+ )
226
+ return "linear"
227
+
228
+ return expert_placement_strategy
229
+
230
+
231
def get_compressed_expert_map(expert_map: torch.Tensor) -> str:
232
"""
233
Compresses the expert map by removing any -1 entries.
@@ -400,6 +436,9 @@ class FusedMoE(CustomOp):
436
self.expert_load_view: torch.Tensor | None = None
437
self.logical_to_physical_map: torch.Tensor | None = None
438
self.logical_replica_count: torch.Tensor | None = None
439
+ self.expert_placement_strategy: ExpertPlacementStrategy = (
440
+ vllm_config.parallel_config.expert_placement_strategy
441
+ )
442
443
# ROCm aiter shared experts fusion
444
self.rocm_aiter_fmoe_enabled = rocm_aiter_ops.is_fused_moe_enabled()
@@ -433,38 +472,27 @@ class FusedMoE(CustomOp):
472
"Redundant experts are only supported with EPLB."
473
)
474
436
- expert_placement_strategy = (
437
- vllm_config.parallel_config.expert_placement_strategy
475
+ self.expert_placement_strategy = determine_expert_placement_strategy(
476
+ expert_placement_strategy=self.expert_placement_strategy,
477
+ moe_parallel_config=self.moe_parallel_config,
478
+ num_expert_group=num_expert_group,
479
+ num_redundant_experts=num_redundant_experts,
480
+ enable_eplb=self.enable_eplb,
481
)
439
- if expert_placement_strategy == "round_robin":
440
- # TODO(Bruce): will support round robin expert placement with
441
- # EPLB enabled in the future.
442
- round_robin_supported = (
443
- (num_expert_group is not None and num_expert_group > 1)
444
- and num_redundant_experts == 0
445
- and not self.enable_eplb
446
- )
447
-
448
- if not round_robin_supported:
449
- logger.warning(
450
- "Round-robin expert placement is only supported for "
451
- "models with multiple expert groups and no redundant "
452
- "experts. Falling back to linear expert placement."
453
- )
454
- expert_placement_strategy = "linear"
482
483
self.expert_map: torch.Tensor | None
484
local_num_experts, expert_map, expert_mask = determine_expert_map(
485
ep_size=self.ep_size,
486
ep_rank=self.ep_rank,
487
global_num_experts=self.global_num_experts,
461
- expert_placement_strategy=expert_placement_strategy,
488
+ expert_placement_strategy=self.expert_placement_strategy,
489
num_fused_shared_experts=self.num_fused_shared_experts,
490
return_expert_mask=self.rocm_aiter_fmoe_enabled,
491
)
492
self.local_num_experts = local_num_experts
493
self.register_buffer("expert_map", expert_map)
494
self.register_buffer("expert_mask", expert_mask)
495
+ self._maybe_init_expert_routing_tables()
496
logger.info_once(
497
"[EP Rank %s/%s] Expert parallelism is enabled. Expert "
498
"placement strategy: %s. Local/global"
@@ -472,7 +500,7 @@ class FusedMoE(CustomOp):
500
" %s.",
501
self.ep_rank,
502
self.ep_size,
475
- expert_placement_strategy,
503
+ self.expert_placement_strategy,
504
self.local_num_experts,
505
self.global_num_experts,
506
get_compressed_expert_map(self.expert_map),
@@ -621,7 +649,12 @@ class FusedMoE(CustomOp):
649
# should be safe to swap out the quant_method.
650
def maybe_init_modular_kernel(self) -> None:
651
self.ensure_moe_quant_config_init()
624
- prepare_finalize = self.quant_method.maybe_make_prepare_finalize()
652
+ # routing_tables only needed for round-robin expert placement with
653
+ # DeepEP all2all backend.
654
+ routing_tables = self._maybe_init_expert_routing_tables()
655
+ prepare_finalize = self.quant_method.maybe_make_prepare_finalize(
656
+ routing_tables=routing_tables
657
+ )
658
if prepare_finalize is not None:
659
logger.debug(
660
"%s for %s(%s)", prepare_finalize.__class__.__name__, self, id(self)
@@ -703,6 +736,84 @@ class FusedMoE(CustomOp):
736
# By default, router/gate is called before FusedMoE forward pass
737
return False
738
739
+ def _maybe_init_expert_routing_tables(
740
+ self,
741
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None:
742
+ # Currently routing_tables only needed for round-robin expert placement
743
+ # with DeepEP-ll all2all backend.
744
+ if (
745
+ self.expert_placement_strategy != "round_robin"
746
+ or not self.use_deepep_ll_kernels
747
+ ):
748
+ return None
749
+
750
+ if hasattr(self, "expert_global_to_physical"):
751
+ return cast(
752
+ tuple[torch.Tensor, torch.Tensor, torch.Tensor],
753
+ (
754
+ self.expert_global_to_physical,
755
+ self.expert_physical_to_global,
756
+ self.expert_local_to_global,
757
+ ),
758
+ )
759
+
760
+ if self.expert_map is None:
761
+ return None
762
+
763
+ routing_tables = self.ensure_round_robin_expert_routing_tables(
764
+ global_num_experts=self.global_num_experts,
765
+ ep_size=self.ep_size,
766
+ ep_rank=self.ep_rank,
767
+ local_num_experts=self.local_num_experts,
768
+ device=self.expert_map.device,
769
+ )
770
+
771
+ global_to_physical, physical_to_global, local_global = routing_tables
772
+ self.register_buffer("expert_global_to_physical", global_to_physical)
773
+ self.register_buffer("expert_physical_to_global", physical_to_global)
774
+ self.register_buffer("expert_local_to_global", local_global)
775
+
776
+ return routing_tables
777
+
778
+ @staticmethod
779
+ def ensure_round_robin_expert_routing_tables(
780
+ global_num_experts: int,
781
+ ep_size: int,
782
+ ep_rank: int,
783
+ local_num_experts: int,
784
+ device: torch.device | None = None,
785
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
786
+ device_kwargs = {"device": device} if device is not None else {}
787
+ global_indices = torch.arange(
788
+ global_num_experts, dtype=torch.long, **device_kwargs
789
+ )
790
+ owner = torch.remainder(global_indices, ep_size)
791
+ local_index = torch.div(global_indices, ep_size, rounding_mode="floor")
792
+ base = global_num_experts // ep_size
793
+ remainder = global_num_experts % ep_size
794
+ physical_offset = owner * base
795
+ if remainder > 0:
796
+ remainder_tensor = torch.tensor(
797
+ remainder, dtype=torch.long, **device_kwargs
798
+ )
799
+ physical_offset = physical_offset + torch.minimum(owner, remainder_tensor)
800
+
801
+ global_to_physical = physical_offset + local_index
802
+ physical_to_global = torch.empty_like(global_to_physical)
803
+ physical_to_global[global_to_physical] = global_indices
804
+
805
+ local_global = torch.arange(
806
+ ep_rank,
807
+ global_num_experts,
808
+ ep_size,
809
+ dtype=torch.long,
810
+ **device_kwargs,
811
+ )
812
+ if local_global.numel() != local_num_experts:
813
+ local_global = local_global[:local_num_experts]
814
+
815
+ return (global_to_physical, physical_to_global, local_global)
816
+
817
def update_expert_map(self):
818
# ep_size and ep_rank should already be updated
819
assert self.expert_map is not None
@@ -711,12 +822,14 @@ class FusedMoE(CustomOp):
822
ep_size=self.ep_size,
823
ep_rank=self.ep_rank,
824
global_num_experts=self.global_num_experts,
825
+ expert_placement_strategy=self.expert_placement_strategy,
826
num_fused_shared_experts=self.num_fused_shared_experts,
827
return_expert_mask=self.rocm_aiter_fmoe_enabled,
828
)
829
self.local_num_experts = local_num_experts
830
self.register_buffer("expert_map", expert_map)
831
self.register_buffer("expert_mask", expert_mask)
832
+ self._maybe_init_expert_routing_tables()
833
if self.aiter_fmoe_shared_expert_enabled:
834
self._init_aiter_shared_experts_topK_buffer(
835
vllm_config=get_current_vllm_config(),
vllm/model_executor/layers/fused_moe/unquantized_fused_moe_method.py
+5
-2
@@ -108,11 +108,14 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
108
def allow_inplace(self) -> bool:
109
return True
110
111
- def maybe_make_prepare_finalize(self) -> FusedMoEPrepareAndFinalize | None:
111
+ def maybe_make_prepare_finalize(
112
+ self,
113
+ routing_tables: tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None = None,
114
+ ) -> FusedMoEPrepareAndFinalize | None:
115
if self.rocm_aiter_moe_enabled:
116
return None
117
else:
115
- return super().maybe_make_prepare_finalize()
118
+ return super().maybe_make_prepare_finalize(routing_tables)
119
120
def select_gemm_impl(
121
self,
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe.py
+10
-4
@@ -380,11 +380,14 @@ class CompressedTensorsW4A4MoeMethod(CompressedTensorsMoEMethod):
380
(layer.w2_input_global_scale), requires_grad=False
381
)
382
383
- def maybe_make_prepare_finalize(self) -> mk.FusedMoEPrepareAndFinalize | None:
383
+ def maybe_make_prepare_finalize(
384
+ self,
385
+ routing_tables: tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None = None,
386
+ ) -> mk.FusedMoEPrepareAndFinalize | None:
387
if self.use_marlin:
388
return None
389
elif not self.allow_flashinfer:
387
- return super().maybe_make_prepare_finalize()
390
+ return super().maybe_make_prepare_finalize(routing_tables)
391
392
prepare_finalize = build_flashinfer_fp4_cutlass_moe_prepare_finalize(self.moe)
393
logger.debug_once("%s", prepare_finalize.__class__.__name__)
@@ -890,11 +893,14 @@ class CompressedTensorsW8A8Fp8MoEMethod(CompressedTensorsMoEMethod):
893
layer.w2_weight_scale
894
)
895
893
- def maybe_make_prepare_finalize(self) -> mk.FusedMoEPrepareAndFinalize | None:
896
+ def maybe_make_prepare_finalize(
897
+ self,
898
+ routing_tables: tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None = None,
899
+ ) -> mk.FusedMoEPrepareAndFinalize | None:
900
if self.use_marlin or self.rocm_aiter_moe_enabled:
901
return None
902
else:
897
- return super().maybe_make_prepare_finalize()
903
+ return super().maybe_make_prepare_finalize(routing_tables)
904
905
def select_gemm_impl(
906
self,
vllm/model_executor/layers/quantization/fp8.py
+5
-2
@@ -1018,7 +1018,10 @@ class Fp8MoEMethod(FusedMoEMethodBase):
1018
del layer.w13_input_scale
1019
del layer.w2_input_scale
1020
1021
- def maybe_make_prepare_finalize(self) -> mk.FusedMoEPrepareAndFinalize | None:
1021
+ def maybe_make_prepare_finalize(
1022
+ self,
1023
+ routing_tables: tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None = None,
1024
+ ) -> mk.FusedMoEPrepareAndFinalize | None:
1025
if (
1026
self.rocm_aiter_moe_enabled
1027
or self.use_marlin
@@ -1039,7 +1042,7 @@ class Fp8MoEMethod(FusedMoEMethodBase):
1042
logger.debug_once("%s", prepare_finalize.__class__.__name__)
1043
return prepare_finalize
1044
else:
1042
- return super().maybe_make_prepare_finalize()
1045
+ return super().maybe_make_prepare_finalize(routing_tables)
1046
1047
def select_gemm_impl(
1048
self,
vllm/model_executor/layers/quantization/modelopt.py
+7
-3
@@ -373,6 +373,7 @@ class ModelOptFp8MoEMethod(FusedMoEMethodBase):
373
374
def maybe_make_prepare_finalize(
375
self,
376
+ routing_tables: tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None = None,
377
) -> mk.FusedMoEPrepareAndFinalize | None:
378
# TRT LLM not supported with all2all yet.
379
if self.flashinfer_moe_backend == FlashinferMoeBackend.TENSORRT_LLM:
@@ -384,7 +385,7 @@ class ModelOptFp8MoEMethod(FusedMoEMethodBase):
385
logger.debug_once("%s", prepare_finalize.__class__.__name__)
386
return prepare_finalize
387
else:
387
- return super().maybe_make_prepare_finalize()
388
+ return super().maybe_make_prepare_finalize(routing_tables)
389
390
def select_gemm_impl(
391
self,
@@ -1179,7 +1180,10 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1180
" for ModelOptNvFp4FusedMoE."
1181
)
1182
1182
- def maybe_make_prepare_finalize(self) -> mk.FusedMoEPrepareAndFinalize | None:
1183
+ def maybe_make_prepare_finalize(
1184
+ self,
1185
+ routing_tables: tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None = None,
1186
+ ) -> mk.FusedMoEPrepareAndFinalize | None:
1187
if self.use_marlin or (
1188
self.allow_flashinfer
1189
and self.flashinfer_moe_backend == FlashinferMoeBackend.TENSORRT_LLM
@@ -1196,7 +1200,7 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1200
logger.debug_once("%s", prepare_finalize.__class__.__name__)
1201
return prepare_finalize
1202
else:
1199
- return super().maybe_make_prepare_finalize()
1203
+ return super().maybe_make_prepare_finalize(routing_tables)
1204
1205
def select_gemm_impl(
1206
self,