main
py 277 lines 9.74 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Tests for the MoE fused topk kernel.
4
5 Run `pytest tests/kernels/moe/test_fused_topk.py`.
6 """
7
8 import pytest
9 import torch
10
11 from vllm.model_executor.layers.fused_moe.router.fused_topk_bias_router import (
12 fused_topk_bias,
13 )
14 from vllm.model_executor.layers.fused_moe.router.fused_topk_router import fused_topk
15 from vllm.platforms import current_platform
16
17
18 def torch_topk(
19 gating_output: torch.Tensor,
20 topk: int,
21 renormalize: bool,
22 e_score_correction_bias: torch.Tensor = None,
23 scoring_func: str = "softmax",
24 ):
25 if scoring_func == "softmax":
26 scores = torch.softmax(gating_output.float(), dim=-1)
27 else:
28 assert scoring_func == "sigmoid"
29 scores = torch.sigmoid(gating_output.float())
30
31 if e_score_correction_bias is not None:
32 num_experts = gating_output.shape[-1]
33 scores_for_choice = scores.view(
34 -1, num_experts
35 ) + e_score_correction_bias.unsqueeze(0)
36 _, topk_ids = torch.topk(scores_for_choice, k=topk, dim=-1)
37 topk_weights = scores.gather(1, topk_ids)
38 else:
39 topk_weights, topk_ids = torch.topk(scores, k=topk, dim=-1)
40
41 if renormalize:
42 topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True)
43
44 return topk_weights, topk_ids
45
46
47 @pytest.mark.skipif(
48 not current_platform.is_cuda_alike(),
49 reason="This test is skipped on non-CUDA platform.",
50 )
51 @pytest.mark.parametrize("num_tokens", [1, 33, 56])
52 @pytest.mark.parametrize("hidden_size", [1024, 2048])
53 @pytest.mark.parametrize("num_experts", [6, 16])
54 @pytest.mark.parametrize("topk", [3, 4])
55 @pytest.mark.parametrize("renormalize", [True, False])
56 @pytest.mark.parametrize("scoring_func", ["softmax", "sigmoid"])
57 @pytest.mark.parametrize("dtype", [torch.bfloat16, torch.half, torch.float32])
58 def test_fused_topk(
59 num_tokens: int,
60 hidden_size: int,
61 num_experts: int,
62 topk: int,
63 renormalize: bool,
64 scoring_func: str,
65 dtype: torch.dtype,
66 ):
67 torch.manual_seed(0)
68 hidden_states = torch.randn((num_tokens, hidden_size), dtype=dtype, device="cuda")
69 gating_output = torch.randn((num_tokens, num_experts), dtype=dtype, device="cuda")
70
71 topk_weights_ref, topk_ids_ref = torch_topk(
72 gating_output=gating_output,
73 topk=topk,
74 renormalize=renormalize,
75 scoring_func=scoring_func,
76 )
77
78 topk_weights, topk_ids, _ = fused_topk(
79 hidden_states=hidden_states,
80 gating_output=gating_output,
81 topk=topk,
82 renormalize=renormalize,
83 scoring_func=scoring_func,
84 )
85
86 torch.testing.assert_close(
87 topk_weights_ref.to(torch.float32), topk_weights, atol=1e-2, rtol=1e-2
88 )
89 torch.testing.assert_close(topk_ids_ref.to(torch.int32), topk_ids, atol=0, rtol=0)
90
91
92 @pytest.mark.skipif(
93 not current_platform.is_cuda_alike(),
94 reason="This test is skipped on non-CUDA platform.",
95 )
96 @pytest.mark.parametrize("num_tokens", [1, 33, 56])
97 @pytest.mark.parametrize("hidden_size", [1024, 2048])
98 @pytest.mark.parametrize("num_experts", [6, 16])
99 @pytest.mark.parametrize("topk", [3, 4])
100 @pytest.mark.parametrize("renormalize", [True, False])
101 @pytest.mark.parametrize("scoring_func", ["softmax", "sigmoid"])
102 @pytest.mark.parametrize("dtype", [torch.bfloat16, torch.half, torch.float32])
103 def test_fused_topk_bias(
104 num_tokens: int,
105 hidden_size: int,
106 num_experts: int,
107 topk: int,
108 renormalize: bool,
109 scoring_func: str,
110 dtype: torch.dtype,
111 ):
112 torch.manual_seed(0)
113 hidden_states = torch.randn((num_tokens, hidden_size), dtype=dtype, device="cuda")
114 gating_output = torch.randn((num_tokens, num_experts), dtype=dtype, device="cuda")
115 e_score_correction_bias = torch.randn(
116 (num_experts,), dtype=torch.float32, device="cuda"
117 )
118
119 topk_weights_ref, topk_ids_ref = torch_topk(
120 gating_output=gating_output,
121 topk=topk,
122 renormalize=renormalize,
123 e_score_correction_bias=e_score_correction_bias,
124 scoring_func=scoring_func,
125 )
126
127 topk_weights, topk_ids = fused_topk_bias(
128 hidden_states=hidden_states,
129 gating_output=gating_output,
130 e_score_correction_bias=e_score_correction_bias,
131 topk=topk,
132 renormalize=renormalize,
133 scoring_func=scoring_func,
134 )
135
136 torch.testing.assert_close(
137 topk_weights_ref.to(torch.float32), topk_weights, atol=1e-2, rtol=1e-2
138 )
139 torch.testing.assert_close(topk_ids_ref.to(torch.int32), topk_ids, atol=0, rtol=0)
140
141
142 @pytest.mark.skipif(
143 not current_platform.is_cuda_alike(),
144 reason="This test is skipped on non-CUDA platform.",
145 )
146 @pytest.mark.parametrize("num_experts", [6, 8, 16])
147 @pytest.mark.parametrize("topk", [3, 4])
148 @pytest.mark.parametrize("scoring_func", ["softmax", "sigmoid"])
149 @pytest.mark.parametrize("bad_value", [float("nan"), float("inf")])
150 @pytest.mark.parametrize("dtype", [torch.bfloat16, torch.half, torch.float32])
151 def test_fused_topk_nan_inf_clamp(
152 num_experts: int,
153 topk: int,
154 scoring_func: str,
155 bad_value: float,
156 dtype: torch.dtype,
157 ):
158 """Regression test for the NaN/Inf clamp in topk_softmax_kernels.cu.
159
160 Degenerate hidden states (e.g., from CUDA graph padding) can produce
161 NaN/Inf gating logits. Without the clamp, softmax/sigmoid outputs are
162 NaN and the argmax loop picks expert 0 for every top-k slot (since
163 "NaN > NaN" is false per IEEE 754), yielding duplicate expert IDs that
164 crash downstream MoE sort kernels. The fix clamps NaN/Inf to 0 before
165 argmax so index tie-breaking selects unique experts [0, 1, ..., k-1].
166 """
167 torch.manual_seed(0)
168 num_tokens = 4
169 hidden_size = 1024
170 hidden_states = torch.randn((num_tokens, hidden_size), dtype=dtype, device="cuda")
171
172 # Row 0: all normal. Rows 1-3: fully poisoned with NaN or Inf.
173 gating_output = torch.randn((num_tokens, num_experts), dtype=dtype, device="cuda")
174 gating_output[1:, :] = bad_value
175
176 topk_weights, topk_ids, _ = fused_topk(
177 hidden_states=hidden_states,
178 gating_output=gating_output,
179 topk=topk,
180 renormalize=False,
181 scoring_func=scoring_func,
182 )
183
184 # Normal row must still match the torch reference.
185 ref_weights, ref_ids = torch_topk(
186 gating_output=gating_output[:1],
187 topk=topk,
188 renormalize=False,
189 scoring_func=scoring_func,
190 )
191 torch.testing.assert_close(
192 ref_weights.to(torch.float32), topk_weights[:1], atol=1e-2, rtol=1e-2
193 )
194 torch.testing.assert_close(ref_ids.to(torch.int32), topk_ids[:1], atol=0, rtol=0)
195
196 # Poisoned rows: IDs must be unique (no duplicates) and weights must be
197 # finite (no NaN/Inf propagation into downstream MoE kernels).
198 for row in range(1, num_tokens):
199 row_ids = topk_ids[row]
200 assert row_ids.unique().numel() == topk, (
201 f"Row {row} has duplicate expert IDs {row_ids.tolist()} "
202 f"(bad_value={bad_value}, scoring_func={scoring_func})"
203 )
204 assert torch.isfinite(topk_weights[row]).all(), (
205 f"Row {row} has non-finite weights {topk_weights[row].tolist()} "
206 f"(bad_value={bad_value}, scoring_func={scoring_func})"
207 )
208
209
210 @pytest.mark.skipif(
211 not current_platform.is_cuda_alike(),
212 reason="This test is skipped on non-CUDA platform.",
213 )
214 @pytest.mark.parametrize("num_experts", [6, 8, 16])
215 @pytest.mark.parametrize("topk", [3, 4])
216 @pytest.mark.parametrize("scoring_func", ["softmax", "sigmoid"])
217 @pytest.mark.parametrize("bad_value", [float("nan"), float("inf")])
218 @pytest.mark.parametrize("dtype", [torch.bfloat16, torch.half, torch.float32])
219 def test_fused_topk_bias_nan_inf_clamp(
220 num_experts: int,
221 topk: int,
222 scoring_func: str,
223 bad_value: float,
224 dtype: torch.dtype,
225 ):
226 """Regression test: NaN/Inf in gating logits must not produce duplicate
227 expert IDs or non-finite weights when e_score_correction_bias is present.
228
229 Same scenario as test_fused_topk_nan_inf_clamp but exercising the bias
230 path (fused_topk_bias) so the fix in topk_softmax_kernels.cu is covered
231 for that entry point as well.
232 """
233 torch.manual_seed(0)
234 num_tokens = 4
235 hidden_size = 1024
236 hidden_states = torch.randn((num_tokens, hidden_size), dtype=dtype, device="cuda")
237 e_score_correction_bias = torch.randn(
238 (num_experts,), dtype=torch.float32, device="cuda"
239 )
240
241 gating_output = torch.randn((num_tokens, num_experts), dtype=dtype, device="cuda")
242 gating_output[1:, :] = bad_value
243
244 topk_weights, topk_ids = fused_topk_bias(
245 hidden_states=hidden_states,
246 gating_output=gating_output,
247 e_score_correction_bias=e_score_correction_bias,
248 topk=topk,
249 renormalize=False,
250 scoring_func=scoring_func,
251 )
252
253 # Normal row must still match the torch reference.
254 ref_weights, ref_ids = torch_topk(
255 gating_output=gating_output[:1],
256 topk=topk,
257 renormalize=False,
258 e_score_correction_bias=e_score_correction_bias,
259 scoring_func=scoring_func,
260 )
261 torch.testing.assert_close(
262 ref_weights.to(torch.float32), topk_weights[:1], atol=1e-2, rtol=1e-2
263 )
264 torch.testing.assert_close(ref_ids.to(torch.int32), topk_ids[:1], atol=0, rtol=0)
265
266 # Poisoned rows: IDs must be unique (no duplicates) and weights must be
267 # finite (no NaN/Inf propagation into downstream MoE kernels).
268 for row in range(1, num_tokens):
269 row_ids = topk_ids[row]
270 assert row_ids.unique().numel() == topk, (
271 f"Row {row} has duplicate expert IDs {row_ids.tolist()} "
272 f"(bad_value={bad_value}, scoring_func={scoring_func})"
273 )
274 assert torch.isfinite(topk_weights[row]).all(), (
275 f"Row {row} has non-finite weights {topk_weights[row].tolist()} "
276 f"(bad_value={bad_value}, scoring_func={scoring_func})"
277 )