[ROCm][PERF] Enable AITER PA gluon decode for MiniMax-M3 MTP and dense layers (#52849)
Signed-off-by: ukannika <uma.kannikanti@amd.com> Signed-off-by: ykamiset <yaminipreethi.kamisetty@amd.com> Co-authored-by: ykamiset <yaminipreethi.kamisetty@amd.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
ukannika committed
Aug 28, 2026 at 15:54 UTC
ae5b8e4a8d77a5de404a0cf9a91a56e8c7f2a94e
5 files changed
+574
-201
vllm/models/minimax_m3/amd/model.py
+39
-12
@@ -101,6 +101,8 @@ from vllm.models.minimax_m3.common.mm_preprocess import (
101
from vllm.models.minimax_m3.common.sparse_attention import (
102
MiniMaxM3SparseBackend,
103
MiniMaxM3SparseImpl,
104
+ MiniMaxM3SparseMetadata,
105
+ minimax_m3_rebase_slots_to_page16,
106
minimax_m3_use_aiter_sparse_pa,
107
select_main_impl_cls,
108
)
@@ -755,35 +757,45 @@ class MiniMaxM3SparseAttention(nn.Module, AttentionLayerBase):
757
if is_quantized_kv_cache(self.kv_cache_dtype):
758
kv_cache = kv_cache.view(self.impl.kv_cache_fp8_dtype)
759
key_cache, value_cache = kv_cache.unbind(1)
758
- if not key_cache.is_contiguous() or not value_cache.is_contiguous():
759
- raise RuntimeError(
760
- "MiniMax-M3 AITER sparse PA requires K/V-separated KV cache "
761
- "storage. Set VLLM_ROCM_SHUFFLE_KV_CACHE_LAYOUT=1 before "
762
- "initializing the engine."
763
- )
760
765
- x = 16 // key_cache.element_size()
761
+ x = 16 // kv_cache.element_size()
762
if self.head_dim % x != 0:
763
raise RuntimeError(
764
"MiniMax-M3 AITER sparse PA requires head_dim divisible by "
765
f"16 / dtype_size, got head_dim={self.head_dim}, x={x}"
766
)
771
- num_blocks = key_cache.shape[0]
772
- num_phys16 = num_blocks * 8
773
- self.kv_cache_k = key_cache.view(
767
+
768
+ num_blocks, _, block_size, _ = kv_cache.shape
769
+ pages_in_side = block_size // 16
770
+ if key_cache.is_contiguous() and value_cache.is_contiguous():
771
+ k_src, v_src = key_cache, value_cache
772
+ block_pages, v_page_offset = pages_in_side, 0
773
+ elif kv_cache.is_contiguous():
774
+ k_src = v_src = kv_cache
775
+ block_pages, v_page_offset = 2 * pages_in_side, pages_in_side
776
+ else:
777
+ raise RuntimeError(
778
+ "MiniMax-M3 AITER sparse PA needs each K/V head slot stored as "
779
+ "whole 16-token pages, but the resolved KV cache layout gives "
780
+ "neither dense planes nor block-contiguous pages."
781
+ )
782
+
783
+ num_phys16 = num_blocks * block_pages
784
+ self.kv_cache_k = k_src.view(
785
num_phys16,
786
self.num_kv_heads,
787
self.head_dim // x,
788
16,
789
x,
790
)
780
- self.kv_cache_v = value_cache.view(
791
+ # Offsetting V by half a block lets both sides share one page id.
792
+ self.kv_cache_v = v_src.view(
793
num_phys16,
794
self.num_kv_heads,
795
16 // x,
796
self.head_dim,
797
x,
786
- )
798
+ )[v_page_offset:]
799
self._aiter_sparse_pa_cache_data_ptr = self.kv_cache.data_ptr()
800
801
def get_aiter_sparse_pa_kv_cache(self) -> tuple[torch.Tensor, torch.Tensor]:
@@ -807,6 +819,21 @@ class MiniMaxM3SparseAttention(nn.Module, AttentionLayerBase):
819
)
820
821
key_cache, value_cache = self.get_aiter_sparse_pa_kv_cache()
822
+ if value_cache.shape[0] != key_cache.shape[0]:
823
+ attn_metadata = get_forward_context().attn_metadata
824
+ page16_slot_mapping = None
825
+ if isinstance(attn_metadata, dict):
826
+ main_md = attn_metadata[self.layer_name]
827
+ assert isinstance(main_md, MiniMaxM3SparseMetadata)
828
+ page16_slot_mapping = main_md.page16_slot_mapping
829
+ if (
830
+ page16_slot_mapping is None
831
+ or page16_slot_mapping.shape != slot_mapping.shape
832
+ ):
833
+ page16_slot_mapping = minimax_m3_rebase_slots_to_page16(
834
+ slot_mapping, self.kv_cache.shape[2]
835
+ )
836
+ slot_mapping = page16_slot_mapping
837
kv_cache_dtype = (
838
self.kv_cache_dtype
839
if is_quantized_kv_cache(self.kv_cache_dtype)
vllm/models/minimax_m3/amd/ops/sparse_pa.py
+155
-118
@@ -32,35 +32,34 @@ def _is_fp8_kv_cache_tensor(kv_cache: torch.Tensor) -> bool:
32
33
34
@triton.jit
35
-def _build_sparse_block_table_kernel(
36
- topk_ptr, # [1, batch, topk] int32, selected logical 128-block ids
37
- block_table_ptr, # [batch, max_blocks] int32, logical 128-page table
38
- seq_lens_ptr, # [batch] int32
39
- sparse_bt_ptr, # [batch, topk * 8] int32, physical 16-page table
40
- sparse_ctx_ptr, # [batch] int32
35
+def _write_sparse_block_table_row(
36
+ topk_row, # [topk] int32, selected logical block ids for this query
37
+ bt_row, # [max_blocks] int32, this request's logical page table
38
+ sbt_row, # [topk * PAGES_PER_BLOCK] int32, physical 16-page table
39
+ ctx_ptr, # int32, this query's attended context length
40
+ abs_pos, # absolute position of this query token, may be negative
41
max_topk,
42
- stride_topk_n,
42
stride_topk_k,
44
- stride_bt_b,
45
- stride_sbt_b,
43
SPARSE_BLOCK_SIZE_C: tl.constexpr,
44
PAGES_PER_BLOCK: tl.constexpr,
45
+ BLOCK_PAGE_STRIDE: tl.constexpr,
46
BLOCK_SIZE_T: tl.constexpr,
47
):
50
- pid_b = tl.program_id(0)
51
- seq_len = tl.load(seq_lens_ptr + pid_b)
52
- has_tokens = seq_len > 0
53
- last_blk = tl.maximum((seq_len - 1) // SPARSE_BLOCK_SIZE_C, 0)
48
+ """Compact one query's selected logical blocks into physical page-16 ids.
49
55
- topk_row = topk_ptr + pid_b * stride_topk_n
56
- bt_row = block_table_ptr + pid_b * stride_bt_b
57
- sbt_row = sparse_bt_ptr + pid_b * stride_sbt_b
50
+ ``BLOCK_PAGE_STRIDE`` is how many page ids a block spans, which is
51
+ ``PAGES_PER_BLOCK`` when each K/V side is its own dense plane and twice
52
+ that when both sides share a block. Padded rows carry a negative
53
+ ``abs_pos``, which clamps the causal range to empty.
54
+ """
55
+ causal_len = tl.maximum(abs_pos + 1, 0)
56
+ self_blk = abs_pos // SPARSE_BLOCK_SIZE_C
57
58
off_t = tl.arange(0, BLOCK_SIZE_T)
59
blk = tl.load(topk_row + off_t * stride_topk_k, mask=off_t < max_topk, other=-1)
61
- valid = has_tokens & (blk >= 0)
62
- is_tail = valid & (blk == last_blk)
63
- is_full = valid & (blk != last_blk)
60
+ valid = (causal_len > 0) & (blk >= 0) & (blk <= self_blk)
61
+ is_tail = valid & (blk == self_blk)
62
+ is_full = valid & (blk < self_blk)
63
64
n_full = tl.sum(is_full.to(tl.int32), axis=0)
65
n_valid = tl.sum(valid.to(tl.int32), axis=0)
@@ -68,7 +67,7 @@ def _build_sparse_block_table_kernel(
67
slot = tl.where(is_full, earlier_full, n_full)
68
69
logical_page = tl.load(bt_row + blk, mask=valid, other=0).to(tl.int32)
71
- base_phys = logical_page * PAGES_PER_BLOCK
70
+ base_phys = logical_page * BLOCK_PAGE_STRIDE
71
dst_base = slot * PAGES_PER_BLOCK
72
73
for j in tl.static_range(PAGES_PER_BLOCK):
@@ -83,42 +82,54 @@ def _build_sparse_block_table_kernel(
82
mask=(off_w >= n_used) & (off_w < row_width),
83
)
84
86
- tail_tokens = tl.where(has_tokens, seq_len - last_blk * SPARSE_BLOCK_SIZE_C, 0)
85
+ tail_tokens = causal_len - self_blk * SPARSE_BLOCK_SIZE_C
86
has_tail = tl.sum(is_tail.to(tl.int32), axis=0) > 0
87
ctx = n_full * SPARSE_BLOCK_SIZE_C + tl.where(has_tail, tail_tokens, 0)
89
- ctx = tl.where(has_tail, ctx, tl.minimum(n_valid * SPARSE_BLOCK_SIZE_C, seq_len))
90
- tl.store(sparse_ctx_ptr + pid_b, ctx)
88
+ ctx = tl.where(has_tail, ctx, tl.minimum(n_valid * SPARSE_BLOCK_SIZE_C, causal_len))
89
+ tl.store(ctx_ptr, ctx)
90
91
93
-@torch.no_grad()
94
-def minimax_m3_build_sparse_block_table(
95
- topk_idx: torch.Tensor, # [1, batch, topk]
96
- block_table: torch.Tensor, # [batch, max_blocks]
97
- seq_lens: torch.Tensor, # [batch]
98
-) -> tuple[torch.Tensor, torch.Tensor]:
99
- """Compact selected logical sparse blocks into physical page-16 tables."""
100
- assert topk_idx.shape[0] == 1, "AITER sparse PA requires num_kv_heads == 1"
101
- batch = topk_idx.shape[1]
102
- topk = topk_idx.shape[-1]
103
- width = topk * PAGES_PER_SPARSE_BLOCK
104
- sparse_bt = torch.empty((batch, width), dtype=torch.int32, device=topk_idx.device)
105
- sparse_ctx = torch.empty((batch,), dtype=torch.int32, device=topk_idx.device)
106
- _build_sparse_block_table_kernel[(batch,)](
107
- topk_idx,
108
- block_table,
109
- seq_lens,
110
- sparse_bt,
111
- sparse_ctx,
112
- topk,
113
- topk_idx.stride(1),
114
- topk_idx.stride(2),
115
- block_table.stride(0),
116
- sparse_bt.stride(0),
117
- SPARSE_BLOCK_SIZE_C=SPARSE_BLOCK_SIZE,
118
- PAGES_PER_BLOCK=PAGES_PER_SPARSE_BLOCK,
119
- BLOCK_SIZE_T=triton.next_power_of_2(topk),
92
+@triton.jit
93
+def _build_sparse_block_table_kernel(
94
+ topk_ptr, # [1, batch * DECODE_QUERY_LEN, topk] int32, logical block ids
95
+ block_table_ptr, # [batch, max_blocks] int32, logical 128-page table
96
+ seq_lens_ptr, # [batch] int32
97
+ sparse_bt_ptr, # [batch * DECODE_QUERY_LEN, topk * 8] int32
98
+ sparse_ctx_ptr, # [batch * DECODE_QUERY_LEN] int32
99
+ max_topk,
100
+ stride_topk_n,
101
+ stride_topk_k,
102
+ stride_bt_b,
103
+ stride_sbt_b,
104
+ SPARSE_BLOCK_SIZE_C: tl.constexpr,
105
+ PAGES_PER_BLOCK: tl.constexpr,
106
+ BLOCK_PAGE_STRIDE: tl.constexpr,
107
+ BLOCK_SIZE_T: tl.constexpr,
108
+ DECODE_QUERY_LEN: tl.constexpr,
109
+):
110
+ # One program per query token. The decode indexer flattens speculative rows
111
+ # as ``req_id * DECODE_QUERY_LEN + local_q``, and a request's query tokens
112
+ # are the last DECODE_QUERY_LEN positions of its sequence, so both the
113
+ # request id and the absolute position follow from the program id.
114
+ pid_n = tl.program_id(0)
115
+ req_id = pid_n // DECODE_QUERY_LEN
116
+ local_q = pid_n % DECODE_QUERY_LEN
117
+ seq_len = tl.load(seq_lens_ptr + req_id)
118
+ abs_pos = seq_len - DECODE_QUERY_LEN + local_q
119
+
120
+ _write_sparse_block_table_row(
121
+ topk_ptr + pid_n * stride_topk_n,
122
+ block_table_ptr + req_id * stride_bt_b,
123
+ sparse_bt_ptr + pid_n * stride_sbt_b,
124
+ sparse_ctx_ptr + pid_n,
125
+ abs_pos,
126
+ max_topk,
127
+ stride_topk_k,
128
+ SPARSE_BLOCK_SIZE_C,
129
+ PAGES_PER_BLOCK,
130
+ BLOCK_PAGE_STRIDE,
131
+ BLOCK_SIZE_T,
132
)
121
- return sparse_bt, sparse_ctx
133
134
135
@triton.jit
@@ -136,52 +147,39 @@ def _build_sparse_block_table_prefill_kernel(
147
stride_sbt_n,
148
SPARSE_BLOCK_SIZE_C: tl.constexpr,
149
PAGES_PER_BLOCK: tl.constexpr,
150
+ BLOCK_PAGE_STRIDE: tl.constexpr,
151
BLOCK_SIZE_T: tl.constexpr,
152
):
153
pid_n = tl.program_id(0)
154
req_id = tl.load(req_id_ptr + pid_n)
155
abs_pos = tl.load(abs_pos_ptr + pid_n)
144
- # Padded speculative rows can have negative positions; clamp to empty range.
145
- causal_len = tl.maximum(abs_pos + 1, 0)
146
- self_blk = abs_pos // SPARSE_BLOCK_SIZE_C
147
-
148
- topk_row = topk_ptr + pid_n * stride_topk_n
149
- bt_row = block_table_ptr + req_id * stride_bt_b
150
- sbt_row = sparse_bt_ptr + pid_n * stride_sbt_n
151
-
152
- off_t = tl.arange(0, BLOCK_SIZE_T)
153
- blk = tl.load(topk_row + off_t * stride_topk_k, mask=off_t < max_topk, other=-1)
154
- valid = (causal_len > 0) & (blk >= 0) & (blk <= self_blk)
155
- is_tail = valid & (blk == self_blk)
156
- is_full = valid & (blk < self_blk)
156
158
- n_full = tl.sum(is_full.to(tl.int32), axis=0)
159
- n_valid = tl.sum(valid.to(tl.int32), axis=0)
160
- earlier_full = tl.cumsum(is_full.to(tl.int32), axis=0) - is_full.to(tl.int32)
161
- slot = tl.where(is_full, earlier_full, n_full)
162
-
163
- logical_page = tl.load(bt_row + blk, mask=valid, other=0).to(tl.int32)
164
- base_phys = logical_page * PAGES_PER_BLOCK
165
- dst_base = slot * PAGES_PER_BLOCK
157
+ _write_sparse_block_table_row(
158
+ topk_ptr + pid_n * stride_topk_n,
159
+ block_table_ptr + req_id * stride_bt_b,
160
+ sparse_bt_ptr + pid_n * stride_sbt_n,
161
+ sparse_ctx_ptr + pid_n,
162
+ abs_pos,
163
+ max_topk,
164
+ stride_topk_k,
165
+ SPARSE_BLOCK_SIZE_C,
166
+ PAGES_PER_BLOCK,
167
+ BLOCK_PAGE_STRIDE,
168
+ BLOCK_SIZE_T,
169
+ )
170
167
- for j in tl.static_range(PAGES_PER_BLOCK):
168
- tl.store(sbt_row + dst_base + j, base_phys + j, mask=valid)
171
170
- n_used = n_valid * PAGES_PER_BLOCK
171
- off_w = tl.arange(0, BLOCK_SIZE_T * PAGES_PER_BLOCK)
172
- row_width = max_topk * PAGES_PER_BLOCK
173
- tl.store(
174
- sbt_row + off_w,
175
- tl.zeros_like(off_w),
176
- mask=(off_w >= n_used) & (off_w < row_width),
172
+def _alloc_sparse_block_table(
173
+ topk_idx: torch.Tensor,
174
+) -> tuple[torch.Tensor, torch.Tensor]:
175
+ assert topk_idx.shape[0] == 1, "AITER sparse PA requires num_kv_heads == 1"
176
+ total_q = topk_idx.shape[1]
177
+ width = topk_idx.shape[-1] * PAGES_PER_SPARSE_BLOCK
178
+ return (
179
+ torch.empty((total_q, width), dtype=torch.int32, device=topk_idx.device),
180
+ torch.empty((total_q,), dtype=torch.int32, device=topk_idx.device),
181
)
182
179
- tail_tokens = causal_len - self_blk * SPARSE_BLOCK_SIZE_C
180
- has_tail = tl.sum(is_tail.to(tl.int32), axis=0) > 0
181
- ctx = n_full * SPARSE_BLOCK_SIZE_C + tl.where(has_tail, tail_tokens, 0)
182
- ctx = tl.where(has_tail, ctx, tl.minimum(n_valid * SPARSE_BLOCK_SIZE_C, causal_len))
183
- tl.store(sparse_ctx_ptr + pid_n, ctx)
184
-
183
184
@torch.no_grad()
185
def minimax_m3_build_sparse_block_table_prefill(
@@ -189,15 +187,12 @@ def minimax_m3_build_sparse_block_table_prefill(
187
block_table: torch.Tensor, # [batch, max_blocks]
188
query_req_id: torch.Tensor, # [total_q]
189
query_abs_pos: torch.Tensor, # [total_q]
190
+ block_page_stride: int = PAGES_PER_SPARSE_BLOCK,
191
) -> tuple[torch.Tensor, torch.Tensor]:
192
"""Build one page-16 sparse block table row per prefill query token."""
194
- assert topk_idx.shape[0] == 1, "AITER sparse PA requires num_kv_heads == 1"
195
- total_q = topk_idx.shape[1]
193
+ sparse_bt, sparse_ctx = _alloc_sparse_block_table(topk_idx)
194
topk = topk_idx.shape[-1]
197
- width = topk * PAGES_PER_SPARSE_BLOCK
198
- sparse_bt = torch.empty((total_q, width), dtype=torch.int32, device=topk_idx.device)
199
- sparse_ctx = torch.empty((total_q,), dtype=torch.int32, device=topk_idx.device)
200
- _build_sparse_block_table_prefill_kernel[(total_q,)](
195
+ _build_sparse_block_table_prefill_kernel[(topk_idx.shape[1],)](
196
topk_idx,
197
block_table,
198
query_req_id,
@@ -211,6 +206,7 @@ def minimax_m3_build_sparse_block_table_prefill(
206
sparse_bt.stride(0),
207
SPARSE_BLOCK_SIZE_C=SPARSE_BLOCK_SIZE,
208
PAGES_PER_BLOCK=PAGES_PER_SPARSE_BLOCK,
209
+ BLOCK_PAGE_STRIDE=block_page_stride,
210
BLOCK_SIZE_T=triton.next_power_of_2(topk),
211
)
212
return sparse_bt, sparse_ctx
@@ -221,27 +217,41 @@ def minimax_m3_build_sparse_block_table_decode(
217
topk_idx: torch.Tensor, # [1, batch * decode_query_len, topk]
218
block_table: torch.Tensor, # [batch, max_blocks]
219
seq_lens: torch.Tensor, # [batch]
224
- decode_query_len: int,
220
+ decode_query_len: int = 1,
221
+ block_page_stride: int = PAGES_PER_SPARSE_BLOCK,
222
) -> tuple[torch.Tensor, torch.Tensor]:
223
"""Build one page-16 sparse block table row per decode query token."""
227
- if decode_query_len == 1:
228
- return minimax_m3_build_sparse_block_table(topk_idx, block_table, seq_lens)
229
-
224
total_q = topk_idx.shape[1]
225
expected_q = block_table.shape[0] * decode_query_len
226
assert total_q == expected_q, (
227
"MiniMax-M3 decode top-k rows must equal batch * decode_query_len: "
228
f"{total_q} != {expected_q}"
229
)
236
- pos = torch.arange(total_q, dtype=torch.int32, device=topk_idx.device)
237
- query_req_id = torch.div(pos, decode_query_len, rounding_mode="floor")
238
- q_offset = pos - query_req_id * decode_query_len
239
- query_abs_pos = (seq_lens[query_req_id] - decode_query_len + q_offset).to(
240
- torch.int32
241
- )
242
- return minimax_m3_build_sparse_block_table_prefill(
243
- topk_idx, block_table, query_req_id, query_abs_pos
230
+ sparse_bt, sparse_ctx = _alloc_sparse_block_table(topk_idx)
231
+ topk = topk_idx.shape[-1]
232
+ _build_sparse_block_table_kernel[(total_q,)](
233
+ topk_idx,
234
+ block_table,
235
+ seq_lens,
236
+ sparse_bt,
237
+ sparse_ctx,
238
+ topk,
239
+ topk_idx.stride(1),
240
+ topk_idx.stride(2),
241
+ block_table.stride(0),
242
+ sparse_bt.stride(0),
243
+ SPARSE_BLOCK_SIZE_C=SPARSE_BLOCK_SIZE,
244
+ PAGES_PER_BLOCK=PAGES_PER_SPARSE_BLOCK,
245
+ BLOCK_PAGE_STRIDE=block_page_stride,
246
+ BLOCK_SIZE_T=triton.next_power_of_2(topk),
247
+ DECODE_QUERY_LEN=decode_query_len,
248
)
249
+ return sparse_bt, sparse_ctx
250
+
251
+
252
+# Retained for the decode_query_len == 1 callers (and tests) that predate the
253
+# speculative-decode rows.
254
+minimax_m3_build_sparse_block_table = minimax_m3_build_sparse_block_table_decode
255
256
257
@triton.jit
@@ -313,6 +323,21 @@ def minimax_m3_insert_index_cache(
323
)
324
325
326
+def _sides_are_packed(k_cache: torch.Tensor, v_cache: torch.Tensor) -> bool:
327
+ """Whether a block holds both its K and V pages instead of one side."""
328
+ return v_cache.shape[0] != k_cache.shape[0]
329
+
330
+
331
+def _block_page_stride(k_cache: torch.Tensor, v_cache: torch.Tensor) -> int:
332
+ """How many page ids one sparse block spans.
333
+
334
+ Each side owns ``PAGES_PER_SPARSE_BLOCK`` pages. When the sides are dense
335
+ planes a block is exactly that wide; when they interleave, a block covers
336
+ both sides' pages and V is reached from the same page id at a fixed offset.
337
+ """
338
+ return PAGES_PER_SPARSE_BLOCK * (2 if _sides_are_packed(k_cache, v_cache) else 1)
339
+
340
+
341
def _gluon_scale_arg(
342
scale: torch.Tensor | None,
343
*,
@@ -361,7 +386,11 @@ def minimax_m3_sparse_attn_decode_aiter(
386
decode_query_len: int = 1,
387
) -> None:
388
sparse_bt, sparse_ctx = minimax_m3_build_sparse_block_table_decode(
364
- topk_idx, block_table, seq_lens, decode_query_len
389
+ topk_idx,
390
+ block_table,
391
+ seq_lens,
392
+ decode_query_len,
393
+ _block_page_stride(k_cache, v_cache),
394
)
395
_run_gluon_decode(
396
q,
@@ -384,23 +413,20 @@ def minimax_m3_sparse_attn_prefill_aiter(
413
v_cache: torch.Tensor,
414
topk_idx: torch.Tensor, # [1, total_q, topk]
415
block_table: torch.Tensor,
387
- cu_seqlens_q: torch.Tensor,
388
- prefix_lens: torch.Tensor,
416
+ query_req_id: torch.Tensor, # [total_q] int32, owning request per token
417
+ query_abs_pos: torch.Tensor, # [total_q] int32, absolute position per token
418
num_kv_heads: int,
419
sm_scale: float,
420
output: torch.Tensor,
421
k_scale: torch.Tensor | None = None,
422
v_scale: torch.Tensor | None = None,
423
) -> None:
395
- total_q = q.shape[0]
396
- pos = torch.arange(total_q, dtype=torch.int32, device=q.device)
397
- query_req_id = torch.searchsorted(cu_seqlens_q[1:].contiguous(), pos, right=True)
398
- query_req_id = query_req_id.to(torch.int32)
399
- query_abs_pos = (prefix_lens[query_req_id] + (pos - cu_seqlens_q[query_req_id])).to(
400
- torch.int32
401
- )
424
sparse_bt, sparse_ctx = minimax_m3_build_sparse_block_table_prefill(
403
- topk_idx, block_table, query_req_id, query_abs_pos
425
+ topk_idx,
426
+ block_table,
427
+ query_req_id,
428
+ query_abs_pos,
429
+ _block_page_stride(k_cache, v_cache),
430
)
431
_run_gluon_decode(
432
q,
@@ -445,8 +471,11 @@ def _run_gluon_decode(
471
group_size = num_q_heads // num_kv_heads
472
473
nphys16, hkv = k_cache.shape[0], k_cache.shape[1]
474
+ # V is offset by half a block when the sides interleave, so it holds fewer
475
+ # pages than K even though both are addressed by the same page id.
476
+ v_nphys16 = v_cache.shape[0]
477
k_cache_view = k_cache.view(nphys16 * hkv, 1, *k_cache.shape[2:])
449
- v_cache_view = v_cache.view(nphys16 * hkv, 1, *v_cache.shape[2:])
478
+ v_cache_view = v_cache.view(v_nphys16 * hkv, 1, *v_cache.shape[2:])
479
q_view = q.view(total_q * num_kv_heads, group_size, head_size)
480
out_view = output.view(total_q * num_kv_heads, group_size, head_size)
481
@@ -463,11 +492,19 @@ def _run_gluon_decode(
492
is_fp8 = _is_fp8_kv_cache_tensor(k_cache)
493
compute_type = aiter_dtypes.fp8 if is_fp8 else q.dtype
494
if is_fp8:
495
+ if _sides_are_packed(k_cache, v_cache) and not all(
496
+ scale is None or scale.numel() == 1 for scale in (k_scale, v_scale)
497
+ ):
498
+ raise NotImplementedError(
499
+ "MiniMax-M3 AITER sparse PA supports only scalar KV scales "
500
+ "when K and V pages share a block; per-token scales would "
501
+ "need the same page renumbering."
502
+ )
503
k_scale_arg = _gluon_scale_arg(
504
k_scale, num_phys_pages=nphys16, num_kv_heads=hkv
505
)
506
v_scale_arg = _gluon_scale_arg(
470
- v_scale, num_phys_pages=nphys16, num_kv_heads=hkv
507
+ v_scale, num_phys_pages=v_nphys16, num_kv_heads=hkv
508
)
509
else:
510
k_scale_arg = v_scale_arg = None
vllm/models/minimax_m3/amd/sparse_attention_msa.py
+3
-2
@@ -75,14 +75,15 @@ class MiniMaxM3SparseAiterPAImpl(MiniMaxM3SparseImpl):
75
if main_md.num_prefills > 0:
76
p = main_md.prefill
77
assert p is not None
78
+ assert p.query_req_id is not None and p.query_abs_pos is not None
79
minimax_m3_sparse_attn_prefill_aiter(
80
q[nd:],
81
k_cache,
82
v_cache,
83
topk[:, nd:num_tokens, :],
84
p.block_table,
84
- p.cu_seqlens_q,
85
- p.context_lens,
85
+ p.query_req_id,
86
+ p.query_abs_pos,
87
self.num_kv_heads,
88
self.scale,
89
out[nd:],
vllm/models/minimax_m3/common/sparse_attention.py
+97
-7
@@ -66,6 +66,49 @@ def _minimax_m3_aiter_sparse_pa_requested() -> bool:
66
return rocm_aiter_ops.is_enabled() and rocm_aiter_ops.is_shuffle_kv_cache_enabled()
67
68
69
+def minimax_m3_rebase_slots_to_page16(
70
+ slot_mapping: torch.Tensor,
71
+ block_size: int,
72
+ out: torch.Tensor | None = None,
73
+) -> torch.Tensor:
74
+ """Rebase a token slot mapping onto AITER's page-16 page numbering.
75
+
76
+ AITER's KV writer derives the destination page from ``slot // 16`` and
77
+ assumes pages are numbered consecutively. When the resolved layout stores
78
+ both K/V sides inside a block, a block spans twice as many pages, so only
79
+ the block component of the slot doubles -- the offset within the page must
80
+ not move. Clamping before the division leaves padding slots at their
81
+ negative sentinel, which is what tells the writer to skip them.
82
+ """
83
+ if out is None:
84
+ out = torch.empty_like(slot_mapping)
85
+ torch.clamp(slot_mapping, min=0, out=out)
86
+ return (
87
+ out.div_(block_size, rounding_mode="floor").mul_(block_size).add_(slot_mapping)
88
+ )
89
+
90
+
91
+def minimax_m3_query_token_positions(
92
+ cu_seqlens_q: torch.Tensor,
93
+ prefix_lens: torch.Tensor,
94
+ total_q: int,
95
+) -> tuple[torch.Tensor, torch.Tensor]:
96
+ """Map each prefill query token to its request id and absolute KV position.
97
+
98
+ Both tensors are the same for every sparse layer in a step, so the AITER
99
+ sparse PA block-table builder reads them from the metadata instead of
100
+ rebuilding them once per layer.
101
+ """
102
+ pos = torch.arange(total_q, dtype=torch.int32, device=cu_seqlens_q.device)
103
+ query_req_id = torch.searchsorted(
104
+ cu_seqlens_q[1:].contiguous(), pos, right=True
105
+ ).to(torch.int32)
106
+ query_abs_pos = (prefix_lens[query_req_id] + (pos - cu_seqlens_q[query_req_id])).to(
107
+ torch.int32
108
+ )
109
+ return query_req_id, query_abs_pos
110
+
111
+
112
def minimax_m3_use_aiter_sparse_pa(num_kv_heads: int) -> bool:
113
"""Whether to use the ROCm AITER page-16 sparse PA prototype."""
114
requested = _minimax_m3_aiter_sparse_pa_requested()
@@ -117,10 +160,14 @@ class MiniMaxM3SparseBackend(AttentionBackend):
160
161
@classmethod
162
def supported_kv_cache_layouts(cls) -> tuple[KVCacheLayout, ...] | None:
120
- # AITER sparse PA unbinds K and V as separate contiguous regions, which
121
- # requires the head dimension to sit outside the block dimension.
163
+ # AITER sparse PA reads K and V as separate page-16 tensors. LHBNC
164
+ # gives each side a dense plane; LBHNC interleaves the sides inside a
165
+ # block, which the page-id rebasing in ops/sparse_pa.py absorbs. LBHNC
166
+ # is listed first because these layers publish two head slots while the
167
+ # indexer publishes one, and mixed HNC shapes need a block-compact
168
+ # layout to share the block-id pool.
169
if _minimax_m3_aiter_sparse_pa_requested():
123
- return (KVCacheLayout.LHBNC,)
170
+ return (KVCacheLayout.LBHNC, KVCacheLayout.LHBNC)
171
return super().supported_kv_cache_layouts()
172
173
@@ -137,6 +184,12 @@ class MiniMaxM3SparsePrefillMetadata:
184
max_seq_len: int
185
total_kv_blocks: int
186
187
+ # Per-query-token request id and absolute KV position, built once per step
188
+ # for the AITER sparse PA block-table kernel. See
189
+ # ``minimax_m3_query_token_positions``.
190
+ query_req_id: torch.Tensor | None = None
191
+ query_abs_pos: torch.Tensor | None = None
192
+
193
194
@dataclass
195
class MiniMaxM3SparseDecodeMetadata:
@@ -167,6 +220,11 @@ class MiniMaxM3SparseMetadata(AttentionMetadata):
220
prefill: MiniMaxM3SparsePrefillMetadata | None = None
221
decode: MiniMaxM3SparseDecodeMetadata | None = None
222
223
+ # ``slot_mapping`` rebased onto the page-16 numbering AITER's KV writer
224
+ # uses when both K/V sides share a block. Only set for the AITER sparse PA
225
+ # path; see ``MiniMaxM3SparseMetadataBuilder.build``.
226
+ page16_slot_mapping: torch.Tensor | None = None
227
+
228
229
class MiniMaxM3SparseMetadataBuilder(AttentionMetadataBuilder[MiniMaxM3SparseMetadata]):
230
# Full cudagraphs for uniform decode batches, incl. spec-decode verify
@@ -191,6 +249,19 @@ class MiniMaxM3SparseMetadataBuilder(AttentionMetadataBuilder[MiniMaxM3SparseMet
249
dtype=torch.int32,
250
device=device,
251
)
252
+ # Every sparse layer shares one slot mapping, so the AITER page-16
253
+ # rebase is done here once per step instead of once per layer. Stable
254
+ # buffer for the same reason as the context lengths above.
255
+ self.use_aiter_sparse_pa = minimax_m3_use_aiter_sparse_pa(
256
+ kv_cache_spec.num_kv_heads
257
+ )
258
+ self.page16_slot_mapping_buffer: torch.Tensor | None = None
259
+ if self.use_aiter_sparse_pa:
260
+ self.page16_slot_mapping_buffer = torch.empty(
261
+ vllm_config.scheduler_config.max_num_batched_tokens,
262
+ dtype=torch.int64,
263
+ device=device,
264
+ )
265
266
def build(
267
self,
@@ -236,17 +307,26 @@ class MiniMaxM3SparseMetadataBuilder(AttentionMetadataBuilder[MiniMaxM3SparseMet
307
)
308
prefill_cu_seqlens_k[0] = 0
309
torch.cumsum(prefill_kv_lens, dim=0, out=prefill_cu_seqlens_k[1:])
310
+ prefill_cu_seqlens_q = (
311
+ query_start_loc[num_decodes:] - num_decode_tokens
312
+ ).to(torch.int32)
313
+ prefill_context_lens = context_lens[num_decodes:]
314
+ query_req_id = query_abs_pos = None
315
+ if self.use_aiter_sparse_pa:
316
+ query_req_id, query_abs_pos = minimax_m3_query_token_positions(
317
+ prefill_cu_seqlens_q, prefill_context_lens, num_prefill_tokens
318
+ )
319
prefill_metadata = MiniMaxM3SparsePrefillMetadata(
240
- cu_seqlens_q=(query_start_loc[num_decodes:] - num_decode_tokens).to(
241
- torch.int32
242
- ),
320
+ cu_seqlens_q=prefill_cu_seqlens_q,
321
cu_seqlens_k=prefill_cu_seqlens_k,
322
seq_lens=prefill_kv_lens,
245
- context_lens=context_lens[num_decodes:],
323
+ context_lens=prefill_context_lens,
324
block_table=block_table[num_decodes:],
325
max_query_len=common_attn_metadata.max_query_len,
326
max_seq_len=common_attn_metadata.max_seq_len,
327
total_kv_blocks=prefill_total_kv_blocks,
328
+ query_req_id=query_req_id,
329
+ query_abs_pos=query_abs_pos,
330
)
331
332
decode_metadata: MiniMaxM3SparseDecodeMetadata | None = None
@@ -265,10 +345,20 @@ class MiniMaxM3SparseMetadataBuilder(AttentionMetadataBuilder[MiniMaxM3SparseMet
345
decode_query_len=decode_query_len,
346
)
347
348
+ page16_slot_mapping = None
349
+ if self.page16_slot_mapping_buffer is not None:
350
+ slot_mapping = common_attn_metadata.slot_mapping
351
+ page16_slot_mapping = minimax_m3_rebase_slots_to_page16(
352
+ slot_mapping,
353
+ self.kv_cache_spec.block_size,
354
+ out=self.page16_slot_mapping_buffer[: slot_mapping.shape[0]],
355
+ )
356
+
357
return MiniMaxM3SparseMetadata(
358
seq_lens=seq_lens,
359
max_seq_len=common_attn_metadata.max_seq_len,
360
slot_mapping=common_attn_metadata.slot_mapping,
361
+ page16_slot_mapping=page16_slot_mapping,
362
num_actual_tokens=num_tokens,
363
num_decodes=num_decodes,
364
num_decode_tokens=num_decode_tokens,
vllm/v1/attention/backends/rocm_aiter_fa.py
+280
-62
@@ -2,13 +2,17 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
"""Attention layer with AiterFlashAttention."""
4
5
-from dataclasses import dataclass
5
+from dataclasses import dataclass, replace
6
from typing import ClassVar
7
8
import torch
9
10
from vllm._aiter_ops import rocm_aiter_ops
11
-from vllm.config import VllmConfig, get_layers_from_vllm_config
11
+from vllm.config import (
12
+ VllmConfig,
13
+ get_current_vllm_config_or_none,
14
+ get_layers_from_vllm_config,
15
+)
16
from vllm.config.cache import CacheDType
17
from vllm.logger import init_logger
18
from vllm.model_executor.layers.attention import Attention
@@ -17,7 +21,7 @@ from vllm.platforms.interface import DeviceCapability
21
from vllm.utils.gpu_sync_debug import gpu_sync_allowed
22
from vllm.utils.math_utils import cdiv
23
from vllm.utils.platform_utils import num_compute_units
20
-from vllm.utils.torch_utils import is_quantized_kv_cache
24
+from vllm.utils.torch_utils import get_dtype_size, is_quantized_kv_cache
25
from vllm.v1.attention.backend import (
26
AttentionBackend,
27
AttentionCGSupport,
@@ -34,9 +38,43 @@ 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
41
+_PA_GLUON_MAX_QUERY_LEN = 4
42
+_PA_GLUON_MAX_QUERY_GROUP_SIZE = 64
43
+# Query group sizes the gluon paged-attention decode kernel is validated for: 8, 16
44
+_PA_GLUON_QUERY_GROUP_SIZES = (8, 16)
45
+
46
+# The kernel is only validated for this head size and kernel block size.
47
+_PA_GLUON_HEAD_SIZE = 128
48
+_PA_GLUON_BLOCK_SIZE = 128
49
+
50
+
51
+def _pa_gluon_supports(num_heads_q: int, num_heads_kv: int, head_size: int) -> bool:
52
+ """Whether the head config can use the PA decode gluon kernel.
53
+
54
+ Requires the shuffle KV cache layout (the kernel reads K/V in that layout)
55
+ plus a head config the kernel is validated for. Both the advertised kernel
56
+ block sizes and the decode dispatch go through this, so a config can never
57
+ be offered a 128-token page that gluon will then decline to serve.
58
+ """
59
+ return (
60
+ rocm_aiter_ops.is_shuffle_kv_cache_enabled()
61
+ and num_heads_kv > 0
62
+ and num_heads_q % num_heads_kv == 0
63
+ and num_heads_q // num_heads_kv in _PA_GLUON_QUERY_GROUP_SIZES
64
+ and head_size == _PA_GLUON_HEAD_SIZE
65
+ )
66
+
67
+
68
_PARTITION_SIZE_ROCM = 256
69
_CP_TOKENS_PER_ITER_ROCM = 32 * 1024
70
if current_platform.is_rocm():
71
+ from aiter.ops.triton.gluon.pa_decode_gluon import (
72
+ get_recommended_splits,
73
+ )
74
+ from aiter.ops.triton.gluon.pa_decode_gluon import (
75
+ pa_decode_gluon as _pa_decode_gluon,
76
+ )
77
+
78
from vllm.triton_utils import tl, triton
79
80
def block_size(x, head_dim):
@@ -126,15 +164,17 @@ if current_platform.is_rocm():
164
# for kv cache layout as
165
# K: [num_blocks, num_head, head_dim // x, page_size, x]
166
# V: [num_blocks, num_head, page_size // x, head_dim, x]
167
+ # The sides only sit a dense page apart when they are separate
168
+ # planes (H outermost), so take the block stride from the tensor.
169
key_cache_ptr_offset = (
170
key_cache_ptr
131
- + block_id * num_heads * head_size * PAGE_SIZE
171
+ + block_id * k_cache_stride0
172
+ head_id * head_size * PAGE_SIZE
173
+ slot_id * x
174
)
175
value_cache_ptr_offset = (
176
value_cache_ptr
137
- + block_id * num_heads * head_size * PAGE_SIZE
177
+ + block_id * v_cache_stride0
178
+ head_id * head_size * PAGE_SIZE
179
+ (slot_id // x) * head_size * x
180
+ slot_id % x
@@ -319,6 +359,7 @@ logger = init_logger(__name__)
359
@dataclass
360
class AiterFlashAttentionDecodeMetadata:
361
max_query_len: int
362
+ uniform_query_len: int | None
363
364
365
@dataclass
@@ -475,10 +516,11 @@ class AiterFlashAttentionMetadataBuilder(
516
and self.scale.numel() == 1
517
and is_quantized_kv_cache(self.vllm_config.cache_config.cache_dtype)
518
):
478
- layers = get_layers_from_vllm_config(self.vllm_config, Attention)
479
- first_layer_name = [k for k in layers][0]
519
+ # Size the scales from a layer this builder owns. The draft model
520
+ # runs its own builder over its own KV cache, so the first layer of
521
+ # the whole config can carry an unrelated block count.
522
kv_cache_shape = self.vllm_config.compilation_config.static_forward_context[
481
- first_layer_name
523
+ self.layer_names[0]
524
].kv_cache.shape
525
num_blocks = kv_cache_shape[0]
526
self.scale = torch.ones(
@@ -510,8 +552,15 @@ class AiterFlashAttentionMetadataBuilder(
552
553
decode_metadata = None
554
if num_decodes > 0:
555
+ decode_max_query_len = query_lens_cpu[:num_decodes].max().item()
556
+ uniform_query_len = (
557
+ decode_max_query_len
558
+ if num_decode_tokens == num_decodes * decode_max_query_len
559
+ else None
560
+ )
561
decode_metadata = AiterFlashAttentionDecodeMetadata(
514
- max_query_len=query_lens_cpu[:num_decodes].max().item(),
562
+ max_query_len=decode_max_query_len,
563
+ uniform_query_len=uniform_query_len,
564
)
565
566
prefill_metadata = None
@@ -682,9 +731,27 @@ class AiterFlashAttentionMetadataBuilder(
731
"""
732
num_reqs = common_attn_metadata.num_reqs
733
num_tokens = common_attn_metadata.num_actual_tokens
734
+ max_query_len = common_attn_metadata.max_query_len
735
+
736
+ # Uniform-decode assumption does not hold for the
737
+ # drafter's first forward after a target step: it inherits the target's
738
+ # per-request query lengths, so rows can be longer than gluon's limit or
739
+ # ragged. Those batches need the real split, which costs a sync.
740
+ # _PA_GLUON_MAX_QUERY_LEN only binds when gluon is the decode consumer,
741
+ # so test that rather than the shuffle layout alone.
742
+ if _pa_gluon_supports(self.num_heads_q, self.num_heads_kv, self.headdim) and (
743
+ max_query_len > _PA_GLUON_MAX_QUERY_LEN
744
+ or num_tokens != num_reqs * max_query_len
745
+ ):
746
+ return self.build(
747
+ common_prefix_len=0, common_attn_metadata=common_attn_metadata
748
+ )
749
750
decode_metadata = AiterFlashAttentionDecodeMetadata(
687
- max_query_len=common_attn_metadata.max_query_len,
751
+ max_query_len=max_query_len,
752
+ uniform_query_len=(
753
+ max_query_len if num_tokens == num_reqs * max_query_len else None
754
+ ),
755
)
756
757
return AiterFlashAttentionMetadata(
@@ -739,6 +806,21 @@ class AiterFlashAttentionBackend(AttentionBackend):
806
807
@staticmethod
808
def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
809
+ if not rocm_aiter_ops.is_shuffle_kv_cache_enabled():
810
+ return [16, 32]
811
+ # Only gluon serves 128-token pages; the pa_fwd_asm/ll4mi decode
812
+ # fallback is limited to 16 and 32. Advertise 128 only when gluon can
813
+ # run so selection never picks a page we cannot serve.
814
+ vllm_config = get_current_vllm_config_or_none()
815
+ if vllm_config is not None and vllm_config.model_config is not None:
816
+ mc = vllm_config.model_config
817
+ pc = vllm_config.parallel_config
818
+ if _pa_gluon_supports(
819
+ mc.get_num_attention_heads(pc),
820
+ mc.get_num_kv_heads(pc),
821
+ mc.get_head_size(),
822
+ ):
823
+ return [16, 32, 128]
824
return [16, 32]
825
826
@classmethod
@@ -765,13 +847,33 @@ class AiterFlashAttentionBackend(AttentionBackend):
847
848
@classmethod
849
def customize_spec(cls, spec: AttentionSpec) -> AttentionSpec:
768
- """Validate the block size the ROCm gather kernels require."""
850
+ """Validate the block size the ROCm gather kernels require, and under
851
+ the shuffle layout publish K and V as two head slots.
852
+
853
+ The shuffle read/write kernels rearrange a whole ``head_size x
854
+ block_size`` tile per side, so each side needs its own contiguous run
855
+ in the page. Packed into the content dim they interleave per token
856
+ instead, which no layout can undo.
857
+ """
858
# block_size == 1 is the per-token page-size probe (see
859
# Platform.get_page_size_bytes); real blocks must be gatherable in
860
# 16-token units by the ROCm kernel.
861
if spec.block_size != 1 and spec.block_size % 16 != 0:
862
raise ValueError("Block size must be a multiple of 16.")
774
- return spec
863
+ if not rocm_aiter_ops.is_shuffle_kv_cache_enabled():
864
+ return spec
865
+ if spec.state_content_bytes is not None:
866
+ return spec
867
+ assert spec.head_size == spec.head_size_v, (
868
+ "Separate K/V head slots require symmetric K/V head sizes."
869
+ )
870
+ return replace(
871
+ spec,
872
+ num_head_slots=2,
873
+ state_content_bytes=spec.num_kv_heads
874
+ * spec.head_size
875
+ * get_dtype_size(spec.dtype),
876
+ )
877
878
@classmethod
879
def supported_kv_cache_layouts(cls) -> tuple[KVCacheLayout, ...]:
@@ -1160,11 +1262,25 @@ class AiterFlashAttentionImpl(AttentionImpl):
1262
if num_decodes > 0:
1263
assert attn_metadata.decode_metadata is not None
1264
decode_max_query_len = attn_metadata.decode_metadata.max_query_len
1265
+ decode_query_len = attn_metadata.decode_metadata.uniform_query_len
1266
1164
- # Use unified_attention for speculative decoding (multi-token),
1165
- # sliding window, or sinks
1166
- # (pa_fwd_asm and paged_attention_v1 don't support sinks)
1167
- if (
1267
+ # check if we can use the gluon paged-attention decode kernel
1268
+ use_gluon = (
1269
+ _pa_gluon_supports(
1270
+ self.num_heads, self.num_kv_heads, self.head_size
1271
+ )
1272
+ and key_cache.shape[1] == _PA_GLUON_BLOCK_SIZE
1273
+ and decode_query_len is not None
1274
+ and decode_query_len <= _PA_GLUON_MAX_QUERY_LEN
1275
+ and decode_query_len * (self.num_heads // self.num_kv_heads)
1276
+ <= _PA_GLUON_MAX_QUERY_GROUP_SIZE
1277
+ and (decode_query_len == 1 or attn_metadata.causal)
1278
+ )
1279
+ # Use unified_attention for the decodes the paged kernels can't
1280
+ # take: sliding window, sinks, or a multi-token batch that gluon
1281
+ # declined (pa_fwd_asm and paged_attention_v1 don't support
1282
+ # sinks).
1283
+ if not use_gluon and (
1284
self.sliding_window[0] != -1
1285
or decode_max_query_len > 1
1286
or self.sinks is not None
@@ -1287,21 +1403,6 @@ class AiterFlashAttentionImpl(AttentionImpl):
1403
)
1404
elif rocm_aiter_ops.is_shuffle_kv_cache_enabled():
1405
_, num_heads, head_size = query.shape
1290
- num_seqs = attn_metadata.seq_lens.shape[0]
1291
- max_num_partitions = (
1292
- attn_metadata.max_seq_len + _PARTITION_SIZE_ROCM - 1
1293
- ) // _PARTITION_SIZE_ROCM
1294
- tmp_out = torch.empty(
1295
- (num_seqs, num_heads, max_num_partitions, head_size),
1296
- dtype=query.dtype,
1297
- device=query.device,
1298
- )
1299
- exp_sums = torch.empty(
1300
- (num_seqs, num_heads, max_num_partitions),
1301
- dtype=torch.float32,
1302
- device=query.device,
1303
- )
1304
- max_logits = torch.empty_like(exp_sums)
1406
num_blocks, block_size, num_kv_heads, _ = key_cache.shape
1407
x = 16 // key_cache.element_size()
1408
new_key_cache = key_cache.reshape(
@@ -1310,37 +1411,146 @@ class AiterFlashAttentionImpl(AttentionImpl):
1411
new_value_cache = value_cache.reshape(
1412
num_blocks, num_kv_heads, block_size // x, head_size, x
1413
)
1313
- k_qscale = (
1314
- layer._k_scale
1315
- if attn_metadata.k_scale is None
1316
- else attn_metadata.k_scale
1317
- )
1318
- v_qscale = (
1319
- layer._v_scale
1320
- if attn_metadata.v_scale is None
1321
- else attn_metadata.v_scale
1322
- )
1323
- rocm_aiter_ops.paged_attention_common(
1324
- Q=query[:num_decode_tokens],
1325
- K=new_key_cache,
1326
- V=new_value_cache,
1327
- tmp_out=tmp_out,
1328
- max_logits=max_logits,
1329
- exp_sums=exp_sums,
1330
- max_seq_len=attn_metadata.max_seq_len,
1331
- block_tables=attn_metadata.block_table[:num_decodes],
1332
- context_lens=attn_metadata.seq_lens[:num_decodes],
1333
- block_tables_stride0=attn_metadata.block_table[
1334
- :num_decodes
1335
- ].stride(0),
1336
- scale=self.scale,
1337
- K_QScale_hip=k_qscale,
1338
- V_QScale_hip=v_qscale,
1339
- K_QScale_asm=k_qscale,
1340
- V_QScale_asm=v_qscale,
1341
- out_=output[:num_decode_tokens],
1342
- kv_cache_dtype=self.kv_cache_dtype,
1343
- )
1414
+
1415
+ if use_gluon:
1416
+ is_fp8_kv = is_quantized_kv_cache(self.kv_cache_dtype)
1417
+ # Per-tensor descale, as a float32 [1] tensor.
1418
+ k_scale_gluon = (
1419
+ layer._k_scale.reshape(1).to(torch.float32)
1420
+ if is_fp8_kv
1421
+ else None
1422
+ )
1423
+ v_scale_gluon = (
1424
+ layer._v_scale.reshape(1).to(torch.float32)
1425
+ if is_fp8_kv
1426
+ else None
1427
+ )
1428
+ compute_type = (
1429
+ current_platform.fp8_dtype() if is_fp8_kv else query.dtype
1430
+ )
1431
+ # The kernel folds the query positions into the group
1432
+ # dim, so the intermediate buffers are sized by the
1433
+ # combined extent.
1434
+ query_group_size = decode_query_len * (
1435
+ num_heads // num_kv_heads
1436
+ )
1437
+
1438
+ sliding_window_int = (
1439
+ self.sliding_window[0] + 1
1440
+ if self.sliding_window[0] > 0
1441
+ else 0
1442
+ )
1443
+ if sliding_window_int > 0:
1444
+ max_context_partition_num = 1
1445
+ context_partition_size = 128
1446
+ else:
1447
+ max_context_partition_num = get_recommended_splits(
1448
+ num_decodes, num_kv_heads
1449
+ )
1450
+ context_partition_size = _PARTITION_SIZE_ROCM
1451
+
1452
+ intermediate_shape = (
1453
+ num_decodes,
1454
+ num_kv_heads,
1455
+ max_context_partition_num,
1456
+ query_group_size,
1457
+ )
1458
+ exp_sums = torch.empty(
1459
+ intermediate_shape,
1460
+ dtype=torch.float32,
1461
+ device=query.device,
1462
+ )
1463
+ max_logits = torch.empty_like(exp_sums)
1464
+ temporary_output = torch.empty(
1465
+ (*intermediate_shape, head_size),
1466
+ dtype=output.dtype,
1467
+ device=query.device,
1468
+ )
1469
+
1470
+ _pa_decode_gluon(
1471
+ output=output[:num_decode_tokens],
1472
+ query=query[:num_decode_tokens],
1473
+ key_cache=new_key_cache,
1474
+ value_cache=new_value_cache,
1475
+ context_lengths=attn_metadata.seq_lens[:num_decodes].to(
1476
+ torch.int32
1477
+ ),
1478
+ block_tables=attn_metadata.block_table[:num_decodes].to(
1479
+ torch.int32
1480
+ ),
1481
+ softmax_scale=self.scale,
1482
+ query_length=decode_query_len,
1483
+ max_context_partition_num=max_context_partition_num,
1484
+ context_partition_size=context_partition_size,
1485
+ compute_type=compute_type,
1486
+ query_scale=None,
1487
+ key_scale=k_scale_gluon,
1488
+ value_scale=v_scale_gluon,
1489
+ exp_sums=exp_sums,
1490
+ max_logits=max_logits,
1491
+ temporary_output=temporary_output,
1492
+ alibi_slopes=self.alibi_slopes,
1493
+ sinks=self.sinks,
1494
+ sliding_window=sliding_window_int,
1495
+ ps=True,
1496
+ )
1497
+ else:
1498
+ # Unlike gluon, this kernel derives block addresses
1499
+ # arithmetically, so it only reads the right bytes when
1500
+ # each side is a dense plane of pages.
1501
+ assert new_key_cache.stride(0) == (
1502
+ num_kv_heads * head_size * block_size
1503
+ ), (
1504
+ "paged_attention_common needs the K/V sides as "
1505
+ "dense planes; the resolved KV cache layout "
1506
+ "interleaves them within a block."
1507
+ )
1508
+ num_seqs = attn_metadata.seq_lens.shape[0]
1509
+ max_num_partitions = (
1510
+ attn_metadata.max_seq_len + _PARTITION_SIZE_ROCM - 1
1511
+ ) // _PARTITION_SIZE_ROCM
1512
+ tmp_out = torch.empty(
1513
+ (num_seqs, num_heads, max_num_partitions, head_size),
1514
+ dtype=query.dtype,
1515
+ device=query.device,
1516
+ )
1517
+ exp_sums = torch.empty(
1518
+ (num_seqs, num_heads, max_num_partitions),
1519
+ dtype=torch.float32,
1520
+ device=query.device,
1521
+ )
1522
+ max_logits = torch.empty_like(exp_sums)
1523
+ k_qscale = (
1524
+ layer._k_scale
1525
+ if attn_metadata.k_scale is None
1526
+ else attn_metadata.k_scale
1527
+ )
1528
+ v_qscale = (
1529
+ layer._v_scale
1530
+ if attn_metadata.v_scale is None
1531
+ else attn_metadata.v_scale
1532
+ )
1533
+ rocm_aiter_ops.paged_attention_common(
1534
+ Q=query[:num_decode_tokens],
1535
+ K=new_key_cache,
1536
+ V=new_value_cache,
1537
+ tmp_out=tmp_out,
1538
+ max_logits=max_logits,
1539
+ exp_sums=exp_sums,
1540
+ max_seq_len=attn_metadata.max_seq_len,
1541
+ block_tables=attn_metadata.block_table[:num_decodes],
1542
+ context_lens=attn_metadata.seq_lens[:num_decodes],
1543
+ block_tables_stride0=attn_metadata.block_table[
1544
+ :num_decodes
1545
+ ].stride(0),
1546
+ scale=self.scale,
1547
+ K_QScale_hip=k_qscale,
1548
+ V_QScale_hip=v_qscale,
1549
+ K_QScale_asm=k_qscale,
1550
+ V_QScale_asm=v_qscale,
1551
+ out_=output[:num_decode_tokens],
1552
+ kv_cache_dtype=self.kv_cache_dtype,
1553
+ )
1554
else:
1555
_, num_heads, head_size = query.shape
1556
nbytes_per_qo_elem = torch.finfo(query.dtype).bits // 8
@@ -1393,6 +1603,14 @@ class AiterFlashAttentionImpl(AttentionImpl):
1603
def _split_kv_cache(
1604
self, kv_cache: torch.Tensor
1605
) -> tuple[torch.Tensor, torch.Tensor]:
1606
+ if rocm_aiter_ops.is_shuffle_kv_cache_enabled():
1607
+ # (B, 2, N, H*hs) -> one (B, N, H, hs) per side, which is what the
1608
+ # shuffle read/write kernels reinterpret in place. The heads are
1609
+ # folded into the content dim by customize_spec.
1610
+ key_cache, value_cache = kv_cache.unflatten(
1611
+ -1, (self.num_kv_heads, self.head_size)
1612
+ ).unbind(1)
1613
+ return key_cache, value_cache
1614
# (B, H, N, 2*hs) -> ((B, N, H, hs), (B, N, H, hs))
1615
return kv_cache.transpose(1, 2).split(self.head_size, dim=-1)
1616