Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 87 additions & 20 deletions tests/kernels/attention/test_aiter_flash_attn.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@
import pytest
import torch

import vllm.v1.attention.backends.rocm_aiter_fa # noqa: F401
from vllm.platforms import current_platform
from vllm.utils.torch_utils import set_random_seed
from vllm.v1.attention.backends.fa_utils import is_flash_attn_varlen_func_available

# Import AITER backend if on ROCm and aiter is available
if current_platform.is_rocm():
from vllm._aiter_ops import is_aiter_found_and_supported

if is_aiter_found_and_supported():
import aiter

from vllm.v1.attention.backends.rocm_aiter_fa import cp_mha_gather_cache

NUM_HEADS = [(4, 4), (8, 2)]
HEAD_SIZES = [128, 256]
Expand Down Expand Up @@ -102,8 +109,11 @@ def test_varlen_with_paged_kv(
num_blocks: int,
q_dtype: torch.dtype | None,
) -> None:
if not is_flash_attn_varlen_func_available():
pytest.skip("flash_attn_varlen_func required to run this test.")
from vllm._aiter_ops import is_aiter_found_and_supported

if not is_aiter_found_and_supported():
pytest.skip("aiter package required for this test.")

torch.set_default_device("cuda")
set_random_seed(0)
num_seqs = len(seq_lens)
Expand All @@ -129,6 +139,8 @@ def test_varlen_with_paged_kv(
cu_seq_lens = torch.tensor([0] + kv_lens, dtype=torch.int32).cumsum(
dim=0, dtype=torch.int32
)
# Save kv_lens as list before converting to tensor
kv_lens_list = kv_lens
kv_lens = torch.tensor(kv_lens, dtype=torch.int32)

max_num_blocks_per_seq = (max_kv_len + block_size - 1) // block_size
Expand All @@ -141,41 +153,91 @@ def test_varlen_with_paged_kv(
maybe_quantized_query = query
maybe_quantized_key_cache = key_cache
maybe_quantized_value_cache = value_cache
k_descale = None
v_descale = None
k_scale_tensor = None
v_scale_tensor = None
dequant = False

if q_dtype is not None:
# QKV are drawn from N(0, 1): no need for a fp8 scaling factor
maybe_quantized_query = query.to(q_dtype)
maybe_quantized_key_cache = key_cache.to(q_dtype)
maybe_quantized_value_cache = value_cache.to(q_dtype)

dequant = True
scale_shape = (num_seqs, num_kv_heads)
k_descale = torch.ones(scale_shape, dtype=torch.float32)
v_descale = torch.ones(scale_shape, dtype=torch.float32)

torch.ops.vllm.flash_attn_varlen_func(
maybe_quantized_query,
maybe_quantized_key_cache,
maybe_quantized_value_cache,
out=output,
# For per-seq-per-head scales (matching AITER backend expectation)
k_scale_tensor = torch.ones(scale_shape, dtype=torch.float32)
v_scale_tensor = torch.ones(scale_shape, dtype=torch.float32)

# Prepare metadata for cp_mha_gather_cache
# token_to_batch: maps each token to its batch index
token_to_batch = torch.zeros(sum(kv_lens_list), dtype=torch.int32)
seq_starts = torch.zeros(num_seqs, dtype=torch.int32)

token_idx = 0
for batch_idx, kv_len in enumerate(kv_lens_list):
token_to_batch[token_idx : token_idx + kv_len] = batch_idx
seq_starts[batch_idx] = 0 # Assuming all sequences start at 0 in their blocks
token_idx += kv_len

# Allocate buffers for gathered KV
total_kv_tokens = sum(kv_lens_list)
gathered_key = torch.empty(
total_kv_tokens, num_kv_heads, head_size, dtype=maybe_quantized_key_cache.dtype
)
gathered_value = torch.empty(
total_kv_tokens,
num_kv_heads,
head_size,
dtype=maybe_quantized_value_cache.dtype,
)

# Gather paged KV cache into contiguous tensors using triton kernel
cp_mha_gather_cache(
key_cache=maybe_quantized_key_cache,
value_cache=maybe_quantized_value_cache,
key=gathered_key,
value=gathered_value,
block_tables=block_tables,
k_scales=k_scale_tensor
if k_scale_tensor is not None
else torch.ones(1, dtype=torch.float32),
v_scales=v_scale_tensor
if v_scale_tensor is not None
else torch.ones(1, dtype=torch.float32),
cu_seqlens_kv=cu_seq_lens,
token_to_batch=token_to_batch,
seq_starts=seq_starts,
dequant=dequant,
kv_cache_layout="NHD",
total_tokens=total_kv_tokens,
)

# Call aiter flash attention with gathered KV
aiter.flash_attn_varlen_func(
q=maybe_quantized_query,
k=gathered_key,
v=gathered_value,
cu_seqlens_q=cu_query_lens,
cu_seqlens_k=cu_seq_lens,
max_seqlen_q=max_query_len,
max_seqlen_k=max_kv_len,
min_seqlen_q=1,
dropout_p=0.0,
softmax_scale=scale,
alibi_slopes=None,
causal=True,
window_size=window_size,
block_table=block_tables,
cu_seqlens_k=cu_seq_lens,
k_scale=k_descale,
v_scale=v_descale,
alibi_slopes=None,
return_lse=False,
out=output,
)

ref_output = ref_paged_attn(
query=query,
key_cache=key_cache,
value_cache=value_cache,
query_lens=query_lens,
kv_lens=kv_lens,
kv_lens=kv_lens_list,
block_tables=block_tables,
scale=scale,
sliding_window=sliding_window,
Expand All @@ -189,3 +251,8 @@ def test_varlen_with_paged_kv(
torch.testing.assert_close(output, ref_output, atol=atol, rtol=rtol),
f"{torch.max(torch.abs(output - ref_output))}",
)

# Log diff stats for tracking changes
print(f"Max abs diff: {torch.max(torch.abs(output - ref_output))}")
print(f"Mean diff: {torch.mean(torch.abs(output - ref_output))}")
print(f"Min diff: {torch.std(torch.abs(output - ref_output))}")
117 changes: 109 additions & 8 deletions vllm/_aiter_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,22 @@ def is_aiter_found() -> bool:


def is_aiter_found_and_supported() -> bool:
"""Check if AITER is available AND enabled via environment variable.
"""Check if AITER library is available and platform supports it.

Checks: platform (ROCm), device arch (gfx9), library existence,
and VLLM_ROCM_USE_AITER env variable.
Checks: platform (ROCm), device arch (gfx9), and library existence.
Does NOT check environment variables - that's handled by rocm_aiter_ops.is_enabled().

This function determines if aiter CAN be used, not if it SHOULD be used.

Separation of concerns:
- This function: Can aiter work on this system? (platform + library availability)
- rocm_aiter_ops.is_enabled(): Should aiter be used by default? (adds env var check)
- Backend selection: Can explicitly request aiter regardless of env var

This allows explicit backend selection via attention_config to work even when
VLLM_ROCM_USE_AITER=0, while preventing unwanted JIT warnings for auto-discovery.
"""
if current_platform.is_rocm() and IS_AITER_FOUND and envs.VLLM_ROCM_USE_AITER:
if current_platform.is_rocm() and IS_AITER_FOUND:
from vllm.platforms.rocm import on_gfx9

return on_gfx9()
Expand All @@ -62,14 +72,23 @@ def wrapper(*args, **kwargs):
# because it returns wrong result on gfx942.
# This is a workaround to get the correct FP8 dtype.
# This might because that the get_gfx() is wrapped as a custom op.
if is_aiter_found_and_supported():
#
# Import strategy to avoid unwanted JIT warnings:
# - Only import aiter dtypes at module load if VLLM_ROCM_USE_AITER=1 (env var set)
# - This prevents JIT warnings during backend auto-discovery when aiter is not preferred
# - Explicit backend selection (via attention_config) still works because:
# 1. Backend modules (rocm_aiter_fa.py) import aiter directly when loaded
# 2. Individual op implementations import aiter locally when called
# 3. This module's ops are only called when an aiter backend is actually in use
if is_aiter_found_and_supported() and envs.VLLM_ROCM_USE_AITER:

@tjtanaa tjtanaa Feb 6, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This chain of bugfix is caused by not doing lazy import for aiter. Especially this DTYPE issue.

I have looked further into the issue. I have doubled check the definition of dtype from AITER, it is the same as current_platform.fp8_dtype(). So we don't have to import dtype here from aiter.

https://github.com/ROCm/aiter/blob/6af8b687480509d67f42a69bca7ed092c432e8dc/aiter/utility/dtypes.py#L15 (commit used in Dockerfile.rocm_base, it is the same definition)

https://github.com/ROCm/aiter/blob/12fe5f0291dad871584db71c49a4c33556519bbf/aiter/utility/dtypes.py#L17 (even on latest main commit, it is the same definition)

So the suggested changes in this file is to define this at the top of the file.

# fp8_dtype is not cached.
# on ROCm the fp8_dtype always call is_fp8_fnuz
# which is a host op
FP8_DTYPE = current_platform.fp8_dtype()

replace all the _FP8_DTYPE and AITER_FP8_DTYPE with FP8_DTYPE.

We don't need this section of the code

# Can't use dtypes.fp8 directly inside an op
# because it returns wrong result on gfx942.
# This is a workaround to get the correct FP8 dtype.
# This might because that the get_gfx() is wrapped as a custom op.
#
# Import strategy to avoid unwanted JIT warnings:
# - Only import aiter dtypes at module load if VLLM_ROCM_USE_AITER=1 (env var set)
# - This prevents JIT warnings during backend auto-discovery when aiter is not preferred
# - Explicit backend selection (via attention_config) still works because:
#   1. Backend modules (rocm_aiter_fa.py) import aiter directly when loaded
#   2. Individual op implementations import aiter locally when called
#   3. This module's ops are only called when an aiter backend is actually in use
if is_aiter_found_and_supported() and envs.VLLM_ROCM_USE_AITER:
    from aiter import dtypes

    AITER_FP8_DTYPE = dtypes.fp8
else:
    # Placeholder when AITER is not the default - prevents NameError during module load.
    # Note: This fallback is used for fake implementations and type checking.
    # If an AITER backend is explicitly selected (even with env var=0),
    # the backend module will import aiter directly (rocm_aiter_fa.py line 35).
    AITER_FP8_DTYPE = _FP8_DTYPE

@tjtanaa tjtanaa Feb 6, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have validated locally with the following command.
The ops that are relying on this OP are fusion pass op of rmsnorm and blockquant.

sudo rm -rf ~/.cache/vllm

ATTN_BACKEND="ROCM_AITER_FA"

VLLM_LOGGING_LEVEL=DEBUG \
VLLM_ROCM_USE_AITER=1 \
VLLM_ROCM_SHUFFLE_KV_CACHE_LAYOUT=1 \
vllm serve Qwen/Qwen3-32B-FP8 \
--attention-backend $ATTN_BACKEND \
-O3 \
> launch_server_$ATTN_BACKEND-preshuffled-compilation.log 2>&1

Log showing replacement occurs

�[0;36m(APIServer pid=1155)�[0;0m DEBUG 02-06 03:32:37 [v1/engine/utils.py:980] Waiting for 1 local, 0 remote core engine proc(s) to start.
�[0;36m(EngineCore_DP0 pid=1441)�[0;0m DEBUG 02-06 03:32:39 [compilation/noop_elimination.py:105] Removed 0 no-op reshapes and slices
�[0;36m(EngineCore_DP0 pid=1441)�[0;0m DEBUG 02-06 03:32:39 [compilation/vllm_inductor_pass.py:79] NoOpEliminationPass completed in 0.5 ms
�[0;36m(EngineCore_DP0 pid=1441)�[0;0m DEBUG 02-06 03:32:39 [compilation/fusion.py:558] Replaced 0 patterns
�[0;36m(EngineCore_DP0 pid=1441)�[0;0m DEBUG 02-06 03:32:39 [compilation/vllm_inductor_pass.py:79] RMSNormQuantFusionPass completed in 0.8 ms
�[0;36m(EngineCore_DP0 pid=1441)�[0;0m DEBUG 02-06 03:32:39 [compilation/rocm_aiter_fusion.py:315] Replaced 1 patterns
�[0;36m(EngineCore_DP0 pid=1441)�[0;0m DEBUG 02-06 03:32:39 [compilation/vllm_inductor_pass.py:79] RocmAiterRMSNormQuantFusionPass completed in 15.7 ms
�[0;36m(EngineCore_DP0 pid=1441)�[0;0m DEBUG 02-06 03:32:39 [compilation/activation_quant_fusion.py:207] Replaced 0 patterns
�[0;36m(EngineCore_DP0 pid=1441)�[0;0m DEBUG 02-06 03:32:39 [compilation/vllm_inductor_pass.py:79] ActivationQuantFusionPass completed in 0.4 ms
�[0;36m(EngineCore_DP0 pid=1441)�[0;0m DEBUG 02-06 03:32:39 [compilation/rocm_aiter_fusion.py:394] Replaced 0 patterns
�[0;36m(EngineCore_DP0 pid=1441)�[0;0m DEBUG 02-06 03:32:39 [compilation/vllm_inductor_pass.py:79] RocmAiterSiluMulFp8GroupQuantFusionPass completed in 0.4 ms
�[0;36m(EngineCore_DP0 pid=1441)�[0;0m DEBUG 02-06 03:32:39 [compilation/vllm_inductor_pass.py:79] PostCleanupPass completed in 0.2 ms

The lmeval score is

2026-02-06:03:40:49 INFO     [loggers.evaluation_tracker:316] Output path not provided, skipping saving results aggregated
local-completions ({'model': 'Qwen/Qwen3-32B-FP8', 'base_url': 'http://127.0.0.1:8000/v1/completions'}), gen_kwargs: ({}), limit: None, num_fewshot: None, batch_size: 100
|Tasks|Version|     Filter     |n-shot|  Metric   |   |Value |   |Stderr|
|-----|------:|----------------|-----:|-----------|---|-----:|---|-----:|
|gsm8k|      3|flexible-extract|     5|exact_match|↑  |0.6353|±  |0.0133|
|     |       |strict-match    |     5|exact_match|↑  |0.7521|±  |0.0119|

from aiter import dtypes

AITER_FP8_DTYPE = dtypes.fp8
else:
# Placeholder when AITER is disabled - prevents NameError during module load.
# Note: When AITER is disabled, ops are not registered, so fake implementations
# referencing this variable won't actually be called at runtime.
# Placeholder when AITER is not the default - prevents NameError during module load.
# Note: This fallback is used for fake implementations and type checking.
# If an AITER backend is explicitly selected (even with env var=0),
# the backend module will import aiter directly (rocm_aiter_fa.py line 35).
AITER_FP8_DTYPE = _FP8_DTYPE


Expand Down Expand Up @@ -1654,5 +1673,87 @@ def shuffle_weights(

return tuple(shuffle_weight(tensor, layout=layout) for tensor in tensors)

@staticmethod
def flash_attn_varlen_func(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
cu_seqlens_q: torch.Tensor,
cu_seqlens_k: torch.Tensor,
max_seqlen_q: int,
max_seqlen_k: int,
min_seqlen_q: int | None = None,
dropout_p: float = 0.0,
softmax_scale: float | None = None,
causal: bool = False,
window_size: tuple[int, int] | None = None,
alibi_slopes: torch.Tensor | None = None,
return_lse: bool = False,
out: torch.Tensor | None = None,
):
"""
Flash attention with variable length sequences.

This function is NOT wrapped with @is_aiter_supported decorator
to allow explicit backend selection via attention_config to work
even when VLLM_ROCM_USE_AITER=0.

Note: This performs lazy import of aiter.flash_attn_varlen_func
"""
from aiter import flash_attn_varlen_func

return flash_attn_varlen_func(
q=q,
k=k,
v=v,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_k=cu_seqlens_k,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
min_seqlen_q=min_seqlen_q,
dropout_p=dropout_p,
softmax_scale=softmax_scale,
causal=causal,
window_size=window_size,
alibi_slopes=alibi_slopes,
return_lse=return_lse,
out=out,
)

@staticmethod
def pa_fwd_asm(
Q: torch.Tensor,
K: torch.Tensor,
V: torch.Tensor,
block_tables: torch.Tensor,
context_lens: torch.Tensor,
block_tables_stride0: int,
K_QScale: torch.Tensor,
V_QScale: torch.Tensor,
out_: torch.Tensor,
):
"""
Paged attention forward pass using assembly kernel.

This function is NOT wrapped with @is_aiter_supported decorator
to allow explicit backend selection via attention_config to work
even when VLLM_ROCM_USE_AITER=0.

Note: This performs lazy import of aiter.pa_fwd_asm
"""
from aiter import pa_fwd_asm

return pa_fwd_asm(
Q=Q,
K=K,
V=V,
block_tables=block_tables,
context_lens=context_lens,
block_tables_stride0=block_tables_stride0,
K_QScale=K_QScale,
V_QScale=V_QScale,
out_=out_,
)


rocm_aiter_ops.register_ops_once()
26 changes: 25 additions & 1 deletion vllm/v1/attention/backends/fa_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ def flash_attn_varlen_func(*args: Any, **kwargs: Any) -> Any: # type: ignore[no
"to be installed. Please install flash-attn first."
)

# ROCm doesn't use scheduler metadata (FA3 feature), provide stub
def get_scheduler_metadata(*args: Any, **kwargs: Any) -> None: # type: ignore[misc]
return None

# ROCm uses the C++ custom op for reshape_and_cache
from vllm import _custom_ops as ops

reshape_and_cache_flash = ops.reshape_and_cache_flash


def get_flash_attn_version(requires_alibi: bool = False) -> int | None:
# import here to avoid circular dependencies
Expand Down Expand Up @@ -128,4 +137,19 @@ def flash_attn_supports_mla():


def is_flash_attn_varlen_func_available() -> bool:
return current_platform.is_cuda() or current_platform.is_xpu()
if current_platform.is_cuda() or current_platform.is_xpu():
return True

# On ROCm, flash_attn_varlen_func is provided by the aiter package
# not vllm_flash_attn extension. This function checks CAPABILITY
# (can it work?) not PREFERENCE (should it be used by default?).
# Tests and other code use this to determine if the functionality
# exists on the system, regardless of VLLM_ROCM_USE_AITER.
# We use is_aiter_found_and_supported() instead of importing aiter directly
# to avoid triggering JIT compilation warnings during the availability check.
if current_platform.is_rocm():
from vllm._aiter_ops import is_aiter_found_and_supported

return is_aiter_found_and_supported()

@tjtanaa tjtanaa Feb 5, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition does not fit the current situation of the code.

This is_flash_attn_varlen_func_available() is a helper function for the ops imported in this vllm/v1/attention/backends/fa_utils.py Line 10- Line 32. Currently in the codebase, the condition to use or can use the aiter.flash_attn_varlen_func is explicitly handle in some other parts of the code.

Let me piece up more context. But I will need sometime.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did my recent commit remedy (a little bit) your concern? If possible, we would like this PR merged because the whole AITER_FA pipeline is currently broken on our CI. We could work on optimizing this on a follow-up PR.


return False
13 changes: 5 additions & 8 deletions vllm/v1/attention/backends/rocm_aiter_fa.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@
if current_platform.is_rocm():
from vllm.triton_utils import tl, triton

if rocm_aiter_ops.is_enabled():
import aiter

def block_size(x, head_dim):
return min(65536 // x.element_size(), triton.next_power_of_2(head_dim))

Expand Down Expand Up @@ -798,7 +795,7 @@ def extend_for_sliding_window(
total_tokens=swa_total_tokens,
)

aiter.flash_attn_varlen_func(
rocm_aiter_ops.flash_attn_varlen_func(
q=query,
k=key_fetched,
v=value_fetched,
Expand Down Expand Up @@ -848,7 +845,7 @@ def extend_forward(
v_scale,
)
return
out, lse = aiter.flash_attn_varlen_func(
out, lse = rocm_aiter_ops.flash_attn_varlen_func(
q=query,
k=key,
v=value,
Expand Down Expand Up @@ -895,7 +892,7 @@ def extend_forward(
total_tokens=total_token_per_batch[chunk_idx],
)

suf_out, suf_lse = aiter.flash_attn_varlen_func(
suf_out, suf_lse = rocm_aiter_ops.flash_attn_varlen_func(
q=query,
k=key_fetched,
v=value_fetched,
Expand Down Expand Up @@ -1053,7 +1050,7 @@ def forward(
prefill_key = key[num_decode_tokens + num_extend_tokens :]
prefill_value = value[num_decode_tokens + num_extend_tokens :]

aiter.flash_attn_varlen_func(
rocm_aiter_ops.flash_attn_varlen_func(
q=prefill_query,
k=prefill_key,
v=prefill_value,
Expand Down Expand Up @@ -1159,7 +1156,7 @@ def forward(
)
new_key_cache = key_cache.view_as(k_cache_template)
new_value_cache = value_cache.view_as(v_cache_template)
aiter.pa_fwd_asm(
rocm_aiter_ops.pa_fwd_asm(
Q=query[:num_decode_tokens],
K=new_key_cache,
V=new_value_cache,
Expand Down
Loading