| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Tests for ZenTorch CPU fused MoE dispatch and forward.""" |
| 4 | |
| 5 | import pytest |
| 6 | import torch |
| 7 | |
| 8 | import vllm.model_executor.layers.fused_moe.modular_kernel as mk |
| 9 | from tests.kernels.allclose_default import get_default_atol, get_default_rtol |
| 10 | from tests.kernels.moe.test_cpu_fused_moe import ( |
| 11 | UNALIGNED_INTERMEDIATE_DIM, |
| 12 | _make_moe_config, |
| 13 | _StubMoELayer, |
| 14 | ref_fused_moe, |
| 15 | ) |
| 16 | from vllm.model_executor.kernels.linear.zentorch_utils import ( |
| 17 | has_zentorch_op, |
| 18 | is_zentorch_moe_config_supported, |
| 19 | ) |
| 20 | from vllm.model_executor.layers.fused_moe.activation import MoEActivation |
| 21 | from vllm.model_executor.layers.fused_moe.config import ( |
| 22 | FusedMoEConfig, |
| 23 | FusedMoEQuantConfig, |
| 24 | biased_moe_quant_config, |
| 25 | ) |
| 26 | from vllm.model_executor.layers.fused_moe.experts.cpu_moe import ( |
| 27 | CPUUnquantizedExperts, |
| 28 | ) |
| 29 | from vllm.model_executor.layers.fused_moe.router.cpu_router import _softmax_topk |
| 30 | from vllm.platforms import current_platform |
| 31 | from vllm.utils.torch_utils import set_random_seed |
| 32 | |
| 33 | if not current_platform.is_cpu() or not current_platform.is_zen_cpu(): |
| 34 | pytest.skip("skipping non-Zen CPU tests", allow_module_level=True) |
| 35 | |
| 36 | if not has_zentorch_op(["zentorch_fused_moe"]): |
| 37 | pytest.skip( |
| 38 | "skipping tests: zentorch_fused_moe op not available", |
| 39 | allow_module_level=True, |
| 40 | ) |
| 41 | |
| 42 | ZENTORCH_ACT = [ |
| 43 | MoEActivation.SILU, |
| 44 | MoEActivation.GELU, |
| 45 | MoEActivation.GELU_TANH, |
| 46 | MoEActivation.SWIGLUOAI, |
| 47 | ] |
| 48 | USE_BIAS = [False, True] |
| 49 | |
| 50 | |
| 51 | def _make_layer_and_config( |
| 52 | act: MoEActivation, |
| 53 | *, |
| 54 | hidden_size: int = 128, |
| 55 | intermediate_size: int = 128, |
| 56 | expert_num: int = 8, |
| 57 | topk_num: int = 4, |
| 58 | use_bias: bool = False, |
| 59 | dtype: torch.dtype = torch.bfloat16, |
| 60 | ) -> tuple[_StubMoELayer, FusedMoEConfig]: |
| 61 | up_dim = 2 * intermediate_size |
| 62 | w13 = torch.randn((expert_num, up_dim, hidden_size), dtype=dtype) / ( |
| 63 | 0.5 * hidden_size**0.5 |
| 64 | ) |
| 65 | w2 = torch.randn((expert_num, hidden_size, intermediate_size), dtype=dtype) / ( |
| 66 | 0.5 * intermediate_size**0.5 |
| 67 | ) |
| 68 | w13_bias = None |
| 69 | w2_bias = None |
| 70 | if use_bias: |
| 71 | w13_bias = torch.randn((expert_num, up_dim), dtype=dtype) / (0.5 * up_dim**0.5) |
| 72 | w2_bias = torch.randn((expert_num, hidden_size), dtype=dtype) / ( |
| 73 | 0.5 * hidden_size**0.5 |
| 74 | ) |
| 75 | |
| 76 | layer = _StubMoELayer( |
| 77 | w13, |
| 78 | w2, |
| 79 | act, |
| 80 | w13_bias, |
| 81 | w2_bias, |
| 82 | ) |
| 83 | moe_config = _make_moe_config( |
| 84 | expert_num, |
| 85 | hidden_size, |
| 86 | intermediate_size, |
| 87 | topk_num, |
| 88 | dtype, |
| 89 | act, |
| 90 | use_bias, |
| 91 | ) |
| 92 | return layer, moe_config |
| 93 | |
| 94 | |
| 95 | @pytest.mark.parametrize("act", ZENTORCH_ACT) |
| 96 | def test_zen_cpu_fused_moe_config_supported(act: MoEActivation): |
| 97 | """CPUUnquantizedExperts is selected when zentorch MoE is supported.""" |
| 98 | _, moe_config = _make_layer_and_config(act) |
| 99 | assert is_zentorch_moe_config_supported(moe_config) |
| 100 | |
| 101 | supported, reason = CPUUnquantizedExperts.is_supported_config( |
| 102 | CPUUnquantizedExperts, |
| 103 | moe_config, |
| 104 | None, |
| 105 | None, |
| 106 | mk.FusedMoEActivationFormat.Standard, |
| 107 | ) |
| 108 | assert supported, reason |
| 109 | |
| 110 | |
| 111 | def test_zen_cpu_fused_moe_config_supported_unaligned_intermediate(): |
| 112 | """Zentorch accepts shapes the grouped-gemm kernel rejects.""" |
| 113 | act = MoEActivation.SILU |
| 114 | moe_config = _make_moe_config( |
| 115 | expert_num=8, |
| 116 | hidden_size=128, |
| 117 | intermediate_size=UNALIGNED_INTERMEDIATE_DIM, |
| 118 | topk_num=4, |
| 119 | dtype=torch.bfloat16, |
| 120 | act=act, |
| 121 | ) |
| 122 | assert is_zentorch_moe_config_supported(moe_config) |
| 123 | |
| 124 | supported, reason = CPUUnquantizedExperts.is_supported_config( |
| 125 | CPUUnquantizedExperts, |
| 126 | moe_config, |
| 127 | None, |
| 128 | None, |
| 129 | mk.FusedMoEActivationFormat.Standard, |
| 130 | ) |
| 131 | assert supported, reason |
| 132 | |
| 133 | |
| 134 | @pytest.mark.parametrize("act", ZENTORCH_ACT) |
| 135 | @pytest.mark.parametrize("use_bias", USE_BIAS) |
| 136 | def test_zen_cpu_fused_moe_forward( |
| 137 | default_vllm_config, |
| 138 | act: MoEActivation, |
| 139 | use_bias: bool, |
| 140 | ): |
| 141 | """The zentorch forward runs end-to-end and matches the reference.""" |
| 142 | set_random_seed(0) |
| 143 | |
| 144 | expert_num = 8 |
| 145 | hidden_size = 128 |
| 146 | batch_size = 64 |
| 147 | dtype = torch.bfloat16 |
| 148 | topk_num = expert_num // 2 |
| 149 | |
| 150 | layer, moe_config = _make_layer_and_config( |
| 151 | act, |
| 152 | expert_num=expert_num, |
| 153 | topk_num=topk_num, |
| 154 | use_bias=use_bias, |
| 155 | ) |
| 156 | |
| 157 | input = torch.randn((batch_size, hidden_size), dtype=dtype) / ( |
| 158 | 0.5 * hidden_size**0.5 |
| 159 | ) |
| 160 | router_logits = torch.randn((batch_size, expert_num), dtype=dtype) |
| 161 | |
| 162 | quant_config = ( |
| 163 | biased_moe_quant_config(layer.w13_bias, layer.w2_bias) |
| 164 | if use_bias |
| 165 | else FusedMoEQuantConfig.make() |
| 166 | ) |
| 167 | experts = CPUUnquantizedExperts(moe_config, quant_config) |
| 168 | experts.process_weights_after_loading(layer) |
| 169 | assert experts._use_zentorch is True |
| 170 | |
| 171 | w13_before = layer.w13_weight.detach().clone() |
| 172 | w2_before = layer.w2_weight.detach().clone() |
| 173 | |
| 174 | topk_weight, topk_ids = _softmax_topk(router_logits, topk_num, False) |
| 175 | |
| 176 | output = torch.empty_like(input) |
| 177 | experts.apply( |
| 178 | output=output, |
| 179 | hidden_states=input, |
| 180 | w1=layer.w13_weight, |
| 181 | w2=layer.w2_weight, |
| 182 | topk_weights=topk_weight, |
| 183 | topk_ids=topk_ids, |
| 184 | activation=act, |
| 185 | global_num_experts=expert_num, |
| 186 | expert_map=None, |
| 187 | a1q_scale=None, |
| 188 | a2_scale=None, |
| 189 | workspace13=torch.empty(0), |
| 190 | workspace2=torch.empty(0), |
| 191 | expert_tokens_meta=None, |
| 192 | apply_router_weight_on_input=False, |
| 193 | ) |
| 194 | |
| 195 | # zentorch needs no prepacking; weights are consumed as loaded. |
| 196 | torch.testing.assert_close(layer.w13_weight, w13_before) |
| 197 | torch.testing.assert_close(layer.w2_weight, w2_before) |
| 198 | |
| 199 | ref_output = ref_fused_moe( |
| 200 | input, |
| 201 | layer.w13_weight, |
| 202 | layer.w2_weight, |
| 203 | getattr(layer, "w13_bias", None), |
| 204 | getattr(layer, "w2_bias", None), |
| 205 | topk_weight, |
| 206 | topk_ids, |
| 207 | act, |
| 208 | ) |
| 209 | |
| 210 | atol, rtol = get_default_atol(output), get_default_rtol(output) |
| 211 | torch.testing.assert_close(output, ref_output, atol=atol, rtol=rtol) |