main
py 482 lines 13.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from types import SimpleNamespace
5
6 import pytest
7 import ray
8 import torch
9 import torch.distributed as dist
10 import torch.nn.functional as F
11
12 from tests.utils import (
13 init_test_distributed_environment,
14 multi_gpu_test,
15 multi_process_parallel,
16 )
17 from vllm.distributed import get_tp_group
18 from vllm.model_executor.layers.fused_moe.config import FusedMoEConfig
19 from vllm.model_executor.layers.fused_moe.experts.trtllm_mxfp4_moe import (
20 TrtLlmMxfp4ExpertsMonolithic,
21 )
22 from vllm.model_executor.layers.fused_moe.moe_output import UnfinalizedMoEOutput
23 from vllm.model_executor.layers.fused_moe.runner.moe_runner import MoERunner
24 from vllm.model_executor.warmup.cutedsl_warmup import cutedsl_warmup
25 from vllm.models.kimi_k3.nvidia import latent_moe_runner
26 from vllm.models.kimi_k3.nvidia.ops.latent_moe_tail import KimiK3LatentMoETailOp
27 from vllm.platforms import current_platform
28
29 HIDDEN_SIZE = 7168
30 LATENT_SIZE = 3584
31 EPS = 0.1
32 TOP_K = 8
33
34
35 def test_deferred_finalize_enabled_before_moe_kernel_setup(
36 monkeypatch: pytest.MonkeyPatch,
37 ) -> None:
38 class FakeMoEConfig:
39 tp_size = 8
40 dp_size = 1
41 ep_size = 1
42 pcp_size = 1
43 is_sequence_parallel = False
44 hidden_dim = LATENT_SIZE
45 hidden_dim_unpadded = LATENT_SIZE
46 experts_per_token = 16
47 _defer_moe_finalize = False
48 defer_moe_finalize_max_num_tokens = -1
49 defer_moe_finalize = FusedMoEConfig.defer_moe_finalize
50 limit_deferred_moe_finalize = FusedMoEConfig.limit_deferred_moe_finalize
51 use_deferred_moe_finalize = FusedMoEConfig.use_deferred_moe_finalize
52
53 moe_config = FakeMoEConfig()
54 quant_method = SimpleNamespace(
55 experts_cls=TrtLlmMxfp4ExpertsMonolithic,
56 moe_kernel=None,
57 )
58 norm_weight = torch.empty(LATENT_SIZE, dtype=torch.bfloat16)
59 transform = SimpleNamespace(
60 norm=SimpleNamespace(weight=norm_weight, variance_epsilon=EPS),
61 up_proj=SimpleNamespace(
62 weight=SimpleNamespace(shape=(HIDDEN_SIZE, LATENT_SIZE))
63 ),
64 )
65
66 def fake_runner_init(runner, *args, **kwargs) -> None:
67 runner.moe_config = moe_config
68 runner.routed_experts = SimpleNamespace(quant_method=quant_method)
69 runner._shared_experts = object()
70 runner.routed_output_transform = transform
71
72 initialized_with: dict[str, object] = {}
73 tail_op = SimpleNamespace(contract=SimpleNamespace(max_num_tokens=128))
74
75 def fake_tail_initialize(**kwargs):
76 initialized_with.update(kwargs)
77 return tail_op
78
79 monkeypatch.setattr(MoERunner, "__init__", fake_runner_init)
80 monkeypatch.setattr(latent_moe_runner.torch.cuda, "Event", lambda: object())
81 monkeypatch.setattr(
82 latent_moe_runner,
83 "current_platform",
84 SimpleNamespace(
85 is_cuda=lambda: True,
86 is_device_capability_family=lambda capability: capability == 100,
87 current_device=lambda: torch.device("cuda"),
88 ),
89 )
90 monkeypatch.setattr(
91 latent_moe_runner,
92 "get_current_vllm_config",
93 lambda: SimpleNamespace(
94 parallel_config=SimpleNamespace(use_ubatching=False),
95 model_config=SimpleNamespace(enable_sleep_mode=False),
96 ),
97 )
98 monkeypatch.setattr(KimiK3LatentMoETailOp, "initialize", fake_tail_initialize)
99
100 latent_moe_runner.LatentMoERunner()
101
102 assert moe_config.use_deferred_moe_finalize
103 assert moe_config.defer_moe_finalize_max_num_tokens == 128
104 assert initialized_with["experts_per_token"] == 16
105
106
107 def _make_deferred_routed_output(
108 num_tokens: int,
109 device: torch.device,
110 ) -> tuple[UnfinalizedMoEOutput, torch.Tensor]:
111 num_routes = num_tokens * TOP_K
112 num_permuted_rows = num_routes + 7
113 expanded_output = torch.randn(
114 num_routes,
115 LATENT_SIZE,
116 device=device,
117 dtype=torch.bfloat16,
118 ).mul_(0.01)
119 expert_weights = torch.rand(
120 num_tokens,
121 TOP_K,
122 device=device,
123 dtype=torch.bfloat16,
124 )
125 expert_weights.div_(expert_weights.sum(dim=-1, keepdim=True))
126 expanded_idx = torch.randperm(num_permuted_rows, device=device)[:num_routes]
127 gemm2_permuted = torch.empty(
128 num_permuted_rows,
129 LATENT_SIZE,
130 device=device,
131 dtype=torch.bfloat16,
132 )
133 gemm2_permuted[expanded_idx] = expanded_output
134
135 finalized = torch.zeros(
136 num_tokens,
137 LATENT_SIZE,
138 device=device,
139 dtype=torch.bfloat16,
140 )
141 expanded_output = expanded_output.view(num_tokens, TOP_K, LATENT_SIZE)
142 for slot in range(TOP_K):
143 finalized.add_(expanded_output[:, slot] * expert_weights[:, slot, None])
144
145 return (
146 UnfinalizedMoEOutput(
147 gemm2_permuted=gemm2_permuted,
148 expert_weights=expert_weights,
149 expanded_idx_to_permuted_idx=expanded_idx.to(torch.int32).view(
150 num_tokens, TOP_K
151 ),
152 ),
153 finalized,
154 )
155
156
157 def _make_bf16_top16_deferred_output(
158 num_tokens: int,
159 device: torch.device,
160 *,
161 drop_last_route: bool = False,
162 ) -> tuple[UnfinalizedMoEOutput, torch.Tensor]:
163 top_k = 16
164 num_routes = num_tokens * top_k
165 num_permuted_rows = num_routes + 7
166 expanded_output = torch.randn(
167 num_routes,
168 LATENT_SIZE,
169 device=device,
170 dtype=torch.bfloat16,
171 ).mul_(0.01)
172 expert_weights = torch.rand(
173 num_tokens,
174 top_k,
175 device=device,
176 dtype=torch.bfloat16,
177 )
178 expert_weights.div_(expert_weights.sum(dim=-1, keepdim=True))
179 expanded_idx = torch.randperm(num_permuted_rows, device=device)[:num_routes]
180 gemm2_permuted = torch.empty(
181 num_permuted_rows,
182 LATENT_SIZE,
183 device=device,
184 dtype=torch.bfloat16,
185 )
186 gemm2_permuted[expanded_idx] = expanded_output
187
188 expanded_output = expanded_output.view(num_tokens, top_k, LATENT_SIZE)
189 expanded_idx = expanded_idx.view(num_tokens, top_k)
190 if drop_last_route:
191 expanded_output[:, -1].zero_()
192 expanded_idx[:, -1] = -1
193 finalized = (
194 (expanded_output.float() * expert_weights[:, :, None].float())
195 .sum(1)
196 .to(torch.bfloat16)
197 )
198 return (
199 UnfinalizedMoEOutput(
200 gemm2_permuted=gemm2_permuted,
201 expert_weights=expert_weights,
202 expanded_idx_to_permuted_idx=expanded_idx.to(torch.int32),
203 ),
204 finalized,
205 )
206
207
208 @ray.remote(num_gpus=1, max_calls=1)
209 def _test_latent_moe_tail_worker(
210 monkeypatch: pytest.MonkeyPatch,
211 tp_size: int,
212 pp_size: int,
213 rank: int,
214 distributed_init_port: str,
215 ) -> None:
216 monkeypatch.delenv("CUDA_VISIBLE_DEVICES", raising=False)
217 device = torch.device(f"cuda:{rank}")
218 torch.accelerator.set_device_index(device)
219 init_test_distributed_environment(
220 tp_size,
221 pp_size,
222 rank,
223 distributed_init_port,
224 )
225
226 torch.manual_seed(0)
227 rms_weight = 1 + 0.1 * torch.randn(
228 LATENT_SIZE,
229 device=device,
230 dtype=torch.bfloat16,
231 )
232 up_weight = (
233 torch.randn(
234 HIDDEN_SIZE,
235 LATENT_SIZE,
236 device=device,
237 dtype=torch.bfloat16,
238 )
239 / LATENT_SIZE**0.5
240 )
241
242 group = get_tp_group().device_group
243 op = KimiK3LatentMoETailOp.initialize(
244 hidden_size=HIDDEN_SIZE,
245 latent_size=LATENT_SIZE,
246 dtype=torch.bfloat16,
247 device=device,
248 rms_eps=EPS,
249 )
250 cutedsl_warmup()
251
252 # M=65 crosses two 32-CTA token waves and reuses the first DSM slot.
253 for iteration, num_tokens in enumerate((1, 5, 8, 16, 33, 65, 5)):
254 torch.manual_seed(100 * iteration + rank + 1)
255 routed_output = torch.randn(
256 num_tokens,
257 LATENT_SIZE,
258 device=device,
259 dtype=torch.bfloat16,
260 ).mul_(0.01)
261 shared_output = torch.randn(
262 num_tokens,
263 HIDDEN_SIZE,
264 device=device,
265 dtype=torch.bfloat16,
266 )
267
268 routed_reference = routed_output.clone()
269 shared_reference = shared_output.clone()
270 dist.all_reduce(routed_reference, group=group)
271 dist.all_reduce(shared_reference, group=group)
272 expected = F.linear(
273 F.rms_norm(
274 routed_reference,
275 (LATENT_SIZE,),
276 rms_weight,
277 EPS,
278 ),
279 up_weight,
280 )
281 expected.add_(shared_reference)
282
283 actual = op(
284 routed_output,
285 shared_output,
286 rms_weight,
287 up_weight,
288 )
289 torch.testing.assert_close(actual, expected, atol=8e-2, rtol=3e-2)
290 assert actual.is_contiguous()
291
292 graph = torch.cuda.CUDAGraph()
293 with torch.cuda.graph(graph):
294 graph_output = op(
295 routed_output,
296 shared_output,
297 rms_weight,
298 up_weight,
299 )
300 graph.replay()
301 torch.testing.assert_close(graph_output, expected, atol=8e-2, rtol=3e-2)
302
303
304 def _run_latent_moe_tail_test(
305 monkeypatch: pytest.MonkeyPatch,
306 tp_size: int,
307 ) -> None:
308 if not current_platform.is_device_capability_family(100):
309 pytest.skip("K3 latent-MoE tail fusion requires SM100")
310 multi_process_parallel(
311 monkeypatch,
312 tp_size,
313 1,
314 _test_latent_moe_tail_worker,
315 )
316
317
318 @ray.remote(num_gpus=1, max_calls=1)
319 def _test_deferred_finalize_parity_worker(
320 monkeypatch: pytest.MonkeyPatch,
321 tp_size: int,
322 pp_size: int,
323 rank: int,
324 distributed_init_port: str,
325 ) -> None:
326 monkeypatch.delenv("CUDA_VISIBLE_DEVICES", raising=False)
327 device = torch.device(f"cuda:{rank}")
328 torch.accelerator.set_device_index(device)
329 init_test_distributed_environment(
330 tp_size,
331 pp_size,
332 rank,
333 distributed_init_port,
334 )
335
336 torch.manual_seed(1000 + rank)
337 rms_weight = 1 + 0.1 * torch.randn(
338 LATENT_SIZE,
339 device=device,
340 dtype=torch.bfloat16,
341 )
342 up_weight = (
343 torch.randn(
344 HIDDEN_SIZE,
345 LATENT_SIZE,
346 device=device,
347 dtype=torch.bfloat16,
348 )
349 / LATENT_SIZE**0.5
350 )
351 finalized_op = KimiK3LatentMoETailOp.initialize(
352 hidden_size=HIDDEN_SIZE,
353 latent_size=LATENT_SIZE,
354 dtype=torch.bfloat16,
355 device=device,
356 rms_eps=EPS,
357 )
358 deferred_op = KimiK3LatentMoETailOp.initialize(
359 hidden_size=HIDDEN_SIZE,
360 latent_size=LATENT_SIZE,
361 dtype=torch.bfloat16,
362 device=device,
363 rms_eps=EPS,
364 experts_per_token=TOP_K,
365 )
366 bf16_deferred_op = KimiK3LatentMoETailOp.initialize(
367 hidden_size=HIDDEN_SIZE,
368 latent_size=LATENT_SIZE,
369 dtype=torch.bfloat16,
370 device=device,
371 rms_eps=EPS,
372 experts_per_token=16,
373 )
374 cutedsl_warmup()
375
376 for iteration, num_tokens in enumerate((1, 5, 16, 33, 65)):
377 torch.manual_seed(2000 + 100 * iteration + rank)
378 deferred_output, finalized_output = _make_deferred_routed_output(
379 num_tokens,
380 device,
381 )
382 shared_output = torch.randn(
383 num_tokens,
384 HIDDEN_SIZE,
385 device=device,
386 dtype=torch.bfloat16,
387 )
388
389 expected = finalized_op(
390 finalized_output,
391 shared_output,
392 rms_weight,
393 up_weight,
394 )
395 actual = deferred_op(
396 deferred_output,
397 shared_output,
398 rms_weight,
399 up_weight,
400 )
401
402 torch.testing.assert_close(actual, expected, atol=8e-2, rtol=3e-2)
403
404 for iteration, num_tokens in enumerate((1, 2, 4, 5, 16)):
405 torch.manual_seed(3000 + 100 * iteration + rank)
406 deferred_output, finalized_output = _make_bf16_top16_deferred_output(
407 num_tokens,
408 device,
409 drop_last_route=num_tokens == 5,
410 )
411 shared_output = torch.randn(
412 num_tokens,
413 HIDDEN_SIZE,
414 device=device,
415 dtype=torch.bfloat16,
416 )
417
418 expected = finalized_op(
419 finalized_output,
420 shared_output,
421 rms_weight,
422 up_weight,
423 )
424 actual = bf16_deferred_op(
425 deferred_output,
426 shared_output,
427 rms_weight,
428 up_weight,
429 )
430
431 torch.testing.assert_close(actual, expected, atol=8e-2, rtol=3e-2)
432 if num_tokens == 4:
433 graph = torch.cuda.CUDAGraph()
434 with torch.cuda.graph(graph):
435 graph_actual = bf16_deferred_op(
436 deferred_output,
437 shared_output,
438 rms_weight,
439 up_weight,
440 )
441 graph.replay()
442 torch.testing.assert_close(
443 graph_actual,
444 expected,
445 atol=8e-2,
446 rtol=3e-2,
447 )
448
449
450 def _run_deferred_finalize_parity_test(
451 monkeypatch: pytest.MonkeyPatch,
452 tp_size: int,
453 ) -> None:
454 if not current_platform.is_device_capability_family(100):
455 pytest.skip("K3 latent-MoE tail fusion requires SM100")
456 multi_process_parallel(
457 monkeypatch,
458 tp_size,
459 1,
460 _test_deferred_finalize_parity_worker,
461 )
462
463
464 @multi_gpu_test(num_gpus=8)
465 def test_latent_moe_tail_tp8_matches_native_path(
466 monkeypatch: pytest.MonkeyPatch,
467 ) -> None:
468 _run_latent_moe_tail_test(monkeypatch, 8)
469
470
471 @multi_gpu_test(num_gpus=16)
472 def test_latent_moe_tail_tp16_matches_native_path(
473 monkeypatch: pytest.MonkeyPatch,
474 ) -> None:
475 _run_latent_moe_tail_test(monkeypatch, 16)
476
477
478 @multi_gpu_test(num_gpus=8)
479 def test_latent_moe_tail_deferred_finalize_matches_finalized(
480 monkeypatch: pytest.MonkeyPatch,
481 ) -> None:
482 _run_deferred_finalize_parity_test(monkeypatch, 8)