A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 7, 19:00 UTC
[ROCm]: Enable customop and rope+kvcache fusion for AITER RoPE (#35180)
Signed-off-by: Rohan138 <rohanpotdar138@gmail.com>
Rohan Potdar committed
Feb 24, 2026 at 22:36 UTC
f38f8c974277a2fbd4bcb915416fd907fc023366
9 files changed
+139
-67
tests/compile/passes/test_rope_kvcache_fusion.py
+9
-1
@@ -177,7 +177,10 @@ class QKRoPEKVCacheTestModel(torch.nn.Module):
177
def ops_in_model_before(self) -> list[torch._ops.OpOverload]:
178
ops = []
179
if self.enable_rope_custom_op:
180
- ops.append(ROTARY_OP)
180
+ if rocm_aiter_ops.is_triton_rotary_embed_enabled():
181
+ ops.append(torch.ops.vllm.rocm_aiter_triton_rotary_embedding.default)
182
+ else:
183
+ ops.append(ROTARY_OP)
184
else:
185
ops.append(INDEX_SELECT_OP)
186
ops.append(torch.ops.vllm.unified_kv_cache_update.default)
@@ -196,6 +199,7 @@ class QKRoPEKVCacheTestModel(torch.nn.Module):
199
],
200
)
201
@pytest.mark.parametrize("enable_rope_custom_op", [True]) # [True, False])
202
+@pytest.mark.parametrize("enable_aiter_triton_rope", [True, False])
203
@pytest.mark.parametrize("num_heads", [64])
204
@pytest.mark.parametrize("num_kv_heads", [8])
205
@pytest.mark.parametrize("head_size", [64])
@@ -210,6 +214,7 @@ class QKRoPEKVCacheTestModel(torch.nn.Module):
214
def test_rope_kvcache_fusion(
215
attn_backend: AttentionBackendEnum,
216
enable_rope_custom_op: bool,
217
+ enable_aiter_triton_rope: bool,
218
num_heads: int,
219
num_kv_heads: int,
220
head_size: int,
@@ -245,6 +250,9 @@ def test_rope_kvcache_fusion(
250
251
with vllm.config.set_current_vllm_config(vllm_config), monkeypatch.context() as m:
252
m.setenv("VLLM_ROCM_USE_AITER", "1")
253
+ m.setenv(
254
+ "VLLM_ROCM_USE_AITER_TRITON_ROPE", "1" if enable_aiter_triton_rope else "0"
255
+ )
256
rocm_aiter_ops.refresh_env_variables()
257
258
model = QKRoPEKVCacheTestModel(
vllm/_aiter_ops.py
+65
-36
@@ -831,6 +831,59 @@ def _rocm_aiter_triton_add_rmsnorm_pad_fake(
831
return out, residual_out
832
833
834
+def _triton_rotary_embedding_impl(
835
+ positions: torch.Tensor,
836
+ query: torch.Tensor,
837
+ key: torch.Tensor,
838
+ head_size: int,
839
+ cos_sin_cache: torch.Tensor,
840
+ is_neox: bool,
841
+ offsets: torch.Tensor | None = None,
842
+) -> None:
843
+ # Modifies query and key in-place
844
+ from aiter.ops.triton.rope.rope import (
845
+ rope_cached_thd_positions_offsets_2c_fwd_inplace,
846
+ )
847
+
848
+ num_tokens = positions.numel()
849
+ cos, sin = cos_sin_cache.chunk(2, dim=-1)
850
+ query_shape = query.shape
851
+ key_shape = key.shape
852
+ rotate_style = 0 if is_neox else 1
853
+ rotary_dim = head_size
854
+
855
+ query = query.view(num_tokens, -1, head_size)
856
+ key = key.view(num_tokens, -1, head_size)
857
+ query_ = query[..., :rotary_dim]
858
+ key_ = key[..., :rotary_dim]
859
+ positions = positions.view(*query.shape[:1])
860
+ rope_cached_thd_positions_offsets_2c_fwd_inplace(
861
+ query_,
862
+ key_,
863
+ cos,
864
+ sin,
865
+ positions,
866
+ offsets,
867
+ rotate_style,
868
+ reuse_freqs_front_part=True,
869
+ nope_first=False,
870
+ )
871
+ query = query.view(query_shape)
872
+ key = key.view(key_shape)
873
+
874
+
875
+def _triton_rotary_embedding_fake(
876
+ positions: torch.Tensor,
877
+ query: torch.Tensor,
878
+ key: torch.Tensor,
879
+ head_size: int,
880
+ cos_sin_cache: torch.Tensor,
881
+ is_neox_style: bool,
882
+ offsets: torch.Tensor | None = None,
883
+) -> None:
884
+ return
885
+
886
+
887
# Global flag to ensure ops are registered only once
888
_OPS_REGISTERED = False
889
@@ -1178,6 +1231,14 @@ class rocm_aiter_ops:
1231
dispatch_key=current_platform.dispatch_key,
1232
)
1233
1234
+ # Register rocm aiter rotary embedding custom op
1235
+ direct_register_custom_op(
1236
+ op_name="rocm_aiter_triton_rotary_embedding",
1237
+ op_func=_triton_rotary_embedding_impl,
1238
+ mutates_args=["query", "key"], # These tensors are modified in-place
1239
+ fake_impl=_triton_rotary_embedding_fake,
1240
+ )
1241
+
1242
_OPS_REGISTERED = True
1243
1244
@staticmethod
@@ -1220,6 +1281,10 @@ class rocm_aiter_ops:
1281
def get_triton_add_rmsnorm_pad_op() -> OpOverload:
1282
return torch.ops.vllm.rocm_aiter_triton_add_rmsnorm_pad.default
1283
1284
+ @staticmethod
1285
+ def get_triton_rotary_embedding_op() -> OpOverload:
1286
+ return torch.ops.vllm.rocm_aiter_triton_rotary_embedding.default
1287
+
1288
@staticmethod
1289
def rms_norm(
1290
x: torch.Tensor, weight: torch.Tensor, variance_epsilon: float
@@ -1482,42 +1547,6 @@ class rocm_aiter_ops:
1547
gemm_afp4wfp4(x_q, weight, x_s, weight_scale.T, out_dtype, y)
1548
return y
1549
1485
- @staticmethod
1486
- def triton_rotary_embed(
1487
- positions: torch.Tensor,
1488
- query: torch.Tensor,
1489
- key: torch.Tensor,
1490
- cos_sin_cache: torch.Tensor,
1491
- head_size: int,
1492
- rotary_dim: int,
1493
- is_neox_style: bool,
1494
- ):
1495
- from aiter.ops.triton.rope import rope_cached_thd_positions_2c_fwd_inplace
1496
-
1497
- num_tokens = positions.numel()
1498
- cos, sin = cos_sin_cache.chunk(2, dim=-1)
1499
- query_shape = query.shape
1500
- key_shape = key.shape
1501
- rotate_style = 0 if is_neox_style else 1
1502
-
1503
- query = query.view(num_tokens, -1, head_size)
1504
- key = key.view(num_tokens, -1, head_size)
1505
- query_ = query[..., :rotary_dim]
1506
- key_ = key[..., :rotary_dim]
1507
- positions = positions.view(*query.shape[:1])
1508
- rope_cached_thd_positions_2c_fwd_inplace(
1509
- query_,
1510
- key_,
1511
- cos,
1512
- sin,
1513
- positions,
1514
- rotate_style,
1515
- reuse_freqs_front_part=True,
1516
- nope_first=False,
1517
- )
1518
- query = query.view(query_shape)
1519
- key = key.view(key_shape)
1520
-
1550
@staticmethod
1551
def triton_rope_and_cache(
1552
query: torch.Tensor,
vllm/compilation/passes/fusion/matcher_utils.py
+5
@@ -89,10 +89,13 @@ class MatcherRotaryEmbedding(MatcherCustomOp):
89
num_heads: int,
90
num_kv_heads: int,
91
use_flashinfer: bool = False,
92
+ match_rocm_aiter: bool | None = None,
93
enabled: bool | None = None,
94
) -> None:
95
if enabled is None:
96
enabled = RotaryEmbedding.enabled()
97
+ if match_rocm_aiter is None:
98
+ match_rocm_aiter = rocm_aiter_ops.is_triton_rotary_embed_enabled()
99
100
super().__init__(enabled)
101
self.is_neox = is_neox
@@ -104,6 +107,8 @@ class MatcherRotaryEmbedding(MatcherCustomOp):
107
self.rotary_dim = head_size
108
if use_flashinfer:
109
self.rotary_op = FLASHINFER_ROTARY_OP
110
+ elif match_rocm_aiter:
111
+ self.rotary_op = rocm_aiter_ops.get_triton_rotary_embedding_op()
112
else:
113
self.rotary_op = ROTARY_OP
114
vllm/compilation/passes/utility/scatter_split_replace.py
+5
-1
@@ -60,6 +60,10 @@ class ScatterSplitReplacementPass(VllmInductorPass):
60
def __call__(self, graph: fx.Graph) -> None:
61
count = 0
62
63
+ target_ops = [torch.ops._C.rotary_embedding.default]
64
+ if hasattr(torch.ops.vllm, "rocm_aiter_triton_rotary_embedding"):
65
+ target_ops.append(torch.ops.vllm.rocm_aiter_triton_rotary_embedding.default)
66
+
67
for node in graph.nodes:
68
if not is_func(node, auto_functionalized):
69
continue
@@ -67,7 +71,7 @@ class ScatterSplitReplacementPass(VllmInductorPass):
71
kwargs = node.kwargs
72
at_target = node.args[0]
73
70
- if at_target == torch.ops._C.rotary_embedding.default:
74
+ if at_target in target_ops:
75
query = kwargs["query"]
76
key = kwargs["key"]
77
getitem_nodes = {}
vllm/config/compilation.py
+13
-16
@@ -123,6 +123,8 @@ class PassConfig:
123
"""Enable async TP."""
124
fuse_allreduce_rms: bool = Field(default=None)
125
"""Enable flashinfer allreduce fusion."""
126
+ enable_qk_norm_rope_fusion: bool = False
127
+ """Enable fused Q/K RMSNorm + RoPE pass."""
128
129
# ROCm/AITER specific fusions
130
fuse_act_padding: bool = Field(default=None)
@@ -153,8 +155,6 @@ class PassConfig:
155
8: 1, # 1MB
156
},
157
}, where key is the device capability"""
156
- enable_qk_norm_rope_fusion: bool = False
157
- """Enable fused Q/K RMSNorm + RoPE pass."""
158
159
# TODO(luka) better pass enabling system.
160
@@ -834,23 +834,20 @@ class CompilationConfig:
834
func if isinstance(func, InductorPass) else CallableInductorPass(func)
835
)
836
837
- if self.pass_config.enable_qk_norm_rope_fusion:
837
+ if (
838
+ self.pass_config.enable_qk_norm_rope_fusion
839
+ and "+rotary_embedding" not in self.custom_ops
840
+ ):
841
# TODO(zhuhaoran): support rope native forward match and remove this.
842
# Linked issue: https://github.com/vllm-project/vllm/issues/28042
843
self.custom_ops.append("+rotary_embedding")
841
- if self.pass_config.fuse_rope_kvcache:
842
- from vllm._aiter_ops import rocm_aiter_ops
843
-
844
- if rocm_aiter_ops.is_triton_rotary_embed_enabled():
845
- logger.warning(
846
- "Cannot use VLLM_ROCM_USE_AITER_TRITON_ROPE with "
847
- "fuse_rope_kvcache. Disabling fuse_rope_kvcache."
848
- )
849
- self.pass_config.fuse_rope_kvcache = False
850
- else:
851
- # TODO(Rohan138): support rope native forward match and remove this.
852
- # Linked issue: https://github.com/vllm-project/vllm/issues/28042
853
- self.custom_ops.append("+rotary_embedding")
844
+ if (
845
+ self.pass_config.fuse_rope_kvcache
846
+ and "+rotary_embedding" not in self.custom_ops
847
+ ):
848
+ # TODO(Rohan138): support rope native forward match and remove this.
849
+ # Linked issue: https://github.com/vllm-project/vllm/issues/28042
850
+ self.custom_ops.append("+rotary_embedding")
851
852
if (
853
is_torch_equal_or_newer("2.9.0.dev")
vllm/config/vllm.py
+20
-3
@@ -126,14 +126,27 @@ def enable_allreduce_rms_fusion(cfg: "VllmConfig") -> bool:
126
)
127
128
129
+def enable_rope_kvcache_fusion(cfg: "VllmConfig") -> bool:
130
+ """Enable if rotary embedding custom op is active and
131
+ use_inductor_graph_partition is enabled.
132
+ """
133
+ from vllm._aiter_ops import rocm_aiter_ops
134
+
135
+ return (
136
+ rocm_aiter_ops.is_enabled()
137
+ and cfg.compilation_config.is_custom_op_enabled("rotary_embedding")
138
+ and cfg.compilation_config.use_inductor_graph_partition
139
+ )
140
+
141
+
142
def enable_norm_pad_fusion(cfg: "VllmConfig") -> bool:
143
"""Enable if using AITER RMSNorm and AITER Triton GEMMs
144
and hidden size is 2880 i.e. gpt-oss; otherwise Inductor handles fusion."""
145
+ from vllm._aiter_ops import rocm_aiter_ops
146
147
return (
134
- envs.VLLM_ROCM_USE_AITER
135
- and envs.VLLM_ROCM_USE_AITER_RMSNORM
136
- and envs.VLLM_ROCM_USE_AITER_TRITON_GEMM
148
+ rocm_aiter_ops.is_rmsnorm_enabled()
149
+ and not rocm_aiter_ops.is_triton_gemm_enabled()
150
and cfg.model_config is not None
151
and cfg.model_config.get_hidden_size() == 2880
152
)
@@ -149,6 +162,7 @@ OPTIMIZATION_LEVEL_00 = {
162
"enable_sp": False,
163
"fuse_gemm_comms": False,
164
"fuse_act_padding": False,
165
+ "fuse_rope_kvcache": False,
166
},
167
"cudagraph_mode": CUDAGraphMode.NONE,
168
"use_inductor_graph_partition": False,
@@ -167,6 +181,7 @@ OPTIMIZATION_LEVEL_01 = {
181
"enable_sp": False,
182
"fuse_gemm_comms": False,
183
"fuse_act_padding": enable_norm_pad_fusion,
184
+ "fuse_rope_kvcache": enable_rope_kvcache_fusion,
185
},
186
"cudagraph_mode": CUDAGraphMode.PIECEWISE,
187
"use_inductor_graph_partition": False,
@@ -185,6 +200,7 @@ OPTIMIZATION_LEVEL_02 = {
200
"enable_sp": IS_DENSE,
201
"fuse_gemm_comms": IS_DENSE,
202
"fuse_act_padding": enable_norm_pad_fusion,
203
+ "fuse_rope_kvcache": enable_rope_kvcache_fusion,
204
},
205
"cudagraph_mode": CUDAGraphMode.FULL_AND_PIECEWISE,
206
"use_inductor_graph_partition": False,
@@ -203,6 +219,7 @@ OPTIMIZATION_LEVEL_03 = {
219
"enable_sp": IS_DENSE,
220
"fuse_gemm_comms": IS_DENSE,
221
"fuse_act_padding": enable_norm_pad_fusion,
222
+ "fuse_rope_kvcache": enable_rope_kvcache_fusion,
223
},
224
"cudagraph_mode": CUDAGraphMode.FULL_AND_PIECEWISE,
225
"use_inductor_graph_partition": False,
vllm/envs.py
+3
-3
@@ -105,7 +105,7 @@ if TYPE_CHECKING:
105
VLLM_ROCM_USE_AITER_MLA: bool = True
106
VLLM_ROCM_USE_AITER_MHA: bool = True
107
VLLM_ROCM_USE_AITER_FP4_ASM_GEMM: bool = False
108
- VLLM_ROCM_USE_AITER_TRITON_ROPE: bool = False
108
+ VLLM_ROCM_USE_AITER_TRITON_ROPE: bool = True
109
VLLM_ROCM_USE_AITER_FP8BMM: bool = True
110
VLLM_ROCM_USE_AITER_FP4BMM: bool = True
111
VLLM_ROCM_USE_AITER_UNIFIED_ATTENTION: bool = False
@@ -937,9 +937,9 @@ environment_variables: dict[str, Callable[[], Any]] = {
937
os.getenv("VLLM_ROCM_USE_AITER_FP4_ASM_GEMM", "False").lower() in ("true", "1")
938
),
939
# Whether to use aiter rope.
940
- # By default is disabled.
940
+ # By default is enabled.
941
"VLLM_ROCM_USE_AITER_TRITON_ROPE": lambda: (
942
- os.getenv("VLLM_ROCM_USE_AITER_TRITON_ROPE", "False").lower() in ("true", "1")
942
+ os.getenv("VLLM_ROCM_USE_AITER_TRITON_ROPE", "True").lower() in ("true", "1")
943
),
944
# Whether to use aiter triton fp8 bmm kernel
945
# By default is enabled.
vllm/model_executor/layers/rotary_embedding/base.py
+11
-7
@@ -47,15 +47,20 @@ class RotaryEmbeddingBase(CustomOp):
47
if not hasattr(self, "use_flashinfer"):
48
self.use_flashinfer = False
49
50
+ self.use_aiter = (
51
+ self.enabled() and rocm_aiter_ops.is_triton_rotary_embed_enabled()
52
+ )
53
+ if self.use_aiter:
54
+ self.rocm_aiter_triton_rotary_embedding = (
55
+ rocm_aiter_ops.get_triton_rotary_embedding_op()
56
+ )
57
+
58
if init_cache:
59
cache = self._compute_cos_sin_cache()
60
if not self.use_flashinfer:
61
cache = cache.to(dtype)
62
self.cos_sin_cache: torch.Tensor
63
self.register_buffer("cos_sin_cache", cache, persistent=False)
56
- self.is_rocm_triton_rotary_embed_enabled = (
57
- rocm_aiter_ops.is_triton_rotary_embed_enabled()
58
- )
64
65
self.apply_rotary_emb = ApplyRotaryEmb(
66
is_neox_style=self.is_neox_style,
@@ -231,15 +236,14 @@ class RotaryEmbedding(RotaryEmbeddingBase):
236
query: torch.Tensor,
237
key: torch.Tensor | None = None,
238
) -> tuple[torch.Tensor, torch.Tensor | None]:
234
- if self.is_rocm_triton_rotary_embed_enabled:
239
+ if self.use_aiter:
240
cos_sin_cache = self._match_cos_sin_cache_dtype(query)
236
- rocm_aiter_ops.triton_rotary_embed(
241
+ self.rocm_aiter_triton_rotary_embedding(
242
positions,
243
query,
244
key,
240
- cos_sin_cache,
245
self.head_size,
242
- self.rotary_dim,
246
+ cos_sin_cache,
247
self.is_neox_style,
248
)
249
return query, key
vllm/platforms/rocm.py
+8
@@ -494,6 +494,7 @@ class RocmPlatform(Platform):
494
use_aiter_rms_norm = rocm_aiter_ops.is_rmsnorm_enabled()
495
use_aiter_fp8_linear = rocm_aiter_ops.is_linear_fp8_enabled()
496
use_aiter_fused_se = rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
497
+ use_aiter_triton_rope = rocm_aiter_ops.is_triton_rotary_embed_enabled()
498
499
if compilation_config.cudagraph_mode.has_full_cudagraphs():
500
# decode context parallel does not support full cudagraphs
@@ -558,6 +559,13 @@ class RocmPlatform(Platform):
559
and "-grouped_topk" not in compilation_config.custom_ops
560
):
561
compilation_config.custom_ops.append("+grouped_topk")
562
+ # Enable rotary embedding when using AITER if its not disabled by user
563
+ if (
564
+ use_aiter_triton_rope
565
+ and "+rotary_embedding" not in compilation_config.custom_ops
566
+ and "-rotary_embedding" not in compilation_config.custom_ops
567
+ ):
568
+ compilation_config.custom_ops.append("+rotary_embedding")
569
570
# Default dispatch to rocm's sparse_attn_indexer implementation
571
compilation_config.custom_ops.append("+sparse_attn_indexer")