[ROCm] Enable AITER and FP8 inference on GFX120x (#43615)

Signed-off-by: skysnow2001 <skysnow9285@gmail.com> Co-authored-by: Douglas Lehr <91553416+dllehr-amd@users.noreply.github.com>

skysnow2001 committed Aug 3, 2026 at 17:45 UTC f43e1d26e3e6b40398be27b218cf2f2786432028
12 files changed +172 -44
vllm/_aiter_ops.py
+84 -14
@@ -72,6 +72,21 @@ def is_aiter_found_and_supported() -> bool:
72 return False
73
74
75 +def is_aiter_found_and_supported_on_rdna4() -> bool:
76 + """RDNA4 (gfx12) analog of `is_aiter_found_and_supported()`.
77 +
78 + gfx12 has no aiter CK build, so this deliberately stays off the gfx9
79 + `@if_aiter_supported` umbrella; it reports only that aiter's Triton
80 + kernels are usable here. Like its gfx9 counterpart it checks platform +
81 + arch + library availability and does not check environment variables.
82 + """
83 + if current_platform.is_rocm() and IS_AITER_FOUND:
84 + from vllm.platforms.rocm import on_rdna4
85 +
86 + return on_rdna4()
87 + return False
88 +
89 +
90 @functools.cache
91 def _load_gemm_tuned_configs(
92 q_dtype_w: torch.dtype, csv_path: str
@@ -1702,6 +1717,24 @@ class rocm_aiter_ops:
1717 def is_enabled(cls) -> bool:
1718 return cls._AITER_ENABLED
1719
1720 + @classmethod
1721 + def is_rdna_aiter_enabled(cls) -> bool:
1722 + """AITER on RDNA4 (gfx12): library present, arch is rdna4, and the user
1723 + enabled aiter. Only aiter's Triton kernels exist on gfx12 (no CK build),
1724 + so this deliberately stays off the gfx9/CK `@if_aiter_supported` umbrella
1725 + and gates only the Triton paths rdna4 uses. The gfx12 analog of
1726 + `is_enabled()`."""
1727 + if not current_platform.is_rocm() or not IS_AITER_FOUND:
1728 + return False
1729 + from vllm.platforms.rocm import on_rdna4
1730 +
1731 + return on_rdna4() and cls._AITER_ENABLED
1732 +
1733 + @classmethod
1734 + def is_rdna_linear_enabled(cls) -> bool:
1735 + """RDNA4 (gfx12) analog of is_linear_enabled() (aiter Triton blockscale)."""
1736 + return cls.is_rdna_aiter_enabled() and cls._LINEAR_ENABLED
1737 +
1738 @classmethod
1739 @if_aiter_supported
1740 def is_linear_enabled(cls) -> bool:
@@ -1864,17 +1897,8 @@ class rocm_aiter_ops:
1897 aiter_ar_comm if isinstance(aiter_ar_comm, AiterCustomAllreduce) else None
1898 )
1899
1867 - @classmethod
1868 - @if_aiter_supported
1869 - def are_gdn_triton_kernels_available(cls) -> bool:
1870 - """Check if AITER Triton kernels for GDN attention are importable.
1871 -
1872 - These are optional Triton kernels (conv1d fast-path, gated delta net)
1873 - used by GatedDeltaNetAttention's decode fast-path. They may be absent
1874 - in older aiter builds.
1875 - """
1876 - if not cls._AITER_ENABLED:
1877 - return False
1900 + @staticmethod
1901 + def _gdn_triton_kernels_importable() -> bool:
1902 try:
1903 import aiter.ops.triton.causal_conv1d_update_single_token # noqa: F401
1904 import aiter.ops.triton.gated_delta_net # noqa: F401
@@ -1886,6 +1910,22 @@ class rocm_aiter_ops:
1910 except (ImportError, ModuleNotFoundError):
1911 return False
1912
1913 + @classmethod
1914 + @if_aiter_supported
1915 + def are_gdn_triton_kernels_available(cls) -> bool:
1916 + """Check if AITER Triton kernels for GDN attention are importable.
1917 +
1918 + These are optional Triton kernels (conv1d fast-path, gated delta net)
1919 + used by GatedDeltaNetAttention's decode fast-path. They may be absent
1920 + in older aiter builds.
1921 + """
1922 + return cls._AITER_ENABLED and cls._gdn_triton_kernels_importable()
1923 +
1924 + @classmethod
1925 + def is_rdna_gdn_triton_kernels_available(cls) -> bool:
1926 + """RDNA4 (gfx12) analog of are_gdn_triton_kernels_available()."""
1927 + return cls.is_rdna_aiter_enabled() and cls._gdn_triton_kernels_importable()
1928 +
1929 @classmethod
1930 @if_aiter_supported
1931 @functools.cache
@@ -1902,8 +1942,12 @@ class rocm_aiter_ops:
1942 return "gate_mode" in inspect.signature(fused_moe).parameters
1943
1944 @staticmethod
1905 - @if_aiter_supported
1945 def register_ops_once() -> None:
1946 + if not (
1947 + is_aiter_found_and_supported() or is_aiter_found_and_supported_on_rdna4()
1948 + ):
1949 + return
1950 +
1951 global _OPS_REGISTERED
1952 if not _OPS_REGISTERED:
1953 # register all the custom ops here
@@ -2774,7 +2818,11 @@ class rocm_aiter_ops:
2818
2819 @staticmethod
2820 def is_triton_gemm_w8a8_tuned(n: int, k: int) -> bool:
2777 - return (n, k) in [
2821 + if not current_platform.is_rocm():
2822 + return False
2823 + from vllm.platforms.rocm import on_gfx950, on_rdna4
2824 +
2825 + gfx950_tuned = {
2826 (1024, 8192),
2827 (2112, 7168),
2828 (3072, 1536),
@@ -2786,7 +2834,29 @@ class rocm_aiter_ops:
2834 (7168, 256),
2835 (8192, 1024),
2836 (8192, 32768),
2789 - ]
2837 + }
2838 + rdna4_tuned = gfx950_tuned | {
2839 + (2048, 2048),
2840 + (2624, 6144),
2841 + (3072, 6144),
2842 + (3584, 512),
2843 + (4096, 512),
2844 + (6144, 1536),
2845 + (6144, 2048),
2846 + (7168, 2304),
2847 + (7168, 16384),
2848 + (7168, 18432),
2849 + (8192, 8192),
2850 + (16384, 1536),
2851 + (24576, 1536),
2852 + (32768, 512),
2853 + (36864, 7168),
2854 + }
2855 + if on_rdna4():
2856 + return (n, k) in rdna4_tuned
2857 + if on_gfx950():
2858 + return (n, k) in gfx950_tuned
2859 + return False
2860
2861 @staticmethod
2862 def is_triton_gemm_afp4wfp4_presh_ws_tuned(n: int, k: int) -> bool:
vllm/compilation/passes/fusion/rocm_aiter_fusion.py
+21 -8
@@ -581,6 +581,9 @@ class RocmAiterRMSNormQuantFusionPass(VllmPatternMatcherPass):
581
582 gated_norm_shapes.add((num_v_heads // layer.tp_size, head_v_dim))
583
584 + # RDNA4 uses native quant ops and supports only Triton replacements.
585 + match_aiter_quant_op = not rocm_aiter_ops.is_rdna_aiter_enabled()
586 +
587 # Make sure fused add patterns are before simple rms norm,
588 # as the latter is a subset of the former in torch ops.
589 # The DoubleQuant patterns handle 1 rms_norm -> 2 group_fp8_quant
@@ -592,22 +595,22 @@ class RocmAiterRMSNormQuantFusionPass(VllmPatternMatcherPass):
595 for epsilon in [1e-5, 1e-6]:
596 # Fuse aiter rms_norm + 2x aiter group fp8 quant
597 DoubleAiterRMSFp8GroupQuantPattern(
595 - epsilon, FP8_DTYPE, GroupShape(1, 128)
598 + epsilon, FP8_DTYPE, GroupShape(1, 128), match_aiter_quant_op
599 ).register(self.patterns)
600
601 # View-tolerant sibling for DSv3.2 q_c norm fan-out
602 DoubleAiterRMSFp8GroupQuantViewPattern(
600 - epsilon, FP8_DTYPE, GroupShape(1, 128)
603 + epsilon, FP8_DTYPE, GroupShape(1, 128), match_aiter_quant_op
604 ).register(self.patterns)
605
606 # Fuse aiter rms_norm + aiter dynamic group fp8 quant
607 AiterRMSFp8GroupQuantPattern(
605 - epsilon, FP8_DTYPE, GroupShape(1, 128)
608 + epsilon, FP8_DTYPE, GroupShape(1, 128), match_aiter_quant_op
609 ).register(self.patterns)
610
611 # Fuse aiter fused_add_rms_norm + aiter dynamic group fp8 quant
612 AiterFusedAddRMSFp8GroupQuantPattern(
610 - epsilon, FP8_DTYPE, GroupShape(1, 128)
613 + epsilon, FP8_DTYPE, GroupShape(1, 128), match_aiter_quant_op
614 ).register(self.patterns)
615
616 # When quant_fp8 custom ops are disabled, both AITER and native
@@ -620,6 +623,8 @@ class RocmAiterRMSNormQuantFusionPass(VllmPatternMatcherPass):
623 match_aiter_quant_options = (
624 [True, False] if is_quant_fp8_enabled else [False]
625 )
626 + if not match_aiter_quant_op:
627 + match_aiter_quant_options = []
628
629 for match_aiter_quant in match_aiter_quant_options:
630 # Fuse aiter rms_norm + (aiter / vllm built-in)
@@ -637,7 +642,10 @@ class RocmAiterRMSNormQuantFusionPass(VllmPatternMatcherPass):
642 # Fuse decomposed RMSNormGated + group fp8 quant.
643 # The replacement op (fused_rms_gated_fp8_group_quant) requires
644 # an aiter version that includes the GDN triton kernel renames.
640 - if gated_norm_shapes and rocm_aiter_ops.are_gdn_triton_kernels_available():
645 + if gated_norm_shapes and (
646 + rocm_aiter_ops.are_gdn_triton_kernels_available()
647 + or rocm_aiter_ops.is_rdna_gdn_triton_kernels_available()
648 + ):
649 for num_heads, head_dim in gated_norm_shapes:
650 if head_dim != 128:
651 continue
@@ -647,6 +655,7 @@ class RocmAiterRMSNormQuantFusionPass(VllmPatternMatcherPass):
655 GroupShape(1, 128),
656 num_heads=num_heads,
657 head_dim=head_dim,
658 + match_aiter_quant=match_aiter_quant_op,
659 ).register(self.patterns)
660
661 self.dump_patterns(config, self.patterns)
@@ -679,10 +688,10 @@ class AiterSiluMulFp8GroupQuantPattern(VllmPatternReplacement):
688
689 FUSED_SILU_MUL_QUANT_OP = rocm_aiter_ops.get_act_mul_fused_fp8_group_quant_op()
690
682 - def __init__(self) -> None:
691 + def __init__(self, match_aiter_quant_op: bool = True) -> None:
692 self.silu_and_mul_matcher = MatcherSiluAndMul()
693 self.quant_matcher = MatcherQuantFP8(
685 - quant_key=kFp8Dynamic128Sym, match_rocm_aiter=True
694 + quant_key=kFp8Dynamic128Sym, match_rocm_aiter=match_aiter_quant_op
695 )
696
697 def get_inputs(self) -> list[torch.Tensor]:
@@ -725,7 +734,11 @@ class RocmAiterSiluMulFp8GroupQuantFusionPass(VllmFusionPatternMatcherPass):
734 def __init__(self, config: VllmConfig) -> None:
735 super().__init__(config, "rocm_aiter_silu_mul_fp8_group_quant_fusion_pass")
736
728 - self.register(AiterSiluMulFp8GroupQuantPattern())
737 + match_aiter_quant_op = not rocm_aiter_ops.is_rdna_aiter_enabled()
738 +
739 + self.register(
740 + AiterSiluMulFp8GroupQuantPattern(match_aiter_quant_op=match_aiter_quant_op)
741 + )
742
743 self.dump_patterns(config, self.pm_pass)
744
vllm/compilation/passes/pass_manager.py
+12 -4
@@ -18,7 +18,7 @@ from .ir.clone_elimination import UnsafeCloneEliminationPass
18 from .ir.lowering_pass import VllmIRLoweringPass
19 from .vllm_inductor_pass import VllmInductorPass, VllmPatternMatcherPass
20
21 -if rocm_aiter_ops.is_enabled():
21 +if rocm_aiter_ops.is_enabled() or rocm_aiter_ops.is_rdna_aiter_enabled():
22 from .fusion.allreduce_rms_fusion import (
23 RocmAiterAllReduceFusionPass,
24 )
@@ -166,7 +166,9 @@ class PostGradPassManager(CustomGraphPass): # type: ignore[misc]
166 if enable_transformers_norm_canonicalization:
167 self.passes += [AddRMSNormFusionPass(config)]
168
169 - if self.pass_config.fuse_act_padding and rocm_aiter_ops.is_enabled():
169 + if self.pass_config.fuse_act_padding and (
170 + rocm_aiter_ops.is_enabled() or rocm_aiter_ops.is_rdna_aiter_enabled()
171 + ):
172 # Run the more specific RMSNorm+router-pad fusion before
173 # AR+RMS, since both consume fused_add_rms_norm.
174 self.passes += [RocmAiterTritonAddRMSNormPadFusionPass(config)]
@@ -183,7 +185,10 @@ class PostGradPassManager(CustomGraphPass): # type: ignore[misc]
185 self.passes += [RMSNormReshapeFusionPass(config)]
186
187 if self.pass_config.fuse_norm_quant:
186 - if rocm_aiter_ops.is_enabled():
188 + if (
189 + rocm_aiter_ops.is_enabled()
190 + or rocm_aiter_ops.is_rdna_aiter_enabled()
191 + ):
192 self.passes += [
193 RocmAiterRMSNormQuantFusionPass(config),
194 ]
@@ -191,7 +196,10 @@ class PostGradPassManager(CustomGraphPass): # type: ignore[misc]
196
197 if self.pass_config.fuse_act_quant:
198 self.passes += [ActivationQuantFusionPass(config)]
194 - if rocm_aiter_ops.is_enabled():
199 + if (
200 + rocm_aiter_ops.is_enabled()
201 + or rocm_aiter_ops.is_rdna_aiter_enabled()
202 + ):
203 self.passes += [RocmAiterSiluMulFp8GroupQuantFusionPass(config)]
204
205 if self.pass_config.fuse_qk_norm_rope_kvcache:
vllm/model_executor/kernels/linear/scaled_mm/aiter.py
+18 -3
@@ -397,10 +397,14 @@ class AiterFp8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel):
397
398 @classmethod
399 def is_supported(cls, compute_capability=None):
400 + if (
401 + rocm_aiter_ops.is_linear_enabled()
402 + or rocm_aiter_ops.is_rdna_linear_enabled()
403 + ):
404 + return True, None
405 return (
401 - rocm_aiter_ops.is_linear_enabled(),
402 - "Only supported on ROCm platform \
403 - with aiter package installed.",
406 + False,
407 + "Only supported on ROCm platform with aiter package installed.",
408 )
409
410 @classmethod
@@ -416,6 +420,17 @@ class AiterFp8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel):
420 "Supports only dynamic per token group activation "
421 "quantization with group_shape=(1,128).",
422 )
423 +
424 + # RDNA4 (gfx12) only has the aiter Triton blockscale backend, which
425 + # needs a per-(N,K) tune. Reject untuned shapes so the dispatcher falls
426 + # through to the generic backend.
427 + if rocm_aiter_ops.is_rdna_linear_enabled():
428 + n, k = config.weight_shape
429 + if not rocm_aiter_ops.is_triton_gemm_w8a8_tuned(n, k):
430 + return (
431 + False,
432 + f"(N={n}, K={k}) is not in the aiter Triton blockscale tuned list.",
433 + )
434 return True, None
435
436 def apply_block_scaled_mm(
vllm/model_executor/kernels/linear/scaled_mm/pytorch.py
+3 -3
@@ -108,10 +108,10 @@ class RowWiseTorchFP8ScaledMMLinearKernel(TorchFP8ScaledMMLinearKernel):
108 if not current_platform.is_rocm():
109 return False, "requires ROCm."
110
111 - from vllm.platforms.rocm import get_cdna_version
111 + from vllm.platforms.rocm import get_cdna_version, on_rdna4
112
113 - if get_cdna_version() <= 2:
114 - return False, "requires CDNA3+"
113 + if get_cdna_version() <= 2 and not on_rdna4():
114 + return False, "requires CDNA3+ or RDNA4"
115
116 if compute_capability is not None and compute_capability < 94:
117 return False, "requires compute capability 94 and above."
vllm/model_executor/layers/fused_moe/experts/fused_batched_moe.py
+4 -4
@@ -814,13 +814,13 @@ class BatchedTritonExperts(mk.FusedMoEExpertsModular):
814 ) -> bool:
815 p = current_platform
816 if p.is_rocm():
817 - from vllm.platforms.rocm import get_cdna_version
817 + from vllm.platforms.rocm import get_cdna_version, on_rdna4
818
819 - _rocm_support_fp8 = get_cdna_version() > 2
819 + is_rocm_fp8_supported = get_cdna_version() > 2 or on_rdna4()
820 else:
821 - _rocm_support_fp8 = False
821 + is_rocm_fp8_supported = False
822
823 - device_supports_fp8 = _rocm_support_fp8 or (
823 + device_supports_fp8 = is_rocm_fp8_supported or (
824 p.is_cuda() and p.has_device_capability((8, 9))
825 )
826
vllm/model_executor/layers/fused_moe/oracle/fp8.py
+8 -2
@@ -379,8 +379,14 @@ def select_fp8_moe_backend(
379
380 # Handle explicit AITER FP8 configuration.
381 if envs.is_set("VLLM_ROCM_USE_AITER") or envs.is_set("VLLM_ROCM_USE_AITER_MOE"):
382 - if not envs.VLLM_ROCM_USE_AITER or not envs.VLLM_ROCM_USE_AITER_MOE:
383 - AVAILABLE_BACKENDS.remove(Fp8MoeBackend.AITER)
382 + skip_aiter_moe = (
383 + not envs.VLLM_ROCM_USE_AITER
384 + or not envs.VLLM_ROCM_USE_AITER_MOE
385 + or rocm_aiter_ops.is_rdna_aiter_enabled()
386 + )
387 + if skip_aiter_moe:
388 + if Fp8MoeBackend.AITER in AVAILABLE_BACKENDS:
389 + AVAILABLE_BACKENDS.remove(Fp8MoeBackend.AITER)
390 else:
391 backend = Fp8MoeBackend.AITER
392 return _return_or_raise(
vllm/model_executor/layers/fused_moe/oracle/unquantized.py
+6 -1
@@ -298,7 +298,12 @@ def select_unquantized_moe_backend(
298
299 # Handle explicit AITER FP8 configuration.
300 if envs.is_set("VLLM_ROCM_USE_AITER") or envs.is_set("VLLM_ROCM_USE_AITER_MOE"):
301 - if not envs.VLLM_ROCM_USE_AITER or not envs.VLLM_ROCM_USE_AITER_MOE:
301 + skip_aiter_moe = (
302 + not envs.VLLM_ROCM_USE_AITER
303 + or not envs.VLLM_ROCM_USE_AITER_MOE
304 + or rocm_aiter_ops.is_rdna_aiter_enabled()
305 + )
306 + if skip_aiter_moe:
307 if UnquantizedMoeBackend.AITER in AVAILABLE_BACKENDS:
308 AVAILABLE_BACKENDS.remove(UnquantizedMoeBackend.AITER)
309 else:
vllm/model_executor/layers/mamba/gdn/qwen_gdn_linear_attn.py
+4 -1
@@ -69,7 +69,10 @@ from vllm.v1.attention.backends.gdn_attn import GDNAttentionMetadata
69 # Availability is checked centrally via rocm_aiter_ops; the actual function
70 # references are imported here so that they can be called without per-call
71 # import overhead.
72 -GDN_AITER_TRITON_AVAILABLE = rocm_aiter_ops.are_gdn_triton_kernels_available()
72 +GDN_AITER_TRITON_AVAILABLE = (
73 + rocm_aiter_ops.are_gdn_triton_kernels_available()
74 + or rocm_aiter_ops.is_rdna_gdn_triton_kernels_available()
75 +)
76
77 if GDN_AITER_TRITON_AVAILABLE:
78 from aiter.ops.triton.causal_conv1d_update_single_token import (
vllm/model_executor/layers/sparse_attn_indexer.py
+1 -1
@@ -850,7 +850,7 @@ class SparseAttnIndexer(CustomOp):
850 assert isinstance(q_quant, torch.Tensor), (
851 "AMD sparse_attn_indexer expects a single FP8 q_quant tensor"
852 )
853 - if rocm_aiter_ops.is_enabled():
853 + if rocm_aiter_ops.is_enabled() or rocm_aiter_ops.is_rdna_aiter_enabled():
854 return torch.ops.vllm.rocm_aiter_sparse_attn_indexer(
855 hidden_states,
856 _encode_layer_name(self.k_cache.prefix),
vllm/platforms/rocm.py
+9 -1
@@ -218,6 +218,7 @@ _ON_GFX1250 = "gfx1250" in _GCN_ARCH
218 _ON_CDNA = any(arch in _GCN_ARCH for arch in ["gfx9", "gfx1250"])
219 # RDNA = gfx11/gfx12 minus the CDNA-classified gfx1250.
220 _ON_RDNA = _ON_GFX1X and not _ON_CDNA
221 +_ON_RDNA4 = any(arch in _GCN_ARCH for arch in ["gfx1200", "gfx1201"])
222
223
224 def _capability_from_gcn_arch(gcn_arch: str) -> tuple[int, int] | None:
@@ -315,6 +316,10 @@ def on_gfx1250() -> bool:
316 return _ON_GFX1250
317
318
319 +def on_rdna4() -> bool:
320 + return _ON_RDNA4
321 +
322 +
323 def on_mi3xx() -> bool:
324 return _ON_MI3XX
325
@@ -471,6 +476,8 @@ def _get_backend_priorities(
476 backends.append(AttentionBackendEnum.ROCM_AITER_FA)
477 if is_aiter_found_and_supported():
478 backends.append(AttentionBackendEnum.ROCM_AITER_UNIFIED_ATTN)
479 + elif rocm_aiter_ops.is_rdna_aiter_enabled():
480 + backends.insert(0, AttentionBackendEnum.ROCM_AITER_UNIFIED_ATTN)
481 backends.append(AttentionBackendEnum.TRITON_ATTN)
482 backends.append(AttentionBackendEnum.TURBOQUANT)
483
@@ -918,7 +925,7 @@ class RocmPlatform(Platform):
925
926 @classmethod
927 def supports_fp8(cls) -> bool:
921 - return on_cdna() or on_gfx12x()
928 + return on_cdna() or on_rdna4()
929
930 @classmethod
931 def is_fp8_fnuz(cls) -> bool:
@@ -1065,6 +1072,7 @@ class RocmPlatform(Platform):
1072 cc.cudagraph_mode != CUDAGraphMode.NONE
1073 and envs.VLLM_ROCM_USE_AITER
1074 and envs.VLLM_ROCM_USE_AITER_RMSNORM
1075 + and not on_rdna4()
1076 ):
1077 rms_norm = ["aiter"] + default
1078 else:
vllm/v1/attention/ops/rocm_aiter_mla_sparse.py
+2 -2
@@ -417,7 +417,7 @@ def rocm_fp8_paged_mqa_logits(
417 batch_size, next_n = q_fp8.shape[:2]
418 block_size = kv_cache_fp8.shape[1]
419
420 - if rocm_aiter_ops.is_enabled():
420 + if rocm_aiter_ops.is_enabled() or rocm_aiter_ops.is_rdna_aiter_enabled():
421 aiter_paged_mqa_logits_module = paged_mqa_logits_module()
422
423 if aiter_paged_mqa_logits_module is not None:
@@ -582,7 +582,7 @@ def rocm_fp8_mqa_logits(
582 )
583
584 aiter_mqa_logits_module = None
585 - if rocm_aiter_ops.is_enabled():
585 + if rocm_aiter_ops.is_enabled() or rocm_aiter_ops.is_rdna_aiter_enabled():
586 aiter_mqa_logits_module = mqa_logits_module()
587
588 if aiter_mqa_logits_module is not None: