main
py 1,038 lines 39 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from collections.abc import Callable, Iterable
4 from contextlib import nullcontext
5 from typing import TYPE_CHECKING, cast
6
7 import torch
8 import torch.nn.functional as F
9
10 from vllm.config import VllmConfig, get_current_vllm_config
11 from vllm.config.parallel import ExpertPlacementStrategy
12 from vllm.distributed import (
13 get_ep_group,
14 get_pcp_group,
15 tensor_model_parallel_all_reduce,
16 )
17 from vllm.distributed.eplb.eplb_state import EplbLayerState
18 from vllm.forward_context import (
19 ForwardContext,
20 get_forward_context,
21 is_forward_context_available,
22 )
23 from vllm.logger import init_logger
24 from vllm.model_executor.layers.fused_moe.activation import MoEActivation
25 from vllm.model_executor.layers.fused_moe.config import (
26 FusedMoEConfig,
27 )
28 from vllm.model_executor.layers.fused_moe.fused_moe_method_base import (
29 FusedMoEMethodBase,
30 )
31 from vllm.model_executor.layers.fused_moe.moe_output import UnfinalizedMoEOutput
32 from vllm.model_executor.layers.fused_moe.routed_experts import (
33 RoutedExperts,
34 )
35 from vllm.model_executor.layers.fused_moe.router.fused_moe_router import (
36 FusedMoERouter,
37 )
38 from vllm.model_executor.layers.fused_moe.router.zero_expert_router import (
39 ZeroExpertRouter,
40 )
41 from vllm.model_executor.layers.fused_moe.runner.moe_runner_interface import (
42 MoERunnerInterface,
43 )
44 from vllm.model_executor.layers.fused_moe.runner.shared_experts import (
45 SharedExperts,
46 SharedExpertsOrder,
47 )
48 from vllm.model_executor.layers.utils import dispatch_unquantized_gemm
49 from vllm.platforms import current_platform
50 from vllm.utils.torch_utils import (
51 _USE_LAYERNAME,
52 LayerName,
53 direct_register_custom_op,
54 )
55
56 logger = init_logger(__name__)
57
58
59 def register_layer_for_moe_forward_op(
60 vllm_config: VllmConfig,
61 layer: "MoERunner",
62 ):
63 # For smuggling this layer into the fused moe custom op
64 prefix = layer.layer_name
65 compilation_config = vllm_config.compilation_config
66 if prefix in compilation_config.static_forward_context:
67 raise ValueError("Duplicate layer name: {}".format(prefix))
68 compilation_config.static_forward_context[prefix] = layer
69 compilation_config.static_all_moe_layers.append(prefix)
70
71
72 def get_layer_from_name(layer_name: str) -> MoERunnerInterface:
73 forward_context: ForwardContext = get_forward_context()
74 if not _USE_LAYERNAME and layer_name == "from_forward_context":
75 all_moe_layers = forward_context.all_moe_layers
76 assert all_moe_layers is not None
77 moe_layer_index = forward_context.moe_layer_index
78 if moe_layer_index >= len(all_moe_layers):
79 raise AssertionError(
80 "We expected the number of MOE layers in `all_moe_layers` "
81 "to be equal to the number of "
82 "{vllm.moe_forward, vllm.moe_forward_shared} calls."
83 )
84 layer_name = all_moe_layers[moe_layer_index]
85 forward_context.moe_layer_index += 1
86 layer = forward_context.no_compile_layers[layer_name]
87 assert isinstance(layer, MoERunnerInterface)
88 return layer
89
90
91 # On torch >= 2.11, layer_name is a hoisted LayerName opaque object;
92 # on older versions it remains a plain str.
93 if TYPE_CHECKING:
94 from typing import TypeAlias
95
96 _layer_name_type: TypeAlias = str | LayerName
97 else:
98 _layer_name_type = LayerName if _USE_LAYERNAME else str
99
100
101 @torch.compiler.assume_constant_result
102 def _resolve_layer_name(layer_name: str | LayerName) -> str:
103 from torch._library.fake_class_registry import FakeScriptObject
104
105 if isinstance(layer_name, LayerName):
106 return layer_name.value
107 elif isinstance(layer_name, FakeScriptObject):
108 return layer_name.real_obj.value
109 return layer_name
110
111
112 # Note: _moe_forward and _moe_forward_shared should not contain any
113 # implementation details, They should merely pass along control to
114 # the runner's '_forward_impl' method.
115 # These functions should never be called directly since they do not
116 # include all the functionality of the MoE layer.
117 def _moe_forward(
118 hidden_states: torch.Tensor,
119 router_logits: torch.Tensor,
120 shared_experts_input: torch.Tensor | None,
121 input_ids: torch.Tensor | None,
122 layer_name: _layer_name_type,
123 hidden_dim_unpadded: int,
124 ) -> torch.Tensor:
125 layer = get_layer_from_name(_resolve_layer_name(layer_name))
126 return cast(
127 torch.Tensor,
128 layer._forward_impl(
129 hidden_states,
130 router_logits,
131 shared_experts_input,
132 input_ids,
133 ),
134 )
135
136
137 def _moe_forward_fake(
138 hidden_states: torch.Tensor,
139 router_logits: torch.Tensor,
140 shared_experts_input: torch.Tensor | None,
141 input_ids: torch.Tensor | None,
142 layer_name: _layer_name_type,
143 hidden_dim_unpadded: int,
144 ) -> torch.Tensor:
145 # `hidden_dim_unpadded > 0` only on the TRT-LLM MXFP4 path, where the
146 # real kernel writes narrower than `hidden_states.shape[-1]`. Plumbed
147 # as an op arg (not peeked from the layer registry) to keep the fake
148 # a pure shape function of its inputs and preserve subgraph dedup.
149 if hidden_dim_unpadded > 0:
150 return hidden_states.new_empty((*hidden_states.shape[:-1], hidden_dim_unpadded))
151 return torch.empty_like(hidden_states)
152
153
154 def _moe_forward_shared(
155 hidden_states: torch.Tensor,
156 router_logits: torch.Tensor,
157 shared_experts_input: torch.Tensor | None,
158 input_ids: torch.Tensor | None,
159 layer_name: _layer_name_type,
160 hidden_dim_unpadded: int,
161 ) -> tuple[torch.Tensor, torch.Tensor]:
162 layer = get_layer_from_name(_resolve_layer_name(layer_name))
163 return cast(
164 tuple[torch.Tensor, torch.Tensor],
165 layer._forward_impl(
166 hidden_states,
167 router_logits,
168 shared_experts_input,
169 input_ids,
170 ),
171 )
172
173
174 def _moe_forward_shared_fake(
175 hidden_states: torch.Tensor,
176 router_logits: torch.Tensor,
177 shared_experts_input: torch.Tensor | None,
178 input_ids: torch.Tensor | None,
179 layer_name: _layer_name_type,
180 hidden_dim_unpadded: int,
181 ) -> tuple[torch.Tensor, torch.Tensor]:
182 # `fused_out`: see `_moe_forward_fake` for hidden_dim_unpadded semantics.
183 # `shared_out`: matches `shared_experts_input` if provided (latent MoE),
184 # else `hidden_states`.
185 if hidden_dim_unpadded > 0:
186 fused_out = hidden_states.new_empty(
187 (*hidden_states.shape[:-1], hidden_dim_unpadded)
188 )
189 else:
190 fused_out = torch.empty_like(hidden_states)
191 if shared_experts_input is not None:
192 shared_out = torch.empty_like(shared_experts_input)
193 else:
194 shared_out = torch.empty_like(hidden_states)
195 return shared_out, fused_out
196
197
198 # NOTE: `moe_forward` and `moe_forward_shared` being opaque custom ops is a
199 # load-bearing assumption for the MoE-LoRA dual-stream path.
200 direct_register_custom_op(
201 op_name="moe_forward",
202 op_func=_moe_forward,
203 mutates_args=["hidden_states"],
204 fake_impl=_moe_forward_fake,
205 tags=(torch.Tag.needs_fixed_stride_order,),
206 )
207
208
209 direct_register_custom_op(
210 op_name="moe_forward_shared",
211 op_func=_moe_forward_shared,
212 fake_impl=_moe_forward_shared_fake,
213 tags=(torch.Tag.needs_fixed_stride_order,),
214 )
215
216
217 def _unpack(
218 result: torch.Tensor
219 | UnfinalizedMoEOutput
220 | tuple[torch.Tensor, torch.Tensor | UnfinalizedMoEOutput],
221 ) -> tuple[torch.Tensor | None, torch.Tensor | UnfinalizedMoEOutput]:
222 if isinstance(result, tuple):
223 return result
224 else:
225 return (None, result)
226
227
228 class MoERunner(MoERunnerInterface):
229 """Standard MoE runner implementation for executing Mixture of Experts layers.
230
231 This is the primary concrete implementation of MoE execution logic, providing
232 comprehensive support for standard MoE operations. It handles:
233 - Expert routing and token dispatching using various routing strategies
234 - Shared experts computation with optional parallel execution using CUDA streams
235 - Tensor model parallel and expert parallel operations
236 - Multiple quantization methods and optimized kernel selection
237 - Both monolithic and decomposed expert execution paths
238 - Integration with various parallel execution modes (TP, EP, DP)
239
240 The runner orchestrates the complete MoE forward pass including routing tokens
241 to experts, executing expert computations in parallel, and combining results.
242 It supports advanced features like overlapped execution of shared experts,
243 optimized kernels for different parallel configurations, and seamless
244 integration with vLLM's distributed execution framework.
245
246 Eventually, this class may be split into more specialized implementations
247 for different configurations (e.g., with/without shared experts, gates, etc.).
248 """
249
250 def __init__(
251 self,
252 layer_name: str,
253 moe_config: FusedMoEConfig,
254 router: FusedMoERouter,
255 routed_experts: RoutedExperts,
256 enable_dbo: bool = False,
257 gate: torch.nn.Module | None = None,
258 shared_experts: torch.nn.Module | None = None,
259 shared_expert_gate: torch.nn.Module | None = None,
260 routed_input_transform: torch.nn.Module | None = None,
261 routed_output_transform: torch.nn.Module | None = None,
262 routed_scaling_factor: float = 1.0,
263 ):
264 super().__init__()
265 self.moe_config = moe_config
266 self.router = router
267 self.routed_input_transform = routed_input_transform
268 self.routed_output_transform = routed_output_transform
269 self.routed_scaling_factor = routed_scaling_factor
270 self.gate = gate
271 self.shared_expert_gate = shared_expert_gate
272 self.routed_experts = routed_experts
273 self.enable_dbo = enable_dbo
274
275 # When both gates are present and FSE is enabled, fuse their
276 # weight matrices into [num_experts + num_shared, hidden] so one
277 # GEMM produces combined logits. The topk kernel can then
278 # apply routing softmax and shared expert activation (sigmoid)
279 # in a single launch.
280 self._fse_fuse_gate = gate is not None and shared_expert_gate is not None
281 self._combined_gate_weight: torch.Tensor | None = None
282
283 self._shared_experts: SharedExperts | None = None
284 if shared_experts is not None:
285 can_overlap = lambda: self._quant_method.mk_can_overlap_shared_experts
286 self._shared_experts = SharedExperts(
287 shared_experts,
288 moe_config=moe_config,
289 enable_dbo=enable_dbo,
290 mk_can_overlap_shared_experts=can_overlap,
291 )
292
293 # Needed for string -> MoERunner layer lookup in custom ops.
294 self.layer_name = layer_name
295
296 self._forward_entry = self._select_forward()
297
298 # For smuggling this layer into the fused moe custom op
299 register_layer_for_moe_forward_op(get_current_vllm_config(), self)
300
301 def load_weights(
302 self, weights: Iterable[tuple[str, torch.Tensor]]
303 ) -> Iterable[str]:
304 return self.routed_experts.load_weights(weights)
305
306 def _select_forward(self) -> Callable:
307 if current_platform.is_tpu():
308 # TODO: Once the OOM issue for the TPU backend is resolved, we
309 # will switch to using the moe_forward custom op.
310 return _moe_forward if self._shared_experts is None else _moe_forward_shared
311
312 if current_platform.is_cpu():
313 # CPU never touches the workspace manager (Monolithic experts
314 # skip it entirely; Modular experts' _allocate_buffers bypasses
315 # it too, see modular_kernel.py) -- the ContextVar-based lane
316 # lookup was the only part of this call graph Dynamo can't
317 # trace, so CPU can always call the fused-MoE op directly.
318 return _moe_forward if self._shared_experts is None else _moe_forward_shared
319
320 return (
321 torch.ops.vllm.moe_forward
322 if self._shared_experts is None
323 else torch.ops.vllm.moe_forward_shared
324 )
325
326 @property
327 def shared_experts(self) -> SharedExperts | None:
328 return self._shared_experts
329
330 # TODO(bnell): Temporary hack. Get rid of this.
331 def _replace_quant_method(self, quant_method: FusedMoEMethodBase):
332 self.routed_experts._replace_quant_method(quant_method)
333
334 # TODO(bnell): Hack for elastic_ep. Get rid of this
335 def _set_moe_config(self, new_moe_config: FusedMoEConfig):
336 self.moe_config = new_moe_config
337 self.routed_experts._set_moe_config(new_moe_config)
338 if self._shared_experts is not None:
339 self._shared_experts._set_moe_config(new_moe_config)
340
341 def _maybe_fuse_gate_weights(self):
342 """Fuse router and shared expert gate weights on first call.
343
344 Cannot be done at __init__ because gate weights are loaded after
345 module construction (via weight_loader). Called once from
346 _forward_impl before the first forward pass.
347 """
348 if self._combined_gate_weight is None:
349 assert self.gate is not None and self.shared_expert_gate is not None
350 self._combined_gate_weight = torch.cat(
351 [self.gate.weight, self.shared_expert_gate.weight],
352 dim=0,
353 )
354
355 @property
356 def _quant_method(self) -> FusedMoEMethodBase:
357 return self.routed_experts.quant_method
358
359 def apply_routed_input_transform(
360 self,
361 hidden_states: torch.Tensor,
362 ) -> tuple[torch.Tensor, torch.Tensor | None]:
363 """Apply transform for routed experts (e.g., latent projection).
364
365 This is called by MoERunner.forward_native. The original hidden_states
366 is saved separately so shared experts get [S, hidden_size] while
367 routed experts get the transformed [S, moe_latent_size].
368
369 Returns (possibly transformed) hidden states and the input for shared
370 experts (or None if there are no shared experts).
371 """
372 if self.routed_input_transform is not None:
373 result = self.routed_input_transform(hidden_states)
374 # ReplicatedLinear returns (output, extra_bias) tuple.
375 # We only need the output tensor; extra_bias is not used here.
376 if isinstance(result, tuple):
377 return result[0], hidden_states
378 return result, hidden_states
379
380 return (
381 hidden_states,
382 hidden_states if self._shared_experts is not None else None,
383 )
384
385 def apply_routed_output_transform(
386 self,
387 fused_output: torch.Tensor,
388 ) -> torch.Tensor:
389 """Apply transform to routed expert output (e.g., latent to full dim).
390
391 Used by latent MoE models (e.g., NemotronH) where routed experts
392 operate in a compressed latent space and need projection back to
393 the full hidden dimension before combining with shared expert output.
394 """
395 if self.routed_output_transform is not None:
396 r = self.routed_output_transform(fused_output)
397 fused_output = r[0] if isinstance(r, tuple) else r
398 return fused_output
399
400 def _maybe_apply_routed_scale_to_output(
401 self,
402 shared_output: torch.Tensor | None,
403 fused_output: torch.Tensor,
404 ) -> tuple[torch.Tensor | None, torch.Tensor]:
405 """Apply routed_scaling_factor to the output with FP16 overflow
406 protection.
407
408 Scale the fused expert output by routed_scaling_factor. For FP16,
409 avoid overflow by dividing shared_output by the scale instead
410 (the decoder layer compensates with matching divisions).
411 """
412 if self.routed_scaling_factor != 1.0:
413 if fused_output.dtype != torch.float16 or shared_output is None:
414 fused_output *= self.routed_scaling_factor
415 elif shared_output is not None:
416 shared_output *= 1.0 / self.routed_scaling_factor
417 return shared_output, fused_output
418
419 @property
420 def _fused_output_is_reduced(self) -> bool:
421 return (
422 self._quant_method.moe_kernel is not None
423 and self._quant_method.moe_kernel.output_is_reduced()
424 )
425
426 def _maybe_reduce_shared_expert_output(
427 self,
428 shared_output: torch.Tensor | None,
429 fused_output_is_reduced: bool | None = None,
430 ) -> torch.Tensor | None:
431 """All-reduce shared expert output when the combine kernel already
432 reduced fused output.
433
434 * If the combine kernel does the reduction for fused_output, reduce
435 shared_output separately. O.w, reduce fused_output+shared_output later.
436 * If we have SP (TP=N, DP=M, EP), there is a separate AG step handled
437 in the model.
438 """
439 if fused_output_is_reduced is None:
440 fused_output_is_reduced = self._fused_output_is_reduced
441
442 if (
443 shared_output is not None
444 and not self.moe_config.is_sequence_parallel
445 and fused_output_is_reduced
446 ):
447 shared_output = tensor_model_parallel_all_reduce(shared_output)
448 return shared_output
449
450 def _maybe_reduce_routed_output_before_transform(
451 self,
452 fused_output: torch.Tensor,
453 fused_output_is_reduced: bool,
454 ) -> tuple[torch.Tensor, bool]:
455 """All-reduce latent routed output before its output transform.
456
457 Latent MoE output transforms may contain non-linear ops, e.g. RMSNorm.
458 TP partial routed outputs must be summed in latent space before such
459 transforms are applied.
460
461 A transform that commutes with the TP sum is exempt: if
462 ``sum_r T(x_r) == T(sum_r x_r)``, applying the transform to the local
463 partial output and letting the existing late all-reduce sum the
464 combined result is equivalent, and costs one collective instead of two.
465 Such a transform opts out by setting ``reduce_commutative = True``.
466 The default is False, so transforms that do not declare themselves
467 keep being reduced early.
468 """
469 if (
470 self.routed_output_transform is not None
471 and not getattr(self.routed_output_transform, "reduce_commutative", False)
472 and not self.moe_config.is_sequence_parallel
473 and (self.moe_config.tp_size > 1 or self.moe_config.ep_size > 1)
474 and not fused_output_is_reduced
475 ):
476 fused_output = tensor_model_parallel_all_reduce(fused_output)
477 fused_output_is_reduced = True
478 return fused_output, fused_output_is_reduced
479
480 def _maybe_reduce_final_output(
481 self,
482 states: torch.Tensor,
483 trunc_size: int | None,
484 output_is_reduced: bool | None = None,
485 ) -> torch.Tensor:
486 """All-reduce the combined output if needed.
487
488 This is the "late" all-reduce path. When neither fused nor shared
489 output was individually reduced, the combined sum is all-reduced
490 here. Skipped when sequence-parallel is active (SP handles its
491 own reduction) or when the early path already reduced both outputs.
492 """
493 # skip_final_all_reduce must not coexist with a pre-reduced fused
494 # output. This should be enforced by MoE config initialization.
495 if self.moe_config.skip_final_all_reduce:
496 assert not self._fused_output_is_reduced, (
497 "skip_final_all_reduce requires an un-reduced fused output"
498 )
499
500 # We don't need to reduce the final output if:
501 # - We are not running with TP or DP
502 # - The MK already reduced the fused output itself.
503 if output_is_reduced is None:
504 output_is_reduced = self._fused_output_is_reduced
505
506 if (
507 not self.moe_config.is_sequence_parallel
508 and not self.moe_config.skip_final_all_reduce
509 and (self.moe_config.tp_size > 1 or self.moe_config.ep_size > 1)
510 and not output_is_reduced
511 ):
512 states = tensor_model_parallel_all_reduce(states)
513
514 return states[..., :trunc_size] if trunc_size is not None else states
515
516 def _encode_layer_name(self) -> str | LayerName:
517 if _USE_LAYERNAME:
518 return LayerName(self.layer_name)
519 # Can be unavailable or None in unittests
520 if (
521 is_forward_context_available()
522 and get_forward_context().all_moe_layers is not None
523 ):
524 return "from_forward_context"
525 return self.layer_name
526
527 def _maybe_pad_hidden_states(
528 self,
529 shared_experts_input: torch.Tensor | None,
530 hidden_states: torch.Tensor,
531 ) -> tuple[torch.Tensor, int | None, int | None]:
532 """Pad hidden_states to moe_config.hidden_dim and compute the
533 original dimension for later truncation.
534
535 For latent MoE, the routed hidden_states may be smaller than
536 hidden_dim. Padding ensures uniform tensor sizes through the
537 fused MoE kernel. The returned trunc_size is used by
538 _maybe_reduce_final_output to strip the padding from the result.
539 """
540 shared_experts_hidden_dim = (
541 shared_experts_input.shape[-1] if shared_experts_input is not None else 0
542 )
543 transformed_hidden_dim: int | None = hidden_states.shape[-1]
544 if (
545 not self._quant_method.skip_forward_padding
546 and self.moe_config.hidden_dim != transformed_hidden_dim
547 ):
548 assert transformed_hidden_dim is not None
549 hidden_states = F.pad(
550 hidden_states,
551 (0, self.moe_config.hidden_dim - transformed_hidden_dim),
552 mode="constant",
553 value=0.0,
554 )
555
556 # Truncation sizes for stripping kernel padding from the output.
557 # None means no truncation needed (no padding was applied).
558 #
559 # Two truncation points exist in forward():
560 # pre_xform: applied to fused_output BEFORE routed_output_transform
561 # post_xform: applied to the final result AFTER all-reduce
562 #
563 # MoE with routed output transform or shared experts:
564 # - pre_xform applies if the transform needs unpadded routed output
565 # or shared+routed add needs matching hidden dims. For Nemotron-3
566 # Nano, TRTLLM NVFP4 pads routed MoE hidden dim 2688->2816, while
567 # shared output stays 2688.
568 # - post_xform uses shared_experts_hidden_dim when transform and shared
569 # experts make the final output full hidden dim.
570 #
571 # Standard MoE / MoE without transforms (GPT-OSS, Mixtral):
572 # - pre_xform is None (no early truncation)
573 # - post_xform strips padding after all-reduce (or None if unpadded)
574 if transformed_hidden_dim == hidden_states.shape[-1]:
575 transformed_hidden_dim = None
576
577 pre_xform_trunc_size = None
578 if self.routed_output_transform is not None or shared_experts_hidden_dim > 0:
579 pre_xform_trunc_size = transformed_hidden_dim
580 post_xform_trunc_size = transformed_hidden_dim
581 if self.routed_output_transform is not None and shared_experts_hidden_dim > 0:
582 post_xform_trunc_size = shared_experts_hidden_dim
583
584 return hidden_states, pre_xform_trunc_size, post_xform_trunc_size
585
586 def _maybe_apply_shared_experts(
587 self,
588 shared_experts_input: torch.Tensor | None,
589 order: SharedExpertsOrder,
590 ):
591 if self._shared_experts is not None:
592 assert shared_experts_input is not None
593 self._shared_experts(shared_experts_input, order)
594
595 def _apply_quant_method(
596 self,
597 hidden_states: torch.Tensor,
598 router_logits: torch.Tensor,
599 shared_experts_input: torch.Tensor | None,
600 input_ids: torch.Tensor | None = None,
601 shared_experts_overlapping: bool = False,
602 ) -> tuple[torch.Tensor | None, torch.Tensor | UnfinalizedMoEOutput]:
603 """Run expert routing and the fused MoE kernel via the quant method.
604
605 Orchestrates shared expert execution (before/after), expert selection
606 via the router, and the actual fused MoE computation. Returns
607 (shared_expert_output, fused_expert_output).
608
609 `shared_experts_overlapping` should be True only if using multi-stream
610 overlap. Then the shared expert was already launched in a separate
611 stream, so the results only have to be awaited here.
612 """
613 self._maybe_apply_shared_experts(
614 shared_experts_input, SharedExpertsOrder.NO_OVERLAP
615 )
616
617 if self.routed_experts.quant_method.is_monolithic:
618 # Monolithic kernels: pass router_logits to routed_experts
619 fused_out = self.routed_experts.forward_monolithic(
620 x=hidden_states,
621 router_logits=router_logits,
622 input_ids=input_ids,
623 )
624 else:
625 # Modular kernels: select experts first, then call routed_experts
626 topk_weights, topk_ids = self.router.select_experts(
627 hidden_states=hidden_states,
628 router_logits=router_logits,
629 topk_indices_dtype=self._quant_method.topk_indices_dtype,
630 input_ids=input_ids,
631 )
632
633 fused_out = self.routed_experts.forward_modular(
634 x=hidden_states,
635 topk_weights=topk_weights,
636 topk_ids=topk_ids,
637 shared_experts=self._shared_experts,
638 shared_experts_input=shared_experts_input,
639 )
640
641 if shared_experts_overlapping:
642 assert self._shared_experts is not None
643 self._shared_experts.wait()
644
645 return (
646 self._shared_experts.output if self._shared_experts is not None else None,
647 fused_out,
648 )
649
650 def _sequence_parallel_context(self):
651 """Return a context manager for sequence-parallel token
652 redistribution.
653
654 When sequence parallelism is active, returns a context that handles
655 local size tracking for proper token scatter/gather. Otherwise
656 returns a no-op context.
657 """
658 ctx = get_forward_context()
659 return (
660 ctx.dp_metadata.sp_local_sizes(
661 self.moe_config.sp_size,
662 pcp_size=self.moe_config.pcp_size,
663 use_ep=self.moe_config.use_ep,
664 )
665 if ctx.dp_metadata
666 else nullcontext()
667 )
668
669 def _maybe_add_zero_expert_output(
670 self,
671 result: torch.Tensor,
672 ) -> torch.Tensor:
673 """Add the zero expert's contribution to the final result.
674
675 When a ZeroExpertRouter is used, it computes a bias-like output
676 from the "zero expert" that is added to the combined routed+shared
677 expert output.
678 """
679 if isinstance(self.router, ZeroExpertRouter):
680 zero_expert_output = self.router.zero_expert_output
681 assert zero_expert_output is not None
682 result = result + zero_expert_output
683 return result
684
685 def forward(
686 self,
687 hidden_states: torch.Tensor,
688 router_logits: torch.Tensor,
689 input_ids: torch.Tensor | None = None,
690 shared_experts_input: torch.Tensor | None = None,
691 ) -> torch.Tensor:
692 """Invoke the fused moe layer.
693
694 Input:
695 - hidden_states
696 - router_logits
697
698 Output:
699 - The new hidden_states.
700
701 Calling sequence
702 - forward
703 - self._forward_entry (_moe_forward or _moe_forward_shared custom op)
704 - _forward_impl
705
706 Note: The existence of _moe_forward and _moe_forward_shared custom ops are due
707 to the following reason:
708 1. pytorch cannot handle union types in custom op signatures so
709 _moe_forward and _moe_forward_shared must be split.
710 """
711 # Apply transform for routed experts (e.g., latent projection for
712 # latent MoE). When the caller pre-applies the routed input transform
713 # outside the runner (e.g. to overlap it on a separate stream), it
714 # passes the already-transformed routed input as ``hidden_states`` and
715 # the original hidden states as ``shared_experts_input``; skip the
716 # transform in that case so shared experts still see the original input.
717 if shared_experts_input is None:
718 hidden_states, shared_experts_input = self.apply_routed_input_transform(
719 hidden_states
720 )
721
722 # Record before `_maybe_pad_hidden_states` pads activations to match
723 # `moe_config.hidden_dim`, e.g. after `align_fp4_moe_hidden_dim_for_fi`
724 # so routed output can be trimmed before
725 # shared+routed add / latent up proj if needed.
726
727 hidden_states, og_hidden_dim_pre_xform, og_hidden_dim_post_xform = (
728 self._maybe_pad_hidden_states(
729 shared_experts_input,
730 hidden_states,
731 )
732 )
733
734 result = self._forward_entry(
735 hidden_states,
736 router_logits,
737 shared_experts_input,
738 input_ids,
739 self._encode_layer_name(),
740 self.moe_config.hidden_dim_unpadded
741 if self._quant_method.has_unpadded_output
742 else 0,
743 )
744
745 #
746 # Note: there are two all-reduce points below. They are mutually
747 # exclusive, controlled by _fused_output_is_reduced
748 # - When True: the combine kernel already reduced fused_output,
749 # so we reduce shared_output here to match, then skip the
750 # all-reduce in _maybe_reduce_final_output.
751 # - When False: neither output is reduced yet, so we combine
752 # them first and all-reduce the sum in _maybe_reduce_final_output.
753
754 # Extract outputs from result
755 shared_output, fused_output = _unpack(result)
756 fused_output = cast(torch.Tensor, fused_output)
757
758 if og_hidden_dim_pre_xform is not None:
759 fused_output = fused_output[..., :og_hidden_dim_pre_xform]
760
761 fused_output_is_reduced = self._fused_output_is_reduced
762
763 # Latent routed output has to be reduced before output transform,
764 # because the transform may include non-linear normalization.
765 fused_output, fused_output_is_reduced = (
766 self._maybe_reduce_routed_output_before_transform(
767 fused_output,
768 fused_output_is_reduced,
769 )
770 )
771
772 # If routed output is already reduced, reduce shared to match.
773 # See note above re: the two all-reduce points.
774 shared_output = self._maybe_reduce_shared_expert_output(
775 shared_output, fused_output_is_reduced
776 )
777
778 shared_output, fused_output = self._maybe_apply_routed_scale_to_output(
779 shared_output, fused_output
780 )
781
782 # Apply output transform (e.g. latent -> full dim)
783 fused_output = self.apply_routed_output_transform(fused_output)
784
785 if shared_output is not None:
786 result = shared_output + fused_output
787 else:
788 result = fused_output
789
790 result = self._maybe_reduce_final_output(
791 result, og_hidden_dim_post_xform, fused_output_is_reduced
792 )
793
794 return self._maybe_add_zero_expert_output(result)
795
796 @property
797 def do_naive_dispatch_combine(self) -> bool:
798 return (
799 self.moe_config.dp_size > 1 or self.moe_config.is_sequence_parallel
800 ) and not self._quant_method.supports_internal_mk
801
802 def _maybe_dispatch(
803 self,
804 hidden_states: torch.Tensor,
805 router_logits: torch.Tensor,
806 ) -> tuple[torch.Tensor, torch.Tensor]:
807 # For naive dispatch/combine Dp/Ep, dispatch the hidden states and
808 # router logits to all experts.
809 # NOTE: this will be removed once all kernels are migrated into the
810 # MoEKernel framework.
811 if self.do_naive_dispatch_combine:
812 result = get_ep_group().dispatch_router_logits(
813 hidden_states,
814 router_logits,
815 self.moe_config.is_sequence_parallel,
816 )
817 assert len(result) == 2
818 hidden_states, router_logits = result
819
820 if (
821 self.moe_config.pcp_size > 1
822 and not self.moe_config.moe_parallel_config.use_all2all_kernels
823 ):
824 hidden_states = get_pcp_group().all_gather(hidden_states, dim=0)
825 router_logits = get_pcp_group().all_gather(router_logits, dim=0)
826
827 return hidden_states, router_logits
828
829 def _maybe_combine(
830 self,
831 shared_output: torch.Tensor | None,
832 hidden_states: torch.Tensor | UnfinalizedMoEOutput,
833 ) -> (
834 torch.Tensor
835 | UnfinalizedMoEOutput
836 | tuple[torch.Tensor | None, torch.Tensor | UnfinalizedMoEOutput]
837 ):
838 if self.do_naive_dispatch_combine:
839 if isinstance(hidden_states, UnfinalizedMoEOutput):
840 raise RuntimeError(
841 "Naive expert-parallel combine cannot consume a deferred "
842 "MoE output."
843 )
844 hidden_states = get_ep_group().combine(
845 hidden_states,
846 self.moe_config.is_sequence_parallel,
847 )
848
849 if (
850 self.moe_config.pcp_size > 1
851 and not self.moe_config.moe_parallel_config.use_all2all_kernels
852 ):
853 if isinstance(hidden_states, UnfinalizedMoEOutput):
854 raise RuntimeError(
855 "PCP reduce-scatter cannot consume a deferred MoE output."
856 )
857 hidden_states = get_pcp_group().reduce_scatter(hidden_states, dim=0)
858
859 if self.shared_experts is not None:
860 assert shared_output is not None
861 return shared_output, hidden_states
862 else:
863 return hidden_states
864
865 def _forward_impl(
866 self,
867 hidden_states: torch.Tensor,
868 router_logits: torch.Tensor,
869 shared_experts_input: torch.Tensor | None,
870 input_ids: torch.Tensor | None = None,
871 ) -> (
872 torch.Tensor
873 | UnfinalizedMoEOutput
874 | tuple[torch.Tensor, torch.Tensor | UnfinalizedMoEOutput]
875 ):
876 """Entry point called by the custom op to run the MoE computation.
877
878 Handles pre-dispatch setup (gate application, external shared expert
879 triggering, quant config init) then performs the following steps
880 within the sequence-parallel context.
881
882 - Performs expert routing
883 - fused MoE kernel execution
884 - shared expert computation.
885
886 Returns routed output, optionally paired with shared-expert output. A
887 fused consumer may request the routed output in deferred-finalize form.
888 """
889 # TODO(bnell): this can be removed after MK migration is complete.
890 self.routed_experts._ensure_moe_quant_config_init()
891
892 # If using multi-stream overlap for shared experts, we must launch it
893 # before routed expert dispatch.
894 shared_experts_overlapping = False
895 if self._shared_experts is not None:
896 shared_experts_overlapping = self._shared_experts.maybe_forward_async(
897 shared_experts_input
898 )
899
900 # If the Runner holds the gate, apply it after the stream sync,
901 # so it can run overlapped with the
902 # NOTE: in future PR, MoE runner will always hold the gate.
903 if self.gate is not None:
904 if self._fse_fuse_gate:
905 self._maybe_fuse_gate_weights()
906 router_logits = dispatch_unquantized_gemm()(
907 self, hidden_states, self._combined_gate_weight, None
908 )
909 else:
910 router_logits, _ = self.gate(hidden_states)
911
912 with self._sequence_parallel_context():
913 # TODO(bnell): parts of the dispatch/combine steps will go away once
914 # #32567 lands and the remaining kernels are made MKs. The PCP
915 # code will probably remain
916 hidden_states, router_logits = self._maybe_dispatch(
917 hidden_states,
918 router_logits,
919 )
920
921 shared_output, hidden_states = self._apply_quant_method(
922 hidden_states=hidden_states,
923 router_logits=router_logits,
924 shared_experts_input=shared_experts_input,
925 input_ids=input_ids,
926 shared_experts_overlapping=shared_experts_overlapping,
927 )
928
929 return self._maybe_combine(
930 shared_output,
931 hidden_states,
932 )
933
934 #########################################################
935 #
936 # Old methods from FusedMoE layer. Remove when possible.
937 #
938 #########################################################
939
940 #
941 # Properties
942 #
943
944 @property
945 def layer_id(self):
946 # Delayed import to avoid circular dependency
947 from vllm.model_executor.models.utils import extract_layer_index
948
949 return extract_layer_index(self.layer_name)
950
951 #
952 # Attributes still needed by models
953 #
954
955 @property
956 def is_monolithic(self) -> bool:
957 return self.routed_experts.quant_method.is_monolithic
958
959 @property
960 def activation(self) -> MoEActivation:
961 return self.routed_experts.activation
962
963 #
964 # Expert maps
965 #
966
967 @property
968 def expert_map_manager(self):
969 """Forward to routed_experts.expert_map_manager for backward compatibility."""
970 return self.routed_experts.expert_map_manager
971
972 @property
973 def expert_placement_strategy(self) -> ExpertPlacementStrategy:
974 return self.expert_map_manager.placement_strategy
975
976 @property
977 def expert_global_to_physical(self) -> torch.Tensor | None:
978 tables = self.expert_map_manager.routing_tables
979 return tables[0] if tables else None
980
981 @property
982 def expert_physical_to_global(self) -> torch.Tensor | None:
983 """Routing table: physical expert ID to global expert ID."""
984 tables = self.expert_map_manager.routing_tables
985 return tables[1] if tables else None
986
987 @property
988 def expert_local_to_global(self) -> torch.Tensor | None:
989 """Routing table: local expert ID to global expert ID."""
990 tables = self.expert_map_manager.routing_tables
991 return tables[2] if tables else None
992
993 @property
994 def expert_map(self) -> torch.Tensor | None:
995 return self.routed_experts.expert_map
996
997 def _expert_routing_tables(
998 self,
999 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None:
1000 return self.routed_experts._expert_routing_tables()
1001
1002 def update_expert_map(self):
1003 self.routed_experts.update_expert_map()
1004
1005 def _map_global_expert_id_to_local_expert_id(self, expert_id: int) -> int:
1006 """Map global expert ID to local expert ID."""
1007 return self.routed_experts._map_global_expert_id_to_local_expert_id(expert_id)
1008
1009 def get_expert_weights(self) -> Iterable[torch.Tensor]:
1010 return self.routed_experts.get_expert_weights()
1011
1012 #
1013 # EPLB
1014 #
1015
1016 @property
1017 def eplb_state(self) -> EplbLayerState | None:
1018 return self.router.eplb_state
1019
1020 def set_eplb_state(
1021 self,
1022 moe_layer_idx: int,
1023 expert_load_view: torch.Tensor,
1024 logical_to_physical_map: torch.Tensor,
1025 logical_replica_count: torch.Tensor,
1026 ) -> None:
1027 """Register the EPLB state in this layer.
1028
1029 This is used later in forward pass, where we get the expert mapping
1030 and record the load metrics in `expert_load_view`.
1031 """
1032 if self.router.eplb_state is not None:
1033 self.router.eplb_state.set_layer_state(
1034 moe_layer_idx,
1035 expert_load_view,
1036 logical_to_physical_map,
1037 logical_replica_count,
1038 )