[MoE] Defer the TRTLLM-Gen top-k finalize on the modular path (#58635)

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

Yongye Zhu committed Sep 25, 2026 at 00:38 UTC 378504a5442b8a9240b359dae3e6f75f35c38f19
26 files changed +652 -117
tests/kernels/moe/test_flashinfer.py
+122 -2
@@ -1,14 +1,19 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import multiprocessing
4 -from dataclasses import dataclass
4 +from dataclasses import dataclass, replace
5 +from types import SimpleNamespace
6 from unittest.mock import MagicMock
7
8 import pytest
9 import torch
10
11 import vllm.model_executor.layers.fused_moe.modular_kernel as mk
11 -from tests.kernels.moe.utils import make_dummy_moe_config
12 +from tests.kernels.moe.utils import (
13 + check_deferred_moe_finalize,
14 + make_dummy_moe_config,
15 + make_test_weights,
16 +)
17 from vllm.config import ParallelConfig, VllmConfig, set_current_vllm_config
18 from vllm.model_executor.layers.fused_moe.activation import MoEActivation
19 from vllm.model_executor.layers.fused_moe.all2all_utils import (
@@ -32,11 +37,20 @@ from vllm.model_executor.layers.fused_moe.experts.trtllm_fp8_moe import (
37 TrtLlmFp8ExpertsMonolithic,
38 )
39 from vllm.model_executor.layers.fused_moe.fused_moe import fused_experts
40 +from vllm.model_executor.layers.fused_moe.oracle.fp8 import (
41 + Fp8MoeBackend,
42 + convert_to_fp8_moe_kernel_format,
43 + make_fp8_moe_quant_config,
44 +)
45 +from vllm.model_executor.layers.fused_moe.router.fused_topk_router import fused_topk
46 from vllm.model_executor.layers.quantization.utils.flashinfer_utils import (
47 rotate_weights_for_fi_trtllm_fp8_per_tensor_moe,
48 swap_w13_to_w31,
49 )
50 from vllm.model_executor.layers.quantization.utils.fp8_utils import input_to_float8
51 +from vllm.model_executor.layers.quantization.utils.mxfp8_utils import (
52 + mxfp8_e4m3_quantize,
53 +)
54 from vllm.model_executor.layers.quantization.utils.quant_utils import (
55 QuantKey,
56 kFp8Dynamic128Sym,
@@ -767,3 +781,109 @@ def test_trtllm_fp8_swiglu_clamp_support(
781 assert supported == expected, reason
782 if not expected:
783 assert "SwiGLU" in reason
784 +
785 +
786 +def _make_mxfp8_moe_weights(
787 + e: int, n: int, k: int
788 +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
789 + def quantize(w: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
790 + q, s = zip(
791 + *(mxfp8_e4m3_quantize(w[i], is_sf_swizzled_layout=False) for i in range(e))
792 + )
793 + return torch.stack(q), torch.stack(s)
794 +
795 + w1, w1_scale = quantize(
796 + torch.randn(e, 2 * n, k, device="cuda", dtype=torch.bfloat16) / 10
797 + )
798 + w2, w2_scale = quantize(
799 + torch.randn(e, k, n, device="cuda", dtype=torch.bfloat16) / 10
800 + )
801 + return w1, w2, w1_scale, w2_scale
802 +
803 +
804 +@pytest.mark.parametrize("block_shape", [[128, 128], [1, 32]], ids=["block", "mxfp8"])
805 +@pytest.mark.parametrize("m", [1, 16])
806 +@pytest.mark.skipif(
807 + not current_platform.is_device_capability_family(100),
808 + reason="Requires TRTLLM-Gen FP8 MoE (SM100)",
809 +)
810 +def test_trtllm_fp8_block_moe_deferred_finalize(
811 + m: int, block_shape: list[int], workspace_init
812 +):
813 + """TRTLLM-Gen block-FP8 and MXFP8 modular experts can leave the top-k
814 + finalize to the caller."""
815 + e, topk, n, k = 32, 4, 1024, 1024
816 + set_random_seed(7)
817 + with set_current_vllm_config(vllm_config):
818 + if block_shape == [1, 32]:
819 + w1, w2, w1_scale, w2_scale = _make_mxfp8_moe_weights(e, n, k)
820 + else:
821 + (_, w1, w1_scale, _), (_, w2, w2_scale, _) = make_test_weights(
822 + e, n, k, quant_dtype=torch.float8_e4m3fn, block_shape=block_shape
823 + )
824 + w1, w2, w1_scale, w2_scale = convert_to_fp8_moe_kernel_format(
825 + Fp8MoeBackend.FLASHINFER_TRTLLM,
826 + SimpleNamespace(
827 + weight_block_size=block_shape,
828 + moe_config=SimpleNamespace(
829 + is_act_and_mul=True, intermediate_size_per_partition=n
830 + ),
831 + activation=MoEActivation.SILU,
832 + ),
833 + w1,
834 + w2,
835 + w1_scale,
836 + w2_scale,
837 + w13_input_scale=None,
838 + w2_input_scale=None,
839 + )
840 + quant_config = make_fp8_moe_quant_config(
841 + Fp8MoeBackend.FLASHINFER_TRTLLM,
842 + w1_scale,
843 + w2_scale,
844 + a1_scale=None,
845 + a2_scale=None,
846 + block_shape=block_shape,
847 + )
848 + # One rank of a TP group, which deferral needs.
849 + moe_config = FusedMoEConfig(
850 + num_experts=e,
851 + experts_per_token=topk,
852 + hidden_dim=k,
853 + intermediate_size=2 * n,
854 + num_local_experts=e,
855 + num_logical_experts=e,
856 + activation=MoEActivation.SILU,
857 + device="cuda",
858 + moe_parallel_config=replace(
859 + FusedMoEParallelConfig.make_no_parallel(), tp_size=2
860 + ),
861 + in_dtype=torch.bfloat16,
862 + routing_method=RoutingMethodType.TopK,
863 + max_num_tokens=next_power_of_2(m),
864 + )
865 + kernel = mk.FusedMoEKernel(
866 + maybe_make_prepare_finalize(
867 + moe=moe_config, quant_config=quant_config, allow_new_interface=True
868 + ),
869 + TrtLlmFp8ExpertsModular(moe_config=moe_config, quant_config=quant_config),
870 + )
871 +
872 + a = torch.randn((m, k), device="cuda", dtype=torch.bfloat16) / 10
873 + score = torch.randn((m, e), device="cuda", dtype=torch.bfloat16)
874 + topk_weights, topk_ids, _ = fused_topk(a, score, topk, renormalize=False)
875 + check_deferred_moe_finalize(
876 + moe_config,
877 + lambda: kernel.apply(
878 + hidden_states=a,
879 + w1=w1,
880 + w2=w2,
881 + topk_weights=topk_weights,
882 + topk_ids=topk_ids,
883 + activation=MoEActivation.SILU,
884 + global_num_experts=e,
885 + expert_map=None,
886 + apply_router_weight_on_input=False,
887 + ),
888 + router_weights=topk_weights,
889 + )
tests/kernels/moe/test_ocp_mx_moe.py
+149 -1
@@ -7,7 +7,7 @@ from dataclasses import dataclass, replace
7 import pytest
8 import torch
9
10 -from tests.kernels.moe.utils import check_accuracy
10 +from tests.kernels.moe.utils import check_accuracy, check_deferred_moe_finalize
11 from vllm._aiter_ops import (
12 is_aiter_found,
13 is_aiter_found_and_supported,
@@ -663,6 +663,154 @@ def test_trtllm_gen_mxfp4_fused_moe(
663 check_accuracy(ref_result, tg_result, atol=0, rtol=0.3, percent=0.8)
664
665
666 +@pytest.mark.parametrize(
667 + "monolithic,num_tokens,chunk_size",
668 + [(False, 1, None), (False, 16, None), (False, 16, 5), (True, 16, None)],
669 +)
670 +@pytest.mark.skipif(
671 + not TRTLLM_GEN_MXFP4_AVAILABLE,
672 + reason="nvidia gpu and compute capability sm100 is required for this test",
673 +)
674 +def test_trtllm_mxfp4_deferred_finalize(
675 + monolithic: bool,
676 + num_tokens: int,
677 + chunk_size: int | None,
678 + monkeypatch: pytest.MonkeyPatch,
679 + workspace_init,
680 +):
681 + """TRTLLM-Gen MXFP4 experts can leave the top-k finalize to the caller."""
682 + from vllm.model_executor.layers.fused_moe.activation import MoEActivation
683 + from vllm.model_executor.layers.fused_moe.config import (
684 + FusedMoEConfig,
685 + FusedMoEParallelConfig,
686 + RoutingMethodType,
687 + )
688 + from vllm.model_executor.layers.fused_moe.experts.trtllm_mxfp4_moe import (
689 + TrtLlmMxfp4ExpertsModular,
690 + TrtLlmMxfp4ExpertsMonolithic,
691 + )
692 + from vllm.model_executor.layers.fused_moe.oracle.mxfp4 import (
693 + Mxfp4MoeBackend,
694 + convert_weight_to_mxfp4_moe_kernel_format,
695 + make_mxfp4_moe_kernel,
696 + make_mxfp4_moe_quant_config,
697 + )
698 +
699 + num_experts, topk, hidden_size, intermediate_size = 32, 4, 1024, 512
700 + device = torch.device("cuda:0")
701 + torch.manual_seed(0)
702 + if chunk_size is not None:
703 + monkeypatch.setattr(
704 + TrtLlmMxfp4ExpertsModular,
705 + "_max_supported_tokens",
706 + lambda self, top_k, global_num_experts: chunk_size,
707 + )
708 +
709 + def quantize(w: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
710 + w_q, w_scale = fp4_quantize(
711 + w,
712 + torch.tensor(1.0, device=device),
713 + 32,
714 + sf_use_ue8m0=True,
715 + is_sf_swizzled_layout=False,
716 + )
717 + return (
718 + w_q.reshape(*w.shape[:-1], -1),
719 + w_scale.view(torch.uint8).reshape(*w.shape[:-1], -1),
720 + )
721 +
722 + w13, w13_scale = quantize(
723 + torch.randn(
724 + num_experts,
725 + 2 * intermediate_size,
726 + hidden_size,
727 + device=device,
728 + dtype=torch.bfloat16,
729 + )
730 + )
731 + w2, w2_scale = quantize(
732 + torch.randn(
733 + num_experts,
734 + hidden_size,
735 + intermediate_size,
736 + device=device,
737 + dtype=torch.bfloat16,
738 + )
739 + )
740 + backend = Mxfp4MoeBackend.FLASHINFER_TRTLLM_MXFP4_MXFP8
741 + w13, w2, w13_scale, w2_scale, _, _ = convert_weight_to_mxfp4_moe_kernel_format(
742 + backend,
743 + torch.nn.Module(),
744 + w13,
745 + w2,
746 + w13_scale,
747 + w2_scale,
748 + _cache_permute_indices={},
749 + )
750 + # One rank of a TP group, which deferral needs.
751 + moe_config = FusedMoEConfig(
752 + num_experts=num_experts,
753 + experts_per_token=topk,
754 + hidden_dim=hidden_size,
755 + intermediate_size=2 * intermediate_size,
756 + num_local_experts=num_experts,
757 + num_logical_experts=num_experts,
758 + activation=MoEActivation.SILU,
759 + device=device,
760 + routing_method=RoutingMethodType.Renormalize,
761 + moe_parallel_config=replace(
762 + FusedMoEParallelConfig.make_no_parallel(), tp_size=2
763 + ),
764 + in_dtype=torch.bfloat16,
765 + )
766 + kernel = make_mxfp4_moe_kernel(
767 + make_mxfp4_moe_quant_config(backend, w1_scale=w13_scale, w2_scale=w2_scale),
768 + moe_config,
769 + TrtLlmMxfp4ExpertsMonolithic if monolithic else TrtLlmMxfp4ExpertsModular,
770 + backend,
771 + )
772 +
773 + hidden_states = torch.randn(
774 + num_tokens, hidden_size, device=device, dtype=torch.bfloat16
775 + )
776 + if monolithic:
777 + router_logits = torch.randn(num_tokens, num_experts, device=device)
778 + check_deferred_moe_finalize(
779 + moe_config,
780 + lambda: kernel.apply_monolithic(
781 + hidden_states,
782 + w13,
783 + w2,
784 + router_logits,
785 + activation=MoEActivation.SILU,
786 + global_num_experts=num_experts,
787 + expert_map=None,
788 + apply_router_weight_on_input=False,
789 + ),
790 + )
791 + return
792 +
793 + topk_ids = torch.rand(num_tokens, num_experts, device=device).argsort(dim=-1)
794 + topk_ids = topk_ids[:, :topk].to(torch.int32)
795 + topk_weights = torch.rand(num_tokens, topk, device=device).softmax(dim=-1)
796 + check_deferred_moe_finalize(
797 + moe_config,
798 + lambda: kernel.apply(
799 + hidden_states=hidden_states,
800 + w1=w13,
801 + w2=w2,
802 + topk_weights=topk_weights,
803 + topk_ids=topk_ids,
804 + activation=MoEActivation.SILU,
805 + global_num_experts=num_experts,
806 + expert_map=None,
807 + apply_router_weight_on_input=False,
808 + ),
809 + router_weights=topk_weights,
810 + chunked=chunk_size is not None,
811 + )
812 +
813 +
814 @pytest.mark.parametrize("topk", [1, 4])
815 @pytest.mark.parametrize("num_experts", [32])
816 @pytest.mark.parametrize("num_tokens", [1, 128])
tests/kernels/moe/test_trtllm_nvfp4_moe.py
+102 -28
@@ -8,11 +8,13 @@ and GELU — including a Gemma4-shaped case (128 experts, top-k 8,
8 intermediate_size 704) that exercises the non-256-aligned padding path.
9 """
10
11 +from dataclasses import replace
12 +
13 import pytest
14 import torch
15
16 import vllm.model_executor.layers.fused_moe.modular_kernel as mk
15 -from tests.kernels.moe.utils import make_test_quant_config
17 +from tests.kernels.moe.utils import check_deferred_moe_finalize, make_test_quant_config
18 from tests.kernels.quantization.nvfp4_utils import (
19 FLOAT4_E2M1_MAX,
20 FLOAT8_E4M3_MAX,
@@ -31,6 +33,7 @@ from vllm.model_executor.layers.fused_moe.all2all_utils import (
33 from vllm.model_executor.layers.fused_moe.config import (
34 FusedMoEConfig,
35 FusedMoEParallelConfig,
36 + FusedMoEQuantConfig,
37 RoutingMethodType,
38 )
39 from vllm.model_executor.layers.fused_moe.experts.trtllm_nvfp4_moe import (
@@ -92,6 +95,36 @@ SILU_WITH_CLAMP = op_registry[_CLAMP_OP_NAME]
95 SITU = op_registry[_SITU_OP_NAME]
96
97
98 +def _make_trtllm_fp4_moe_kernel(
99 + moe_config: FusedMoEConfig, quant_config: FusedMoEQuantConfig
100 +) -> mk.FusedMoEKernel:
101 + experts = TrtLlmNvFp4ExpertsModular(
102 + moe_config=moe_config, quant_config=quant_config
103 + )
104 + # Mimic the production weight-loader path so per-expert tensors that
105 + # are normally precomputed in process_weights_after_loading (g1_scale_c
106 + # and the rescaled gemm1_clamp_limit) get materialized. The test's
107 + # synthetic quant_config has g1_alphas/g2_alphas already at their
108 + # post-fusion values, so we set w13_weight_scale_2 to alias g1_alphas
109 + # (same tensor) and use input_scale=1 to make the in-place
110 + # weight_scale_2 *= input_scale step a no-op.
111 + fake_layer = torch.nn.Module()
112 + fake_layer.w13_weight_scale_2 = quant_config.g1_alphas
113 + fake_layer.w2_weight_scale_2 = quant_config.g2_alphas
114 + fake_layer.w13_input_scale = torch.ones_like(quant_config.g1_alphas)
115 + fake_layer.w2_input_scale = torch.ones_like(quant_config.g2_alphas)
116 + experts.process_weights_after_loading(fake_layer)
117 + return mk.FusedMoEKernel(
118 + maybe_make_prepare_finalize(
119 + moe=moe_config,
120 + quant_config=quant_config,
121 + allow_new_interface=True,
122 + use_monolithic=False,
123 + ),
124 + experts,
125 + )
126 +
127 +
128 ACTIVATION_CASES = [
129 pytest.param(MoEActivation.SILU, MoEActivation.SILU, None, id="silu"),
130 pytest.param(MoEActivation.SILU, SILU_WITH_CLAMP, _SWIGLU_LIMIT, id="silu_clamp"),
@@ -191,34 +224,11 @@ def test_trtllm_fp4_moe_no_graph(
224 ),
225 )
226
194 - trtllm_inner = TrtLlmNvFp4ExpertsModular(
195 - moe_config=moe_config, quant_config=quant_config
196 - )
197 - # Mimic the production weight-loader path so per-expert tensors that
198 - # are normally precomputed in process_weights_after_loading (g1_scale_c
199 - # and the rescaled gemm1_clamp_limit) get materialized. The test's
200 - # synthetic quant_config has g1_alphas/g2_alphas already at their
201 - # post-fusion values, so we set w13_weight_scale_2 to alias g1_alphas
202 - # (same tensor) and use input_scale=1 to make the in-place
203 - # weight_scale_2 *= input_scale step a no-op.
204 - fake_layer = torch.nn.Module()
205 - fake_layer.w13_weight_scale_2 = quant_config.g1_alphas
206 - fake_layer.w2_weight_scale_2 = quant_config.g2_alphas
207 - fake_layer.w13_input_scale = torch.ones_like(quant_config.g1_alphas)
208 - fake_layer.w2_input_scale = torch.ones_like(quant_config.g2_alphas)
209 - trtllm_inner.process_weights_after_loading(fake_layer)
227 + trtllm_experts = _make_trtllm_fp4_moe_kernel(moe_config, quant_config)
228 if activation == MoEActivation.SITU:
211 - torch.testing.assert_close(trtllm_inner.g1_scale_c, quant_config.a2_gscale)
212 -
213 - trtllm_experts = mk.FusedMoEKernel(
214 - maybe_make_prepare_finalize(
215 - moe=moe_config,
216 - quant_config=quant_config,
217 - allow_new_interface=True,
218 - use_monolithic=False,
219 - ),
220 - trtllm_inner,
221 - )
229 + torch.testing.assert_close(
230 + trtllm_experts.fused_experts.g1_scale_c, quant_config.a2_gscale
231 + )
232
233 trtllm_output = trtllm_experts.apply(
234 hidden_states=a,
@@ -331,6 +341,70 @@ def test_trtllm_fp4_moe_reprocess_does_not_refold_swiglu_params():
341 torch.testing.assert_close(experts.gemm1_beta, beta * expected)
342
343
344 +@pytest.mark.parametrize("m,chunk_size", [(1, None), (16, None), (16, 5)])
345 +@torch.inference_mode()
346 +def test_trtllm_fp4_moe_deferred_finalize(
347 + m: int,
348 + chunk_size: int | None,
349 + monkeypatch: pytest.MonkeyPatch,
350 + workspace_init,
351 +):
352 + """TRTLLM-Gen NvFP4 modular experts can leave the top-k finalize to the caller."""
353 + e, topk, n, k = 128, 8, 1024, 1024
354 + dtype = torch.bfloat16
355 + if chunk_size is not None:
356 + monkeypatch.setattr(
357 + TrtLlmNvFp4ExpertsModular, "_get_chunk_size", lambda self: chunk_size
358 + )
359 +
360 + set_random_seed(7)
361 + with set_current_vllm_config(
362 + VllmConfig(parallel_config=ParallelConfig(pipeline_parallel_size=1))
363 + ):
364 + a = torch.randn((m, k), device="cuda", dtype=dtype) / 10
365 + w1_q, w2_q, quant_config = make_test_quant_config(
366 + e, n, k, in_dtype=dtype, quant_dtype="nvfp4", is_scale_swizzled=False
367 + )
368 + score = torch.randn((m, e), device="cuda", dtype=dtype)
369 + topk_weights, topk_ids, _ = fused_topk(a, score, topk, renormalize=False)
370 +
371 + # One rank of a TP group, which deferral needs.
372 + moe_config = FusedMoEConfig(
373 + num_experts=e,
374 + experts_per_token=topk,
375 + hidden_dim=k,
376 + intermediate_size=2 * n,
377 + num_local_experts=e,
378 + num_logical_experts=e,
379 + activation=MoEActivation.SILU,
380 + device="cuda",
381 + moe_parallel_config=replace(
382 + FusedMoEParallelConfig.make_no_parallel(), tp_size=2
383 + ),
384 + in_dtype=dtype,
385 + routing_method=RoutingMethodType.TopK,
386 + max_num_tokens=next_power_of_2(m),
387 + )
388 + kernel = _make_trtllm_fp4_moe_kernel(moe_config, quant_config)
389 +
390 + check_deferred_moe_finalize(
391 + moe_config,
392 + lambda: kernel.apply(
393 + hidden_states=a,
394 + w1=w1_q,
395 + w2=w2_q,
396 + topk_weights=topk_weights,
397 + topk_ids=topk_ids,
398 + activation=MoEActivation.SILU,
399 + global_num_experts=e,
400 + expert_map=None,
401 + apply_router_weight_on_input=False,
402 + ),
403 + router_weights=topk_weights,
404 + chunked=chunk_size is not None,
405 + )
406 +
407 +
408 if __name__ == "__main__":
409 test_trtllm_fp4_moe_no_graph(
410 64,
tests/kernels/moe/utils.py
+48
@@ -1,5 +1,6 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +from collections.abc import Callable
4
5 import torch
6
@@ -28,6 +29,8 @@ from vllm.model_executor.layers.fused_moe.fused_moe import (
29 fused_experts,
30 )
31 from vllm.model_executor.layers.fused_moe.modular_kernel import FusedMoEKernel
32 +from vllm.model_executor.layers.fused_moe.moe_output import UnfinalizedMoEOutput
33 +from vllm.model_executor.layers.fused_moe.moe_permute_unpermute import moe_unpermute
34 from vllm.model_executor.layers.fused_moe.prepare_finalize.batched import (
35 BatchedPrepareAndFinalize,
36 )
@@ -707,6 +710,51 @@ def check_accuracy(a, b, atol, rtol, percent):
710 )
711
712
713 +def check_deferred_moe_finalize(
714 + moe_config: FusedMoEConfig,
715 + run: Callable[[], torch.Tensor | UnfinalizedMoEOutput],
716 + router_weights: torch.Tensor | None = None,
717 + chunked: bool = False,
718 +) -> None:
719 + """Check a kernel that defers its finalize against the finalize it skips.
720 +
721 + ``run`` calls the kernel on fixed inputs, first as built and then with
722 + ``moe_config`` asking to defer, which ``should_defer_moe_finalize`` must
723 + report truthfully. A deferred output reduced by ``moe_unpermute``, the
724 + TRT-LLM finalize kernel, must give the kernel's own finalized output bit for
725 + bit, and modular experts hand the router's weights back as-is. A call the
726 + experts split across kernel launches finalizes instead.
727 + """
728 + # The deferred output views the router's buffer, so compare with a copy.
729 + expected_weights = None if router_weights is None else router_weights.clone()
730 + finalized = run()
731 + assert isinstance(finalized, torch.Tensor)
732 + moe_config.defer_moe_finalize()
733 + output = run()
734 + assert moe_config.should_defer_moe_finalize(finalized.shape[0]) != chunked
735 + if chunked:
736 + torch.testing.assert_close(output, finalized, atol=0, rtol=0)
737 + return
738 +
739 + assert isinstance(output, UnfinalizedMoEOutput)
740 + if expected_weights is not None:
741 + torch.testing.assert_close(
742 + output.expert_weights, expected_weights, atol=0, rtol=0
743 + )
744 + reference = torch.empty_like(finalized)
745 + moe_unpermute(
746 + reference,
747 + output.gemm2_permuted,
748 + output.expert_weights.float(),
749 + output.expanded_idx_to_permuted_idx,
750 + # The kernel reads its valid-row count through this pointer either way.
751 + expert_first_token_offset=output.gemm2_permuted.new_full(
752 + (1,), output.gemm2_permuted.shape[0], dtype=torch.int64
753 + ),
754 + )
755 + torch.testing.assert_close(reference, finalized, atol=0, rtol=0)
756 +
757 +
758 def mxfp4_w_layouts(mx_axis: int, num_warps: int = 8):
759 """Weight/scale layouts for mxfp4 MoE, as (layout, opts) pairs.
760
tests/models/kimi_k3/test_latent_moe_tail.py
+6 -6
@@ -15,6 +15,7 @@ from tests.utils import (
15 multi_process_parallel,
16 )
17 from vllm.distributed import get_tp_group
18 +from vllm.model_executor.layers.fused_moe.config import FusedMoEConfig
19 from vllm.model_executor.layers.fused_moe.experts.trtllm_mxfp4_moe import (
20 TrtLlmMxfp4ExpertsMonolithic,
21 )
@@ -43,12 +44,11 @@ def test_deferred_finalize_enabled_before_moe_kernel_setup(
44 hidden_dim = LATENT_SIZE
45 hidden_dim_unpadded = LATENT_SIZE
46 experts_per_token = 16
46 - defer_moe_finalize = False
47 + _defer_moe_finalize = False
48 defer_moe_finalize_max_num_tokens = -1
48 -
49 - @property
50 - def use_deferred_moe_finalize(self) -> bool:
51 - return self.defer_moe_finalize
49 + defer_moe_finalize = FusedMoEConfig.defer_moe_finalize
50 + limit_deferred_moe_finalize = FusedMoEConfig.limit_deferred_moe_finalize
51 + use_deferred_moe_finalize = FusedMoEConfig.use_deferred_moe_finalize
52
53 moe_config = FakeMoEConfig()
54 quant_method = SimpleNamespace(
@@ -99,7 +99,7 @@ def test_deferred_finalize_enabled_before_moe_kernel_setup(
99
100 latent_moe_runner.LatentMoERunner()
101
102 - assert moe_config.defer_moe_finalize
102 + assert moe_config.use_deferred_moe_finalize
103 assert moe_config.defer_moe_finalize_max_num_tokens == 128
104 assert initialized_with["experts_per_token"] == 16
105
vllm/model_executor/layers/fused_moe/config.py
+57 -13
@@ -1,6 +1,6 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -from dataclasses import dataclass
3 +from dataclasses import dataclass, field
4 from enum import IntEnum
5 from typing import Union
6
@@ -1290,13 +1290,12 @@ class FusedMoEConfig:
1290 # Only honored on the non-reduced (late-AR) TP path. Default False.
1291 skip_final_all_reduce: bool = False
1292
1293 - # When True, experts that can stop after GEMM2 are allowed to hand back an
1294 - # UnfinalizedMoEOutput instead of finalized states, leaving the top-k
1295 - # reduction to fuse into the consumer. Set by layers that have such a
1296 - # consumer; read through `use_deferred_moe_finalize`, which applies the
1297 - # guards. Kernels without the capability ignore it. Default False.
1298 - defer_moe_finalize: bool = False
1299 - # Optional consumer capacity for deferred finalize. Negative means unbounded.
1293 + # Requested through `defer_moe_finalize()`, read through
1294 + # `should_defer_moe_finalize()`.
1295 + _defer_moe_finalize: bool = field(default=False, init=False)
1296 + # Most tokens a deferred call covers: the consumer's capacity, lowered by
1297 + # experts that would split a larger call across kernel launches. Negative
1298 + # means unbounded.
1299 defer_moe_finalize_max_num_tokens: int = -1
1300
1301 # SwiGLU clamp limit. When set, backends that do not implement the clamp
@@ -1422,21 +1421,57 @@ class FusedMoEConfig:
1421 def use_deferred_moe_finalize(self) -> bool:
1422 """Whether experts may return an unfinalized output on this deployment.
1423
1425 - Evaluated on read rather than in ``__post_init__`` because
1426 - ``defer_moe_finalize`` is set after construction, like
1427 - ``skip_final_all_reduce``.
1424 + Evaluated on read rather than in ``__post_init__`` because deferral is
1425 + requested after construction, like ``skip_final_all_reduce``.
1426 """
1427 # The consumer fuses a TP all-reduce. Other parallel modes require a
1430 - # combine or reduce-scatter after the experts and cannot defer it.
1428 + # combine or reduce-scatter after the experts and cannot defer it, and
1429 + # the consumer has no way to strip hidden-dim padding from GEMM2 rows.
1430 return (
1432 - self.defer_moe_finalize
1431 + self._defer_moe_finalize
1432 and self.tp_size > 1
1433 and self.dp_size == 1
1434 and self.ep_size == 1
1435 and self.pcp_size == 1
1436 and not self.is_sequence_parallel
1437 + and self.hidden_dim == self.hidden_dim_unpadded
1438 )
1439
1440 + def defer_moe_finalize(self, max_num_tokens: int = -1) -> None:
1441 + """Ask the experts to leave the top-k reduction to the layer's consumer.
1442 +
1443 + A layer whose consumer can fuse the top-k reduction (e.g. into its TP
1444 + all-reduce) calls this once while the model is built, and only when its
1445 + experts are TRTLLM-Gen ones that can stop after GEMM2 (the
1446 + ``do_finalize=False`` path). Other experts ignore the request and
1447 + always finalize, which ``should_defer_moe_finalize`` can't see, so the
1448 + layer checks the quant method's ``experts_cls`` first, as Kimi-K3 does.
1449 + From then on:
1450 +
1451 + - The experts return an ``UnfinalizedMoEOutput`` instead of finalized
1452 + states for every call ``should_defer_moe_finalize`` accepts.
1453 + - ``should_defer_moe_finalize(num_tokens)`` is the answer for a call:
1454 + it requires a TP-only deployment without hidden-dim padding
1455 + (``use_deferred_moe_finalize``), a non-empty call and at most
1456 + ``defer_moe_finalize_max_num_tokens`` tokens. The cap starts at
1457 + ``max_num_tokens`` and only ever goes down: experts that would split a
1458 + larger call across kernel launches lower it to their single-launch
1459 + size when they are built, since each launch permutes into its own
1460 + buffer.
1461 + - The model asks ``should_defer_moe_finalize`` before each call and takes
1462 + the matching path. A deferred call runs the runner's ``_forward_impl``
1463 + directly, since the MoE custom op returns tensors only, and the
1464 + consumer then owns the top-k reduction, the shared-expert add and the
1465 + all-reduce.
1466 +
1467 + Args:
1468 + max_num_tokens: Most tokens per call the consumer can take in
1469 + deferred form. Negative means no limit of its own.
1470 +
1471 + """
1472 + self._defer_moe_finalize = True
1473 + self.limit_deferred_moe_finalize(max_num_tokens)
1474 +
1475 def should_defer_moe_finalize(self, num_tokens: int) -> bool:
1476 """Return whether this invocation may defer the top-k reduction."""
1477 max_num_tokens = self.defer_moe_finalize_max_num_tokens
@@ -1446,6 +1481,15 @@ class FusedMoEConfig:
1481 and (max_num_tokens < 0 or num_tokens <= max_num_tokens)
1482 )
1483
1484 + def limit_deferred_moe_finalize(self, max_num_tokens: int) -> None:
1485 + """Finalize calls above ``max_num_tokens`` even when deferring.
1486 +
1487 + Negative means no limit, and leaves the current one in place.
1488 + """
1489 + current = self.defer_moe_finalize_max_num_tokens
1490 + if max_num_tokens >= 0 and (current < 0 or max_num_tokens < current):
1491 + self.defer_moe_finalize_max_num_tokens = max_num_tokens
1492 +
1493 @property
1494 def use_deepep_ht_kernels(self):
1495 return self.moe_parallel_config.use_deepep_ht_kernels
vllm/model_executor/layers/fused_moe/experts/trtllm_fp8_moe.py
+20 -3
@@ -12,6 +12,10 @@ from vllm.model_executor.layers.fused_moe.config import (
12 FusedMoEQuantConfig,
13 RoutingMethodType,
14 )
15 +from vllm.model_executor.layers.fused_moe.moe_output import (
16 + UnfinalizedMoEOutput,
17 + convert_flashinfer_moe_output,
18 +)
19 from vllm.model_executor.layers.fused_moe.topk_weight_and_reduce import (
20 TopKWeightAndReduceNoOP,
21 )
@@ -273,7 +277,7 @@ class TrtLlmFp8ExpertsModular(TrtLlmFp8ExpertsBase, mk.FusedMoEExpertsModular):
277 workspace2: torch.Tensor,
278 expert_tokens_meta: mk.ExpertTokensMetadata | None,
279 apply_router_weight_on_input: bool,
276 - ):
280 + ) -> UnfinalizedMoEOutput | None:
281 import flashinfer
282 from flashinfer.fused_moe import Fp8QuantizationType, WeightLayout
283
@@ -297,7 +301,9 @@ class TrtLlmFp8ExpertsModular(TrtLlmFp8ExpertsBase, mk.FusedMoEExpertsModular):
301 weight_layout = WeightLayout.BlockMajorK
302 hidden_states_scale = prepare_deepseek_fp8_x_sf(hidden_states, a1q_scale)
303
300 - flashinfer.fused_moe.trtllm_fp8_block_scale_routed_moe(
304 + num_tokens = hidden_states.shape[0]
305 + defer = self.moe_config.should_defer_moe_finalize(num_tokens)
306 + flashinfer_output = flashinfer.fused_moe.trtllm_fp8_block_scale_routed_moe(
307 topk_ids=(topk_ids, topk_weights),
308 routing_bias=None,
309 hidden_states=hidden_states,
@@ -321,9 +327,20 @@ class TrtLlmFp8ExpertsModular(TrtLlmFp8ExpertsBase, mk.FusedMoEExpertsModular):
327 use_shuffled_weight=use_shuffled_weight,
328 weight_layout=weight_layout,
329 fp8_quantization_type=fp8_quant_type,
324 - output=output,
330 + do_finalize=not defer,
331 + output=None if defer else output,
332 tune_max_num_tokens=fi_moe_largest_bucket(self.moe_config),
333 )
334 + if not defer:
335 + return None
336 + routed_output = convert_flashinfer_moe_output(
337 + flashinfer_output,
338 + do_finalize=False,
339 + num_tokens=num_tokens,
340 + top_k=topk_ids.size(1),
341 + )
342 + assert isinstance(routed_output, UnfinalizedMoEOutput)
343 + return routed_output
344
345
346 class TrtLlmFp8ExpertsMonolithic(TrtLlmFp8ExpertsBase, mk.FusedMoEExpertsMonolithic):
vllm/model_executor/layers/fused_moe/experts/trtllm_mxfp4_moe.py
+36 -9
@@ -289,6 +289,18 @@ class TrtLlmMxfp4ExpertsModular(TrtLlmMxfp4ExpertsBase, mk.FusedMoEExpertsModula
289 Moved from trtllm_moe.py.
290 """
291
292 + def __init__(
293 + self,
294 + moe_config: FusedMoEConfig,
295 + quant_config: FusedMoEQuantConfig,
296 + **kwargs,
297 + ):
298 + super().__init__(moe_config, quant_config, **kwargs)
299 + # Each launch permutes into its own buffer, so a chunked call finalizes.
300 + moe_config.limit_deferred_moe_finalize(
301 + self._max_supported_tokens(self.topk, moe_config.num_experts)
302 + )
303 +
304 @staticmethod
305 def _supports_parallel_config(
306 moe_parallel_config: FusedMoEParallelConfig,
@@ -343,7 +355,7 @@ class TrtLlmMxfp4ExpertsModular(TrtLlmMxfp4ExpertsBase, mk.FusedMoEExpertsModula
355
356 def _invoke_kernel(
357 self,
346 - output: torch.Tensor,
358 + output: torch.Tensor | None,
359 x_quant: torch.Tensor,
360 x_scale: torch.Tensor | None,
361 topk_ids: torch.Tensor,
@@ -355,10 +367,11 @@ class TrtLlmMxfp4ExpertsModular(TrtLlmMxfp4ExpertsBase, mk.FusedMoEExpertsModula
367 local_num_experts: int,
368 local_expert_offset: int,
369 topk: int,
358 - ) -> None:
370 + ) -> UnfinalizedMoEOutput | None:
371 + """Finalize into ``output``, or stop after GEMM2 when it is None."""
372 from flashinfer import trtllm_fp4_block_scale_routed_moe
373
361 - trtllm_fp4_block_scale_routed_moe(
374 + flashinfer_output = trtllm_fp4_block_scale_routed_moe(
375 topk_ids=(topk_ids, topk_weights),
376 routing_bias=None,
377 hidden_states=x_quant,
@@ -386,12 +399,22 @@ class TrtLlmMxfp4ExpertsModular(TrtLlmMxfp4ExpertsBase, mk.FusedMoEExpertsModula
399 # Modular kernel receives pre-routed tokens, so routing is already
400 # done. Use Renormalize as a safe default the TRTLLM kernel supports.
401 routing_method_type=RoutingMethodType.Renormalize,
389 - do_finalize=True,
402 + do_finalize=output is not None,
403 enable_pdl=True,
404 activation_type=self._flashinfer_activation_type(activation),
405 output=output,
406 tune_max_num_tokens=fi_moe_largest_bucket(self.moe_config),
407 )
408 + if output is not None:
409 + return None
410 + routed_output = convert_flashinfer_moe_output(
411 + flashinfer_output,
412 + do_finalize=False,
413 + num_tokens=x_quant.shape[0],
414 + top_k=topk,
415 + )
416 + assert isinstance(routed_output, UnfinalizedMoEOutput)
417 + return routed_output
418
419 def apply(
420 self,
@@ -410,7 +433,7 @@ class TrtLlmMxfp4ExpertsModular(TrtLlmMxfp4ExpertsBase, mk.FusedMoEExpertsModula
433 workspace2: torch.Tensor,
434 expert_tokens_meta: mk.ExpertTokensMetadata | None,
435 apply_router_weight_on_input: bool,
413 - ):
436 + ) -> UnfinalizedMoEOutput | None:
437 topk_ids = topk_ids.to(dtype=torch.int32)
438
439 topk = topk_ids.size(-1)
@@ -428,13 +451,17 @@ class TrtLlmMxfp4ExpertsModular(TrtLlmMxfp4ExpertsBase, mk.FusedMoEExpertsModula
451 assert self.w1_scale is not None
452 assert self.w2_scale is not None
453
431 - # Chunk tokens so the batched-GEMM grid stays within CUDA limits.
454 + # Chunk tokens so the batched-GEMM grid stays within CUDA limits. Each
455 + # launch permutes into its own buffer, so only a run that fits in one
456 + # launch can leave the top-k reduction to a deferring consumer.
457 M = x_quant.size(0)
458 chunk_size = self._max_supported_tokens(topk, global_num_experts)
459 + defer = chunk_size >= M and self.moe_config.should_defer_moe_finalize(M)
460 + unfinalized: UnfinalizedMoEOutput | None = None
461 for start in range(0, M, chunk_size):
462 end = min(start + chunk_size, M)
436 - self._invoke_kernel(
437 - output[start:end],
463 + unfinalized = self._invoke_kernel(
464 + None if defer else output[start:end],
465 x_quant[start:end],
466 None if x_scale is None else x_scale[start:end],
467 topk_ids[start:end],
@@ -448,4 +475,4 @@ class TrtLlmMxfp4ExpertsModular(TrtLlmMxfp4ExpertsBase, mk.FusedMoEExpertsModula
475 topk,
476 )
477
451 - return output
478 + return unfinalized
vllm/model_executor/layers/fused_moe/experts/trtllm_nvfp4_moe.py
+33 -7
@@ -302,6 +302,16 @@ class TrtLlmNvFp4ExpertsBase:
302 class TrtLlmNvFp4ExpertsModular(TrtLlmNvFp4ExpertsBase, mk.FusedMoEExpertsModular):
303 """Modular version of the implementation (just the experts)."""
304
305 + def __init__(
306 + self,
307 + moe_config: FusedMoEConfig,
308 + quant_config: FusedMoEQuantConfig,
309 + per_token_activation: bool = False,
310 + ):
311 + super().__init__(moe_config, quant_config, per_token_activation)
312 + # Each launch permutes into its own buffer, so a chunked call finalizes.
313 + moe_config.limit_deferred_moe_finalize(self._get_chunk_size())
314 +
315 @staticmethod
316 def _supports_parallel_config(moe_parallel_config: FusedMoEParallelConfig) -> bool:
317 """The modular implementation supports all parallel configs."""
@@ -333,7 +343,7 @@ class TrtLlmNvFp4ExpertsModular(TrtLlmNvFp4ExpertsBase, mk.FusedMoEExpertsModula
343
344 def _invoke_kernel(
345 self,
336 - output: torch.Tensor,
346 + output: torch.Tensor | None,
347 hidden_states: torch.Tensor,
348 w1: torch.Tensor,
349 w2: torch.Tensor,
@@ -342,7 +352,8 @@ class TrtLlmNvFp4ExpertsModular(TrtLlmNvFp4ExpertsBase, mk.FusedMoEExpertsModula
352 activation: MoEActivation,
353 global_num_experts: int,
354 a1q_scale: torch.Tensor | None,
345 - ):
355 + ) -> UnfinalizedMoEOutput | None:
356 + """Finalize into ``output``, or stop after GEMM2 when it is None."""
357 import flashinfer
358
359 assert self.quant_config.w1_scale is not None
@@ -359,7 +370,7 @@ class TrtLlmNvFp4ExpertsModular(TrtLlmNvFp4ExpertsBase, mk.FusedMoEExpertsModula
370 output1_scale_gate_scalar = self.quant_config.g1_alphas
371
372 # Invoke kernel.
362 - flashinfer.fused_moe.trtllm_fp4_block_scale_routed_moe(
373 + flashinfer_output = flashinfer.fused_moe.trtllm_fp4_block_scale_routed_moe(
374 topk_ids=(topk_ids, topk_weights),
375 routing_bias=None,
376 hidden_states=hidden_states,
@@ -387,7 +398,7 @@ class TrtLlmNvFp4ExpertsModular(TrtLlmNvFp4ExpertsBase, mk.FusedMoEExpertsModula
398 local_num_experts=self.local_num_experts,
399 routed_scaling_factor=None,
400 routing_method_type=1, # not used
390 - do_finalize=True,
401 + do_finalize=output is not None,
402 activation_type=activation_to_flashinfer_int(activation),
403 per_token_scale=per_token_scale,
404 output=output,
@@ -395,6 +406,16 @@ class TrtLlmNvFp4ExpertsModular(TrtLlmNvFp4ExpertsBase, mk.FusedMoEExpertsModula
406 fi_moe_largest_bucket(self.moe_config), self._get_chunk_size()
407 ),
408 )
409 + if output is not None:
410 + return None
411 + routed_output = convert_flashinfer_moe_output(
412 + flashinfer_output,
413 + do_finalize=False,
414 + num_tokens=hidden_states.shape[0],
415 + top_k=self.topk,
416 + )
417 + assert isinstance(routed_output, UnfinalizedMoEOutput)
418 + return routed_output
419
420 def apply(
421 self,
@@ -413,7 +434,7 @@ class TrtLlmNvFp4ExpertsModular(TrtLlmNvFp4ExpertsBase, mk.FusedMoEExpertsModula
434 workspace2: torch.Tensor,
435 expert_tokens_meta: mk.ExpertTokensMetadata | None,
436 apply_router_weight_on_input: bool,
416 - ):
437 + ) -> UnfinalizedMoEOutput | None:
438 assert self._supports_activation(activation)
439 # Per-token defers input quant to _invoke_kernel, so a1q_scale is None.
440 assert a1q_scale is not None or self.per_token_activation
@@ -425,8 +446,12 @@ class TrtLlmNvFp4ExpertsModular(TrtLlmNvFp4ExpertsBase, mk.FusedMoEExpertsModula
446 chunk_size = self._get_chunk_size()
447
448 if chunk_size >= M:
428 - self._invoke_kernel(
429 - output,
449 + # Each launch permutes into its own buffer, so only a run that fits
450 + # in one launch can leave the top-k reduction to a deferring
451 + # consumer.
452 + defer = self.moe_config.should_defer_moe_finalize(M)
453 + return self._invoke_kernel(
454 + None if defer else output,
455 hidden_states,
456 w1,
457 w2,
@@ -450,6 +475,7 @@ class TrtLlmNvFp4ExpertsModular(TrtLlmNvFp4ExpertsBase, mk.FusedMoEExpertsModula
475 global_num_experts,
476 None if a1q_scale is None else a1q_scale[start:end],
477 )
478 + return None
479
480
481 class TrtLlmNvFp4ExpertsMonolithic(
vllm/model_executor/layers/fused_moe/fused_moe_method_base.py
+2 -2
@@ -148,7 +148,7 @@ class FusedMoEMethodBase(QuantizeMethodBase):
148 topk_ids: torch.Tensor,
149 shared_experts: "SharedExperts | None",
150 shared_experts_input: torch.Tensor | None,
151 - ) -> torch.Tensor:
151 + ) -> torch.Tensor | UnfinalizedMoEOutput:
152 """Apply the MoE operation using modular kernels.
153
154 Args:
@@ -160,7 +160,7 @@ class FusedMoEMethodBase(QuantizeMethodBase):
160 shared_experts: The shared experts module, if any
161
162 Returns:
163 - Output tensor from routed experts.
163 + Finalized routed states or a deferred-finalize output.
164
165 """
166 raise NotImplementedError
vllm/model_executor/layers/fused_moe/modular_kernel.py
+38 -13
@@ -238,6 +238,15 @@ class FusedMoEPrepareAndFinalize(ABC):
238 """
239 return False
240
241 + def supports_deferred_moe_finalize(self) -> bool:
242 + """Whether ``finalize`` can be skipped for a deferring consumer.
243 +
244 + An implementation opts in only if everything it does in ``finalize``
245 + -- the top-k reduction, and any combine or reduce-scatter -- is work
246 + the consumer takes over.
247 + """
248 + return False
249 +
250
251 # TODO: pass FusedMoEParallelConfig in as ctor parameter?
252 class FusedMoEPrepareAndFinalizeModular(FusedMoEPrepareAndFinalize):
@@ -404,9 +413,6 @@ class FusedMoEPrepareAndFinalizeMonolithic(FusedMoEPrepareAndFinalize):
413 described above for the monolithic case.
414 """
415
407 - def supports_deferred_moe_finalize(self) -> bool:
408 - return False
409 -
416 @abstractmethod
417 def prepare(
418 self,
@@ -895,10 +901,14 @@ class FusedMoEExpertsModular(FusedMoEExperts):
901 workspace2: torch.Tensor,
902 expert_tokens_meta: ExpertTokensMetadata | None,
903 apply_router_weight_on_input: bool,
898 - ) -> None:
904 + ) -> UnfinalizedMoEOutput | None:
905 """This function computes the intermediate result of a Mixture of Experts
906 (MoE) layer using two sets of weights, w1 and w2.
907
908 + Writes into `output` and returns None, unless the implementation stopped
909 + after GEMM2 and left the top-k reduction to a fused consumer, in which
910 + case `output` is untouched and the unfinalized result is returned.
911 +
912 Args:
913 output: (torch.Tensor): The unweighted, unreduced output tensor.
914 hidden_states: (torch.Tensor): The (quantized) input tensor to the MoE
@@ -1270,7 +1280,7 @@ class FusedMoEKernelModularImpl:
1280 apply_router_weight_on_input: bool,
1281 expert_tokens_meta: ExpertTokensMetadata | None,
1282 output_alias: torch.Tensor | None = None,
1273 - ) -> torch.Tensor:
1283 + ) -> torch.Tensor | UnfinalizedMoEOutput:
1284 _, M_full, N, K, top_k = self.fused_experts.moe_problem_size(
1285 a1q, w1, w2, topk_ids
1286 )
@@ -1318,7 +1328,7 @@ class FusedMoEKernelModularImpl:
1328 elif use_output_alias:
1329 fused_out = output_alias
1330
1321 - self.fused_experts.apply(
1331 + unfinalized = self.fused_experts.apply(
1332 output=fused_out,
1333 hidden_states=a1q,
1334 w1=w1,
@@ -1336,6 +1346,11 @@ class FusedMoEKernelModularImpl:
1346 apply_router_weight_on_input=apply_router_weight_on_input,
1347 )
1348
1349 + # Experts that stopped after GEMM2 wrote nothing into `fused_out`; the
1350 + # top-k reduction they left open belongs to whoever takes this.
1351 + if isinstance(unfinalized, UnfinalizedMoEOutput):
1352 + return unfinalized
1353 +
1354 return fused_out
1355
1356 def _finalize(
@@ -1426,7 +1441,7 @@ class FusedMoEKernelModularImpl:
1441 apply_router_weight_on_input: bool = False,
1442 shared_experts: SharedExperts | None = None,
1443 shared_experts_input: torch.Tensor | None = None,
1429 - ) -> torch.Tensor:
1444 + ) -> torch.Tensor | UnfinalizedMoEOutput:
1445 """This function computes a Mixture of Experts (MoE) layer using two sets
1446 of weights, w1 and w2, and top-k gating mechanism.
1447
@@ -1453,7 +1468,9 @@ class FusedMoEKernelModularImpl:
1468 hidden_states before latent projection.
1469
1470 Returns:
1456 - torch.Tensor: The output tensor after applying the MoE layer.
1471 + torch.Tensor: The output tensor after applying the MoE layer, or
1472 + the unfinalized output when the experts left the top-k reduction to
1473 + a fused consumer.
1474
1475 """
1476 output = torch.empty_like(hidden_states)
@@ -1498,6 +1515,17 @@ class FusedMoEKernelModularImpl:
1515 if lora_ctx is not None:
1516 lora_ctx.original_hidden_states = None
1517
1518 + if isinstance(fused_out, UnfinalizedMoEOutput):
1519 + # Nothing below can run on an unfinalized output: finalize is the
1520 + # local top-k reduction (and, for DP/EP, the combine) that the
1521 + # consumer takes over.
1522 + if not self.prepare_finalize.supports_deferred_moe_finalize():
1523 + raise RuntimeError(
1524 + f"{type(self.prepare_finalize).__name__} cannot pass through "
1525 + "a deferred MoE output."
1526 + )
1527 + return fused_out
1528 +
1529 return self._finalize(
1530 output,
1531 fused_out,
@@ -1653,10 +1681,7 @@ class FusedMoEKernel:
1681 return self.prepare_finalize.output_is_reduced()
1682
1683 def supports_deferred_moe_finalize(self) -> bool:
1656 - return (
1657 - isinstance(self.prepare_finalize, FusedMoEPrepareAndFinalizeMonolithic)
1658 - and self.prepare_finalize.supports_deferred_moe_finalize()
1659 - )
1684 + return self.prepare_finalize.supports_deferred_moe_finalize()
1685
1686 def apply_monolithic(
1687 self,
@@ -1703,7 +1728,7 @@ class FusedMoEKernel:
1728 apply_router_weight_on_input: bool,
1729 shared_experts: SharedExperts | None = None,
1730 shared_experts_input: torch.Tensor | None = None,
1706 - ) -> torch.Tensor:
1731 + ) -> torch.Tensor | UnfinalizedMoEOutput:
1732 assert isinstance(self.impl, FusedMoEKernelModularImpl)
1733 return self.impl.apply(
1734 hidden_states=hidden_states,
vllm/model_executor/layers/fused_moe/moe_output.py
+6 -5
@@ -9,7 +9,7 @@ import torch
9
10 @dataclass
11 class UnfinalizedMoEOutput:
12 - """Unfinalized output of a monolithic MoE kernel.
12 + """Unfinalized output of a MoE kernel.
13
14 Kernels that can stop after GEMM2 (the TRTLLM-Gen ``do_finalize=False`` path)
15 hand back their permuted, unweighted output plus the routing weights and the
@@ -24,10 +24,11 @@ class UnfinalizedMoEOutput:
24
25 # [num_permuted_rows, hidden_dim] permuted, unweighted GEMM2 output.
26 gemm2_permuted: torch.Tensor
27 - # [num_tokens, top_k] routing weights, in the activation dtype -- consumers
28 - # index them as such, and a buffer typed wider than its contents reads as
29 - # garbage. These already carry routed_scaling_factor for routing methods that
30 - # fold it in.
27 + # [num_tokens, top_k] routing weights, in the activation dtype when the
28 + # kernel routed itself and in the router's dtype for modular experts --
29 + # consumers index them as such, and a buffer typed wider than its contents
30 + # reads as garbage. These already carry routed_scaling_factor for routing
31 + # methods that fold it in.
32 expert_weights: torch.Tensor
33 # [num_tokens, top_k] int32 permute map; -1 = expert not local to this rank.
34 expanded_idx_to_permuted_idx: torch.Tensor
vllm/model_executor/layers/fused_moe/prepare_finalize/no_dp_ep.py
+5
@@ -38,6 +38,11 @@ def _quantize_input(
38
39
40 class MoEPrepareAndFinalizeNoDPEPModular(mk.FusedMoEPrepareAndFinalizeModular):
41 + def supports_deferred_moe_finalize(self) -> bool:
42 + # finalize() here is the top-k weight-and-reduce and nothing else,
43 + # which is exactly the work a deferring consumer takes over.
44 + return True
45 +
46 @property
47 def activation_format(self) -> mk.FusedMoEActivationFormat:
48 return mk.FusedMoEActivationFormat.Standard
vllm/model_executor/layers/fused_moe/routed_experts.py
+2 -2
@@ -1224,7 +1224,7 @@ class RoutedExperts(PluggableLayer):
1224 topk_ids: torch.Tensor,
1225 shared_experts: "SharedExperts | None" = None,
1226 shared_experts_input: torch.Tensor | None = None,
1227 - ) -> torch.Tensor:
1227 + ) -> torch.Tensor | UnfinalizedMoEOutput:
1228 """Execute routed experts using the quantization method's apply function.
1229
1230 This is called by the runner after router selection (for modular kernels)
@@ -1239,7 +1239,7 @@ class RoutedExperts(PluggableLayer):
1239 shared_experts_input: Input for shared experts (if any)
1240
1241 Returns:
1242 - Output tensor from routed experts.
1242 + Finalized routed states or a deferred-finalize output.
1243
1244 """
1245 assert not self.quant_method.is_monolithic
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe/compressed_tensors_moe_w4a4_mxfp4.py
+2 -1
@@ -24,6 +24,7 @@ from vllm.model_executor.layers.fused_moe.experts.marlin_moe import (
24 from vllm.model_executor.layers.fused_moe.experts.xpu_moe import (
25 XPUExpertsMxFp4,
26 )
27 +from vllm.model_executor.layers.fused_moe.moe_output import UnfinalizedMoEOutput
28 from vllm.model_executor.layers.fused_moe.oracle.mxfp4 import (
29 B12X_BACKENDS,
30 Mxfp4MoeBackend,
@@ -227,7 +228,7 @@ class CompressedTensorsW4A4Mxfp4MoEMethod(CompressedTensorsMoEMethod):
228 topk_ids: torch.Tensor,
229 shared_experts: SharedExperts | None,
230 shared_experts_input: torch.Tensor | None,
230 - ) -> torch.Tensor:
231 + ) -> torch.Tensor | UnfinalizedMoEOutput:
232 assert self.moe_kernel is not None
233 return self.moe_kernel.apply(
234 x,
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe/compressed_tensors_moe_w4a4_nvfp4.py
+1 -1
@@ -303,7 +303,7 @@ class CompressedTensorsW4A4Nvfp4MoEMethod(CompressedTensorsMoEMethod):
303 topk_ids: torch.Tensor,
304 shared_experts: SharedExperts | None,
305 shared_experts_input: torch.Tensor | None,
306 - ) -> torch.Tensor:
306 + ) -> torch.Tensor | UnfinalizedMoEOutput:
307 assert self.moe_kernel is not None
308 return self.moe_kernel.apply(
309 x,
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe/compressed_tensors_moe_w8a8_fp8.py
+2 -1
@@ -18,6 +18,7 @@ from vllm.model_executor.layers.fused_moe.config import (
18 FusedMoEConfig,
19 FusedMoEQuantConfig,
20 )
21 +from vllm.model_executor.layers.fused_moe.moe_output import UnfinalizedMoEOutput
22 from vllm.model_executor.layers.fused_moe.oracle.fp8 import (
23 convert_to_fp8_moe_kernel_format,
24 make_fp8_moe_kernel,
@@ -375,7 +376,7 @@ class CompressedTensorsW8A8Fp8MoEMethod(CompressedTensorsMoEMethod):
376 topk_ids: torch.Tensor,
377 shared_experts: SharedExperts | None,
378 shared_experts_input: torch.Tensor | None,
378 - ) -> torch.Tensor:
379 + ) -> torch.Tensor | UnfinalizedMoEOutput:
380 assert not self.is_monolithic
381 assert self.moe_kernel is not None
382 return self.moe_kernel.apply(
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe/compressed_tensors_moe_w8a8_mxfp8.py
+2 -1
@@ -12,6 +12,7 @@ from vllm.model_executor.layers.fused_moe.config import (
12 FusedMoEConfig,
13 FusedMoEQuantConfig,
14 )
15 +from vllm.model_executor.layers.fused_moe.moe_output import UnfinalizedMoEOutput
16 from vllm.model_executor.layers.fused_moe.oracle.fp8 import (
17 convert_to_fp8_moe_kernel_format,
18 make_fp8_moe_kernel,
@@ -188,7 +189,7 @@ class CompressedTensorsW8A8Mxfp8MoEMethod(CompressedTensorsMoEMethod):
189 topk_ids: torch.Tensor,
190 shared_experts: SharedExperts | None,
191 shared_experts_input: torch.Tensor | None,
191 - ) -> torch.Tensor:
192 + ) -> torch.Tensor | UnfinalizedMoEOutput:
193 assert not self.is_monolithic
194 assert self.moe_kernel is not None
195 return self.moe_kernel.apply(
vllm/model_executor/layers/quantization/fp8.py
+2 -1
@@ -24,6 +24,7 @@ from vllm.model_executor.layers.fused_moe import (
24 UnquantizedFusedMoEMethod,
25 )
26 from vllm.model_executor.layers.fused_moe.config import FusedMoEQuantConfig
27 +from vllm.model_executor.layers.fused_moe.moe_output import UnfinalizedMoEOutput
28 from vllm.model_executor.layers.fused_moe.oracle.fp8 import (
29 convert_to_fp8_moe_kernel_format,
30 make_fp8_moe_kernel,
@@ -827,7 +828,7 @@ class Fp8MoEMethod(FusedMoEMethodBase):
828 topk_ids: torch.Tensor,
829 shared_experts: SharedExperts | None,
830 shared_experts_input: torch.Tensor | None,
830 - ) -> torch.Tensor:
831 + ) -> torch.Tensor | UnfinalizedMoEOutput:
832 assert not self.is_monolithic
833 assert self.moe_kernel is not None
834 return self.moe_kernel.apply(
vllm/model_executor/layers/quantization/inc/schemes/inc_mxfp4_moe.py
+2 -1
@@ -23,6 +23,7 @@ from vllm.model_executor.layers.fused_moe.experts.marlin_moe import (
23 from vllm.model_executor.layers.fused_moe.fused_moe_method_base import (
24 FusedMoEMethodBase,
25 )
26 +from vllm.model_executor.layers.fused_moe.moe_output import UnfinalizedMoEOutput
27 from vllm.model_executor.layers.fused_moe.oracle.mxfp4 import (
28 B12X_BACKENDS,
29 Mxfp4MoeBackend,
@@ -228,7 +229,7 @@ class INCMxfp4MoEMethod(FusedMoEMethodBase):
229 topk_ids: torch.Tensor,
230 shared_experts: SharedExperts | None,
231 shared_experts_input: torch.Tensor | None,
231 - ) -> torch.Tensor:
232 + ) -> torch.Tensor | UnfinalizedMoEOutput:
233 assert self.moe_kernel is not None
234 return self.moe_kernel.apply(
235 x,
vllm/model_executor/layers/quantization/inc/schemes/inc_mxfp8_moe.py
+2 -1
@@ -12,6 +12,7 @@ from vllm.model_executor.layers.fused_moe import (
12 from vllm.model_executor.layers.fused_moe.fused_moe_method_base import (
13 FusedMoEMethodBase,
14 )
15 +from vllm.model_executor.layers.fused_moe.moe_output import UnfinalizedMoEOutput
16 from vllm.model_executor.layers.fused_moe.oracle.fp8 import (
17 convert_to_fp8_moe_kernel_format,
18 make_fp8_moe_kernel,
@@ -179,7 +180,7 @@ class INCMxfp8MoEMethod(FusedMoEMethodBase):
180 topk_ids: torch.Tensor,
181 shared_experts: SharedExperts | None,
182 shared_experts_input: torch.Tensor | None,
182 - ) -> torch.Tensor:
183 + ) -> torch.Tensor | UnfinalizedMoEOutput:
184 assert self.moe_kernel is not None
185 return self.moe_kernel.apply(
186 x,
vllm/model_executor/layers/quantization/modelopt.py
+3 -3
@@ -690,7 +690,7 @@ class ModelOptFp8MoEMethod(FusedMoEMethodBase):
690 topk_ids: torch.Tensor,
691 shared_experts: SharedExperts | None,
692 shared_experts_input: torch.Tensor | None,
693 - ) -> torch.Tensor:
693 + ) -> torch.Tensor | UnfinalizedMoEOutput:
694 assert not self.is_monolithic
695 assert self.moe_kernel is not None
696 return self.moe_kernel.apply(
@@ -1102,7 +1102,7 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1102 topk_ids: torch.Tensor,
1103 shared_experts: SharedExperts | None,
1104 shared_experts_input: torch.Tensor | None,
1105 - ) -> torch.Tensor:
1105 + ) -> torch.Tensor | UnfinalizedMoEOutput:
1106 assert not self.is_monolithic
1107 assert self.moe_kernel is not None
1108 return self.moe_kernel.apply(
@@ -1471,7 +1471,7 @@ class ModelOptMxFp8FusedMoE(FusedMoEMethodBase):
1471 topk_ids: torch.Tensor,
1472 shared_experts: SharedExperts | None,
1473 shared_experts_input: torch.Tensor | None,
1474 - ) -> torch.Tensor:
1474 + ) -> torch.Tensor | UnfinalizedMoEOutput:
1475 assert not self.is_monolithic
1476 assert self.moe_kernel is not None
1477 return self.moe_kernel.apply(
vllm/model_executor/layers/quantization/mxfp4.py
+2 -2
@@ -433,7 +433,7 @@ class GptOssMxfp4MoEMethod(FusedMoEMethodBase):
433 topk_ids: torch.Tensor,
434 shared_experts: SharedExperts | None,
435 shared_experts_input: torch.Tensor | None,
436 - ) -> torch.Tensor:
436 + ) -> torch.Tensor | UnfinalizedMoEOutput:
437 assert not self.is_monolithic
438 assert self.moe_kernel is not None
439 return self.moe_kernel.apply(
@@ -860,7 +860,7 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
860 topk_ids: torch.Tensor,
861 shared_experts: SharedExperts | None,
862 shared_experts_input: torch.Tensor | None,
863 - ) -> torch.Tensor:
863 + ) -> torch.Tensor | UnfinalizedMoEOutput:
864 assert not self.is_monolithic
865 assert self.moe_kernel is not None
866 return self.moe_kernel.apply(
vllm/model_executor/layers/quantization/online/moe_base.py
+1 -1
@@ -166,7 +166,7 @@ class OnlineMoEMethodBase(FusedMoEMethodBase):
166 topk_ids: torch.Tensor,
167 shared_experts: SharedExperts | None,
168 shared_experts_input: torch.Tensor | None,
169 - ) -> torch.Tensor:
169 + ) -> torch.Tensor | UnfinalizedMoEOutput:
170 assert not self.is_monolithic
171 assert self.moe_kernel is not None
172 return self.moe_kernel.apply(
vllm/model_executor/layers/quantization/quark/quark_moe.py
+3 -3
@@ -976,7 +976,7 @@ class QuarkW8A8Fp8MoEMethod(QuarkMoEMethod):
976 topk_ids: torch.Tensor,
977 shared_experts: SharedExperts | None,
978 shared_experts_input: torch.Tensor | None,
979 - ) -> torch.Tensor:
979 + ) -> torch.Tensor | UnfinalizedMoEOutput:
980 assert self.moe_kernel is not None
981 return self.moe_kernel.apply(
982 hidden_states=x,
@@ -1935,7 +1935,7 @@ class QuarkOCP_MX_MoEMethod(QuarkMoEMethod):
1935 topk_ids: torch.Tensor,
1936 shared_experts: SharedExperts | None,
1937 shared_experts_input: torch.Tensor | None,
1938 - ) -> torch.Tensor:
1938 + ) -> torch.Tensor | UnfinalizedMoEOutput:
1939 assert self.moe_kernel is not None
1940 return self.moe_kernel.apply(
1941 hidden_states=x,
@@ -2182,7 +2182,7 @@ class QuarkNvfp4MoEMethod(QuarkMoEMethod):
2182 topk_ids: torch.Tensor,
2183 shared_experts: SharedExperts | None,
2184 shared_experts_input: torch.Tensor | None,
2185 - ) -> torch.Tensor:
2185 + ) -> torch.Tensor | UnfinalizedMoEOutput:
2186 assert self.moe_kernel is not None
2187 return self.moe_kernel.apply(
2188 x,
vllm/models/kimi_k3/nvidia/latent_moe_runner.py
+4 -10
@@ -115,10 +115,11 @@ class LatentMoERunner(MoERunner):
115 experts_cls = getattr(self._quant_method, "experts_cls", None)
116 # The kernel instance is built after weight loading, while the tail
117 # must register its CuTeDSL warmup units during runner construction.
118 - self.moe_config.defer_moe_finalize = (
118 + if (
119 experts_cls is TrtLlmMxfp4ExpertsMonolithic
120 or experts_cls is TrtLlmNvFp4ExpertsMonolithic
121 - ) and self.moe_config.hidden_dim == self.moe_config.hidden_dim_unpadded
121 + ) and self.moe_config.hidden_dim == self.moe_config.hidden_dim_unpadded:
122 + self.moe_config.defer_moe_finalize()
123 from vllm.models.kimi_k3.nvidia.ops.latent_moe_tail import (
124 KimiK3LatentMoETailOp,
125 )
@@ -136,20 +137,13 @@ class LatentMoERunner(MoERunner):
137 rms_eps=norm.variance_epsilon,
138 )
139 self._k3_latent_moe_tail_op = op
139 - self.moe_config.defer_moe_finalize_max_num_tokens = (
140 - op.contract.max_num_tokens
141 - if self.moe_config.use_deferred_moe_finalize
142 - else -1
143 - )
140 if self.moe_config.use_deferred_moe_finalize:
141 + self.moe_config.limit_deferred_moe_finalize(op.contract.max_num_tokens)
142 logger.info_once(
143 "K3 latent-MoE tail fusion with deferred top-k finalization "
144 "is enabled for up to %d tokens.",
145 op.contract.max_num_tokens,
146 )
150 - else:
151 - self.moe_config.defer_moe_finalize = False
152 - self.moe_config.defer_moe_finalize_max_num_tokens = -1
147
148 def _get_zero_residual(
149 self,