[Bugfix][CPU] Zero-pad MoE intermediate size for grouped-gemm TP alignment (#49591)

Signed-off-by: jiang1.li <[email protected]>
Co-authored-by: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Li, Jiang
2026-07-27 16:32:23 +08:00
committed by GitHub
co-authored by Claude Sonnet 5
parent cbc3a87200
commit eb290ab673
2 changed files with 211 additions and 14 deletions
+130
View File
@@ -14,6 +14,7 @@ from vllm._custom_ops import (
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
from vllm.model_executor.layers.fused_moe.cpu_fused_moe import (
_CPU_MOE_ACT_FN,
CPUFusedMOE,
)
from vllm.platforms import CpuArchEnum, current_platform
from vllm.utils.torch_utils import set_random_seed
@@ -346,3 +347,132 @@ def test_cpu_fused_moe_int8(
)
torch.testing.assert_close(output, ref_output, atol=2e-2, rtol=2e-2)
# moe_intermediate_size not a multiple of 32, e.g. what tensor-parallel
# sharding of moe_intermediate_size=704 at tp=4 produces (704 // 4 == 176).
UNALIGNED_INTERMEDIATE_DIM = 176
class _StubMoELayer(torch.nn.Module):
"""Minimal stand-in for the real MoE layer module, exposing just what
CPUFusedMOE reads/replaces (w13_weight, w2_weight, activation, and
optionally w13_bias/w2_bias)."""
def __init__(
self,
w13_weight: torch.Tensor,
w2_weight: torch.Tensor,
activation: MoEActivation,
w13_bias: torch.Tensor | None = None,
w2_bias: torch.Tensor | None = None,
):
super().__init__()
self.w13_weight = torch.nn.Parameter(w13_weight, requires_grad=False)
self.w2_weight = torch.nn.Parameter(w2_weight, requires_grad=False)
self.activation = activation
if w13_bias is not None:
self.w13_bias = torch.nn.Parameter(w13_bias, requires_grad=False)
if w2_bias is not None:
self.w2_bias = torch.nn.Parameter(w2_bias, requires_grad=False)
@pytest.mark.parametrize("expert_num", EXPERT_NUM)
@pytest.mark.parametrize("hidden_size", HIDDEN_DIM)
@pytest.mark.parametrize("use_bias", USE_BIAS)
@pytest.mark.parametrize("dtype", DTYPE)
@pytest.mark.parametrize(
"act",
[MoEActivation.SILU, MoEActivation.GELU, MoEActivation.GELU_TANH],
)
def test_cpu_fused_moe_unaligned_intermediate_size(
default_vllm_config,
expert_num: int,
hidden_size: int,
use_bias: bool,
dtype: torch.dtype,
act: MoEActivation,
):
"""An unaligned per-partition moe_intermediate_size must still hit the
grouped-gemm fast path via automatic zero-padding, with numerically
correct output, instead of silently falling back to the much slower
per-expert torch loop."""
if current_platform.get_cpu_architecture() == CpuArchEnum.ARM:
pytest.skip("padding is only applied on the x86 AMX/vector kernels")
set_random_seed(0)
batch_size = 64
intermediate_size = UNALIGNED_INTERMEDIATE_DIM
topk_num = max(expert_num // 2, 1)
up_dim = 2 * intermediate_size
input = torch.randn((batch_size, hidden_size), dtype=dtype) / (
0.5 * hidden_size**0.5
)
w13 = torch.randn((expert_num, up_dim, hidden_size), dtype=dtype) / (
0.5 * hidden_size**0.5
)
w2 = torch.randn((expert_num, hidden_size, intermediate_size), dtype=dtype) / (
0.5 * intermediate_size**0.5
)
router_logits = torch.randn((batch_size, expert_num), dtype=dtype)
w13_bias = None
w2_bias = None
if use_bias:
w13_bias = torch.randn((expert_num, up_dim), dtype=dtype) / (0.5 * up_dim**0.5)
w2_bias = torch.randn((expert_num, hidden_size), dtype=dtype) / (
0.5 * hidden_size**0.5
)
score = torch.softmax(router_logits, dim=-1, dtype=torch.float32)
topk_weight, topk_ids = torch.topk(score, topk_num)
topk_ids = topk_ids.to(torch.int32)
ref_output = ref_fused_moe(
input, w13, w2, w13_bias, w2_bias, topk_weight, topk_ids, act
)
layer = _StubMoELayer(
w13.clone(),
w2.clone(),
act,
w13_bias.clone() if w13_bias is not None else None,
w2_bias.clone() if w2_bias is not None else None,
)
cpu_moe = CPUFusedMOE(layer)
assert cpu_moe.forward_method == cpu_moe.forward_grouped_gemm, (
"expected the padded intermediate size to hit the grouped-gemm fast path"
)
output = cpu_moe.forward_method(
layer, input, topk_weight, topk_ids, act, expert_num, False
)
atol, rtol = get_default_atol(output), get_default_rtol(output)
torch.testing.assert_close(output, ref_output, atol=atol, rtol=rtol)
def test_cpu_fused_moe_unaligned_intermediate_size_swigluoai(default_vllm_config):
"""swigluoai's interleaved gate/up layout isn't padded automatically. On
AMX-capable CPUs this must raise rather than silently falling back to
the (correct but much slower) per-expert torch loop; elsewhere it should
fall back exactly as before."""
set_random_seed(0)
expert_num = 8
hidden_size = 128
intermediate_size = UNALIGNED_INTERMEDIATE_DIM
dtype = torch.bfloat16
up_dim = 2 * intermediate_size
layer = _StubMoELayer(
torch.randn((expert_num, up_dim, hidden_size), dtype=dtype),
torch.randn((expert_num, hidden_size, intermediate_size), dtype=dtype),
MoEActivation.SWIGLUOAI,
)
if torch.cpu._is_amx_tile_supported():
with pytest.raises(RuntimeError):
CPUFusedMOE(layer)
else:
cpu_moe = CPUFusedMOE(layer)
assert cpu_moe.forward_method == cpu_moe.forward_torch
@@ -20,6 +20,11 @@ from vllm.platforms import CpuArchEnum, current_platform
from vllm.utils.torch_utils import direct_register_custom_op
_CPU_MOE_LAYER_CACHE = {}
# The CPU grouped-gemm MoE kernels (AMX and vector) tile the expert
# intermediate ("N") dimension in blocks of this size and have no tail/
# remainder handling, so a shard is only eligible for the fast path when
# its per-partition intermediate size is a multiple of it.
_MOE_GROUPED_GEMM_N_TILE = 32
def _swigluoai_forward_native(
@@ -230,6 +235,7 @@ class CPUFusedMOE:
"""CPU-based fused MoE implementation."""
def __init__(self, layer: torch.nn.Module) -> None:
self._pad_moe_intermediate_for_grouped_gemm(layer)
use_grouped_gemm, isa = self.check_grouped_gemm(layer)
self.isa = isa
if use_grouped_gemm:
@@ -284,6 +290,69 @@ class CPUFusedMOE:
apply_router_weight_on_input,
)
def _pad_moe_intermediate_for_grouped_gemm(self, layer: torch.nn.Module) -> None:
"""Zero-pad the per-partition MoE intermediate dim up to a multiple
of _MOE_GROUPED_GEMM_N_TILE, so the AMX/vector grouped-gemm kernels
can be used even when TP sharding (moe_intermediate_size // tp_size)
lands on an unaligned value (e.g. moe_intermediate_size=704 at tp=4
-> 176). Only applies to the x86 (AMX/vec) kernels and half-split
gate/up activations; interleaved layouts (swigluoai) are left
untouched.
"""
if not hasattr(torch.ops._C, "prepack_moe_weight"):
return
if current_platform.get_cpu_architecture() == CpuArchEnum.ARM:
return
intermediate_size = layer.w2_weight.size(2)
remainder = intermediate_size % _MOE_GROUPED_GEMM_N_TILE
if remainder == 0:
return
if layer.activation == MoEActivation.SWIGLUOAI:
return
pad = _MOE_GROUPED_GEMM_N_TILE - remainder
padded_size = intermediate_size + pad
num_experts, _, hidden_size = layer.w13_weight.shape
new_w13 = layer.w13_weight.new_zeros(num_experts, 2 * padded_size, hidden_size)
new_w13[:, :intermediate_size] = layer.w13_weight[:, :intermediate_size]
new_w13[:, padded_size : padded_size + intermediate_size] = layer.w13_weight[
:, intermediate_size:
]
replace_parameter(layer, "w13_weight", new_w13)
new_w2 = layer.w2_weight.new_zeros(num_experts, hidden_size, padded_size)
new_w2[:, :, :intermediate_size] = layer.w2_weight
replace_parameter(layer, "w2_weight", new_w2)
if hasattr(layer, "w13_bias"):
new_bias = layer.w13_bias.new_zeros(num_experts, 2 * padded_size)
new_bias[:, :intermediate_size] = layer.w13_bias[:, :intermediate_size]
new_bias[:, padded_size : padded_size + intermediate_size] = layer.w13_bias[
:, intermediate_size:
]
replace_parameter(layer, "w13_bias", new_bias)
def _grouped_gemm_alignment_error(self, layer: torch.nn.Module) -> str:
# w2's input size is the per-partition MoE intermediate size (the
# dimension TP-sharding splits), and it's what most commonly breaks
# alignment, e.g. moe_intermediate_size=704 at tp=4 gives
# 704 // 4 == 176, which isn't a multiple of 32.
intermediate_size_per_partition = layer.w2_weight.size(2)
return (
"CPU fused-MoE AMX grouped-gemm kernel cannot be used for a "
f"layer with w13 shape {tuple(layer.w13_weight.shape)} / w2 "
f"shape {tuple(layer.w2_weight.shape)}: the per-partition MoE "
f"intermediate size ({intermediate_size_per_partition}) is not "
f"a multiple of {_MOE_GROUPED_GEMM_N_TILE}, and automatic "
"zero-padding could not resolve this (typically because the "
"activation uses an interleaved gate/up layout, e.g. "
"swigluoai). vLLM refuses to silently fall back to the much "
"slower per-expert torch loop on AMX-capable CPUs; consider a "
"different --tensor-parallel-size."
)
def check_grouped_gemm(
self,
layer: torch.nn.Module,
@@ -297,20 +366,19 @@ class CPUFusedMOE:
w2_input_size = layer.w2_weight.size(2)
w2_output_size = layer.w2_weight.size(1)
if not (w13_output_size % 32 == 0 and w2_output_size % 32 == 0):
return False, "none"
supports_amx = torch.cpu._is_amx_tile_supported()
if (
supports_amx
and dtype == torch.bfloat16
and w13_input_size % 32 == 0
and w2_input_size % 32 == 0
):
return True, "amx"
if supports_amx:
if (
dtype == torch.bfloat16
and w13_output_size % 32 == 0
and w2_output_size % 32 == 0
and w13_input_size % 32 == 0
and w2_input_size % 32 == 0
):
return True, "amx"
raise RuntimeError(self._grouped_gemm_alignment_error(layer))
if not (w13_output_size % 32 == 0 and w2_output_size % 32 == 0):
return False, "none"
supports_neon = current_platform.get_cpu_architecture() == CpuArchEnum.ARM
@@ -321,8 +389,7 @@ class CPUFusedMOE:
and w2_input_size % 4 == 0
):
return True, "neon"
else:
return False, "none"
return False, "none"
return True, "vec"