main
py 252 lines 8.38 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Tests for the Zen CPU INT8 MoE experts (ZenCPUExpertsInt8)."""
4
5 import pytest
6 import torch
7
8 from tests.kernels.moe.test_cpu_fused_moe import (
9 _make_moe_config,
10 quantize_per_channel,
11 ref_fused_moe_int8,
12 )
13 from vllm.model_executor.kernels.linear.zentorch_utils import (
14 _ZENTORCH_MOE_ACTIVATIONS,
15 )
16 from vllm.model_executor.layers.fused_moe.activation import MoEActivation
17 from vllm.model_executor.layers.fused_moe.config import (
18 FusedMoEParallelConfig,
19 int8_w8a8_moe_quant_config,
20 )
21 from vllm.model_executor.layers.fused_moe.experts.cpu_moe import ZenCPUExpertsInt8
22 from vllm.model_executor.layers.fused_moe.oracle.int8 import (
23 Int8MoeBackend,
24 backend_to_kernel_cls,
25 )
26 from vllm.model_executor.layers.fused_moe.router.cpu_router import _softmax_topk
27 from vllm.model_executor.layers.quantization.utils.quant_utils import (
28 kInt8DynamicTokenSym,
29 kInt8StaticChannelSym,
30 )
31 from vllm.platforms import current_platform
32 from vllm.utils.torch_utils import set_random_seed
33
34 if not current_platform.is_cpu():
35 pytest.skip("skipping CPU-only tests", allow_module_level=True)
36
37 EXPERT_NUM = 8
38 HIDDEN_SIZE = 128
39 INTERMEDIATE_SIZE = 128
40 BATCH_SIZE = 32
41 # zentorch names its activations as strings; the experts API is enum-typed.
42 ZENTORCH_ACT = [act for act in MoEActivation if act.value in _ZENTORCH_MOE_ACTIVATIONS]
43
44
45 @pytest.fixture
46 def mock_zentorch_fused_moe():
47 """Reference zentorch_fused_moe, registered when zentorch is absent."""
48 if hasattr(torch.ops.zentorch, "zentorch_fused_moe"):
49 yield None
50 return
51
52 calls: dict[str, dict] = {}
53
54 def _fused_moe(
55 output,
56 input,
57 w13,
58 w2,
59 w13_bias,
60 w2_bias,
61 topk_weights,
62 topk_id,
63 skip_weighted,
64 act,
65 w13_scales=None,
66 w2_scales=None,
67 zentorch_op_name="",
68 ):
69 calls["fused_moe"] = {
70 "w13": w13,
71 "w2": w2,
72 "w13_bias": w13_bias,
73 "w2_bias": w2_bias,
74 "topk_weights": topk_weights,
75 "topk_id": topk_id,
76 "skip_weighted": skip_weighted,
77 "act": act,
78 "w13_scales": w13_scales,
79 "w2_scales": w2_scales,
80 }
81 output.copy_(
82 ref_fused_moe_int8(
83 input,
84 w13,
85 w2,
86 w13_scales.float(),
87 w2_scales.float(),
88 None if w13_bias is None else w13_bias.float(),
89 None if w2_bias is None else w2_bias.float(),
90 topk_weights,
91 topk_id,
92 MoEActivation(act),
93 )
94 )
95
96 lib_def = torch.library.Library("zentorch", "DEF")
97 lib_def.define(
98 "zentorch_fused_moe(Tensor(a!) output, Tensor input, Tensor w13, "
99 "Tensor w2, Tensor? w13_bias, Tensor? w2_bias, Tensor topk_weights, "
100 "Tensor topk_id, bool skip_weighted, str act, "
101 "Tensor? w13_scales=None, Tensor? w2_scales=None, *, "
102 "str zentorch_op_name='zentorch::zentorch_fused_moe') -> ()"
103 )
104 lib_impl = torch.library.Library("zentorch", "IMPL", "CPU")
105 lib_impl.impl("zentorch_fused_moe", _fused_moe)
106
107 yield calls
108
109 lib_impl._destroy()
110 lib_def._destroy()
111
112
113 def _make_int8_moe_weights(use_bias: bool, dtype: torch.dtype = torch.bfloat16):
114 up_dim = 2 * INTERMEDIATE_SIZE
115 w13_fp = torch.randn((EXPERT_NUM, up_dim, HIDDEN_SIZE), dtype=dtype) / (
116 0.5 * HIDDEN_SIZE**0.5
117 )
118 w2_fp = torch.randn((EXPERT_NUM, HIDDEN_SIZE, INTERMEDIATE_SIZE), dtype=dtype) / (
119 0.5 * INTERMEDIATE_SIZE**0.5
120 )
121 w13, w13_scale = quantize_per_channel(w13_fp)
122 w2, w2_scale = quantize_per_channel(w2_fp)
123 w13_scale = w13_scale.to(torch.bfloat16).float()
124 w2_scale = w2_scale.to(torch.bfloat16).float()
125
126 w13_bias = w2_bias = None
127 if use_bias:
128 w13_bias = torch.randn((EXPERT_NUM, up_dim), dtype=dtype) / (0.5 * up_dim**0.5)
129 w2_bias = torch.randn((EXPERT_NUM, HIDDEN_SIZE), dtype=dtype) / (
130 0.5 * HIDDEN_SIZE**0.5
131 )
132 return w13, w2, w13_scale, w2_scale, w13_bias, w2_bias
133
134
135 def test_zen_int8_first_in_cpu_backend_priority():
136 """The Zen experts are preferred over the generic CPU int8 experts."""
137 assert backend_to_kernel_cls(Int8MoeBackend.CPU)[0] is ZenCPUExpertsInt8
138
139
140 def test_zen_int8_support_predicates(mock_zentorch_fused_moe, monkeypatch):
141 monkeypatch.setattr(current_platform, "is_zen_cpu", lambda: False)
142 assert not ZenCPUExpertsInt8._supports_current_device()
143 monkeypatch.setattr(current_platform, "is_zen_cpu", lambda: True)
144 assert ZenCPUExpertsInt8._supports_current_device()
145 assert ZenCPUExpertsInt8._supports_quant_scheme(
146 kInt8StaticChannelSym, kInt8DynamicTokenSym
147 )
148 assert not ZenCPUExpertsInt8._supports_quant_scheme(kInt8StaticChannelSym, None)
149 for act in ZENTORCH_ACT:
150 assert ZenCPUExpertsInt8._supports_activation(act)
151 assert not ZenCPUExpertsInt8._supports_activation(MoEActivation.RELU2)
152 assert not ZenCPUExpertsInt8._supports_activation(
153 MoEActivation.SWIGLUOAI_UNINTERLEAVE
154 )
155 assert not ZenCPUExpertsInt8._supports_no_act_and_mul()
156 assert ZenCPUExpertsInt8._supports_parallel_config(
157 FusedMoEParallelConfig.make_no_parallel()
158 )
159
160
161 @pytest.mark.parametrize("act", ZENTORCH_ACT)
162 @pytest.mark.parametrize("use_bias", [False, True])
163 def test_zen_int8_dispatch_contract(
164 default_vllm_config,
165 mock_zentorch_fused_moe,
166 act: MoEActivation,
167 use_bias: bool,
168 ):
169 """apply() hands zentorch the weights, scales and biases it expects."""
170 if mock_zentorch_fused_moe is None:
171 pytest.skip("real zentorch is installed; the recorded call is unavailable")
172 set_random_seed(0)
173
174 topk_num = EXPERT_NUM // 2
175 w13, w2, w13_scale, w2_scale, w13_bias, w2_bias = _make_int8_moe_weights(use_bias)
176 moe_config = _make_moe_config(
177 EXPERT_NUM,
178 HIDDEN_SIZE,
179 INTERMEDIATE_SIZE,
180 topk_num,
181 torch.bfloat16,
182 act,
183 use_bias,
184 )
185 # A layer carries per-channel scales as [E, N, 1].
186 quant_config = int8_w8a8_moe_quant_config(
187 w1_scale=w13_scale.unsqueeze(-1),
188 w2_scale=w2_scale.unsqueeze(-1),
189 a1_scale=None,
190 a2_scale=None,
191 w1_bias=w13_bias,
192 w2_bias=w2_bias,
193 per_act_token_quant=True,
194 )
195
196 input = torch.randn((BATCH_SIZE, HIDDEN_SIZE), dtype=torch.bfloat16) / (
197 0.5 * HIDDEN_SIZE**0.5
198 )
199 router_logits = torch.randn((BATCH_SIZE, EXPERT_NUM), dtype=torch.bfloat16)
200 topk_weights, topk_ids = _softmax_topk(router_logits, topk_num, False)
201
202 experts = ZenCPUExpertsInt8(moe_config, quant_config)
203 # zentorch_fused_moe takes no expert_map.
204 assert not experts.supports_expert_map()
205 output = torch.empty_like(input)
206 experts.apply(
207 output=output,
208 hidden_states=input,
209 w1=w13,
210 w2=w2,
211 topk_weights=topk_weights,
212 topk_ids=topk_ids,
213 activation=act,
214 global_num_experts=EXPERT_NUM,
215 expert_map=None,
216 a1q_scale=None,
217 a2_scale=None,
218 workspace13=torch.empty(0),
219 workspace2=torch.empty(0),
220 expert_tokens_meta=None,
221 apply_router_weight_on_input=False,
222 )
223
224 call = mock_zentorch_fused_moe["fused_moe"]
225 assert call["w13_scales"].shape == (EXPERT_NUM, 2 * INTERMEDIATE_SIZE)
226 assert call["w2_scales"].shape == (EXPERT_NUM, HIDDEN_SIZE)
227 # f32 scales keep the layer on zentorch's fast routed MoE executor.
228 assert call["w13_scales"].dtype == torch.float32
229 assert call["w2_scales"].dtype == torch.float32
230 assert call["act"] == act.value.lower()
231 assert call["skip_weighted"] is False
232 assert call["topk_weights"].dtype == torch.float32
233 assert call["topk_id"].dtype == torch.int32
234 if use_bias:
235 assert call["w13_bias"].shape == (EXPERT_NUM, 2 * INTERMEDIATE_SIZE)
236 assert call["w2_bias"].shape == (EXPERT_NUM, HIDDEN_SIZE)
237 else:
238 assert call["w13_bias"] is None and call["w2_bias"] is None
239
240 ref = ref_fused_moe_int8(
241 input,
242 w13,
243 w2,
244 w13_scale,
245 w2_scale,
246 None if w13_bias is None else w13_bias.float(),
247 None if w2_bias is None else w2_bias.float(),
248 topk_weights.to(torch.float32),
249 topk_ids.to(torch.int32),
250 act,
251 )
252 torch.testing.assert_close(output, ref)