main
py 849 lines 29.5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from importlib.util import find_spec
4 from types import SimpleNamespace
5
6 import pytest
7 import torch
8
9 import vllm.envs as envs
10 from tests.compile.backend import TestBackend
11 from tests.utils import TestFP8Layer, has_module_attribute, multi_gpu_test
12 from vllm._aiter_ops import IS_AITER_FOUND, rocm_aiter_ops
13 from vllm._custom_ops import cutlass_scaled_fp4_mm, scaled_fp4_quant
14 from vllm.compilation.passes.fusion.allreduce_rms_fusion import (
15 AllReduceFusionPass,
16 RocmAiterAllReduceFusionPass,
17 _fused_ar_workspace_hidden_dim,
18 _select_flashinfer_allreduce_use_oneshot,
19 )
20 from vllm.compilation.passes.fx_utils import find_op_nodes
21 from vllm.compilation.passes.utility.fix_functionalization import (
22 FixFunctionalizationPass,
23 )
24 from vllm.compilation.passes.utility.noop_elimination import NoOpEliminationPass
25 from vllm.compilation.passes.utility.post_cleanup import PostCleanupPass
26 from vllm.config import (
27 CompilationConfig,
28 CompilationMode,
29 DeviceConfig,
30 ModelConfig,
31 PassConfig,
32 VllmConfig,
33 set_current_vllm_config,
34 )
35 from vllm.distributed import tensor_model_parallel_all_reduce
36 from vllm.distributed.device_communicators.aiter_custom_all_reduce import (
37 AiterCustomAllreduce,
38 )
39 from vllm.distributed.parallel_state import (
40 init_distributed_environment,
41 initialize_model_parallel,
42 )
43 from vllm.model_executor.layers.layernorm import GemmaRMSNorm, RMSNorm
44 from vllm.model_executor.layers.quantization.utils.quant_utils import (
45 kFp8StaticTensorSym,
46 )
47 from vllm.platforms import current_platform
48 from vllm.utils.network_utils import get_open_port
49 from vllm.utils.system_utils import update_environment_variables
50 from vllm.utils.torch_utils import set_random_seed
51
52 DEVICE_TYPE = current_platform.device_type
53
54
55 @pytest.mark.parametrize(
56 ("workspace_backend", "device_capability", "world_size", "tensor_size", "expected"),
57 [
58 ("mnnvl", 103, 8, 2 * 1024 * 1024, None),
59 ("trtllm", 103, 8, 2 * 1024 * 1024, True),
60 ("trtllm", 103, 8, 2 * 1024 * 1024 + 1, False),
61 ("trtllm", 100, 4, 4 * 1024 * 1024, True),
62 ("trtllm", 100, 4, 4 * 1024 * 1024 + 1, False),
63 ("trtllm", None, 8, 128 * 1024 * 1024, True),
64 ],
65 )
66 def test_select_flashinfer_allreduce_use_oneshot(
67 workspace_backend: str,
68 device_capability: int | None,
69 world_size: int,
70 tensor_size: int,
71 expected: bool | None,
72 ):
73 assert (
74 _select_flashinfer_allreduce_use_oneshot(
75 workspace_backend,
76 device_capability,
77 world_size,
78 tensor_size,
79 )
80 is expected
81 )
82
83
84 def _make_workspace_hidden_config(
85 target_hidden: int, draft_hidden: int | None
86 ) -> SimpleNamespace:
87 """Minimal stand-in exposing only what _fused_ar_workspace_hidden_dim reads."""
88 speculative_config = None
89 if draft_hidden is not None:
90 speculative_config = SimpleNamespace(
91 draft_model_config=SimpleNamespace(
92 get_hidden_size=lambda: draft_hidden,
93 )
94 )
95 return SimpleNamespace(
96 model_config=SimpleNamespace(get_hidden_size=lambda: target_hidden),
97 speculative_config=speculative_config,
98 )
99
100
101 @pytest.mark.parametrize(
102 ("target_hidden", "draft_hidden", "expected"),
103 [
104 (2048, None, 2048), # no speculative decoding -> target only
105 (2048, 2560, 2560), # draft wider than target -> draft (issue #52023)
106 (2048, 1024, 2048), # draft narrower than target -> target
107 (2048, 2048, 2048), # equal -> target
108 ],
109 )
110 def test_fused_ar_workspace_hidden_dim(
111 target_hidden: int, draft_hidden: int | None, expected: int
112 ):
113 config = _make_workspace_hidden_config(target_hidden, draft_hidden)
114 assert _fused_ar_workspace_hidden_dim(config) == expected
115
116
117 def test_fused_ar_workspace_hidden_dim_spec_without_draft():
118 # speculative_config present but no draft model (e.g. ngram) -> target only.
119 config = SimpleNamespace(
120 model_config=SimpleNamespace(get_hidden_size=lambda: 2048),
121 speculative_config=SimpleNamespace(draft_model_config=None),
122 )
123 assert _fused_ar_workspace_hidden_dim(config) == 2048
124
125
126 class TestAllReduceRMSNormModel(torch.nn.Module):
127 def __init__(
128 self,
129 hidden_size=16,
130 token_num=16,
131 eps=1e-6,
132 dtype: torch.dtype = torch.float16,
133 use_aiter: bool = False,
134 ):
135 super().__init__()
136 self.hidden_size = hidden_size
137 self.eps = eps
138 self.norm = [RMSNorm(hidden_size, eps) for i in range(4)]
139 self.w = [torch.rand(hidden_size, hidden_size) for _ in range(3)]
140 self.use_aiter = use_aiter
141
142 def forward(self, x):
143 # avoid having graph input be an arg to a pattern directly
144 z = torch.relu(x)
145 x = resid = tensor_model_parallel_all_reduce(z)
146 y = self.norm[0](x)
147
148 z2 = torch.mm(y, self.w[0])
149 x2 = tensor_model_parallel_all_reduce(z2)
150
151 y2, resid = self.norm[1](x2, resid)
152
153 z3 = torch.mm(y2, self.w[1])
154 x3 = tensor_model_parallel_all_reduce(z3)
155
156 y3, resid = self.norm[2](x3, resid)
157
158 z4 = torch.mm(y3, self.w[2])
159 x4 = tensor_model_parallel_all_reduce(z4)
160
161 y4, resid = self.norm[3](x4, resid)
162 return y4
163
164 def ops_in_model_before(self):
165 return [torch.ops.vllm.all_reduce.default]
166
167 def ops_in_model_after(self):
168 if self.use_aiter:
169 return [rocm_aiter_ops.get_fused_allreduce_rmsnorm_op()]
170 return [torch.ops.vllm.flashinfer_trtllm_fused_allreduce_norm.default]
171
172
173 class TestAllReduceGemmaRMSNormModel(torch.nn.Module):
174 def __init__(
175 self,
176 hidden_size=16,
177 token_num=16,
178 eps=1e-6,
179 dtype: torch.dtype = torch.float16,
180 ):
181 super().__init__()
182 self.hidden_size = hidden_size
183 self.eps = eps
184 self.norm = [GemmaRMSNorm(hidden_size, eps) for _ in range(4)]
185 # Non-trivial weight (~Gemma range) so (1 + w) exercises the scale path.
186 for n in self.norm:
187 n.weight.data.normal_(mean=0.0, std=0.1)
188 self.w = [torch.rand(hidden_size, hidden_size) for _ in range(3)]
189
190 def forward(self, x):
191 # avoid having graph input be an arg to a pattern directly
192 z = torch.relu(x)
193 x = resid = tensor_model_parallel_all_reduce(z)
194 y = self.norm[0](x)
195
196 z2 = torch.mm(y, self.w[0])
197 x2 = tensor_model_parallel_all_reduce(z2)
198 y2, resid = self.norm[1](x2, resid)
199
200 z3 = torch.mm(y2, self.w[1])
201 x3 = tensor_model_parallel_all_reduce(z3)
202 y3, resid = self.norm[2](x3, resid)
203
204 z4 = torch.mm(y3, self.w[2])
205 x4 = tensor_model_parallel_all_reduce(z4)
206 y4, resid = self.norm[3](x4, resid)
207 return y4
208
209 def ops_in_model_before(self):
210 return [torch.ops.vllm.all_reduce.default]
211
212 def ops_in_model_after(self):
213 return [torch.ops.vllm.flashinfer_trtllm_fused_allreduce_norm.default]
214
215
216 class TestAllReduceRMSNormStaticQuantFP8Model(torch.nn.Module):
217 quant_key = kFp8StaticTensorSym
218
219 def __init__(
220 self, hidden_size=16, token_num=16, eps=1e-6, dtype: torch.dtype = torch.float16
221 ):
222 super().__init__()
223 self.hidden_size = hidden_size
224 self.eps = eps
225 self.norm = [RMSNorm(hidden_size, eps) for i in range(4)]
226 self.fp8_linear_layers = [
227 TestFP8Layer(
228 weight_shape=(hidden_size, hidden_size),
229 activation_quant_key=self.quant_key,
230 weight_quant_key=self.quant_key,
231 input_dtype=dtype,
232 )
233 for i in range(3)
234 ]
235
236 def forward(self, hidden_states):
237 # avoid having graph input be an arg to a pattern directly
238 z = torch.relu(hidden_states)
239 x = resid = tensor_model_parallel_all_reduce(z)
240 y = self.norm[0](x)
241
242 z2 = self.fp8_linear_layers[0](y)
243
244 x2 = tensor_model_parallel_all_reduce(z2)
245 y2, resid = self.norm[1](x2, resid)
246
247 z3 = self.fp8_linear_layers[1](y2)
248
249 x3 = tensor_model_parallel_all_reduce(z3)
250 y3, resid = self.norm[2](x3, resid) # use resid here
251
252 z4 = self.fp8_linear_layers[2](y3)
253
254 x4 = tensor_model_parallel_all_reduce(z4)
255 y4, resid = self.norm[3](x4, resid) # use resid here
256 return y4
257
258 def ops_in_model_after(self):
259 return [torch.ops.vllm.flashinfer_trtllm_fused_allreduce_norm.default]
260
261 def ops_in_model_before(self):
262 return [
263 torch.ops.vllm.all_reduce.default,
264 torch.ops._C.static_scaled_fp8_quant.default
265 if self.fp8_linear_layers[0].is_quant_fp8_enabled()
266 else torch.ops.aten.reciprocal.default,
267 ]
268
269
270 class TestAllReduceGemmaRMSNormStaticQuantFP8Model(
271 TestAllReduceRMSNormStaticQuantFP8Model
272 ):
273 def __init__(
274 self,
275 hidden_size=16,
276 token_num=16,
277 eps=1e-6,
278 dtype: torch.dtype = torch.float16,
279 ):
280 super().__init__(hidden_size, token_num, eps, dtype)
281 self.norm = [GemmaRMSNorm(hidden_size, eps) for _ in range(4)]
282 for norm in self.norm:
283 norm.weight.requires_grad_(False)
284
285 def ops_in_model_before(self):
286 return [torch.ops.vllm.all_reduce.default]
287
288
289 class TestAiterAllReduceRMSNormGroupQuantFP8Model(torch.nn.Module):
290 """Exercises the new ROCm AITER AR+RMS+per-group-FP8-quant patterns.
291
292 Four ``rms_norm`` sites that together hit every pattern registered by
293 ``RocmAiterAllReduceFusionPass`` for the per-group FP8 quant path:
294
295 * ``norm[0]``: ``all_reduce -> rms_norm -> group_fp8_quant`` (no residual)
296 -> ``AiterAllreduceFusedRMSNormGroupQuantFP8Pattern``
297 * ``norm[1]``: ``all_reduce -> fused_add_rms_norm -> group_fp8_quant``
298 (single ``rms`` consumer)
299 -> ``AiterAllreduceFusedAddRMSNormGroupQuantFP8Pattern``
300 * ``norm[2..3]``: ``all_reduce -> fused_add_rms_norm
301 -> (group_fp8_quant + rocm_unquantized_gemm)`` (two ``rms`` consumers,
302 modeling the DSv3.2 indexer fan-out)
303 -> ``AiterAllreduceFusedAddRMSNormGroupQuantWithIndexerPattern``
304
305 Each site starts from the same bf16 activation and is returned separately.
306 This avoids compounding four quantize/dequantize errors while still testing
307 all four pattern firings without depending on a real FP8 block-scaled GEMM.
308 """
309
310 quant_group_size = 128
311 indexer_out_dim = 8
312
313 def __init__(
314 self,
315 hidden_size=128,
316 token_num=16,
317 eps=1e-6,
318 dtype: torch.dtype = torch.bfloat16,
319 ):
320 super().__init__()
321 self.hidden_size = hidden_size
322 self.eps = eps
323 self.dtype = dtype
324 assert hidden_size % self.quant_group_size == 0, (
325 f"hidden_size ({hidden_size}) must be a multiple of "
326 f"quant_group_size ({self.quant_group_size}) for per-group FP8 quant"
327 )
328 self.norm = [RMSNorm(hidden_size, eps) for _ in range(4)]
329 self.w = [torch.rand(hidden_size, hidden_size, dtype=dtype) for _ in range(3)]
330 self.indexer_w = [
331 torch.rand(self.indexer_out_dim, hidden_size, dtype=dtype) for _ in range(2)
332 ]
333
334 def _group_quant(self, rms: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
335 return torch.ops.vllm.rocm_aiter_group_fp8_quant.default(
336 rms, self.quant_group_size
337 )
338
339 def _dequantize_to_bf16(self, q: torch.Tensor, s: torch.Tensor) -> torch.Tensor:
340 # Broadcast the per-group scale across each group of `quant_group_size`
341 # to validate the quantized result without depending on a real FP8
342 # block-scaled GEMM kernel in the test.
343 s_full = s.repeat_interleave(self.quant_group_size, dim=-1).to(self.dtype)
344 return q.to(self.dtype) * s_full
345
346 def forward(self, hidden_states):
347 z = torch.relu(hidden_states)
348 x0 = tensor_model_parallel_all_reduce(z)
349 rms0 = self.norm[0](x0)
350 q0, s0 = self._group_quant(rms0)
351 y0 = self._dequantize_to_bf16(q0, s0)
352
353 x1 = tensor_model_parallel_all_reduce(torch.mm(z, self.w[0]))
354 rms1, resid1 = self.norm[1](x1, z.clone())
355 q1, s1 = self._group_quant(rms1)
356 y1 = self._dequantize_to_bf16(q1, s1)
357
358 x2 = tensor_model_parallel_all_reduce(torch.mm(z, self.w[1]))
359 rms2, resid2 = self.norm[2](x2, z.clone())
360 q2, s2 = self._group_quant(rms2)
361 # Second consumer of ``rms2``: forces the with-indexer pattern.
362 idx2 = torch.ops.vllm.rocm_unquantized_gemm(rms2, self.indexer_w[0], None)
363 y2 = self._dequantize_to_bf16(q2, s2)
364
365 x3 = tensor_model_parallel_all_reduce(torch.mm(z, self.w[2]))
366 rms3, resid3 = self.norm[3](x3, z.clone())
367 q3, s3 = self._group_quant(rms3)
368 # Second consumer of ``rms3``: forces the with-indexer pattern.
369 idx3 = torch.ops.vllm.rocm_unquantized_gemm(rms3, self.indexer_w[1], None)
370 y3 = self._dequantize_to_bf16(q3, s3)
371 return y0, y1, y2, y3, resid1, resid2, resid3, idx2, idx3
372
373 def ops_in_model_before(self):
374 return [
375 torch.ops.vllm.all_reduce.default,
376 torch.ops.vllm.rocm_aiter_group_fp8_quant.default,
377 ]
378
379 def ops_in_model_after(self):
380 return [
381 rocm_aiter_ops.get_fused_allreduce_rmsnorm_quant_per_group_op(),
382 rocm_aiter_ops.get_fused_allreduce_rmsnorm_quant_per_group_with_bf16_norm_op(), # noqa: E501
383 ]
384
385
386 class TestAllReduceFusedAddRMSNormStaticQuantFP4Model(torch.nn.Module):
387 def __init__(
388 self, hidden_size=16, token_num=16, eps=1e-6, dtype: torch.dtype = torch.float16
389 ):
390 super().__init__()
391 self.hidden_size = hidden_size
392 self.eps = eps
393 self.norm = [RMSNorm(hidden_size, eps) for i in range(4)]
394
395 self.w = [torch.rand(hidden_size, hidden_size) for _ in range(3)]
396 self.agscale = [torch.rand(1, dtype=torch.float32) for _ in range(3)]
397 wgscale = [torch.rand(1, dtype=torch.float32) for _ in range(3)]
398 self.alpha = [1 / (w * a) for w, a in zip(wgscale, self.agscale)]
399
400 wq_gen, wscale_gen = zip(
401 *(scaled_fp4_quant(w, wg) for w, wg in zip(self.w, wgscale))
402 )
403 self.wq, self.wscale = list(wq_gen), list(wscale_gen)
404
405 def forward(self, hidden_states):
406 # avoid having graph input be an arg to a pattern directly
407 z = torch.relu(hidden_states)
408 x = resid = tensor_model_parallel_all_reduce(z)
409 y = self.norm[0](x)
410
411 yq, y_scale = scaled_fp4_quant(y, self.agscale[0])
412 z2 = cutlass_scaled_fp4_mm(
413 yq, self.wq[0], y_scale, self.wscale[0], self.alpha[0], out_dtype=y.dtype
414 )
415
416 x2 = tensor_model_parallel_all_reduce(z2)
417 y2, resid = self.norm[1](x2, resid)
418
419 yq2, y_scale2 = scaled_fp4_quant(y2, self.agscale[1])
420 z3 = cutlass_scaled_fp4_mm(
421 yq2, self.wq[1], y_scale2, self.wscale[1], self.alpha[1], out_dtype=y2.dtype
422 )
423
424 x3 = tensor_model_parallel_all_reduce(z3)
425 y3, resid = self.norm[2](x3, resid) # use resid here
426
427 yq3, y_scale3 = scaled_fp4_quant(y3, self.agscale[2])
428 z4 = cutlass_scaled_fp4_mm(
429 yq3, self.wq[2], y_scale3, self.wscale[2], self.alpha[2], out_dtype=y3.dtype
430 )
431 x4 = tensor_model_parallel_all_reduce(z4)
432 y4, resid = self.norm[3](x4, resid) # use resid here
433 return y4
434
435 def ops_in_model_after(self):
436 return [torch.ops.vllm.flashinfer_trtllm_fused_allreduce_norm.default]
437
438 def ops_in_model_before(self):
439 return [
440 torch.ops.vllm.all_reduce.default,
441 torch.ops._C.scaled_fp4_quant.out,
442 ]
443
444
445 @multi_gpu_test(num_gpus=2)
446 @pytest.mark.parametrize(
447 "test_model, enable_quant_fp8_custom_op, use_aiter",
448 [
449 (TestAllReduceRMSNormModel, False, IS_AITER_FOUND),
450 pytest.param(
451 TestAllReduceGemmaRMSNormModel,
452 False,
453 False,
454 marks=pytest.mark.skipif(
455 current_platform.is_rocm(),
456 reason="Not supported on ROCm platform",
457 ),
458 ),
459 pytest.param(
460 TestAllReduceRMSNormStaticQuantFP8Model,
461 True,
462 False,
463 marks=pytest.mark.skipif(
464 current_platform.is_rocm(),
465 reason="Not supported on ROCm platform",
466 ),
467 ),
468 pytest.param(
469 TestAllReduceGemmaRMSNormStaticQuantFP8Model,
470 True,
471 False,
472 marks=pytest.mark.skipif(
473 current_platform.is_rocm(),
474 reason="Not supported on ROCm platform",
475 ),
476 ),
477 pytest.param(
478 TestAllReduceRMSNormStaticQuantFP8Model,
479 False,
480 False,
481 marks=pytest.mark.skipif(
482 current_platform.is_rocm(),
483 reason="Not supported on ROCm platform",
484 ),
485 ),
486 pytest.param(
487 TestAllReduceFusedAddRMSNormStaticQuantFP4Model,
488 False,
489 False,
490 marks=pytest.mark.skipif(
491 current_platform.is_rocm(),
492 reason="Not supported on ROCm platform",
493 ),
494 ),
495 ],
496 )
497 @pytest.mark.parametrize("batch_size", [8])
498 @pytest.mark.parametrize("seq_len", [8])
499 @pytest.mark.parametrize("hidden_size", [64])
500 @pytest.mark.parametrize("dtype", [torch.bfloat16])
501 @pytest.mark.parametrize("enable_rms_norm_custom_op", [True, False])
502 @pytest.mark.parametrize("flashinfer_allreduce_backend", ["trtllm", "mnnvl"])
503 @pytest.mark.skipif(envs.VLLM_TARGET_DEVICE not in ["cuda"], reason="Only test on CUDA")
504 @pytest.mark.skipif(
505 current_platform.is_rocm() and not IS_AITER_FOUND,
506 reason="aiter is not found",
507 )
508 @pytest.mark.skipif(
509 current_platform.is_cuda()
510 and (
511 not find_spec("flashinfer")
512 or not has_module_attribute("flashinfer.comm", "allreduce_fusion")
513 or not has_module_attribute(
514 "flashinfer.comm", "create_allreduce_fusion_workspace"
515 )
516 ),
517 reason="flashinfer is not found or flashinfer "
518 "is not compiled with allreduce_fusion",
519 )
520 def test_all_reduce_fusion_pass_replace(
521 test_model: torch.nn.Module,
522 batch_size: int,
523 seq_len: int,
524 hidden_size: int,
525 dtype: torch.dtype,
526 enable_rms_norm_custom_op,
527 enable_quant_fp8_custom_op,
528 flashinfer_allreduce_backend,
529 use_aiter: bool,
530 monkeypatch: pytest.MonkeyPatch,
531 ):
532 if use_aiter:
533 with monkeypatch.context() as m:
534 m.setenv("VLLM_ROCM_USE_AITER", str(use_aiter))
535 rocm_aiter_ops.refresh_env_variables()
536
537 num_processes = 2
538 if (
539 test_model == TestAllReduceFusedAddRMSNormStaticQuantFP4Model
540 and not current_platform.has_device_capability(100)
541 ):
542 pytest.skip(
543 "Skip as nvfp4 is only supported on "
544 "devices with compute capability 10.0 (Blackwell)"
545 )
546
547 def run_torch_spawn(fn, nprocs):
548 master_port = get_open_port()
549 torch.multiprocessing.spawn(
550 fn,
551 args=(
552 num_processes,
553 master_port,
554 test_model,
555 batch_size,
556 seq_len,
557 hidden_size,
558 dtype,
559 enable_rms_norm_custom_op,
560 enable_quant_fp8_custom_op,
561 flashinfer_allreduce_backend,
562 use_aiter,
563 monkeypatch,
564 ),
565 nprocs=nprocs,
566 )
567
568 run_torch_spawn(all_reduce_fusion_pass_on_test_model, num_processes)
569
570
571 def all_reduce_fusion_pass_on_test_model(
572 local_rank: int,
573 world_size: int,
574 master_port: int,
575 test_model_cls: torch.nn.Module,
576 batch_size: int,
577 seq_len: int,
578 hidden_size: int,
579 dtype: torch.dtype,
580 enable_rms_norm_custom_op,
581 enable_quant_fp8_custom_op,
582 flashinfer_allreduce_backend,
583 use_aiter: bool,
584 monkeypatch: pytest.MonkeyPatch,
585 ):
586 set_random_seed(0)
587
588 device = torch.device(f"{DEVICE_TYPE}:{local_rank}")
589 torch.accelerator.set_device_index(device)
590 torch.set_default_device(device)
591 torch.set_default_dtype(dtype)
592
593 update_environment_variables(
594 {
595 "RANK": str(local_rank),
596 "LOCAL_RANK": str(local_rank),
597 "WORLD_SIZE": str(world_size),
598 "MASTER_ADDR": "localhost",
599 "MASTER_PORT": str(master_port),
600 "VLLM_FLASHINFER_ALLREDUCE_BACKEND": flashinfer_allreduce_backend,
601 "VLLM_ROCM_USE_AITER": str(int(use_aiter)),
602 "VLLM_ROCM_USE_AITER_CUSTOM_AR": str(int(use_aiter)),
603 }
604 )
605 if use_aiter:
606 rocm_aiter_ops.refresh_env_variables()
607
608 init_distributed_environment()
609
610 custom_ops = []
611 if enable_rms_norm_custom_op:
612 custom_ops.append("+rms_norm")
613 if enable_quant_fp8_custom_op:
614 custom_ops.append("+quant_fp8")
615
616 vllm_config = VllmConfig(
617 compilation_config=CompilationConfig(
618 mode=CompilationMode.VLLM_COMPILE, custom_ops=custom_ops
619 )
620 )
621 vllm_config.compilation_config.pass_config = PassConfig(
622 fuse_allreduce_rms=True, eliminate_noops=True
623 )
624 vllm_config.device_config = DeviceConfig(device=torch.device(DEVICE_TYPE))
625 vllm_config.parallel_config.rank = local_rank # Setup rank for debug path
626
627 # this is a fake model name to construct the model config
628 # in the vllm_config, it's not really used.
629 model_name = "RedHatAI/Llama-3.2-1B-Instruct-FP8"
630 vllm_config.model_config = ModelConfig(
631 model=model_name, trust_remote_code=True, dtype=dtype, seed=42
632 )
633 with set_current_vllm_config(vllm_config):
634 initialize_model_parallel(tensor_model_parallel_size=world_size)
635 all_reduce_fusion_pass = (
636 RocmAiterAllReduceFusionPass(vllm_config)
637 if use_aiter
638 else AllReduceFusionPass(vllm_config)
639 )
640 noop_pass = NoOpEliminationPass(vllm_config)
641 func_pass = FixFunctionalizationPass(vllm_config)
642 cleanup_pass = PostCleanupPass(vllm_config)
643
644 backend = TestBackend(
645 noop_pass, all_reduce_fusion_pass, func_pass, cleanup_pass
646 )
647
648 token_num = batch_size * seq_len
649 if test_model_cls is TestAllReduceRMSNormModel:
650 model = test_model_cls(
651 hidden_size, token_num, dtype=dtype, use_aiter=use_aiter
652 )
653 else:
654 model = test_model_cls(hidden_size, token_num, dtype=dtype)
655
656 hidden_states = torch.randn((token_num, hidden_size), requires_grad=False)
657
658 compiled_model = torch.compile(model, backend=backend)
659 compiled_model(hidden_states)
660
661 results_unfused = model(hidden_states)
662 results_fused = compiled_model(hidden_states)
663 torch.testing.assert_close(results_unfused, results_fused, atol=1e-2, rtol=1e-2)
664
665 assert all_reduce_fusion_pass.matched_count == 4, (
666 f"{all_reduce_fusion_pass.matched_count=}"
667 )
668 backend.check_before_ops(model.ops_in_model_before(), fully_replaced=False)
669 backend.check_after_ops(model.ops_in_model_after())
670 if test_model_cls in (
671 TestAllReduceGemmaRMSNormModel,
672 TestAllReduceGemmaRMSNormStaticQuantFP8Model,
673 ):
674 fused_op = torch.ops.vllm.flashinfer_trtllm_fused_allreduce_norm.default
675 fused_nodes = list(find_op_nodes(fused_op, backend.graph_post_pass))
676 assert fused_nodes
677 assert all(n.kwargs.get("weight_bias") == 1.0 for n in fused_nodes)
678 del all_reduce_fusion_pass
679
680
681 @multi_gpu_test(num_gpus=2)
682 @pytest.mark.parametrize("batch_size", [8])
683 @pytest.mark.parametrize("seq_len", [8])
684 @pytest.mark.parametrize("hidden_size", [128])
685 @pytest.mark.parametrize("dtype", [torch.bfloat16])
686 @pytest.mark.parametrize("enable_rms_norm_custom_op", [True, False])
687 @pytest.mark.skipif(
688 not current_platform.is_rocm(),
689 reason="ROCm AITER AR+RMS+per-group-FP8-quant fusion is ROCm-only",
690 )
691 @pytest.mark.skipif(not IS_AITER_FOUND, reason="aiter is not found")
692 def test_rocm_aiter_all_reduce_rmsnorm_group_quant_fp8_fusion_pass_replace(
693 batch_size: int,
694 seq_len: int,
695 hidden_size: int,
696 dtype: torch.dtype,
697 enable_rms_norm_custom_op: bool,
698 monkeypatch: pytest.MonkeyPatch,
699 ):
700 """Sibling of ``test_all_reduce_fusion_pass_replace`` for the new
701 ROCm AITER AR+RMS+per-group-FP8-quant fusion patterns.
702
703 Validates the three new ``VllmPatternReplacement`` patterns added to
704 ``RocmAiterAllReduceFusionPass``:
705
706 * ``AiterAllreduceFusedRMSNormGroupQuantFP8Pattern`` (no-residual)
707 * ``AiterAllreduceFusedAddRMSNormGroupQuantFP8Pattern`` (with-residual,
708 single ``rms`` consumer)
709 * ``AiterAllreduceFusedAddRMSNormGroupQuantWithIndexerPattern`` (with-
710 residual, DSv3.2 indexer fan-out; parametrized over
711 ``rocm_aiter_group_fp8_quant``
712 producer).
713 """
714 with monkeypatch.context() as m:
715 m.setenv("VLLM_ROCM_USE_AITER", "1")
716 rocm_aiter_ops.refresh_env_variables()
717
718 if not AiterCustomAllreduce.build_supports_per_group_quant():
719 pytest.skip(
720 "aiter build is missing 'fused_ar_rms_per_group_quant' (needs "
721 "ROCm/aiter PR #2823); the new patterns aren't registered."
722 )
723
724 num_processes = 2
725
726 def run_torch_spawn(fn, nprocs):
727 master_port = get_open_port()
728 torch.multiprocessing.spawn(
729 fn,
730 args=(
731 num_processes,
732 master_port,
733 TestAiterAllReduceRMSNormGroupQuantFP8Model,
734 batch_size,
735 seq_len,
736 hidden_size,
737 dtype,
738 enable_rms_norm_custom_op,
739 monkeypatch,
740 ),
741 nprocs=nprocs,
742 )
743
744 run_torch_spawn(rocm_aiter_group_quant_fusion_pass_on_test_model, num_processes)
745
746
747 def rocm_aiter_group_quant_fusion_pass_on_test_model(
748 local_rank: int,
749 world_size: int,
750 master_port: int,
751 test_model_cls: torch.nn.Module,
752 batch_size: int,
753 seq_len: int,
754 hidden_size: int,
755 dtype: torch.dtype,
756 enable_rms_norm_custom_op: bool,
757 monkeypatch: pytest.MonkeyPatch,
758 ):
759 set_random_seed(0)
760
761 device = torch.device(f"{DEVICE_TYPE}:{local_rank}")
762 torch.accelerator.set_device_index(device)
763 torch.set_default_device(device)
764 torch.set_default_dtype(dtype)
765
766 update_environment_variables(
767 {
768 "RANK": str(local_rank),
769 "LOCAL_RANK": str(local_rank),
770 "WORLD_SIZE": str(world_size),
771 "MASTER_ADDR": "localhost",
772 "MASTER_PORT": str(master_port),
773 "VLLM_ROCM_USE_AITER": "1",
774 "VLLM_ROCM_USE_AITER_CUSTOM_AR": "1",
775 }
776 )
777 rocm_aiter_ops.refresh_env_variables()
778
779 init_distributed_environment()
780
781 custom_ops = []
782 if enable_rms_norm_custom_op:
783 custom_ops.append("+rms_norm")
784 # We always enable +quant_fp8 so the matcher's example
785 # trace finds the same form the test model uses.
786 custom_ops.append("+quant_fp8")
787
788 vllm_config = VllmConfig(
789 compilation_config=CompilationConfig(
790 mode=CompilationMode.VLLM_COMPILE, custom_ops=custom_ops
791 )
792 )
793 vllm_config.compilation_config.pass_config = PassConfig(
794 fuse_allreduce_rms=True, eliminate_noops=True
795 )
796 vllm_config.device_config = DeviceConfig(device=torch.device(DEVICE_TYPE))
797 vllm_config.parallel_config.rank = local_rank
798
799 model_name = "RedHatAI/Llama-3.2-1B-Instruct-FP8"
800 vllm_config.model_config = ModelConfig(
801 model=model_name, trust_remote_code=True, dtype=dtype, seed=42
802 )
803 with set_current_vllm_config(vllm_config):
804 initialize_model_parallel(tensor_model_parallel_size=world_size)
805 all_reduce_fusion_pass = RocmAiterAllReduceFusionPass(vllm_config)
806 noop_pass = NoOpEliminationPass(vllm_config)
807 func_pass = FixFunctionalizationPass(vllm_config)
808 cleanup_pass = PostCleanupPass(vllm_config)
809
810 backend = TestBackend(
811 noop_pass, all_reduce_fusion_pass, func_pass, cleanup_pass
812 )
813
814 token_num = batch_size * seq_len
815 model = test_model_cls(hidden_size, token_num, dtype=dtype)
816
817 hidden_states = torch.randn((token_num, hidden_size), requires_grad=False)
818
819 compiled_model = torch.compile(model, backend=backend)
820 compiled_model(hidden_states)
821
822 results_unfused = model(hidden_states)
823 results_fused = compiled_model(hidden_states)
824 # AITER's kernel-level oracle permits up to 5% isolated FP8 values to
825 # fall outside a 5% allclose envelope after collective reordering.
826 for site, (reference, actual) in enumerate(
827 zip(results_unfused[:4], results_fused[:4])
828 ):
829 close = torch.isclose(reference, actual, atol=5e-2, rtol=5e-2)
830 mismatch_ratio = float((~close).float().mean().item())
831 assert mismatch_ratio <= 0.05, (
832 f"FP8 site {site} mismatch ratio {mismatch_ratio:.2%} exceeds 5%"
833 )
834
835 # Residual and bf16 indexer outputs do not incur FP8 quantization noise.
836 torch.testing.assert_close(
837 results_unfused[4:], results_fused[4:], atol=1e-2, rtol=1e-2
838 )
839
840 # Four pattern firings: norm[0] (no-add quant), norm[1] (add quant,
841 # single ``rms`` consumer), norm[2..3] (add quant + indexer fan-out).
842 assert all_reduce_fusion_pass.matched_count == 4, (
843 f"{all_reduce_fusion_pass.matched_count=}"
844 )
845 backend.check_before_ops(model.ops_in_model_before(), fully_replaced=True)
846 fused_quant_op, fused_indexer_op = model.ops_in_model_after()
847 assert backend.op_count(fused_quant_op) == 2
848 assert backend.op_count(fused_indexer_op) == 2
849 del all_reduce_fusion_pass