[ROCm][BugFix] Revert AITER PA gluon decode from ROCM_AITER_FA (#54805)

Signed-off-by: ukannika <uma.kannikanti@amd.com> Co-authored-by: Andreas Karatzas <akaratza@amd.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>

ukannika committed Oct 1, 2026 at 17:21 UTC eec3c36a478bdacc5ae322af9a0cde6c5604182b
1 file changed +74 -240
vllm/v1/attention/backends/rocm_aiter_fa.py
+74 -240
@@ -8,11 +8,7 @@ from typing import ClassVar
8 import torch
9
10 from vllm._aiter_ops import rocm_aiter_ops
11 -from vllm.config import (
12 - VllmConfig,
13 - get_current_vllm_config_or_none,
14 - get_layers_from_vllm_config,
15 -)
11 +from vllm.config import VllmConfig, get_layers_from_vllm_config
12 from vllm.config.cache import CacheDType
13 from vllm.logger import init_logger
14 from vllm.model_executor.layers.attention import Attention
@@ -42,43 +38,9 @@ from vllm.v1.attention.backends.utils import (
38 from vllm.v1.attention.ops.merge_attn_states import merge_attn_states
39 from vllm.v1.kv_cache_interface import AttentionSpec, KVCacheLayout
40
45 -_PA_GLUON_MAX_QUERY_LEN = 4
46 -_PA_GLUON_MAX_QUERY_GROUP_SIZE = 64
47 -# Query group sizes the gluon paged-attention decode kernel is validated for: 8, 16
48 -_PA_GLUON_QUERY_GROUP_SIZES = (8, 16)
49 -
50 -# The kernel is only validated for this head size and kernel block size.
51 -_PA_GLUON_HEAD_SIZE = 128
52 -_PA_GLUON_BLOCK_SIZE = 128
53 -
54 -
55 -def _pa_gluon_supports(num_heads_q: int, num_heads_kv: int, head_size: int) -> bool:
56 - """Whether the head config can use the PA decode gluon kernel.
57 -
58 - Requires the shuffle KV cache layout (the kernel reads K/V in that layout)
59 - plus a head config the kernel is validated for. Both the advertised kernel
60 - block sizes and the decode dispatch go through this, so a config can never
61 - be offered a 128-token page that gluon will then decline to serve.
62 - """
63 - return (
64 - rocm_aiter_ops.is_shuffle_kv_cache_enabled()
65 - and num_heads_kv > 0
66 - and num_heads_q % num_heads_kv == 0
67 - and num_heads_q // num_heads_kv in _PA_GLUON_QUERY_GROUP_SIZES
68 - and head_size == _PA_GLUON_HEAD_SIZE
69 - )
70 -
71 -
41 _PARTITION_SIZE_ROCM = 256
42 _CP_TOKENS_PER_ITER_ROCM = 32 * 1024
43 if current_platform.is_rocm():
75 - from aiter.ops.triton.gluon.pa_decode_gluon import (
76 - get_recommended_splits,
77 - )
78 - from aiter.ops.triton.gluon.pa_decode_gluon import (
79 - pa_decode_gluon as _pa_decode_gluon,
80 - )
81 -
44 from vllm.triton_utils import tl, triton
45
46 def block_size(x, head_dim):
@@ -168,8 +130,10 @@ if current_platform.is_rocm():
130 # for kv cache layout as
131 # K: [num_blocks, num_head, head_dim // x, page_size, x]
132 # V: [num_blocks, num_head, page_size // x, head_dim, x]
171 - # The sides only sit a dense page apart when they are separate
172 - # planes (H outermost), so take the block stride from the tensor.
133 + # Take the block stride from the tensor, as the NHD branch does:
134 + # the two sides only sit a dense page apart when they are separate
135 + # planes, and models that pin a block-compact layout interleave
136 + # them instead.
137 key_cache_ptr_offset = (
138 key_cache_ptr
139 + block_id * k_cache_stride0
@@ -363,7 +327,6 @@ logger = init_logger(__name__)
327 @dataclass
328 class AiterFlashAttentionDecodeMetadata:
329 max_query_len: int
366 - uniform_query_len: int | None
330
331
332 @dataclass
@@ -598,15 +561,8 @@ class AiterFlashAttentionMetadataBuilder(
561
562 decode_metadata = None
563 if num_decodes > 0:
601 - decode_max_query_len = query_lens_cpu[:num_decodes].max().item()
602 - uniform_query_len = (
603 - decode_max_query_len
604 - if num_decode_tokens == num_decodes * decode_max_query_len
605 - else None
606 - )
564 decode_metadata = AiterFlashAttentionDecodeMetadata(
608 - max_query_len=decode_max_query_len,
609 - uniform_query_len=uniform_query_len,
565 + max_query_len=query_lens_cpu[:num_decodes].max().item(),
566 )
567
568 prefill_metadata = None
@@ -778,27 +734,9 @@ class AiterFlashAttentionMetadataBuilder(
734 """
735 num_reqs = common_attn_metadata.num_reqs
736 num_tokens = common_attn_metadata.num_actual_tokens
781 - max_query_len = common_attn_metadata.max_query_len
782 -
783 - # Uniform-decode assumption does not hold for the
784 - # drafter's first forward after a target step: it inherits the target's
785 - # per-request query lengths, so rows can be longer than gluon's limit or
786 - # ragged. Those batches need the real split, which costs a sync.
787 - # _PA_GLUON_MAX_QUERY_LEN only binds when gluon is the decode consumer,
788 - # so test that rather than the shuffle layout alone.
789 - if _pa_gluon_supports(self.num_heads_q, self.num_heads_kv, self.headdim) and (
790 - max_query_len > _PA_GLUON_MAX_QUERY_LEN
791 - or num_tokens != num_reqs * max_query_len
792 - ):
793 - return self.build(
794 - common_prefix_len=0, common_attn_metadata=common_attn_metadata
795 - )
737
738 decode_metadata = AiterFlashAttentionDecodeMetadata(
798 - max_query_len=max_query_len,
799 - uniform_query_len=(
800 - max_query_len if num_tokens == num_reqs * max_query_len else None
801 - ),
739 + max_query_len=common_attn_metadata.max_query_len,
740 )
741
742 return AiterFlashAttentionMetadata(
@@ -853,21 +791,6 @@ class AiterFlashAttentionBackend(AttentionBackend):
791
792 @staticmethod
793 def get_supported_kernel_block_sizes(kv_cache_spec=None) -> list[int | MultipleOf]:
856 - if not rocm_aiter_ops.is_shuffle_kv_cache_enabled():
857 - return [16, 32]
858 - # Only gluon serves 128-token pages; the pa_fwd_asm/ll4mi decode
859 - # fallback is limited to 16 and 32. Advertise 128 only when gluon can
860 - # run so selection never picks a page we cannot serve.
861 - vllm_config = get_current_vllm_config_or_none()
862 - if vllm_config is not None and vllm_config.model_config is not None:
863 - mc = vllm_config.model_config
864 - pc = vllm_config.parallel_config
865 - if _pa_gluon_supports(
866 - mc.get_num_attention_heads(pc),
867 - mc.get_num_kv_heads(pc),
868 - mc.get_head_size(),
869 - ):
870 - return [16, 32, 128]
794 return [16, 32]
795
796 @classmethod
@@ -924,6 +847,14 @@ class AiterFlashAttentionBackend(AttentionBackend):
847
848 @classmethod
849 def supported_kv_cache_layouts(cls) -> tuple[KVCacheLayout, ...]:
850 + if rocm_aiter_ops.is_shuffle_kv_cache_enabled():
851 + # pa_fwd_asm strides between pages by a whole dense page, so the two
852 + # head slots customize_spec publishes have to be separate planes
853 + # spanning every block: H outermost. Under LBHNC the slots alternate
854 + # block by block, doubling the stride the kernel assumes. LBHNC
855 + # stays listed for models whose mixed HNC shapes need a
856 + # block-compact layout; those read K/V by stride instead.
857 + return (KVCacheLayout.LHBNC, KVCacheLayout.LBHNC)
858 # K and V come out of the content dim as transposed views rather than
859 # copies, so the head dim may sit on either side of the block dim, but
860 # the layer must stay outermost.
@@ -1358,25 +1289,11 @@ class AiterFlashAttentionImpl(AttentionImpl):
1289 if num_decodes > 0:
1290 assert attn_metadata.decode_metadata is not None
1291 decode_max_query_len = attn_metadata.decode_metadata.max_query_len
1361 - decode_query_len = attn_metadata.decode_metadata.uniform_query_len
1292
1363 - # check if we can use the gluon paged-attention decode kernel
1364 - use_gluon = (
1365 - _pa_gluon_supports(
1366 - self.num_heads, self.num_kv_heads, self.head_size
1367 - )
1368 - and key_cache.shape[1] == _PA_GLUON_BLOCK_SIZE
1369 - and decode_query_len is not None
1370 - and decode_query_len <= _PA_GLUON_MAX_QUERY_LEN
1371 - and decode_query_len * (self.num_heads // self.num_kv_heads)
1372 - <= _PA_GLUON_MAX_QUERY_GROUP_SIZE
1373 - and (decode_query_len == 1 or attn_metadata.causal)
1374 - )
1293 # Use unified_attention for the decodes the paged kernels can't
1376 - # take: sliding window, sinks, or a multi-token batch that gluon
1377 - # declined (pa_fwd_asm and paged_attention_v1 don't support
1378 - # sinks).
1379 - if not use_gluon and (
1294 + # take: sliding window, sinks, or a multi-token batch
1295 + # (pa_fwd_asm and paged_attention_v1 don't support sinks).
1296 + if (
1297 self.sliding_window[0] != -1
1298 or decode_max_query_len > 1
1299 or self.sinks is not None
@@ -1505,145 +1422,62 @@ class AiterFlashAttentionImpl(AttentionImpl):
1422 num_blocks, num_kv_heads, block_size // x, head_size, x
1423 )
1424
1508 - if use_gluon:
1509 - is_fp8_kv = is_quantized_kv_cache(self.kv_cache_dtype)
1510 - # Per-tensor descale, as a float32 [1] tensor.
1511 - k_scale_gluon = (
1512 - layer._k_scale.reshape(1).to(torch.float32)
1513 - if is_fp8_kv
1514 - else None
1515 - )
1516 - v_scale_gluon = (
1517 - layer._v_scale.reshape(1).to(torch.float32)
1518 - if is_fp8_kv
1519 - else None
1520 - )
1521 - compute_type = (
1522 - current_platform.fp8_dtype() if is_fp8_kv else query.dtype
1523 - )
1524 - # The kernel folds the query positions into the group
1525 - # dim, so the intermediate buffers are sized by the
1526 - # combined extent.
1527 - query_group_size = decode_query_len * (
1528 - num_heads // num_kv_heads
1529 - )
1530 -
1531 - sliding_window_int = (
1532 - self.sliding_window[0] + 1
1533 - if self.sliding_window[0] > 0
1534 - else 0
1535 - )
1536 - if sliding_window_int > 0:
1537 - max_context_partition_num = 1
1538 - context_partition_size = 128
1539 - else:
1540 - max_context_partition_num = get_recommended_splits(
1541 - num_decodes, num_kv_heads
1542 - )
1543 - context_partition_size = _PARTITION_SIZE_ROCM
1544 -
1545 - intermediate_shape = (
1546 - num_decodes,
1547 - num_kv_heads,
1548 - max_context_partition_num,
1549 - query_group_size,
1550 - )
1551 - exp_sums = torch.empty(
1552 - intermediate_shape,
1553 - dtype=torch.float32,
1554 - device=query.device,
1555 - )
1556 - max_logits = torch.empty_like(exp_sums)
1557 - temporary_output = torch.empty(
1558 - (*intermediate_shape, head_size),
1559 - dtype=output.dtype,
1560 - device=query.device,
1561 - )
1562 -
1563 - _pa_decode_gluon(
1564 - output=output[:num_decode_tokens],
1565 - query=query[:num_decode_tokens],
1566 - key_cache=new_key_cache,
1567 - value_cache=new_value_cache,
1568 - context_lengths=attn_metadata.seq_lens[:num_decodes].to(
1569 - torch.int32
1570 - ),
1571 - block_tables=attn_metadata.block_table[:num_decodes].to(
1572 - torch.int32
1573 - ),
1574 - softmax_scale=self.scale,
1575 - query_length=decode_query_len,
1576 - max_context_partition_num=max_context_partition_num,
1577 - context_partition_size=context_partition_size,
1578 - compute_type=compute_type,
1579 - query_scale=None,
1580 - key_scale=k_scale_gluon,
1581 - value_scale=v_scale_gluon,
1582 - exp_sums=exp_sums,
1583 - max_logits=max_logits,
1584 - temporary_output=temporary_output,
1585 - alibi_slopes=self.alibi_slopes,
1586 - sinks=self.sinks,
1587 - sliding_window=sliding_window_int,
1588 - ps=True,
1589 - )
1590 - else:
1591 - # Unlike gluon, this kernel derives block addresses
1592 - # arithmetically, so it only reads the right bytes when
1593 - # each side is a dense plane of pages.
1594 - assert new_key_cache.stride(0) == (
1595 - num_kv_heads * head_size * block_size
1596 - ), (
1597 - "paged_attention_common needs the K/V sides as "
1598 - "dense planes; the resolved KV cache layout "
1599 - "interleaves them within a block."
1600 - )
1601 - num_seqs = attn_metadata.seq_lens.shape[0]
1602 - max_num_partitions = (
1603 - attn_metadata.max_seq_len + _PARTITION_SIZE_ROCM - 1
1604 - ) // _PARTITION_SIZE_ROCM
1605 - tmp_out = torch.empty(
1606 - (num_seqs, num_heads, max_num_partitions, head_size),
1607 - dtype=query.dtype,
1608 - device=query.device,
1609 - )
1610 - exp_sums = torch.empty(
1611 - (num_seqs, num_heads, max_num_partitions),
1612 - dtype=torch.float32,
1613 - device=query.device,
1614 - )
1615 - max_logits = torch.empty_like(exp_sums)
1616 - k_qscale = (
1617 - layer._k_scale
1618 - if attn_metadata.k_scale is None
1619 - else attn_metadata.k_scale
1620 - )
1621 - v_qscale = (
1622 - layer._v_scale
1623 - if attn_metadata.v_scale is None
1624 - else attn_metadata.v_scale
1625 - )
1626 - rocm_aiter_ops.paged_attention_common(
1627 - Q=query[:num_decode_tokens],
1628 - K=new_key_cache,
1629 - V=new_value_cache,
1630 - tmp_out=tmp_out,
1631 - max_logits=max_logits,
1632 - exp_sums=exp_sums,
1633 - max_seq_len=attn_metadata.max_seq_len,
1634 - block_tables=attn_metadata.block_table[:num_decodes],
1635 - context_lens=attn_metadata.seq_lens[:num_decodes],
1636 - block_tables_stride0=attn_metadata.block_table[
1637 - :num_decodes
1638 - ].stride(0),
1639 - scale=self.scale,
1640 - K_QScale_hip=k_qscale,
1641 - V_QScale_hip=v_qscale,
1642 - K_QScale_asm=k_qscale,
1643 - V_QScale_asm=v_qscale,
1644 - out_=output[:num_decode_tokens],
1645 - kv_cache_dtype=self.kv_cache_dtype,
1646 - )
1425 + # This kernel derives block addresses arithmetically, so it
1426 + # only reads the right bytes when each side is a dense
1427 + # plane of pages.
1428 + assert new_key_cache.stride(0) == (
1429 + num_kv_heads * head_size * block_size
1430 + ), (
1431 + "paged_attention_common needs the K/V sides as dense "
1432 + "planes; the resolved KV cache layout interleaves them "
1433 + "within a block."
1434 + )
1435 + num_seqs = attn_metadata.seq_lens.shape[0]
1436 + max_num_partitions = (
1437 + attn_metadata.max_seq_len + _PARTITION_SIZE_ROCM - 1
1438 + ) // _PARTITION_SIZE_ROCM
1439 + tmp_out = torch.empty(
1440 + (num_seqs, num_heads, max_num_partitions, head_size),
1441 + dtype=query.dtype,
1442 + device=query.device,
1443 + )
1444 + exp_sums = torch.empty(
1445 + (num_seqs, num_heads, max_num_partitions),
1446 + dtype=torch.float32,
1447 + device=query.device,
1448 + )
1449 + max_logits = torch.empty_like(exp_sums)
1450 + k_qscale = (
1451 + layer._k_scale
1452 + if attn_metadata.k_scale is None
1453 + else attn_metadata.k_scale
1454 + )
1455 + v_qscale = (
1456 + layer._v_scale
1457 + if attn_metadata.v_scale is None
1458 + else attn_metadata.v_scale
1459 + )
1460 + rocm_aiter_ops.paged_attention_common(
1461 + Q=query[:num_decode_tokens],
1462 + K=new_key_cache,
1463 + V=new_value_cache,
1464 + tmp_out=tmp_out,
1465 + max_logits=max_logits,
1466 + exp_sums=exp_sums,
1467 + max_seq_len=attn_metadata.max_seq_len,
1468 + block_tables=attn_metadata.block_table[:num_decodes],
1469 + context_lens=attn_metadata.seq_lens[:num_decodes],
1470 + block_tables_stride0=attn_metadata.block_table[
1471 + :num_decodes
1472 + ].stride(0),
1473 + scale=self.scale,
1474 + K_QScale_hip=k_qscale,
1475 + V_QScale_hip=v_qscale,
1476 + K_QScale_asm=k_qscale,
1477 + V_QScale_asm=v_qscale,
1478 + out_=output[:num_decode_tokens],
1479 + kv_cache_dtype=self.kv_cache_dtype,
1480 + )
1481 else:
1482 _, num_heads, head_size = query.shape
1483 nbytes_per_qo_elem = torch.finfo(query.dtype).bits // 8