[ROCM][Perf] Add ROCM_SEGMENTED_ATTN attention-backend works across RDNA3/3.5/4 (#59132)

Signed-off-by: root <jeffaw99@hotmail.com>
Signed-off-by: big-yellow-duck <jeffaw99@hotmail.com>
Co-authored-by: Tan Pin Siang <1716735+tanpinsiang@users.noreply.github.com>
Co-authored-by: vllmellm <190700713+vllmellm@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
big_yellow_duck
2026-10-06 07:53:47 +00:00
committed by GitHub
co-authored by Tan Pin Siang vllmellm Codex
parent 15c1cc12ac
commit edfccc9469
9 changed files with 4431 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -61,6 +61,216 @@ def test_aiter_unified_attention_uses_dedicated_metadata_builder():
)
def test_segmented_attention_uses_dedicated_backend_components():
from vllm.v1.attention.backends.rocm_segmented_attn import (
RocmSegmentedAttentionBackend,
RocmSegmentedAttentionImpl,
RocmSegmentedAttentionMetadataBuilder,
)
assert (
RocmSegmentedAttentionBackend.get_builder_cls()
is RocmSegmentedAttentionMetadataBuilder
)
assert RocmSegmentedAttentionBackend.get_impl_cls() is RocmSegmentedAttentionImpl
assert RocmSegmentedAttentionBackend.get_name() == "ROCM_SEGMENTED_ATTN"
assert RocmSegmentedAttentionBackend.supports_non_causal()
assert RocmSegmentedAttentionBackend.supports_sliding_window()
assert RocmSegmentedAttentionBackend.supports_sink()
assert not RocmSegmentedAttentionBackend.supports_mm_prefix()
@pytest.mark.parametrize(
"sliding_window,logits_soft_cap,has_sinks",
[(4096, None, False), (None, 50.0, False), (None, None, True)],
)
def test_segmented_attention_admits_unified_fallback_features(
monkeypatch, sliding_window, logits_soft_cap, has_sinks
):
from vllm.platforms import rocm
from vllm.v1.attention.backend import AttentionType
from vllm.v1.attention.backends.rocm_segmented_attn import (
RocmSegmentedAttentionImpl,
)
monkeypatch.setattr(rocm, "on_gfx1x", lambda: True)
monkeypatch.setattr(rocm, "on_gfx12x", lambda: True)
sinks = torch.zeros(8) if has_sinks else None
impl = RocmSegmentedAttentionImpl(
num_heads=8,
head_size=128,
scale=128**-0.5,
num_kv_heads=2,
alibi_slopes=None,
sliding_window=sliding_window,
kv_cache_dtype="auto",
logits_soft_cap=logits_soft_cap,
attn_type=AttentionType.DECODER,
sinks=sinks,
)
assert isinstance(impl, RocmSegmentedAttentionImpl)
@pytest.mark.parametrize(
"platform,rdna",
[("cuda", False), ("cpu", False), ("xpu", False), ("rocm", False), ("rocm", True)],
)
def test_segmented_attention_requires_rocm_rdna(monkeypatch, platform, rdna):
"""Reject unsupported platforms before probing ROCm or constructing kernels."""
from vllm.platforms import rocm
from vllm.platforms.interface import DeviceCapability
from vllm.v1.attention.backends import rocm_segmented_attn as segmented
monkeypatch.setattr(
segmented,
"current_platform",
SimpleNamespace(is_rocm=lambda: platform == "rocm"),
)
def on_rdna():
assert platform == "rocm", "Must not probe ROCm on another platform"
return rdna
monkeypatch.setattr(rocm, "on_gfx1x", on_rdna)
reason = segmented.RocmSegmentedAttentionBackend.supports_combination(
128,
torch.bfloat16,
"auto",
32,
False,
False,
False,
False,
DeviceCapability(12, 0),
)
if rdna:
assert reason is None
segmented.RocmSegmentedAttentionImpl(8, 128, 128**-0.5, 2, None, None, "auto")
else:
assert reason == "ROCM_SEGMENTED_ATTN requires AMD RDNA GPUs on ROCm"
with pytest.raises(ValueError, match="requires AMD RDNA GPUs on ROCm"):
segmented.RocmSegmentedAttentionImpl(
8, 128, 128**-0.5, 2, None, None, "auto"
)
@pytest.mark.parametrize("explicit_cache", [False, True])
def test_segmented_triton_cache_survives_standalone_compilation(
monkeypatch, tmp_path, explicit_cache
):
"""Backend initialization preserves one cache across model compilations."""
import os
from vllm.compilation.compiler_interface import InductorStandaloneAdaptor
from vllm.platforms import rocm
from vllm.v1.attention.backends.rocm_segmented_attn import (
RocmSegmentedAttentionImpl,
)
monkeypatch.setattr(rocm, "on_gfx1x", lambda: True)
monkeypatch.setenv("VLLM_CACHE_ROOT", str(tmp_path))
monkeypatch.delenv("TRITON_CACHE_DIR", raising=False)
monkeypatch.delenv("TORCHINDUCTOR_CACHE_DIR", raising=False)
expected = tmp_path / "rocm_segmented_attention" / "triton"
if explicit_cache:
expected = tmp_path / "custom"
monkeypatch.setenv("TRITON_CACHE_DIR", str(expected))
for model in ("target", "draft", "restart_target"):
RocmSegmentedAttentionImpl(8, 128, 128**-0.5, 2, None, None, "auto")
assert os.environ["TRITON_CACHE_DIR"] == str(expected)
assert expected.is_dir()
InductorStandaloneAdaptor("binary").initialize_cache(str(tmp_path / model))
assert os.environ["TRITON_CACHE_DIR"] == str(expected)
def test_segmented_attention_is_opt_in(monkeypatch):
from vllm.platforms import rocm
from vllm.platforms.rocm import RocmPlatform, _get_backend_priorities
assert AttentionBackendEnum.ROCM_SEGMENTED_ATTN not in _get_backend_priorities(
use_mla=False, use_sparse=False
)
monkeypatch.setattr(rocm, "on_gfx1x", lambda: True)
monkeypatch.setattr(rocm, "on_gfx12x", lambda: True)
config = AttentionSelectorConfig(
head_size=128,
dtype=torch.bfloat16,
kv_cache_dtype="fp8",
block_size=32,
)
path = RocmPlatform.get_attn_backend_cls(
selected_backend=AttentionBackendEnum.ROCM_SEGMENTED_ATTN,
attn_selector_config=config,
)
assert path == AttentionBackendEnum.ROCM_SEGMENTED_ATTN.get_path()
def test_segmented_attention_rejects_mm_prefix_at_selection(monkeypatch):
from vllm.platforms import rocm
from vllm.platforms.rocm import RocmPlatform
monkeypatch.setattr(rocm, "on_gfx1x", lambda: True)
config = AttentionSelectorConfig(
head_size=128,
dtype=torch.bfloat16,
kv_cache_dtype="auto",
block_size=32,
use_mm_prefix=True,
)
with pytest.raises(ValueError, match="multimodal token full attention"):
RocmPlatform.get_attn_backend_cls(
selected_backend=AttentionBackendEnum.ROCM_SEGMENTED_ATTN,
attn_selector_config=config,
)
def test_segmented_attention_forward_uses_dedicated_dispatch(monkeypatch):
from vllm.config import KernelConfig
from vllm.platforms import rocm
from vllm.v1.attention.backends.rocm_segmented_attn import (
RocmSegmentedAttentionImpl,
)
monkeypatch.setattr(rocm, "on_gfx1x", lambda: True)
impl = RocmSegmentedAttentionImpl(8, 128, 128**-0.5, 2, None, None, "auto")
impl._segmented_attention_config = SimpleNamespace(
kernel_config=KernelConfig(),
scheduler_config=SimpleNamespace(max_num_seqs=1, max_num_batched_tokens=3),
model_config=SimpleNamespace(max_model_len=16),
)
query = torch.empty(3, 8, 128, dtype=torch.bfloat16)
key = torch.empty(3, 2, 128, dtype=torch.bfloat16)
value = torch.empty_like(key)
output = torch.empty_like(query)
kv_cache = torch.empty(2, 2, 16, 256, dtype=torch.bfloat16)
metadata = SimpleNamespace(
use_cascade=False,
num_actual_tokens=3,
block_table=torch.zeros(1, 1, dtype=torch.int32),
query_start_loc=torch.tensor([0, 3], dtype=torch.int32),
seq_lens=torch.tensor([3], dtype=torch.int32),
max_seq_len=3,
max_query_len=3,
causal=True,
)
layer = SimpleNamespace(_k_scale=torch.ones(()), _v_scale=torch.ones(()))
with patch(
"vllm.v1.attention.backends.rocm_segmented_attn.segmented_attention"
) as dispatch:
result = impl.forward(layer, query, key, value, kv_cache, metadata, output)
assert result is output
dispatch.assert_called_once()
assert dispatch.call_args.kwargs["key_cache"].shape == (2, 16, 2, 128)
def test_aiter_unified_attention_capture_preserves_query_start_locations():
from vllm.v1.attention.backends.rocm_aiter_unified_attn import (
RocmAiterUnifiedAttentionMetadataBuilder,
+4
View File
@@ -278,6 +278,9 @@ class KernelConfig:
enable_flashinfer_autotune: bool = None # type: ignore[assignment]
"""If True, run FlashInfer autotuning during kernel warmup."""
enable_rocm_segmented_attn_autotune: bool = False
"""If True, autotune ROCm segmented attention during kernel warmup on RDNA GPUs."""
# TODO(roberto): Remove after registered CuTeDSL warmups are migrated
# to the shared JIT warmup infrastructure.
# https://github.com/vllm-project/vllm/pull/47451
@@ -406,6 +409,7 @@ class KernelConfig:
"enable_cutedsl_warmup",
"enable_jit_warmup",
"enable_flashinfer_autotune",
"enable_rocm_segmented_attn_autotune",
"ir_op_priority", # handled separately below
}
if self.linear_backend_per_quant is None:
@@ -34,6 +34,7 @@ from vllm.model_executor.layers.vocab_parallel_embedding import (
VocabParallelEmbedding,
)
from vllm.multimodal.inputs import NestedTensors
from vllm.platforms import current_platform
from vllm.transformers_utils.config import set_default_rope_theta
from vllm.transformers_utils.repo_utils import get_hf_file_bytes
from vllm.v1.attention.backend import AttentionType
@@ -269,6 +270,24 @@ class DFlashQwen3Attention(nn.Module):
sinks=self.attention_sink_bias,
)
self.causal = causal
if current_platform.is_rocm():
from vllm.platforms.rocm import on_gfx1x
from vllm.v1.attention.backends.rocm_segmented_attn import (
RocmSegmentedAttentionImpl,
)
if on_gfx1x() and isinstance(self.attn.impl, RocmSegmentedAttentionImpl):
# Startup segmented tuning needs the draft's actual attention mode.
# Causality is normally supplied later through attention metadata.
self.attn.segmented_causal = causal
speculative_config = get_current_vllm_config().speculative_config
if (
speculative_config is not None
and speculative_config.num_speculative_tokens is not None
):
self.attn.segmented_query_limit = (
speculative_config.num_speculative_tokens + 1
)
self.q_norm = RMSNorm(self.head_dim, eps=rms_norm_eps)
self.k_norm = RMSNorm(self.head_dim, eps=rms_norm_eps)
@@ -220,6 +220,13 @@ def kernel_warmup(worker: "Worker", *, process_local_only: bool = False):
if process_local_only:
return
if current_platform.is_rocm():
from vllm.model_executor.warmup.rocm_segmented_attn_autotune_warmup import (
rocm_segmented_attn_autotune_warmup,
)
rocm_segmented_attn_autotune_warmup(worker)
flashinfer_sparse_mla_decode_autotune_warmup(worker)
deepseek_v4_sparse_mla_attention_warmup(worker)
File diff suppressed because it is too large Load Diff
+3
View File
@@ -53,6 +53,9 @@ class AttentionBackendEnum(Enum, metaclass=_AttentionBackendEnumMeta):
"vllm.v1.attention.backends.triton_flash_attn.TritonFlashAttentionBackend"
)
ROCM_ATTN = "vllm.v1.attention.backends.rocm_attn.RocmAttentionBackend"
ROCM_SEGMENTED_ATTN = (
"vllm.v1.attention.backends.rocm_segmented_attn.RocmSegmentedAttentionBackend"
)
ROCM_AITER_MLA = "vllm.v1.attention.backends.mla.rocm_aiter_mla.AiterMLABackend"
ROCM_AITER_TRITON_MLA = (
"vllm.v1.attention.backends.mla.aiter_triton_mla.AiterTritonMLABackend"
@@ -0,0 +1,371 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""ROCm attention backend for token-major segmented Triton attention."""
import os
from pathlib import Path
from typing import TYPE_CHECKING, ClassVar
import torch
import vllm.envs as envs
from vllm._aiter_ops import rocm_aiter_ops
from vllm.config import VllmConfig, get_current_vllm_config
from vllm.config.cache import CacheDType
from vllm.logger import init_logger
from vllm.platforms import current_platform
from vllm.utils.torch_utils import is_quantized_kv_cache
from vllm.v1.attention.backend import AttentionLayer, AttentionType
from vllm.v1.attention.backends.rocm_attn import (
RocmAttentionBackend,
RocmAttentionImpl,
RocmAttentionMetadata,
RocmAttentionMetadataBuilder,
)
from vllm.v1.attention.backends.utils import get_num_attention_heads_from_layers
from vllm.v1.attention.ops.segmented_attention import (
get_segmented_attention_workspace,
segmented_attention,
segmented_attention_workspace_size,
)
from vllm.v1.attention.ops.triton_reshape_and_cache_flash import (
triton_reshape_and_cache_flash,
)
from vllm.v1.kv_cache_interface import AttentionSpec
if TYPE_CHECKING:
from vllm.platforms.interface import DeviceCapability
logger = init_logger(__name__)
def is_rdna() -> bool:
if not current_platform.is_rocm():
return False
from vllm.platforms.rocm import on_gfx1x
return on_gfx1x()
class RocmSegmentedAttentionMetadataBuilder(RocmAttentionMetadataBuilder):
def __init__(
self,
kv_cache_spec: AttentionSpec,
layer_names: list[str],
vllm_config: VllmConfig,
device: torch.device,
):
super().__init__(kv_cache_spec, layer_names, vllm_config, device)
model_config = vllm_config.model_config
self.num_heads_q = get_num_attention_heads_from_layers(
vllm_config, layer_names
) or model_config.get_num_attention_heads(vllm_config.parallel_config)
self.num_heads_kv = kv_cache_spec.num_kv_heads
self.headdim = kv_cache_spec.head_size
class RocmSegmentedAttentionBackend(RocmAttentionBackend):
"""Explicit token-major ROCm backend optimized for segmented prefill."""
supported_dtypes: ClassVar[list[torch.dtype]] = [
torch.float16,
torch.bfloat16,
]
supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
"auto",
"float16",
"bfloat16",
"fp8",
"fp8_e4m3",
]
@staticmethod
def get_name() -> str:
return "ROCM_SEGMENTED_ATTN"
@staticmethod
def get_impl_cls() -> type["RocmSegmentedAttentionImpl"]:
return RocmSegmentedAttentionImpl
@staticmethod
def get_builder_cls() -> type["RocmSegmentedAttentionMetadataBuilder"]:
return RocmSegmentedAttentionMetadataBuilder
@classmethod
def get_supported_head_sizes(cls) -> list[int]:
return [64, 128, 256]
@classmethod
def supports_attn_type(cls, attn_type: str) -> bool:
return attn_type == AttentionType.DECODER
@classmethod
def supports_non_causal(cls) -> bool:
return True
@classmethod
def supports_sliding_window(cls) -> bool:
return True
@classmethod
def supports_sink(cls) -> bool:
return True
@classmethod
def supports_mm_prefix(cls) -> bool:
return False
@classmethod
def supports_combination(
cls,
head_size: int,
dtype: torch.dtype,
kv_cache_dtype: CacheDType | None,
block_size: int | None,
use_mla: bool,
has_sink: bool,
use_sparse: bool,
use_mm_prefix: bool,
device_capability: "DeviceCapability",
) -> str | None:
del (
head_size,
dtype,
block_size,
use_mla,
has_sink,
use_sparse,
use_mm_prefix,
device_capability,
)
if not is_rdna():
return "ROCM_SEGMENTED_ATTN requires AMD RDNA GPUs on ROCm"
from vllm.platforms.rocm import on_gfx12x
if kv_cache_dtype in ("fp8", "fp8_e4m3") and not on_gfx12x():
return "FP8 segmented attention requires gfx12"
return None
class RocmSegmentedAttentionImpl(RocmAttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
scale: float,
num_kv_heads: int,
alibi_slopes: list[float] | None,
sliding_window: int | None,
kv_cache_dtype: str,
logits_soft_cap: float | None = None,
attn_type: AttentionType = AttentionType.DECODER,
kv_sharing_target_layer_name: int | None = None,
sinks: torch.Tensor | None = None,
) -> None:
if not is_rdna():
raise ValueError("ROCM_SEGMENTED_ATTN requires AMD RDNA GPUs on ROCm")
super().__init__(
num_heads,
head_size,
scale,
num_kv_heads,
alibi_slopes,
sliding_window,
kv_cache_dtype,
logits_soft_cap,
attn_type,
kv_sharing_target_layer_name,
sinks,
)
from vllm.platforms.rocm import on_gfx1x, on_gfx12x
fp8 = kv_cache_dtype in ("fp8", "fp8_e4m3")
arch_supported = on_gfx12x() if fp8 else on_gfx1x()
gqa_ratio_supported = (
num_kv_heads > 0
and num_heads % num_kv_heads == 0
and 1 <= self.num_queries_per_kv <= 16
)
if not (
arch_supported
and attn_type == AttentionType.DECODER
and head_size in (64, 128, 256)
and gqa_ratio_supported
and kv_cache_dtype in ("auto", "float16", "bfloat16", "fp8", "fp8_e4m3")
and alibi_slopes is None
):
raise ValueError(
"ROCM_SEGMENTED_ATTN requires decoder attention on gfx1x "
"(gfx12 for FP8), head size 64, 128 or 256, GQA ratio 1-16, and "
"does not support ALiBi."
)
# Startup tuning and target/draft compilation must share a Triton cache.
cache_dir = os.environ.setdefault(
"TRITON_CACHE_DIR",
str(Path(envs.VLLM_CACHE_ROOT) / "rocm_segmented_attention" / "triton"),
)
Path(cache_dir).mkdir(parents=True, exist_ok=True)
logger.info_once("Using token-major ROCm segmented Triton attention")
self._segmented_attention_warmed_up = False
self._segmented_attention_config: VllmConfig | None = None
def process_weights_after_loading(self, act_dtype: torch.dtype):
super().process_weights_after_loading(act_dtype)
config = get_current_vllm_config()
self._segmented_attention_config = config
def _autotune_enabled(self) -> bool:
config = self._segmented_attention_config
return (
config is not None
and config.kernel_config.enable_rocm_segmented_attn_autotune
)
def _get_workspace(self, device):
config = self._segmented_attention_config
assert config is not None
sizes = segmented_attention_workspace_size(
config.scheduler_config.max_num_seqs,
self.num_heads,
self.num_kv_heads,
self.head_size,
config.model_config.max_model_len,
max_tokens=config.scheduler_config.max_num_batched_tokens,
fp8=self.kv_cache_dtype in ("fp8", "fp8_e4m3"),
autotune=self._autotune_enabled(),
)
return get_segmented_attention_workspace(device, sizes)
def _split_kv_cache(
self,
kv_cache: torch.Tensor,
num_kv_heads: int | None = None,
head_size: int | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
num_kv_heads = num_kv_heads or self.num_kv_heads
head_size = head_size or self.head_size
return (
kv_cache[:, 0].unflatten(-1, (num_kv_heads, head_size)),
kv_cache[:, 1].unflatten(-1, (num_kv_heads, head_size)),
)
def forward(
self,
layer: torch.nn.Module,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
kv_cache: torch.Tensor,
attn_metadata: RocmAttentionMetadata,
output: torch.Tensor,
output_scale: torch.Tensor | None = None,
output_block_scale: torch.Tensor | None = None,
) -> torch.Tensor:
if output_block_scale is not None:
raise NotImplementedError(
"fused block_scale output quantization is not supported"
)
# Profile runs allocate the same fixed scratch used during graph replay.
workspace = self._get_workspace(query.device)
if attn_metadata is None:
return output.fill_(0)
assert not attn_metadata.use_cascade
num_actual_tokens = attn_metadata.num_actual_tokens
key_cache, value_cache = self._split_kv_cache(kv_cache)
if is_quantized_kv_cache(self.kv_cache_dtype):
key_cache = key_cache.view(self.fp8_dtype)
value_cache = value_cache.view(self.fp8_dtype)
if query.dtype == self.fp8_dtype and layer._q_scale_float != 1.0:
raise NotImplementedError(
"A non-1.0 q_scale with an FP8 query is not supported"
)
segmented_attention(
query=query[:num_actual_tokens],
key=key[:num_actual_tokens],
value=value[:num_actual_tokens],
output=output[:num_actual_tokens],
kv_cache_dtype=self.kv_cache_dtype,
key_cache=key_cache,
value_cache=value_cache,
block_table=attn_metadata.block_table,
query_start_loc=attn_metadata.query_start_loc,
seq_lens=attn_metadata.seq_lens,
max_seq_len=attn_metadata.max_seq_len,
max_query_len=attn_metadata.max_query_len,
k_scale=layer._k_scale,
v_scale=layer._v_scale,
sm_scale=self.scale,
sliding_window=self.sliding_window[0],
output_scale=output_scale,
sinks=self.sinks,
causal=attn_metadata.causal,
softcap=self.logits_soft_cap,
workspace=workspace,
use_tuned_config=self._autotune_enabled(),
)
return output
def do_kv_cache_update(
self,
layer: AttentionLayer,
key: torch.Tensor,
value: torch.Tensor,
kv_cache: torch.Tensor,
slot_mapping: torch.Tensor,
):
key_cache, value_cache = self._split_kv_cache(kv_cache)
if is_quantized_kv_cache(self.kv_cache_dtype):
key_cache = key_cache.view(self.fp8_dtype)
value_cache = value_cache.view(self.fp8_dtype)
triton_reshape_and_cache_flash(
key,
value,
key_cache,
value_cache,
slot_mapping,
self.kv_cache_dtype,
layer._k_scale,
layer._v_scale,
)
def do_rope_and_kv_cache_update(
self,
layer: AttentionLayer,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
positions: torch.Tensor,
cos_sin_cache: torch.Tensor,
is_neox: bool,
kv_cache: torch.Tensor,
layer_slot_mapping: torch.Tensor,
):
key_cache, value_cache = self._split_kv_cache(
kv_cache,
layer.num_kv_heads, # type: ignore[attr-defined]
layer.head_size, # type: ignore[attr-defined]
)
is_fp8_kv_cache = is_quantized_kv_cache(self.kv_cache_dtype)
if is_fp8_kv_cache:
key_cache = key_cache.view(self.fp8_dtype)
value_cache = value_cache.view(self.fp8_dtype)
rocm_aiter_ops.triton_rope_and_cache(
query,
key,
value,
positions,
cos_sin_cache,
is_neox,
key_cache,
value_cache,
layer_slot_mapping,
layer._k_scale,
layer._v_scale,
True,
is_fp8_kv_cache,
)
File diff suppressed because it is too large Load Diff