[ROCm]Remove special-case SiTU support model-specific gating (#50597)
Signed-off-by: Stacy Roberts <sroberts@amd.com> Signed-off-by: Stacy Roberts <robers23@utexas.edu> Co-authored-by: Stacy Roberts <robers23@utexas.edu>
sroberts-amd committed
Aug 14, 2026 at 13:38 UTC
c794754062d49a8fdb63ab3c5215b488b865030c
3 files changed
+97
-146
vllm/model_executor/layers/fused_moe/experts/rocm_aiter_moe.py
+1
@@ -484,6 +484,7 @@ class AiterExperts(mk.FusedMoEExpertsModular):
484
return activation in [
485
MoEActivation.SILU,
486
MoEActivation.GELU,
487
+ MoEActivation.SITU,
488
MoEActivation.SWIGLUOAI,
489
MoEActivation.SWIGLUOAI_UNINTERLEAVE,
490
]
vllm/model_executor/layers/fused_moe/oracle/mxfp4.py
+45
-19
@@ -644,20 +644,7 @@ def mxfp4_round_up_hidden_size_and_intermediate_size(
644
activation: MoEActivation | None = None,
645
) -> tuple[int, int]:
646
"""Round up hidden_size and intermediate_size based on backend requirements."""
647
- if backend == Mxfp4MoeBackend.AITER_MXFP4_BF16 and activation == MoEActivation.SITU:
648
- # K3's AITER A16W4 SiTU kernel handles K3's native intermediate size
649
- # (moe_intermediate 3072; e.g. 384/partition at TP8). Align to 128 (a
650
- # no-op for K3's shapes) rather than the generic ROCm 256 round-up,
651
- # which would inflate weights and OOM.
652
- intermediate_size = round_up(intermediate_size, 128)
653
- hidden_size = round_up(hidden_size, 128)
654
- elif (
655
- backend == Mxfp4MoeBackend.AITER_MXFP4_BF16 and activation == MoEActivation.SILU
656
- ):
657
- # AITER's A16W4 SiLU kernel handles native dimensions aligned to 128.
658
- intermediate_size = round_up(intermediate_size, 128)
659
- hidden_size = round_up(hidden_size, 128)
660
- elif backend == Mxfp4MoeBackend.EMULATION:
647
+ if backend == Mxfp4MoeBackend.EMULATION:
648
# Emulation has no kernel tile; it only needs OCP MX block alignment so the
649
# per-block scale buffers (`dim // OCP_MX_BLOCK_SIZE`) aren't floor-truncated
650
# by a non-block-aligned TP/DP shard (e.g. 2880 // 4 = 720).
@@ -683,8 +670,18 @@ def mxfp4_round_up_hidden_size_and_intermediate_size(
670
intermediate_size = round_up(intermediate_size, 128)
671
hidden_size = round_up(hidden_size, 128)
672
elif current_platform.is_rocm():
686
- intermediate_size = round_up(intermediate_size, 256)
687
- hidden_size = round_up(hidden_size, 256)
673
+ if backend == Mxfp4MoeBackend.AITER_MXFP4_BF16 and (
674
+ activation == MoEActivation.SITU or activation == MoEActivation.SILU
675
+ ):
676
+ # K3's AITER A16W4 SiTU kernel handles K3's native intermediate size
677
+ # (moe_intermediate 3072; e.g. 384/partition at TP8). Align to 128 (a
678
+ # no-op for K3's shapes) rather than the generic ROCm 256 round-up,
679
+ # which would inflate weights and OOM.
680
+ intermediate_size = round_up(intermediate_size, 128)
681
+ hidden_size = round_up(hidden_size, 128)
682
+ else:
683
+ intermediate_size = round_up(intermediate_size, 256)
684
+ hidden_size = round_up(hidden_size, 256)
685
elif backend == Mxfp4MoeBackend.CPU:
686
# CPU AMX kernel uses BLOCK_N=32, align to 32
687
intermediate_size = round_up(intermediate_size, 32)
@@ -1280,6 +1277,7 @@ def convert_weight_to_mxfp4_moe_kernel_format(
1277
w13_bias: torch.Tensor | None = None,
1278
w2_bias: torch.Tensor | None = None,
1279
_cache_permute_indices: dict[torch.Size, torch.Tensor] | None = None,
1280
+ activation: MoEActivation | None = None,
1281
) -> tuple[
1282
torch.Tensor,
1283
torch.Tensor,
@@ -1479,13 +1477,41 @@ def convert_weight_to_mxfp4_moe_kernel_format(
1477
1478
import os
1479
1482
- from aiter.ops.shuffle import shuffle_scale as _shuf_s
1483
- from aiter.ops.shuffle import shuffle_weight as _shuf_w
1484
-
1480
# TODO: Remove this once AITER is fixed
1481
# Necessary for AITER side from crashing
1482
os.environ["AITER_BF16_FP8_MOE_BOUND"] = "0"
1483
1484
+ if activation == MoEActivation.SITU:
1485
+ from aiter.utility.fp4_utils import e8m0_shuffle
1486
+
1487
+ from vllm._aiter_ops import rocm_aiter_ops
1488
+
1489
+ fp4_dtype = torch.float4_e2m1fn_x2
1490
+ e8m0_dtype = torch.float8_e8m0fnu
1491
+ # a8w4 uses gate/up-interleaved flydsl kernels;
1492
+ # default a16w4 keeps the separated layout.
1493
+ guinterleave = rocm_aiter_ops.is_fused_moe_situv2_a8w4_enabled()
1494
+ w13 = rocm_aiter_ops.shuffle_weight_a16w4(
1495
+ w13_weight.data.view(fp4_dtype), 16, guinterleave
1496
+ )
1497
+ w2 = rocm_aiter_ops.shuffle_weight_a16w4(
1498
+ w2_weight.data.view(fp4_dtype), 16, False
1499
+ )
1500
+ w13_scale_raw = w13_weight_scale.data.view(e8m0_dtype)
1501
+ w2_scale_raw = w2_weight_scale.data.view(e8m0_dtype)
1502
+ w13_scale = rocm_aiter_ops.shuffle_scale_a16w4(
1503
+ w13_scale_raw.view(-1, w13_scale_raw.shape[-1]),
1504
+ num_experts,
1505
+ guinterleave,
1506
+ )
1507
+ w2_scale = e8m0_shuffle(w2_scale_raw.view(-1, w2_scale_raw.shape[-1]))
1508
+ w13.is_shuffled = True
1509
+ w2.is_shuffled = True
1510
+ return (w13, w2, w13_scale, w2_scale, w13_bias, w2_bias)
1511
+
1512
+ from aiter.ops.shuffle import shuffle_scale as _shuf_s
1513
+ from aiter.ops.shuffle import shuffle_weight as _shuf_w
1514
+
1515
w13_weight = torch.nn.Parameter(
1516
_shuf_w(
1517
w13_weight.data.view(torch.float4_e2m1fn_x2),
vllm/model_executor/layers/quantization/mxfp4.py
+51
-127
@@ -1,8 +1,6 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
-import os
5
-
4
import torch
5
6
from vllm.logger import init_logger
@@ -22,7 +20,6 @@ from vllm.model_executor.layers.fused_moe.config import (
20
from vllm.model_executor.layers.fused_moe.oracle.mxfp4 import (
21
TRITON_BACKENDS,
22
Mxfp4MoeBackend,
25
- backend_to_kernel_cls,
23
convert_gpt_oss_weight_to_mxfp4_moe_kernel_format,
24
convert_weight_to_mxfp4_moe_kernel_format,
25
make_mxfp4_moe_kernel,
@@ -472,51 +469,14 @@ class GptOssMxfp4MoEMethod(FusedMoEMethodBase):
469
)
470
471
475
-def _use_k3_situ_aiter(moe: FusedMoEConfig) -> bool:
476
- """Whether Kimi-K3's SiTU MXFP4 MoE should use the AITER A16W4 kernel.
477
-
478
- K3 is weight-only MXFP4 (W4A16) with SiTU activation, which the generic
479
- MXFP4 backend selector does not cover; route it to AITER on gfx950.
480
- """
481
- if not current_platform.is_rocm():
482
- return False
483
- from vllm._aiter_ops import rocm_aiter_ops
484
- from vllm.model_executor.layers.fused_moe.activation import MoEActivation
485
- from vllm.platforms.rocm import on_gfx950
486
-
487
- return (
488
- rocm_aiter_ops.is_fused_moe_enabled()
489
- and on_gfx950()
490
- and moe.activation == MoEActivation.SITU
491
- and moe.activation_situ_linear_beta is not None
492
- and rocm_aiter_ops.get_aiter_activation_type("situ") is not None
493
- )
494
-
495
-
472
class Mxfp4MoEMethod(FusedMoEMethodBase):
473
"""MXFP4 MoE quantization method."""
474
475
def __init__(self, moe: FusedMoEConfig):
476
super().__init__(moe)
477
+
478
self.weight_dtype = "mxfp4"
502
- self.is_k3_situ_aiter = _use_k3_situ_aiter(moe)
503
- self.experts_cls: type[mk.FusedMoEExperts] | None
504
- if self.is_k3_situ_aiter:
505
- self.mxfp4_backend = Mxfp4MoeBackend.AITER_MXFP4_BF16
506
- self.experts_cls = backend_to_kernel_cls(self.mxfp4_backend)[0]
507
- logger.info_once("Using AITER_MXFP4_BF16 for Kimi-K3 SiTU MXFP4 MoE.")
508
- from vllm._aiter_ops import rocm_aiter_ops
509
-
510
- if rocm_aiter_ops.is_fused_moe_situv2_a8w4_enabled():
511
- # AITER keeps bf16 activations below this token count, which
512
- # would not match the fp8 a8w4 kernels the interleaved SiTU
513
- # path is tuned for. The a16w4 path never reads it.
514
- # TODO: Remove once AITER takes this as a kernel argument.
515
- os.environ["AITER_BF16_FP8_MOE_BOUND"] = "0"
516
- else:
517
- self.mxfp4_backend, self.experts_cls = select_deepseek_v4_mxfp4_moe_backend(
518
- moe
519
- )
479
+ self.mxfp4_backend, self.experts_cls = select_deepseek_v4_mxfp4_moe_backend(moe)
480
481
self.max_capture_size = moe.max_capture_size
482
@@ -714,62 +674,62 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
674
hidden_size = self.hidden_size
675
sf_block_size = 32
676
717
- # Shape assertions
718
- assert (
719
- w13.dim() == 3
720
- and w13.shape[0] == num_experts
721
- and w13.shape[1] == intermediate_size * self.moe.w13_num_shards
722
- and w13.shape[2] == hidden_size // 2
723
- )
724
- assert (
725
- w13_scale.dim() == 3
726
- and w13_scale.shape[0] == num_experts
727
- and w13_scale.shape[1] == intermediate_size * self.moe.w13_num_shards
728
- and w13_scale.shape[2] == hidden_size // sf_block_size
729
- )
730
- assert (
731
- w2.dim() == 3
732
- and w2.shape[0] == num_experts
733
- and w2.shape[1] == hidden_size
734
- and w2.shape[2] == intermediate_size // 2
735
- )
736
- assert (
737
- w2_scale.dim() == 3
738
- and w2_scale.shape[1] == hidden_size
739
- and w2_scale.shape[2] == intermediate_size // sf_block_size
740
- )
741
- if w13_bias is not None:
677
+ # Shape assertions — skipped for SITU since its kernel handles native
678
+ # (non-256-aligned) intermediate sizes without prior round-up.
679
+ from vllm.model_executor.layers.fused_moe.activation import MoEActivation
680
+
681
+ if self.moe.activation != MoEActivation.SITU:
682
assert (
743
- w13_bias.dim() == 2
744
- and w13_bias.shape[0] == num_experts
745
- and w13_bias.shape[1] == intermediate_size * self.moe.w13_num_shards
683
+ w13.dim() == 3
684
+ and w13.shape[0] == num_experts
685
+ and w13.shape[1] == intermediate_size * self.moe.w13_num_shards
686
+ and w13.shape[2] == hidden_size // 2
687
)
747
- if w2_bias is not None:
688
assert (
749
- w2_bias.dim() == 2
750
- and w2_bias.shape[0] == num_experts
751
- and w2_bias.shape[1] == hidden_size
689
+ w13_scale.dim() == 3
690
+ and w13_scale.shape[0] == num_experts
691
+ and w13_scale.shape[1] == intermediate_size * self.moe.w13_num_shards
692
+ and w13_scale.shape[2] == hidden_size // sf_block_size
693
)
753
-
754
- # Convert weights to kernel format
755
- if self.is_k3_situ_aiter:
756
- w13, w2, w13_scale, w2_scale = (
757
- self._convert_k3_situ_weight_to_kernel_format(layer)
694
+ assert (
695
+ w2.dim() == 3
696
+ and w2.shape[0] == num_experts
697
+ and w2.shape[1] == hidden_size
698
+ and w2.shape[2] == intermediate_size // 2
699
)
759
- else:
760
- w13, w2, w13_scale, w2_scale, w13_bias, w2_bias = (
761
- convert_weight_to_mxfp4_moe_kernel_format(
762
- mxfp4_backend=self.mxfp4_backend,
763
- layer=layer,
764
- w13_weight=w13,
765
- w2_weight=w2,
766
- w13_weight_scale=w13_scale,
767
- w2_weight_scale=w2_scale,
768
- w13_bias=w13_bias,
769
- w2_bias=w2_bias,
770
- _cache_permute_indices=self._cache_permute_indices,
700
+ assert (
701
+ w2_scale.dim() == 3
702
+ and w2_scale.shape[1] == hidden_size
703
+ and w2_scale.shape[2] == intermediate_size // sf_block_size
704
+ )
705
+ if w13_bias is not None:
706
+ assert (
707
+ w13_bias.dim() == 2
708
+ and w13_bias.shape[0] == num_experts
709
+ and w13_bias.shape[1] == intermediate_size * self.moe.w13_num_shards
710
)
711
+ if w2_bias is not None:
712
+ assert (
713
+ w2_bias.dim() == 2
714
+ and w2_bias.shape[0] == num_experts
715
+ and w2_bias.shape[1] == hidden_size
716
+ )
717
+
718
+ # Convert weights to kernel format
719
+ w13, w2, w13_scale, w2_scale, w13_bias, w2_bias = (
720
+ convert_weight_to_mxfp4_moe_kernel_format(
721
+ mxfp4_backend=self.mxfp4_backend,
722
+ layer=layer,
723
+ w13_weight=w13,
724
+ w2_weight=w2,
725
+ w13_weight_scale=w13_scale,
726
+ w2_weight_scale=w2_scale,
727
+ w13_bias=w13_bias,
728
+ w2_bias=w2_bias,
729
+ _cache_permute_indices=self._cache_permute_indices,
730
+ activation=self.moe.activation,
731
)
732
+ )
733
734
# For TRITON backends, weights are wrapped tensors from triton_kernels
735
# that don't support .detach(). Manually assign parameters.
@@ -810,42 +770,6 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
770
routing_tables=layer._expert_routing_tables(),
771
)
772
813
- def _convert_k3_situ_weight_to_kernel_format(
814
- self, layer: RoutedExperts
815
- ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
816
- # K3's AITER A16W4 kernel wants the separated ([gate_all, up_all])
817
- # stage-1 layout, unlike the interleaved gpt-oss/DeepSeek path in
818
- # convert_weight_to_mxfp4_moe_kernel_format. Preshuffle once here.
819
- from aiter.utility.fp4_utils import e8m0_shuffle
820
-
821
- from vllm._aiter_ops import rocm_aiter_ops
822
-
823
- fp4_dtype = torch.float4_e2m1fn_x2
824
- e8m0_dtype = torch.float8_e8m0fnu
825
- num_experts = layer.w13_weight.shape[0]
826
-
827
- # a8w4 (VLLM_ROCM_USE_AITER_MOE_SITUV2_A8W4=1) uses the gate/up-
828
- # interleaved (_gui_) fp8 flydsl kernels, which need w13 weight+scale
829
- # in interleave layout. Default a16w4 keeps the separated layout.
830
- guinterleave = rocm_aiter_ops.is_fused_moe_situv2_a8w4_enabled()
831
- w13 = rocm_aiter_ops.shuffle_weight_a16w4(
832
- layer.w13_weight.data.view(fp4_dtype), 16, guinterleave
833
- )
834
- w2 = rocm_aiter_ops.shuffle_weight_a16w4(
835
- layer.w2_weight.data.view(fp4_dtype), 16, False
836
- )
837
- w13_scale_raw = layer.w13_weight_scale.data.view(e8m0_dtype)
838
- w2_scale_raw = layer.w2_weight_scale.data.view(e8m0_dtype)
839
- w13_scale = rocm_aiter_ops.shuffle_scale_a16w4(
840
- w13_scale_raw.view(-1, w13_scale_raw.shape[-1]), num_experts, guinterleave
841
- )
842
- w2_scale = e8m0_shuffle(w2_scale_raw.view(-1, w2_scale_raw.shape[-1]))
843
-
844
- w13.is_shuffled = True
845
- w2.is_shuffled = True
846
-
847
- return w13, w2, w13_scale, w2_scale
848
-
773
def process_weights_after_loading(self, layer):
774
w13 = layer.w13_weight
775
w2 = layer.w2_weight