| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | from collections.abc import Callable |
| 5 | from contextlib import suppress |
| 6 | |
| 7 | import torch |
| 8 | import torch._inductor.pattern_matcher as pm |
| 9 | import torch.distributed.distributed_c10d as c10d |
| 10 | import torch.fx as fx |
| 11 | from torch._inductor.pattern_matcher import PatternMatcherPass |
| 12 | from torch.distributed._symmetric_memory import enable_symm_mem_for_group |
| 13 | |
| 14 | from vllm.config import VllmConfig |
| 15 | from vllm.config.utils import Range |
| 16 | from vllm.distributed import get_tp_group |
| 17 | from vllm.distributed.parallel_state import ( |
| 18 | get_tensor_model_parallel_world_size, |
| 19 | ) |
| 20 | from vllm.logger import init_logger |
| 21 | from vllm.platforms import current_platform |
| 22 | from vllm.utils.torch_utils import direct_register_custom_op |
| 23 | |
| 24 | from ..inductor_pass import enable_fake_mode |
| 25 | from ..vllm_inductor_pass import ( |
| 26 | VllmFusionPatternMatcherPass, |
| 27 | VllmInductorPass, |
| 28 | VllmPatternMatcherPass, |
| 29 | VllmPatternReplacement, |
| 30 | ) |
| 31 | |
| 32 | FP8_DTYPE = current_platform.fp8_dtype() |
| 33 | |
| 34 | logger = init_logger(__name__) |
| 35 | |
| 36 | |
| 37 | def _flashinfer_scaled_mm_out( |
| 38 | A: torch.Tensor, |
| 39 | B: torch.Tensor, |
| 40 | *, |
| 41 | scale_a: torch.Tensor, |
| 42 | scale_b: torch.Tensor, |
| 43 | out: torch.Tensor, |
| 44 | bias: torch.Tensor | None = None, |
| 45 | scale_result: torch.Tensor | None = None, |
| 46 | out_dtype: torch.dtype | None = None, |
| 47 | use_fast_accum: bool = False, |
| 48 | ) -> None: |
| 49 | # Import lazily to avoid a circular import during module initialization |
| 50 | # when docs or other tooling import the pass without FlashInfer. |
| 51 | from vllm.utils.flashinfer import flashinfer_scaled_fp8_mm_out |
| 52 | |
| 53 | assert bias is None, "FlashInfer symm_mem adapter does not support bias" |
| 54 | assert scale_result is None, ( |
| 55 | "FlashInfer symm_mem adapter does not support result scaling" |
| 56 | ) |
| 57 | assert not use_fast_accum, ( |
| 58 | "FlashInfer symm_mem adapter does not support use_fast_accum" |
| 59 | ) |
| 60 | assert A.ndim == 2 and B.ndim == 2 and out.ndim == 2, ( |
| 61 | "FlashInfer symm_mem adapter expects 2D inputs and output" |
| 62 | ) |
| 63 | assert scale_a.numel() == 1 and scale_b.numel() == 1, ( |
| 64 | "FlashInfer symm_mem adapter only supports tensor-wise FP8 scales" |
| 65 | ) |
| 66 | |
| 67 | flashinfer_scaled_fp8_mm_out( |
| 68 | A, |
| 69 | B, |
| 70 | scale_a, |
| 71 | scale_b, |
| 72 | out=out, |
| 73 | out_dtype=out_dtype or out.dtype, |
| 74 | ) |
| 75 | |
| 76 | |
| 77 | def _flashinfer_fp4_mm_out( |
| 78 | A: torch.Tensor, |
| 79 | B: torch.Tensor, |
| 80 | *, |
| 81 | scale_a: torch.Tensor, |
| 82 | scale_b: torch.Tensor, |
| 83 | out: torch.Tensor, |
| 84 | alpha: torch.Tensor, |
| 85 | out_dtype: torch.dtype | None = None, |
| 86 | use_8x4_sf_layout: bool = False, |
| 87 | backend: str = "cutlass", |
| 88 | ) -> None: |
| 89 | from vllm.utils.flashinfer import flashinfer_scaled_fp4_mm_out |
| 90 | |
| 91 | assert A.ndim == 2 and B.ndim == 2 and out.ndim == 2, ( |
| 92 | "FlashInfer FP4 symm_mem adapter expects 2D inputs and output" |
| 93 | ) |
| 94 | flashinfer_scaled_fp4_mm_out( |
| 95 | A, |
| 96 | B, |
| 97 | scale_a, |
| 98 | scale_b, |
| 99 | alpha, |
| 100 | out=out, |
| 101 | out_dtype=out_dtype or out.dtype, |
| 102 | use_8x4_sf_layout=use_8x4_sf_layout, |
| 103 | backend=backend, |
| 104 | ) |
| 105 | |
| 106 | |
| 107 | def fused_flashinfer_scaled_matmul_reduce_scatter_fake( |
| 108 | A: torch.Tensor, |
| 109 | B: torch.Tensor, |
| 110 | A_scale: torch.Tensor, |
| 111 | B_scale: torch.Tensor, |
| 112 | reduce_op: str, |
| 113 | orig_scatter_dim: int, |
| 114 | scatter_dim_after_maybe_reshape: int, |
| 115 | group_name: str, |
| 116 | output_shape: list[int], |
| 117 | out_dtype: torch.dtype | None = None, |
| 118 | ) -> torch.Tensor: |
| 119 | world_size = c10d._resolve_process_group(group_name).size() |
| 120 | result_shape = list(output_shape) |
| 121 | result_shape[orig_scatter_dim] //= world_size |
| 122 | return torch.empty( |
| 123 | result_shape, |
| 124 | dtype=out_dtype or torch.bfloat16, |
| 125 | device=A.device, |
| 126 | ) |
| 127 | |
| 128 | |
| 129 | def fused_flashinfer_scaled_matmul_reduce_scatter( |
| 130 | A: torch.Tensor, |
| 131 | B: torch.Tensor, |
| 132 | A_scale: torch.Tensor, |
| 133 | B_scale: torch.Tensor, |
| 134 | reduce_op: str, |
| 135 | orig_scatter_dim: int, |
| 136 | scatter_dim_after_maybe_reshape: int, |
| 137 | group_name: str, |
| 138 | output_shape: list[int], |
| 139 | out_dtype: torch.dtype | None = None, |
| 140 | ) -> torch.Tensor: |
| 141 | assert orig_scatter_dim == 0 and scatter_dim_after_maybe_reshape == 0, ( |
| 142 | "FlashInfer symm_mem adapter currently only supports scatter_dim=0" |
| 143 | ) |
| 144 | world_size = c10d._resolve_process_group(group_name).size() |
| 145 | assert A.ndim == 2 and B.ndim == 2, "FlashInfer symm_mem adapter expects 2D inputs" |
| 146 | assert A.is_contiguous(), "FlashInfer symm_mem adapter expects contiguous A" |
| 147 | assert A_scale.numel() == 1 and B_scale.numel() == 1, ( |
| 148 | "FlashInfer symm_mem adapter only supports tensor-wise FP8 scales" |
| 149 | ) |
| 150 | assert A.shape[0] % world_size == 0, ( |
| 151 | "FlashInfer symm_mem adapter expects M divisible by world size" |
| 152 | ) |
| 153 | |
| 154 | kwargs = { |
| 155 | "scale_b": B_scale, |
| 156 | "bias": None, |
| 157 | "scale_result": None, |
| 158 | "out_dtype": out_dtype, |
| 159 | "use_fast_accum": False, |
| 160 | } |
| 161 | return torch.distributed._symmetric_memory._fused_scaled_matmul_reduce_scatter_impl( |
| 162 | mm_out_op=_flashinfer_scaled_mm_out, |
| 163 | A=A, |
| 164 | B=B, |
| 165 | A_scale=A_scale, |
| 166 | kwargs=kwargs, |
| 167 | out_dtype=out_dtype, |
| 168 | reduce_op=reduce_op, |
| 169 | orig_scatter_dim=orig_scatter_dim, |
| 170 | scatter_dim_after_maybe_reshape=scatter_dim_after_maybe_reshape, |
| 171 | group_name=group_name, |
| 172 | output_shape=output_shape, |
| 173 | ) |
| 174 | |
| 175 | |
| 176 | def fused_all_gather_flashinfer_scaled_matmul_fake( |
| 177 | A_shard: torch.Tensor, |
| 178 | B: torch.Tensor, |
| 179 | A_scale: torch.Tensor, |
| 180 | B_scale: torch.Tensor, |
| 181 | gather_dim: int, |
| 182 | group_name: str, |
| 183 | out_dtype: torch.dtype | None = None, |
| 184 | ) -> torch.Tensor: |
| 185 | world_size = c10d._resolve_process_group(group_name).size() |
| 186 | output_shape = list(A_shard.shape) |
| 187 | output_shape[gather_dim] *= world_size |
| 188 | output_shape[-1] = B.shape[1] |
| 189 | return torch.empty( |
| 190 | output_shape, |
| 191 | dtype=out_dtype or torch.bfloat16, |
| 192 | device=A_shard.device, |
| 193 | ) |
| 194 | |
| 195 | |
| 196 | def fused_all_gather_flashinfer_scaled_matmul( |
| 197 | A_shard: torch.Tensor, |
| 198 | B: torch.Tensor, |
| 199 | A_scale: torch.Tensor, |
| 200 | B_scale: torch.Tensor, |
| 201 | gather_dim: int, |
| 202 | group_name: str, |
| 203 | out_dtype: torch.dtype | None = None, |
| 204 | ) -> torch.Tensor: |
| 205 | assert gather_dim == 0, ( |
| 206 | "FlashInfer symm_mem adapter currently only supports gather_dim=0" |
| 207 | ) |
| 208 | _, outputs = torch.distributed._symmetric_memory._fused_all_gather_matmul_impl( |
| 209 | mm_out_op=_flashinfer_scaled_mm_out, |
| 210 | A_shard=A_shard, |
| 211 | Bs=[B], |
| 212 | A_scale=A_scale, |
| 213 | kwargs_list=[ |
| 214 | { |
| 215 | "scale_b": B_scale, |
| 216 | "bias": None, |
| 217 | "scale_result": None, |
| 218 | "out_dtype": out_dtype, |
| 219 | "use_fast_accum": False, |
| 220 | } |
| 221 | ], |
| 222 | out_dtypes=[out_dtype], |
| 223 | gather_dim=gather_dim, |
| 224 | group_name=group_name, |
| 225 | return_A=False, |
| 226 | ) |
| 227 | return outputs[0] |
| 228 | |
| 229 | |
| 230 | def fused_all_gather_flashinfer_fp4_matmul_fake( |
| 231 | A_shard: torch.Tensor, |
| 232 | B: torch.Tensor, |
| 233 | A_scale_shard: torch.Tensor, |
| 234 | B_scale: torch.Tensor, |
| 235 | alpha: torch.Tensor, |
| 236 | gather_dim: int, |
| 237 | group_name: str, |
| 238 | out_dtype: torch.dtype | None = None, |
| 239 | view_a_scale_as_fp8: bool = False, |
| 240 | use_8x4_sf_layout: bool = False, |
| 241 | backend: str = "cutlass", |
| 242 | ) -> torch.Tensor: |
| 243 | world_size = c10d._resolve_process_group(group_name).size() |
| 244 | output_shape = list(A_shard.shape) |
| 245 | output_shape[gather_dim] *= world_size |
| 246 | output_shape[-1] = B.shape[1] |
| 247 | return torch.empty( |
| 248 | output_shape, |
| 249 | dtype=out_dtype or torch.bfloat16, |
| 250 | device=A_shard.device, |
| 251 | ) |
| 252 | |
| 253 | |
| 254 | def fused_all_gather_flashinfer_fp4_matmul( |
| 255 | A_shard: torch.Tensor, |
| 256 | B: torch.Tensor, |
| 257 | A_scale_shard: torch.Tensor, |
| 258 | B_scale: torch.Tensor, |
| 259 | alpha: torch.Tensor, |
| 260 | gather_dim: int, |
| 261 | group_name: str, |
| 262 | out_dtype: torch.dtype | None = None, |
| 263 | view_a_scale_as_fp8: bool = False, |
| 264 | use_8x4_sf_layout: bool = False, |
| 265 | backend: str = "cutlass", |
| 266 | ) -> torch.Tensor: |
| 267 | assert gather_dim == 0, ( |
| 268 | "FlashInfer FP4 symm_mem adapter currently only supports gather_dim=0" |
| 269 | ) |
| 270 | assert A_shard.ndim == 2 and A_scale_shard.ndim == 2 and B.ndim == 2, ( |
| 271 | "FlashInfer FP4 symm_mem adapter expects 2D inputs" |
| 272 | ) |
| 273 | if view_a_scale_as_fp8: |
| 274 | A_scale_shard = A_scale_shard.view(torch.float8_e4m3fn) |
| 275 | |
| 276 | group = c10d._resolve_process_group(group_name) |
| 277 | world_size = group.size() |
| 278 | output = A_shard.new_empty( |
| 279 | A_shard.shape[0] * world_size, |
| 280 | B.shape[1], |
| 281 | dtype=out_dtype or torch.bfloat16, |
| 282 | ) |
| 283 | output_shards = output.chunk(world_size) |
| 284 | |
| 285 | A = A_shard.new_empty(A_shard.shape[0] * world_size, A_shard.shape[1]) |
| 286 | A_scale = A_scale_shard.new_empty( |
| 287 | A_scale_shard.shape[0] * world_size, |
| 288 | A_scale_shard.shape[1], |
| 289 | ) |
| 290 | |
| 291 | def fp4_shard_consumer(shards: list[torch.Tensor], rank: int) -> None: |
| 292 | _flashinfer_fp4_mm_out( |
| 293 | shards[0], |
| 294 | B, |
| 295 | scale_a=shards[1], |
| 296 | scale_b=B_scale, |
| 297 | alpha=alpha, |
| 298 | out=output_shards[rank], |
| 299 | out_dtype=out_dtype, |
| 300 | use_8x4_sf_layout=use_8x4_sf_layout, |
| 301 | backend=backend, |
| 302 | ) |
| 303 | |
| 304 | torch.distributed._symmetric_memory._pipelined_multi_all_gather_and_consume( |
| 305 | [A_shard, A_scale_shard], |
| 306 | fp4_shard_consumer, |
| 307 | [A, A_scale], |
| 308 | group_name, |
| 309 | False, |
| 310 | ) |
| 311 | return output |
| 312 | |
| 313 | |
| 314 | direct_register_custom_op( |
| 315 | op_name="fused_flashinfer_scaled_matmul_reduce_scatter", |
| 316 | op_func=fused_flashinfer_scaled_matmul_reduce_scatter, |
| 317 | fake_impl=fused_flashinfer_scaled_matmul_reduce_scatter_fake, |
| 318 | ) |
| 319 | |
| 320 | direct_register_custom_op( |
| 321 | op_name="fused_all_gather_flashinfer_scaled_matmul", |
| 322 | op_func=fused_all_gather_flashinfer_scaled_matmul, |
| 323 | fake_impl=fused_all_gather_flashinfer_scaled_matmul_fake, |
| 324 | ) |
| 325 | |
| 326 | direct_register_custom_op( |
| 327 | op_name="fused_all_gather_flashinfer_fp4_matmul", |
| 328 | op_func=fused_all_gather_flashinfer_fp4_matmul, |
| 329 | fake_impl=fused_all_gather_flashinfer_fp4_matmul_fake, |
| 330 | ) |
| 331 | |
| 332 | |
| 333 | class BasePattern: |
| 334 | def __init__(self, dtype: torch.dtype, device: str | None) -> None: |
| 335 | self.dtype = dtype |
| 336 | self.device = device |
| 337 | self.tp = get_tp_group() |
| 338 | self.tp_size = get_tensor_model_parallel_world_size() |
| 339 | |
| 340 | |
| 341 | class GEMMReduceScatterPattern(BasePattern): |
| 342 | rocm_gemm = False |
| 343 | |
| 344 | def get_inputs(self) -> list[torch.Tensor]: |
| 345 | mul = torch.empty([16, 4], device=self.device, dtype=self.dtype) |
| 346 | mm_weight = torch.empty([4, 4], device=self.device, dtype=self.dtype) |
| 347 | return [mul, mm_weight] |
| 348 | |
| 349 | def register(self, pm_pass: PatternMatcherPass) -> None: |
| 350 | def pattern(mul: torch.Tensor, mm_weight: torch.Tensor) -> torch.Tensor: |
| 351 | if self.rocm_gemm: |
| 352 | mm = torch.ops.vllm.rocm_unquantized_gemm(mul, mm_weight, None) |
| 353 | else: |
| 354 | mm = torch.ops.aten.mm.default(mul, mm_weight) |
| 355 | reduce_scatter = torch.ops.vllm.reduce_scatter.default( |
| 356 | mm, |
| 357 | dim=0, |
| 358 | world_size=self.tp_size, |
| 359 | group_name=self.tp.unique_name, |
| 360 | ) |
| 361 | return reduce_scatter |
| 362 | |
| 363 | def replacement(mul: torch.Tensor, mm_weight: torch.Tensor) -> torch.Tensor: |
| 364 | gemm_rs = torch.ops.symm_mem.fused_matmul_reduce_scatter( |
| 365 | mul, |
| 366 | mm_weight.t() if self.rocm_gemm else mm_weight, |
| 367 | "sum", |
| 368 | scatter_dim=0, |
| 369 | group_name=self.tp.device_group.group_name, |
| 370 | ) |
| 371 | |
| 372 | return gemm_rs |
| 373 | |
| 374 | pm.register_replacement( |
| 375 | pattern, replacement, self.get_inputs(), pm.fwd_only, pm_pass |
| 376 | ) |
| 377 | |
| 378 | |
| 379 | class RocmGEMMReduceScatterPattern(GEMMReduceScatterPattern): |
| 380 | rocm_gemm = True |
| 381 | |
| 382 | |
| 383 | class AllGatherGEMMPattern(BasePattern): |
| 384 | rocm_gemm = False |
| 385 | |
| 386 | def get_inputs(self) -> list[torch.Tensor]: |
| 387 | x = torch.empty([4, 4], device=self.device, dtype=self.dtype) |
| 388 | weight = torch.empty([4, 4], device=self.device, dtype=self.dtype) |
| 389 | |
| 390 | return [x, weight] |
| 391 | |
| 392 | def register(self, pm_pass: PatternMatcherPass) -> None: |
| 393 | def pattern( |
| 394 | x: torch.Tensor, |
| 395 | weight: torch.Tensor, |
| 396 | ) -> torch.Tensor: |
| 397 | all_gather = torch.ops.vllm.all_gather.default( |
| 398 | x, |
| 399 | dim=0, |
| 400 | world_size=self.tp_size, |
| 401 | group_name=self.tp.unique_name, |
| 402 | ) |
| 403 | |
| 404 | if self.rocm_gemm: |
| 405 | return torch.ops.vllm.rocm_unquantized_gemm(all_gather, weight, None) |
| 406 | return torch.ops.aten.mm.default(all_gather, weight) |
| 407 | |
| 408 | def replacement(x: torch.Tensor, weight: torch.Tensor) -> torch.Tensor: |
| 409 | ag_output, mm_outputs = torch.ops.symm_mem.fused_all_gather_matmul( |
| 410 | x, |
| 411 | [weight.t() if self.rocm_gemm else weight], |
| 412 | gather_dim=0, |
| 413 | group_name=self.tp.device_group.group_name, |
| 414 | ) |
| 415 | return mm_outputs |
| 416 | |
| 417 | pm.register_replacement( |
| 418 | pattern, replacement, self.get_inputs(), pm.fwd_only, pm_pass |
| 419 | ) |
| 420 | |
| 421 | |
| 422 | class RocmAllGatherGEMMPattern(AllGatherGEMMPattern): |
| 423 | rocm_gemm = True |
| 424 | |
| 425 | |
| 426 | class ScaledMMReduceScatterPattern(BasePattern): |
| 427 | def get_inputs(self) -> list[torch.Tensor]: |
| 428 | input = torch.empty([16, 16], device=self.device, dtype=FP8_DTYPE) |
| 429 | mm_weight = ( |
| 430 | torch.empty([16, 16], device=self.device, dtype=FP8_DTYPE) |
| 431 | .contiguous() |
| 432 | .transpose(0, 1) |
| 433 | ) |
| 434 | scale_a = torch.empty([16, 1], device=self.device, dtype=torch.float32) |
| 435 | scale_b = torch.empty([1, 16], device=self.device, dtype=torch.float32) |
| 436 | return [input, mm_weight, scale_a, scale_b] |
| 437 | |
| 438 | def register(self, pm_pass: PatternMatcherPass) -> None: |
| 439 | def pattern( |
| 440 | input: torch.Tensor, |
| 441 | mat2: torch.Tensor, |
| 442 | scale_a: torch.Tensor, |
| 443 | scale_b: torch.Tensor, |
| 444 | ) -> torch.Tensor: |
| 445 | scaled_mm = torch.ops.aten._scaled_mm.default( |
| 446 | input, |
| 447 | mat2=mat2, |
| 448 | scale_a=scale_a, |
| 449 | scale_b=scale_b, |
| 450 | bias=None, |
| 451 | scale_result=None, |
| 452 | out_dtype=self.dtype, |
| 453 | ) |
| 454 | reduce_scatter = torch.ops.vllm.reduce_scatter.default( |
| 455 | scaled_mm, |
| 456 | dim=0, |
| 457 | world_size=self.tp_size, |
| 458 | group_name=self.tp.unique_name, |
| 459 | ) |
| 460 | return reduce_scatter |
| 461 | |
| 462 | def replacement( |
| 463 | input: torch.Tensor, |
| 464 | mat2: torch.Tensor, |
| 465 | scale_a: torch.Tensor, |
| 466 | scale_b: torch.Tensor, |
| 467 | ) -> torch.Tensor: |
| 468 | # Calculate output shape: input @ mat2 with scatter_dim reduced |
| 469 | output_shape = [*input.shape[:-1], mat2.shape[1]] |
| 470 | scatter_dim = 0 |
| 471 | gemm_rs = torch.ops.vllm.patched_fused_scaled_matmul_reduce_scatter( |
| 472 | input, |
| 473 | mat2, |
| 474 | scale_a, |
| 475 | scale_b, |
| 476 | "sum", |
| 477 | scatter_dim, # orig_scatter_dim |
| 478 | scatter_dim, # scatter_dim_after_maybe_reshape |
| 479 | self.tp.device_group.group_name, |
| 480 | output_shape, |
| 481 | None, # bias |
| 482 | None, # result_scale |
| 483 | self.dtype, # out_dtype |
| 484 | False, # use_fast_accum |
| 485 | ) |
| 486 | |
| 487 | return gemm_rs |
| 488 | |
| 489 | pm.register_replacement( |
| 490 | pattern, replacement, self.get_inputs(), pm.fwd_only, pm_pass |
| 491 | ) |
| 492 | |
| 493 | |
| 494 | class AllGatherScaledMMPattern(BasePattern): |
| 495 | def get_inputs(self) -> list[torch.Tensor]: |
| 496 | x = torch.empty([8, 16], device=self.device, dtype=FP8_DTYPE) |
| 497 | weight = ( |
| 498 | torch.empty([16, 16], device=self.device, dtype=FP8_DTYPE) |
| 499 | .contiguous() |
| 500 | .transpose(0, 1) |
| 501 | ) |
| 502 | |
| 503 | s1 = x.shape[0] * self.tp_size |
| 504 | |
| 505 | scale_a = torch.empty([s1, 1], device=self.device, dtype=torch.float32) |
| 506 | scale_b = torch.empty([1, 16], device=self.device, dtype=torch.float32) |
| 507 | |
| 508 | return [x, weight, scale_a, scale_b] |
| 509 | |
| 510 | def register(self, pm_pass: PatternMatcherPass) -> None: |
| 511 | def pattern( |
| 512 | x: torch.Tensor, |
| 513 | weight: torch.Tensor, |
| 514 | scale_a: torch.Tensor, |
| 515 | scale_b: torch.Tensor, |
| 516 | ) -> torch.Tensor: |
| 517 | all_gather = torch.ops.vllm.all_gather.default( |
| 518 | x, dim=0, world_size=self.tp_size, group_name=self.tp.unique_name |
| 519 | ) |
| 520 | |
| 521 | return torch.ops.aten._scaled_mm.default( |
| 522 | all_gather, |
| 523 | mat2=weight, |
| 524 | scale_a=scale_a, |
| 525 | scale_b=scale_b, |
| 526 | bias=None, |
| 527 | scale_result=None, |
| 528 | out_dtype=self.dtype, |
| 529 | ) |
| 530 | |
| 531 | def replacement( |
| 532 | x: torch.Tensor, |
| 533 | weight: torch.Tensor, |
| 534 | scale_a: torch.Tensor, |
| 535 | scale_b: torch.Tensor, |
| 536 | ) -> torch.Tensor: |
| 537 | ag_output, mm_outputs = torch.ops.symm_mem.fused_all_gather_scaled_matmul( # noqa |
| 538 | x, |
| 539 | [weight], |
| 540 | scale_a, |
| 541 | [scale_b], |
| 542 | gather_dim=0, |
| 543 | biases=[None], |
| 544 | result_scales=[None], |
| 545 | out_dtypes=[self.dtype], |
| 546 | use_fast_accum=[False], |
| 547 | group_name=self.tp.device_group.group_name, |
| 548 | ) |
| 549 | return mm_outputs |
| 550 | |
| 551 | pm.register_replacement( |
| 552 | pattern, replacement, self.get_inputs(), pm.fwd_only, pm_pass |
| 553 | ) |
| 554 | |
| 555 | |
| 556 | class CutlassScaledMMReduceScatterPattern(BasePattern): |
| 557 | def get_inputs(self) -> list[torch.Tensor]: |
| 558 | input = torch.empty([16, 16], device=self.device, dtype=FP8_DTYPE) |
| 559 | mm_weight = ( |
| 560 | torch.empty([16, 16], device=self.device, dtype=FP8_DTYPE) |
| 561 | .contiguous() |
| 562 | .transpose(0, 1) |
| 563 | ) |
| 564 | scale_a = torch.empty([16, 1], device=self.device, dtype=torch.float32) |
| 565 | scale_b = torch.empty([1, 16], device=self.device, dtype=torch.float32) |
| 566 | |
| 567 | cutlass_mm_output = torch.empty([16, 16], device=self.device, dtype=self.dtype) |
| 568 | return [input, mm_weight, scale_a, scale_b, cutlass_mm_output] |
| 569 | |
| 570 | def register(self, pm_pass: PatternMatcherPass) -> None: |
| 571 | def pattern( |
| 572 | input: torch.Tensor, |
| 573 | weight: torch.Tensor, |
| 574 | scale_a: torch.Tensor, |
| 575 | scale_b: torch.Tensor, |
| 576 | cutlass_mm_output: torch.Tensor, |
| 577 | ) -> torch.Tensor: |
| 578 | cutlass_scaled_mm = torch.ops.higher_order.auto_functionalized( |
| 579 | torch.ops._C.cutlass_scaled_mm.default, |
| 580 | out=cutlass_mm_output, |
| 581 | a=input, |
| 582 | b=weight, |
| 583 | a_scales=scale_a, |
| 584 | b_scales=scale_b, |
| 585 | bias=None, |
| 586 | ) |
| 587 | |
| 588 | reduce_scatter = torch.ops.vllm.reduce_scatter.default( |
| 589 | cutlass_scaled_mm[1], |
| 590 | dim=0, |
| 591 | world_size=self.tp_size, |
| 592 | group_name=self.tp.unique_name, |
| 593 | ) |
| 594 | return reduce_scatter |
| 595 | |
| 596 | def replacement( |
| 597 | input: torch.Tensor, |
| 598 | mat2: torch.Tensor, |
| 599 | scale_a: torch.Tensor, |
| 600 | scale_b: torch.Tensor, |
| 601 | cutlass_mm_output: torch.Tensor, |
| 602 | ) -> torch.Tensor: |
| 603 | # Calculate output shape: input @ mat2 with scatter_dim reduced |
| 604 | output_shape = [*input.shape[:-1], mat2.shape[1]] |
| 605 | scatter_dim = 0 |
| 606 | gemm_rs = torch.ops.vllm.patched_fused_scaled_matmul_reduce_scatter( |
| 607 | input, |
| 608 | mat2, |
| 609 | scale_a, |
| 610 | scale_b, |
| 611 | "sum", |
| 612 | scatter_dim, # orig_scatter_dim |
| 613 | scatter_dim, # scatter_dim_after_maybe_reshape |
| 614 | self.tp.device_group.group_name, |
| 615 | output_shape, |
| 616 | None, # bias |
| 617 | None, # result_scale |
| 618 | self.dtype, # out_dtype |
| 619 | False, # use_fast_accum |
| 620 | ) |
| 621 | |
| 622 | return gemm_rs |
| 623 | |
| 624 | pm.register_replacement( |
| 625 | pattern, replacement, self.get_inputs(), pm.fwd_only, pm_pass |
| 626 | ) |
| 627 | |
| 628 | |
| 629 | class AllGatherCutlassScaledMMPattern(BasePattern): |
| 630 | def get_inputs(self) -> list[torch.Tensor]: |
| 631 | x = torch.empty([8, 16], device=self.device, dtype=FP8_DTYPE) |
| 632 | weight = ( |
| 633 | torch.empty([16, 16], device=self.device, dtype=FP8_DTYPE) |
| 634 | .contiguous() |
| 635 | .transpose(0, 1) |
| 636 | ) |
| 637 | |
| 638 | s1 = x.shape[0] * self.tp_size |
| 639 | |
| 640 | scale_a = torch.empty([s1, 1], device=self.device, dtype=torch.float32) |
| 641 | scale_b = torch.empty([1, 16], device=self.device, dtype=torch.float32) |
| 642 | |
| 643 | s2 = weight.shape[1] |
| 644 | output = torch.empty([s1, s2], device=self.device, dtype=self.dtype) |
| 645 | |
| 646 | return [x, weight, scale_a, scale_b, output] |
| 647 | |
| 648 | def register(self, pm_pass: PatternMatcherPass) -> None: |
| 649 | def pattern( |
| 650 | x: torch.Tensor, |
| 651 | weight: torch.Tensor, |
| 652 | scale_a: torch.Tensor, |
| 653 | scale_b: torch.Tensor, |
| 654 | output: torch.Tensor, |
| 655 | ) -> torch.Tensor: |
| 656 | all_gather = torch.ops.vllm.all_gather.default( |
| 657 | x, dim=0, world_size=self.tp_size, group_name=self.tp.unique_name |
| 658 | ) |
| 659 | |
| 660 | cutlass_scaled_mm = torch.ops.higher_order.auto_functionalized( |
| 661 | torch.ops._C.cutlass_scaled_mm.default, |
| 662 | out=output, |
| 663 | a=all_gather, |
| 664 | b=weight, |
| 665 | a_scales=scale_a, |
| 666 | b_scales=scale_b, |
| 667 | bias=None, |
| 668 | ) |
| 669 | return cutlass_scaled_mm[1] |
| 670 | |
| 671 | def replacement( |
| 672 | x: torch.Tensor, |
| 673 | weight: torch.Tensor, |
| 674 | scale_a: torch.Tensor, |
| 675 | scale_b: torch.Tensor, |
| 676 | output: torch.Tensor, |
| 677 | ) -> torch.Tensor: |
| 678 | ag_output, mm_outputs = torch.ops.symm_mem.fused_all_gather_scaled_matmul( # noqa |
| 679 | x, |
| 680 | [weight], |
| 681 | scale_a, |
| 682 | [scale_b], |
| 683 | gather_dim=0, |
| 684 | biases=[None], |
| 685 | result_scales=[None], |
| 686 | out_dtypes=[self.dtype], |
| 687 | use_fast_accum=[False], |
| 688 | group_name=self.tp.device_group.group_name, |
| 689 | ) |
| 690 | return mm_outputs |
| 691 | |
| 692 | pm.register_replacement( |
| 693 | pattern, replacement, self.get_inputs(), pm.fwd_only, pm_pass |
| 694 | ) |
| 695 | |
| 696 | |
| 697 | class FlashInferBMMFP8ReduceScatterPattern( |
| 698 | BasePattern, VllmPatternReplacement[..., torch.Tensor] |
| 699 | ): |
| 700 | def get_inputs(self) -> list[torch.Tensor]: |
| 701 | a_2d = torch.empty([16, 16], device=self.device, dtype=FP8_DTYPE) |
| 702 | b_2d = ( |
| 703 | torch.empty([16, 16], device=self.device, dtype=FP8_DTYPE) |
| 704 | .contiguous() |
| 705 | .transpose(0, 1) |
| 706 | ) |
| 707 | a_scale = torch.empty([1], device=self.device, dtype=torch.float32) |
| 708 | b_scale = torch.empty([1], device=self.device, dtype=torch.float32) |
| 709 | return [a_2d, b_2d, a_scale, b_scale] |
| 710 | |
| 711 | @property |
| 712 | def pattern(self) -> Callable[..., torch.Tensor]: |
| 713 | def _pattern( |
| 714 | a_2d: torch.Tensor, |
| 715 | b_2d: torch.Tensor, |
| 716 | a_scale: torch.Tensor, |
| 717 | b_scale: torch.Tensor, |
| 718 | ) -> torch.Tensor: |
| 719 | bmm = torch.ops.vllm.bmm_fp8.default( |
| 720 | torch.ops.aten.unsqueeze.default(a_2d, 0), |
| 721 | torch.ops.aten.unsqueeze.default(b_2d, 0), |
| 722 | a_scale, |
| 723 | b_scale, |
| 724 | self.dtype, |
| 725 | "auto", |
| 726 | ) |
| 727 | output = torch.ops.aten.reshape.default(bmm, list(bmm.shape[1:])) |
| 728 | return torch.ops.vllm.reduce_scatter.default( |
| 729 | output, |
| 730 | dim=0, |
| 731 | world_size=self.tp_size, |
| 732 | group_name=self.tp.unique_name, |
| 733 | ) |
| 734 | |
| 735 | return _pattern |
| 736 | |
| 737 | @property |
| 738 | def replacement(self) -> Callable[..., torch.Tensor]: |
| 739 | def _replacement( |
| 740 | a_2d: torch.Tensor, |
| 741 | b_2d: torch.Tensor, |
| 742 | a_scale: torch.Tensor, |
| 743 | b_scale: torch.Tensor, |
| 744 | ) -> torch.Tensor: |
| 745 | return torch.ops.vllm.fused_flashinfer_scaled_matmul_reduce_scatter.default( |
| 746 | a_2d, |
| 747 | b_2d, |
| 748 | a_scale, |
| 749 | b_scale, |
| 750 | "sum", |
| 751 | 0, |
| 752 | 0, |
| 753 | self.tp.device_group.group_name, |
| 754 | [a_2d.shape[0], b_2d.shape[1]], |
| 755 | self.dtype, |
| 756 | ) |
| 757 | |
| 758 | return _replacement |
| 759 | |
| 760 | |
| 761 | class FlashInferAllGatherBMMFP8Pattern( |
| 762 | BasePattern, VllmPatternReplacement[..., torch.Tensor] |
| 763 | ): |
| 764 | def get_inputs(self) -> list[torch.Tensor]: |
| 765 | a_shard_2d = torch.empty([8, 16], device=self.device, dtype=FP8_DTYPE) |
| 766 | b_2d = ( |
| 767 | torch.empty([16, 16], device=self.device, dtype=FP8_DTYPE) |
| 768 | .contiguous() |
| 769 | .transpose(0, 1) |
| 770 | ) |
| 771 | a_scale = torch.empty([1], device=self.device, dtype=torch.float32) |
| 772 | b_scale = torch.empty([1], device=self.device, dtype=torch.float32) |
| 773 | return [a_shard_2d, b_2d, a_scale, b_scale] |
| 774 | |
| 775 | @property |
| 776 | def pattern(self) -> Callable[..., torch.Tensor]: |
| 777 | def _pattern( |
| 778 | a_shard_2d: torch.Tensor, |
| 779 | b_2d: torch.Tensor, |
| 780 | a_scale: torch.Tensor, |
| 781 | b_scale: torch.Tensor, |
| 782 | ) -> torch.Tensor: |
| 783 | all_gather = torch.ops.vllm.all_gather.default( |
| 784 | a_shard_2d, |
| 785 | dim=0, |
| 786 | world_size=self.tp_size, |
| 787 | group_name=self.tp.unique_name, |
| 788 | ) |
| 789 | return torch.ops.vllm.bmm_fp8.default( |
| 790 | torch.ops.aten.unsqueeze.default(all_gather, 0), |
| 791 | torch.ops.aten.unsqueeze.default(b_2d, 0), |
| 792 | a_scale, |
| 793 | b_scale, |
| 794 | self.dtype, |
| 795 | "auto", |
| 796 | ) |
| 797 | |
| 798 | return _pattern |
| 799 | |
| 800 | @property |
| 801 | def replacement(self) -> Callable[..., torch.Tensor]: |
| 802 | def _replacement( |
| 803 | a_shard_2d: torch.Tensor, |
| 804 | b_2d: torch.Tensor, |
| 805 | a_scale: torch.Tensor, |
| 806 | b_scale: torch.Tensor, |
| 807 | ) -> torch.Tensor: |
| 808 | fused = torch.ops.vllm.fused_all_gather_flashinfer_scaled_matmul.default( |
| 809 | a_shard_2d, |
| 810 | b_2d, |
| 811 | a_scale, |
| 812 | b_scale, |
| 813 | 0, |
| 814 | self.tp.device_group.group_name, |
| 815 | self.dtype, |
| 816 | ) |
| 817 | return torch.ops.aten.unsqueeze.default(fused, 0) |
| 818 | |
| 819 | return _replacement |
| 820 | |
| 821 | |
| 822 | class FlashInferAllGatherFP4Pattern( |
| 823 | BasePattern, VllmPatternReplacement[..., torch.Tensor] |
| 824 | ): |
| 825 | def __init__( |
| 826 | self, |
| 827 | dtype: torch.dtype, |
| 828 | device: str | None, |
| 829 | backend: str, |
| 830 | use_8x4_sf_layout: bool, |
| 831 | a_scale_view: str, |
| 832 | ) -> None: |
| 833 | super().__init__(dtype, device) |
| 834 | self.backend = backend |
| 835 | self.use_8x4_sf_layout = use_8x4_sf_layout |
| 836 | self.a_scale_view = a_scale_view |
| 837 | |
| 838 | def get_inputs(self) -> list[torch.Tensor]: |
| 839 | a_shard_2d = torch.empty([8, 8], device=self.device, dtype=torch.uint8) |
| 840 | b_2d = torch.empty([8, 16], device=self.device, dtype=torch.uint8) |
| 841 | a_scale_shard = torch.empty([128, 4], device=self.device, dtype=torch.int32) |
| 842 | b_scale = torch.empty([4, 128], device=self.device, dtype=torch.uint8) |
| 843 | alpha = torch.empty([], device=self.device, dtype=torch.float32) |
| 844 | return [ |
| 845 | a_shard_2d, |
| 846 | b_2d, |
| 847 | a_scale_shard, |
| 848 | b_scale, |
| 849 | alpha, |
| 850 | ] |
| 851 | |
| 852 | @property |
| 853 | def pattern(self) -> Callable[..., torch.Tensor]: |
| 854 | def _pattern( |
| 855 | a_shard_2d: torch.Tensor, |
| 856 | b_2d: torch.Tensor, |
| 857 | a_scale_shard: torch.Tensor, |
| 858 | b_scale: torch.Tensor, |
| 859 | alpha: torch.Tensor, |
| 860 | ) -> torch.Tensor: |
| 861 | all_gather_a = torch.ops.vllm.all_gather.default( |
| 862 | a_shard_2d, |
| 863 | dim=0, |
| 864 | world_size=self.tp_size, |
| 865 | group_name=self.tp.unique_name, |
| 866 | ) |
| 867 | all_gather_a_scale = torch.ops.vllm.all_gather.default( |
| 868 | a_scale_shard, |
| 869 | dim=0, |
| 870 | world_size=self.tp_size, |
| 871 | group_name=self.tp.unique_name, |
| 872 | ) |
| 873 | a_scale = all_gather_a_scale |
| 874 | if self.a_scale_view in ("float8", "float8_uint8"): |
| 875 | a_scale = torch.ops.aten.view.dtype(a_scale, torch.float8_e4m3fn) |
| 876 | if self.a_scale_view in ("uint8", "float8_uint8"): |
| 877 | a_scale = torch.ops.aten.view.dtype(a_scale, torch.uint8) |
| 878 | return torch.ops.vllm.flashinfer_mm_fp4.default( |
| 879 | all_gather_a, |
| 880 | b_2d, |
| 881 | a_scale, |
| 882 | b_scale, |
| 883 | alpha, |
| 884 | self.dtype, |
| 885 | self.use_8x4_sf_layout, |
| 886 | self.backend, |
| 887 | ) |
| 888 | |
| 889 | return _pattern |
| 890 | |
| 891 | @property |
| 892 | def replacement(self) -> Callable[..., torch.Tensor]: |
| 893 | def _replacement( |
| 894 | a_shard_2d: torch.Tensor, |
| 895 | b_2d: torch.Tensor, |
| 896 | a_scale_shard: torch.Tensor, |
| 897 | b_scale: torch.Tensor, |
| 898 | alpha: torch.Tensor, |
| 899 | ) -> torch.Tensor: |
| 900 | return torch.ops.vllm.fused_all_gather_flashinfer_fp4_matmul.default( |
| 901 | a_shard_2d, |
| 902 | b_2d, |
| 903 | a_scale_shard, |
| 904 | b_scale, |
| 905 | alpha, |
| 906 | 0, |
| 907 | self.tp.device_group.group_name, |
| 908 | self.dtype, |
| 909 | self.a_scale_view in ("float8", "float8_uint8"), |
| 910 | self.use_8x4_sf_layout, |
| 911 | self.backend, |
| 912 | ) |
| 913 | |
| 914 | return _replacement |
| 915 | |
| 916 | |
| 917 | class AsyncTPPass(VllmFusionPatternMatcherPass): |
| 918 | @enable_fake_mode |
| 919 | def __init__(self, config: VllmConfig) -> None: |
| 920 | super().__init__(config, pass_name="async_tp_pass") |
| 921 | |
| 922 | enable_symm_mem_for_group(get_tp_group().device_group.group_name) |
| 923 | GEMMReduceScatterPattern(self.model_dtype, self.device).register(self.pm_pass) |
| 924 | |
| 925 | AllGatherGEMMPattern(self.model_dtype, self.device).register(self.pm_pass) |
| 926 | if current_platform.is_rocm() and self.model_dtype == torch.bfloat16: |
| 927 | RocmGEMMReduceScatterPattern(self.model_dtype, self.device).register( |
| 928 | self.pm_pass |
| 929 | ) |
| 930 | RocmAllGatherGEMMPattern(self.model_dtype, self.device).register( |
| 931 | self.pm_pass |
| 932 | ) |
| 933 | |
| 934 | # These fusions are enabled only for bfloat16 models because |
| 935 | # `scaled_mm` or `cutlass_scaled_mm` with per-token (row-wise) scaling |
| 936 | # only supports bfloat16 as the output dtype. |
| 937 | if self.model_dtype == torch.bfloat16: |
| 938 | ScaledMMReduceScatterPattern(self.model_dtype, self.device).register( |
| 939 | self.pm_pass |
| 940 | ) |
| 941 | AllGatherScaledMMPattern(self.model_dtype, self.device).register( |
| 942 | self.pm_pass |
| 943 | ) |
| 944 | if hasattr(torch.ops._C, "cutlass_scaled_mm"): |
| 945 | CutlassScaledMMReduceScatterPattern( |
| 946 | self.model_dtype, self.device |
| 947 | ).register(self.pm_pass) |
| 948 | AllGatherCutlassScaledMMPattern(self.model_dtype, self.device).register( |
| 949 | self.pm_pass |
| 950 | ) |
| 951 | with suppress(ImportError): |
| 952 | import vllm.utils.flashinfer # noqa: F401 |
| 953 | if hasattr(torch.ops.vllm, "bmm_fp8"): |
| 954 | self.register( |
| 955 | FlashInferAllGatherBMMFP8Pattern(self.model_dtype, self.device) |
| 956 | ) |
| 957 | self.register( |
| 958 | FlashInferBMMFP8ReduceScatterPattern(self.model_dtype, self.device) |
| 959 | ) |
| 960 | if hasattr(torch.ops.vllm, "flashinfer_mm_fp4"): |
| 961 | for backend in ("cutlass", "cudnn"): |
| 962 | for a_scale_view in ("float8_uint8", "uint8"): |
| 963 | self.register( |
| 964 | FlashInferAllGatherFP4Pattern( |
| 965 | self.model_dtype, |
| 966 | self.device, |
| 967 | backend, |
| 968 | use_8x4_sf_layout=False, |
| 969 | a_scale_view=a_scale_view, |
| 970 | ) |
| 971 | ) |
| 972 | for use_8x4_sf_layout in (False, True): |
| 973 | for a_scale_view in ("float8",): |
| 974 | self.register( |
| 975 | FlashInferAllGatherFP4Pattern( |
| 976 | self.model_dtype, |
| 977 | self.device, |
| 978 | "trtllm", |
| 979 | use_8x4_sf_layout=use_8x4_sf_layout, |
| 980 | a_scale_view=a_scale_view, |
| 981 | ) |
| 982 | ) |
| 983 | self.register( |
| 984 | FlashInferAllGatherFP4Pattern( |
| 985 | self.model_dtype, |
| 986 | self.device, |
| 987 | "cute-dsl", |
| 988 | use_8x4_sf_layout=False, |
| 989 | a_scale_view="float8", |
| 990 | ) |
| 991 | ) |
| 992 | # NVFP4 reduce-scatter does not need scale communication: FP4 |
| 993 | # scales are consumed by the local GEMM and only BF16 partial |
| 994 | # outputs are reduced. Keep this PR scoped to the all-gather |
| 995 | # path; reduce-scatter needs a dedicated FP4 producer rather |
| 996 | # than the existing FP8-style helper. |
| 997 | |
| 998 | self.dump_patterns(config, self.pm_pass) |
| 999 | |
| 1000 | def is_applicable_for_range(self, compile_range: Range) -> bool: |
| 1001 | # This pass is applied on top of the sequence parallelism pass, |
| 1002 | # which is only supported in fullgraph compilation mode. |
| 1003 | assert ( |
| 1004 | self.compilation_config.use_inductor_graph_partition |
| 1005 | or not self.compilation_config.splitting_ops |
| 1006 | ), "AsyncTPPass requires full-graph compilation" |
| 1007 | return True |
| 1008 | |
| 1009 | @VllmInductorPass.time_and_log |
| 1010 | def __call__(self, graph: fx.Graph) -> None: |
| 1011 | self.matched_count = self.pm_pass.apply(graph) |
| 1012 | VllmPatternMatcherPass.match_table[self.pass_name] += self.matched_count |
| 1013 | logger.debug("Replaced %s patterns", self.matched_count) |