A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 10, 10:15 UTC
[Bugfix][ROCm]Fix Qwen3-Next-80B-A3B-Thinking inference and optimize non-standard block size (544) support under rocm_atten (#31380)
Signed-off-by: vllmellm <vllm.ellm@embeddedllm.com>
vllmellm committed
Jan 9, 2026 at 12:28 UTC
1a19e9cd87b6bb091fc9c530e6446601db52b58e
5 files changed
+283
-84
tests/kernels/attention/test_prefix_prefill.py
+33
-2
@@ -112,6 +112,7 @@ def test_contexted_kv_attention(
112
kv_cache_dtype: str,
113
device: str,
114
op: Callable,
115
+ block_size: int = 32,
116
) -> None:
117
if "fp8" in kv_cache_dtype and not current_platform.has_device_capability(89):
118
pytest.skip(
@@ -138,7 +139,6 @@ def test_contexted_kv_attention(
139
MAX_CTX_LEN = 1024
140
BS = 10
141
cache_size = 640
141
- block_size = 32
142
max_block_per_request = 64
143
query_lens = [random.randint(16, MAX_SEQ_LEN) for _ in range(BS)]
144
# ensure one sequence in batch is a decode
@@ -333,6 +333,7 @@ def test_contexted_kv_attention_alibi(
333
kv_cache_dtype: str,
334
device: str,
335
op: Callable,
336
+ block_size: int = 32,
337
) -> None:
338
if "fp8" in kv_cache_dtype and not current_platform.has_device_capability(89):
339
pytest.skip(
@@ -385,7 +386,6 @@ def test_contexted_kv_attention_alibi(
386
MAX_CTX_LEN = 1024
387
BS = 10
388
cache_size = 640
388
- block_size = 32
389
max_block_per_request = 64
390
query_lens = [random.randint(16, MAX_SEQ_LEN) for _ in range(BS)]
391
ctx_lens = [random.randint(16, MAX_CTX_LEN) for _ in range(BS)]
@@ -637,3 +637,34 @@ def test_contexted_kv_attention_alibi_f32(
637
test_contexted_kv_attention_alibi(
638
num_heads, num_queries_per_kv, head_size, dtype, kv_cache_dtype, device, op
639
)
640
+
641
+
642
+@pytest.mark.parametrize("head_size", [128])
643
+@pytest.mark.parametrize("dtype", DTYPES)
644
+@pytest.mark.parametrize("device", CUDA_DEVICES)
645
+@pytest.mark.parametrize("op", OPS)
646
+@torch.inference_mode()
647
+def test_qwen3_nonstandard_block_size(
648
+ head_size: int,
649
+ dtype: torch.dtype,
650
+ device: str,
651
+ op: Callable,
652
+) -> None:
653
+ """
654
+ A separate test function specifically added
655
+ for Qwen3-Next-80B (Block Size 544).
656
+ """
657
+ if not current_platform.is_rocm():
658
+ pytest.skip("544 block size optimization is only for ROCm.")
659
+
660
+ test_contexted_kv_attention(
661
+ num_heads=64,
662
+ num_queries_per_kv=1,
663
+ head_size=head_size,
664
+ block_size=544,
665
+ sliding_window=0,
666
+ dtype=dtype,
667
+ kv_cache_dtype="auto",
668
+ device=device,
669
+ op=op,
670
+ )
vllm/attention/ops/chunked_prefill_paged_decode.py
+83
-28
@@ -46,6 +46,7 @@ def kernel_paged_attention_2d(
46
output_stride_0: tl.int64, # int
47
output_stride_1: tl.int64, # int, should be equal to head_size
48
BLOCK_SIZE: tl.constexpr, # int
49
+ PHYSICAL_BLOCK_SIZE: tl.constexpr, # int
50
HEAD_SIZE: tl.constexpr, # int
51
HEAD_SIZE_PADDED: tl.constexpr, # int, must be power of 2
52
USE_ALIBI_SLOPES: tl.constexpr, # bool
@@ -104,14 +105,15 @@ def kernel_paged_attention_2d(
105
106
if not USE_SINKS:
107
M = tl.full([num_queries_per_kv_padded], float("-inf"), dtype=tl.float32)
108
+ L = tl.zeros([num_queries_per_kv_padded], dtype=tl.float32)
109
else:
110
M = tl.load(
111
sink_ptr + query_head_idx,
112
mask=head_mask,
113
other=float("-inf"),
114
).to(dtype=tl.float32)
115
+ L = tl.where(float("-inf") < M, 1.0, 0.0)
116
114
- L = tl.full([num_queries_per_kv_padded], 1.0, dtype=tl.float32)
117
acc = tl.zeros([num_queries_per_kv_padded, HEAD_SIZE_PADDED], dtype=tl.float32)
118
119
# sequence len for this particular sequence
@@ -125,30 +127,45 @@ def kernel_paged_attention_2d(
127
128
num_blocks = cdiv_fn(seq_len, BLOCK_SIZE)
129
130
+ offs_n = tl.arange(0, BLOCK_SIZE)
131
+ offs_d = tl.arange(0, HEAD_SIZE_PADDED)
132
# iterate through tiles
133
for j in range(0, num_blocks):
130
- physical_block_idx = tl.load(block_tables_ptr + block_table_offset + j)
131
-
132
- offs_n = tl.arange(0, BLOCK_SIZE)
133
- offs_d = tl.arange(0, HEAD_SIZE_PADDED)
134
-
135
- v_offset = (
136
- physical_block_idx * stride_v_cache_0
137
- + kv_head_idx * stride_v_cache_1
138
- + offs_d[None, :] * stride_v_cache_2
139
- + offs_n[:, None] * stride_v_cache_3
140
- )
141
-
134
+ start_n = j * BLOCK_SIZE
135
+ # Calculate the logical location within a non-standard physical block,
136
+ # such as 544 in Qwen/Qwen3-Next-80B-A3B-Thinking.
137
+ # Supports non-contiguous mapping
138
+ # from logical blocks to physical blocks
139
+ abs_token_idx = start_n + offs_n
140
+ l_block_idx = abs_token_idx // PHYSICAL_BLOCK_SIZE
141
+ # Vectorized loading of physical block IDs
142
+ p_block_idx = tl.load(block_tables_ptr + block_table_offset + l_block_idx)
143
+ internal_offsets = abs_token_idx % PHYSICAL_BLOCK_SIZE
144
+
145
+ # 5D addressing logic of K
146
k_offset = (
143
- physical_block_idx * stride_k_cache_0
147
+ p_block_idx[None, :] * stride_k_cache_0
148
+ kv_head_idx * stride_k_cache_1
149
+ (offs_d[:, None] // x) * stride_k_cache_2
146
- + offs_n[None, :] * stride_k_cache_3
150
+ + internal_offsets[None, :] * stride_k_cache_3
151
+ (offs_d[:, None] % x) * stride_k_cache_4
152
)
153
154
+ # 4D addressing logic of V (Slot is innermost)
155
+ v_offset = (
156
+ p_block_idx[:, None] * stride_v_cache_0
157
+ + kv_head_idx * stride_v_cache_1
158
+ + offs_d[None, :] * stride_v_cache_2
159
+ + internal_offsets[:, None] * stride_v_cache_3
160
+ )
161
+
162
# K : (HEAD_SIZE, BLOCK_SIZE)
151
- K_load = tl.load(key_cache_ptr + k_offset, mask=dim_mask[:, None], other=0.0)
163
+ K_load = tl.load(
164
+ key_cache_ptr + k_offset,
165
+ mask=dim_mask[:, None],
166
+ other=0.0,
167
+ eviction_policy="evict_last",
168
+ )
169
170
if K_load.dtype.is_fp8():
171
K = (K_load.to(tl.float32) * tl.load(k_scale)).to(Q.dtype)
@@ -156,7 +173,12 @@ def kernel_paged_attention_2d(
173
K = K_load
174
175
# V : (BLOCK_SIZE, HEAD_SIZE)
159
- V_load = tl.load(value_cache_ptr + v_offset, mask=dim_mask[None, :], other=0.0)
176
+ V_load = tl.load(
177
+ value_cache_ptr + v_offset,
178
+ mask=dim_mask[None, :],
179
+ other=0.0,
180
+ eviction_policy="evict_last",
181
+ )
182
183
if V_load.dtype.is_fp8():
184
V = (V_load.to(tl.float32) * tl.load(v_scale)).to(Q.dtype)
@@ -167,9 +189,9 @@ def kernel_paged_attention_2d(
189
boundary = tl.full([BLOCK_SIZE], seq_len, dtype=tl.int32)
190
seq_mask = seq_offset[None, :] < boundary
191
170
- # S : (num_queries_per_kv, BLOCK_SIZE,)
171
- S = tl.where(head_mask[:, None] & seq_mask, 0.0, float("-inf")).to(tl.float32)
172
- S += scale * tl.dot(Q, K)
192
+ # First calculate the dot, then apply the mask.
193
+ qk = scale * tl.dot(Q, K)
194
+ S = tl.where(head_mask[:, None] & seq_mask, qk, float("-inf"))
195
196
context_len = seq_len - 1
197
@@ -184,13 +206,15 @@ def kernel_paged_attention_2d(
206
m_j = tl.maximum(M, tl.max(S, axis=1))
207
208
# P : (num_queries_per_kv, BLOCK_SIZE,)
187
- P = tl.exp(S - m_j[:, None])
209
+ p = tl.exp(S - m_j[:, None])
210
+ p = tl.where(m_j[:, None] == float("-inf"), 0.0, p)
211
212
# l_j : (num_queries_per_kv,)
190
- l_j = tl.sum(P, axis=1)
213
+ l_j = tl.sum(p, axis=1)
214
215
# alpha : (num_queries_per_kv, )
216
alpha = tl.exp(M - m_j)
217
+ alpha = tl.where(float("-inf") == M, 0.0, alpha)
218
219
# acc : (num_queries_per_kv, BLOCK_SIZE,)
220
acc = acc * alpha[:, None]
@@ -200,10 +224,10 @@ def kernel_paged_attention_2d(
224
M = m_j
225
226
# acc : (num_queries_per_kv, BLOCK_SIZE,)
203
- acc += tl.dot(P.to(V.dtype), V)
227
+ acc += tl.dot(p.to(V.dtype), V)
228
229
# epilogue
206
- acc = acc / L[:, None]
230
+ acc = acc / (L[:, None] + 1e-10)
231
if USE_FP8:
232
acc = acc * tl.load(out_scale_inv)
233
acc = tl.clamp(acc, FP8_MIN, FP8_MAX)
@@ -241,9 +265,10 @@ def chunked_prefill_paged_decode(
265
output_scale=None,
266
# Optional tensor for sinks
267
sinks=None,
268
+ is_block_table_ptr: bool = False,
269
):
270
if sm_scale is None:
246
- sm_scale = 1.0 / (query.shape[1] ** 0.5)
271
+ sm_scale = 1.0 / (query.shape[2] ** 0.5)
272
273
use_alibi_slopes = alibi_slopes is not None
274
@@ -315,6 +340,16 @@ def chunked_prefill_paged_decode(
340
alibi_slopes,
341
sinks,
342
)
343
+ # Triton is only forced when encountering a non-standard block
344
+ # like Qwen3 with a size of 544.
345
+ # 1. Check if block_size is a power of 2 (16, 32, 64...)
346
+ # 2. If it's a power of 2, we trust the vLLM's native use_custom decision.
347
+ # 3. If it's not a power of 2 (such as Qwen3's 544),
348
+ # then our Triton path is forced.
349
+ is_pow2 = block_size > 0 and (block_size & (block_size - 1) == 0)
350
+ if not is_pow2:
351
+ use_custom = False
352
+
353
if use_custom:
354
_PARTITION_SIZE_ROCM = 256
355
max_num_partitions = (
@@ -356,6 +391,25 @@ def chunked_prefill_paged_decode(
391
fp8_out_scale=output_scale,
392
)
393
else:
394
+ real_block_size = value_cache.shape[3]
395
+ # The standard model directly uses the original block_size.
396
+ # Non-standard 544 uses 32 to accommodate integer division logic.
397
+ TRITON_BLOCK_SIZE = block_size if is_pow2 else 32
398
+ if is_block_table_ptr:
399
+ # Using the physical base address of tensors
400
+ kv_element_size = key_cache.element_size()
401
+ block_byte_stride = key_cache.stride(0) * kv_element_size
402
+ # Get the starting physical address of the KV Cache
403
+ base_addr = key_cache.data_ptr()
404
+
405
+ # Normalization: Directly calculate the block offset
406
+ # of the pointer relative to the base address
407
+ processed_block_table = ((block_table - base_addr) // block_byte_stride).to(
408
+ torch.int32
409
+ )
410
+ else:
411
+ processed_block_table = block_table.to(torch.int32)
412
+
413
kernel_paged_attention_2d[
414
(
415
num_seqs,
@@ -367,7 +421,7 @@ def chunked_prefill_paged_decode(
421
key_cache_ptr=key_cache,
422
value_cache_ptr=value_cache,
423
sink_ptr=sinks,
370
- block_tables_ptr=block_table,
424
+ block_tables_ptr=processed_block_table,
425
seq_lens_ptr=seq_lens,
426
alibi_slopes_ptr=alibi_slopes,
427
scale=sm_scale,
@@ -377,12 +431,13 @@ def chunked_prefill_paged_decode(
431
num_query_heads=num_query_heads,
432
num_queries_per_kv=num_queries_per_kv,
433
num_queries_per_kv_padded=num_queries_per_kv_padded,
380
- block_table_stride=block_table.stride(0),
434
+ block_table_stride=processed_block_table.stride(0),
435
query_stride_0=query.stride(0),
436
query_stride_1=query.stride(1),
437
output_stride_0=output.stride(0),
438
output_stride_1=output.stride(1),
385
- BLOCK_SIZE=block_size,
439
+ BLOCK_SIZE=TRITON_BLOCK_SIZE,
440
+ PHYSICAL_BLOCK_SIZE=real_block_size,
441
HEAD_SIZE=head_size,
442
HEAD_SIZE_PADDED=triton.next_power_of_2(head_size),
443
USE_ALIBI_SLOPES=use_alibi_slopes,
vllm/attention/ops/prefix_prefill.py
+80
-32
@@ -79,6 +79,7 @@ def _fwd_kernel(
79
BLOCK_DMODEL: tl.constexpr,
80
BLOCK_DMODEL_PADDED: tl.constexpr,
81
BLOCK_SIZE: tl.constexpr,
82
+ PHYSICAL_BLOCK_SIZE: tl.constexpr,
83
BLOCK_N: tl.constexpr,
84
SLIDING_WINDOW: tl.constexpr,
85
num_unroll_cache: tl.constexpr,
@@ -139,42 +140,52 @@ def _fwd_kernel(
140
# initialize pointer to m and l
141
if not USE_SINKS:
142
m_i = tl.full([BLOCK_M], float("-inf"), dtype=tl.float32)
143
+ l_i = tl.zeros([BLOCK_M], dtype=tl.float32)
144
else:
145
m_i = tl.load(
146
sink_ptr + tl.full([BLOCK_M], cur_head, dtype=tl.int64),
147
mask=(offs_m < cur_batch_query_len),
148
other=float("-inf"),
149
).to(dtype=tl.float32)
150
+ l_i = tl.where(m_i > float("-inf"), 1.0, 0.0)
151
149
- l_i = tl.full([BLOCK_M], 1.0, dtype=tl.float32)
152
acc = tl.zeros([BLOCK_M, BLOCK_DMODEL_PADDED], dtype=tl.float32) # [M,D]
153
154
# compute query against context (no causal mask here)
155
for start_n in tl.range(
156
0, cur_batch_ctx_len, BLOCK_SIZE, loop_unroll_factor=num_unroll_cache
157
):
156
- start_n = tl.multiple_of(start_n, BLOCK_SIZE)
157
- # -- compute qk ----
158
+ # Under a block size of 544 (Qwen/Qwen3-Next-80B-A3B-Thinking),
159
+ # replace one physical block every 17 32-Tile blocks
160
+ # Calculate the logical block index of each of the 32 tokens
161
+ # in the current Tile (handling cross-block cases).
162
+ token_indices = start_n + offs_bs_n
163
+ bn_logical_indices = token_indices // PHYSICAL_BLOCK_SIZE
164
+
165
+ # 2. Vectorized loading of physical block IDs from B_Loc
166
bn = tl.load(
159
- B_Loc
160
- + cur_batch * stride_b_loc_b
161
- + (start_n // BLOCK_SIZE) * stride_b_loc_s
167
+ B_Loc + cur_batch * stride_b_loc_b + bn_logical_indices * stride_b_loc_s
168
).to(tl.int64)
163
- # [D,BLOCK_SIZE]
169
+
170
+ # 3. Calculate the exact offset of
171
+ # each token within its physical block.
172
+ internal_offsets = token_indices % PHYSICAL_BLOCK_SIZE
173
+
174
+ # Addressing of K (5D)
175
off_k = (
176
bn[None, :] * stride_k_cache_bs
177
+ cur_kv_head * stride_k_cache_h
178
+ (offs_d[:, None] // x) * stride_k_cache_d
168
- + ((start_n + offs_bs_n[None, :]) % BLOCK_SIZE) * stride_k_cache_bl
179
+ + internal_offsets[None, :] * stride_k_cache_bl
180
+ (offs_d[:, None] % x) * stride_k_cache_x
181
)
182
172
- # [BLOCK_SIZE,D]
183
+ # Addressing of V (4D)
184
off_v = (
185
bn[:, None] * stride_v_cache_bs
186
+ cur_kv_head * stride_v_cache_h
187
+ offs_d[None, :] * stride_v_cache_d
177
- + offs_bs_n[:, None] * stride_v_cache_bl
188
+ + internal_offsets[:, None] * stride_v_cache_bl
189
)
190
191
if (
@@ -195,12 +206,12 @@ def _fwd_kernel(
206
else:
207
k = k_load
208
198
- qk = tl.zeros([BLOCK_M, BLOCK_SIZE], dtype=tl.float32) # [M,N]
199
- qk = tl.dot(q, k, acc=qk, input_precision=IN_PRECISION)
209
+ # qk = tl.zeros([BLOCK_M, BLOCK_SIZE], dtype=tl.float32) # [M,N]
210
+ qk = sm_scale * tl.dot(q, k, input_precision=IN_PRECISION)
211
qk = tl.where(
212
(start_n + offs_bs_n[None, :]) < cur_batch_ctx_len, qk, float("-inf")
213
)
203
- qk *= sm_scale
214
+ # qk *= sm_scale
215
if SLIDING_WINDOW > 0:
216
# (cur_batch_ctx_len + offs_m[:, None]) are the positions of
217
# Q entries in sequence
@@ -217,14 +228,16 @@ def _fwd_kernel(
228
(cur_batch_ctx_len + offs_m[:, None]) - (start_n + offs_bs_n[None, :])
229
< SLIDING_WINDOW,
230
qk,
220
- -10000,
231
+ float("-inf"),
232
)
233
234
# compute running maximum
235
m_ij = tl.maximum(m_i, tl.max(qk, axis=1))
236
p = tl.exp(qk - m_ij[:, None])
237
+ p = tl.where(m_ij[:, None] == float("-inf"), 0.0, p)
238
l_ij = tl.sum(p, axis=1)
239
alpha = tl.exp(m_i - m_ij)
240
+ alpha = tl.where(m_i == float("-inf"), 0.0, alpha)
241
acc = acc * alpha[:, None]
242
243
# update acc
@@ -293,14 +306,17 @@ def _fwd_kernel(
306
qk = tl.where(
307
offs_m[:, None] - (start_n + offs_n[None, :]) < SLIDING_WINDOW,
308
qk,
296
- -10000,
309
+ float("-inf"),
310
)
311
312
# compute running maximum
313
m_ij = tl.maximum(m_i, tl.max(qk, axis=1))
314
p = tl.exp(qk - m_ij[:, None])
315
+ p = tl.where(m_ij[:, None] == float("-inf"), 0.0, p)
316
l_ij = tl.sum(p, axis=1)
317
alpha = tl.exp(m_i - m_ij)
318
+ # To prevent NaN from appearing in the first round
319
+ alpha = tl.where(m_i == float("-inf"), 0.0, alpha)
320
acc = acc * alpha[:, None]
321
322
# update acc
@@ -317,7 +333,7 @@ def _fwd_kernel(
333
l_i = l_i * alpha + l_ij
334
m_i = m_ij
335
320
- acc = acc / l_i[:, None]
336
+ acc = acc / (l_i[:, None] + 1e-10)
337
338
# initialize pointers to output
339
off_o = (
@@ -637,6 +653,7 @@ def context_attention_fwd(
653
skip_decode=False,
654
fp8_out_scale=None,
655
sinks=None,
656
+ is_block_table_ptr: bool = False,
657
):
658
q_dtype_is_f32 = q.dtype is torch.float32
659
@@ -689,6 +706,19 @@ def context_attention_fwd(
706
if sliding_window is None or sliding_window <= 0:
707
sliding_window = 0
708
709
+ if is_block_table_ptr:
710
+ kv_element_size = k_cache.element_size()
711
+ block_byte_stride = k_cache.stride(0) * kv_element_size
712
+ # The physical starting point of the obtained KV Cache Pool
713
+ base_addr = k_cache.data_ptr()
714
+
715
+ mask = b_loc > 0
716
+ processed_b_loc = torch.where(
717
+ mask, (b_loc - base_addr) // block_byte_stride, b_loc
718
+ ).to(torch.int32)
719
+ else:
720
+ processed_b_loc = b_loc.to(torch.int32)
721
+
722
if alibi_slopes is not None:
723
assert sinks is None, "Sinks arg is not supported with alibi"
724
assert fp8_out_scale is None, "FP8 output not supported with alibi"
@@ -752,7 +782,24 @@ def context_attention_fwd(
782
max_seq_len = 0 if max_seq_len is None else max_seq_len
783
extra_kargs = {}
784
if current_platform.is_rocm():
755
- extra_kargs = {"kpack": 1, "waves_per_eu": 2}
785
+ extra_kargs = {}
786
+
787
+ real_block_size = v_cache.shape[3]
788
+ is_pow2 = real_block_size > 0 and (real_block_size & (real_block_size - 1) == 0)
789
+ # For standard models involving powers of 2,
790
+ # follow the original logic (Llama 128/64)
791
+ # For non-standard models (Qwen3-next block_size 544), set to 32.
792
+ if is_pow2:
793
+ BLOCK_M = 128
794
+ BLOCK_N = 64
795
+ else:
796
+ BLOCK_M = 32
797
+ BLOCK_N = 32
798
+
799
+ # TRITON_BLOCK_SIZE is kept at 32 to ensure
800
+ # correct alignment logic when the kernel handles
801
+ # non-standard sizes (such as 544).
802
+ TRITON_BLOCK_SIZE = 32
803
804
grid_fn = lambda META: (batch, head, triton.cdiv(max_input_len, META["BLOCK_M"]))
805
_fwd_kernel[grid_fn](
@@ -762,7 +809,7 @@ def context_attention_fwd(
809
k_cache,
810
v_cache,
811
sinks,
765
- b_loc,
812
+ processed_b_loc,
813
sm_scale,
814
k_scale,
815
v_scale,
@@ -771,8 +818,8 @@ def context_attention_fwd(
818
b_seq_len,
819
k_cache.shape[4],
820
o,
774
- b_loc.stride(0),
775
- b_loc.stride(1),
821
+ processed_b_loc.stride(0),
822
+ processed_b_loc.stride(1),
823
q.stride(0),
824
q.stride(1),
825
q.stride(2),
@@ -785,16 +832,17 @@ def context_attention_fwd(
832
o.stride(0),
833
o.stride(1),
834
o.stride(2),
788
- k_cache.stride(0),
789
- k_cache.stride(1),
790
- k_cache.stride(2),
791
- k_cache.stride(3),
792
- k_cache.stride(4), # [num_blocks, num_kv_heads, head_size/x, block_size, x]
793
- v_cache.stride(0),
794
- v_cache.stride(1),
795
- v_cache.stride(2),
796
- v_cache.stride(3), # [num_blocks, num_kv_heads, head_size, block_size]
797
- BLOCK_SIZE=v_cache.shape[3],
835
+ stride_k_cache_bs=k_cache.stride(0),
836
+ stride_k_cache_h=k_cache.stride(1),
837
+ stride_k_cache_d=k_cache.stride(2),
838
+ stride_k_cache_bl=k_cache.stride(3),
839
+ stride_k_cache_x=k_cache.stride(4),
840
+ stride_v_cache_bs=v_cache.stride(0),
841
+ stride_v_cache_h=v_cache.stride(1),
842
+ stride_v_cache_d=v_cache.stride(2),
843
+ stride_v_cache_bl=v_cache.stride(3),
844
+ BLOCK_SIZE=TRITON_BLOCK_SIZE,
845
+ PHYSICAL_BLOCK_SIZE=real_block_size,
846
num_queries_per_kv=num_queries_per_kv,
847
IN_PRECISION=IN_PRECISION,
848
BLOCK_DMODEL=Lk,
@@ -802,8 +850,8 @@ def context_attention_fwd(
850
SLIDING_WINDOW=sliding_window,
851
SKIP_DECODE=skip_decode,
852
USE_FP8=fp8_out_scale is not None,
805
- BLOCK_M=128,
806
- BLOCK_N=64,
853
+ BLOCK_M=BLOCK_M,
854
+ BLOCK_N=BLOCK_N,
855
num_unroll_cache=4,
856
num_unroll_request=1,
857
num_warps=4,
vllm/attention/ops/triton_reshape_and_cache_flash.py
+52
-12
@@ -20,10 +20,15 @@ def reshape_and_cache_kernel_flash(
20
key_stride: tl.int64,
21
value_stride: tl.int64,
22
block_stride: tl.int64,
23
+ head_stride: tl.int64,
24
+ dim_stride_k: tl.int64,
25
+ dim_stride_v: tl.int64,
26
page_stride: tl.int64,
27
num_heads: tl.constexpr,
28
head_size: tl.constexpr,
29
block_size: tl.constexpr,
30
+ x: tl.constexpr,
31
+ USE_HEAD_MAJOR_LAYOUT: tl.constexpr,
32
# FP8 flags
33
FP8_KV_CACHE: tl.constexpr,
34
# tune parameters
@@ -35,17 +40,38 @@ def reshape_and_cache_kernel_flash(
40
# Padding token that should be ignored.
41
return
42
38
- tile_i = tl.program_id(axis=1)
39
- tile_offs = tl.arange(0, TILE_SIZE)
40
- tile_pos = tile_i * TILE_SIZE + tile_offs
41
-
43
block_idx = slot_idx // block_size
44
block_offset = slot_idx % block_size
45
46
+ tile_i = tl.program_id(axis=1)
47
+ tile_offs = tl.arange(0, TILE_SIZE)
48
+ tile_pos = tile_i * TILE_SIZE + tile_offs
49
src_key_idx = token_idx * key_stride
50
src_value_idx = token_idx * value_stride
51
48
- tgt_idx = block_idx * block_stride + block_offset * page_stride
52
+ if USE_HEAD_MAJOR_LAYOUT:
53
+ # Decompose the tile index back into head and dim coordinates.
54
+ cur_head = tile_pos // head_size
55
+ cur_dim = tile_pos % head_size
56
+ # Value addressing (4D): [Block, Head, Dim, Slot]
57
+ tgt_idx_v = (
58
+ block_idx * block_stride
59
+ + cur_head * head_stride
60
+ + cur_dim * dim_stride_v
61
+ + block_offset * 1
62
+ )
63
+ # Key addressing (5D): [Block, Head, Dim//8, Slot, 8]
64
+ tgt_idx_k = (
65
+ block_idx * block_stride
66
+ + cur_head * head_stride
67
+ + (cur_dim // x) * dim_stride_k
68
+ + block_offset * x
69
+ + (cur_dim % x)
70
+ )
71
+ else:
72
+ tgt_base = block_idx * block_stride + block_offset * page_stride
73
+ tgt_idx_k = tgt_base + tile_pos
74
+ tgt_idx_v = tgt_base + tile_pos
75
76
# [TILE_SIZE]
77
key_load = tl.load(
@@ -73,12 +99,12 @@ def reshape_and_cache_kernel_flash(
99
value_tile = value_load
100
101
tl.store(
76
- key_cache_ptr + tgt_idx + tile_pos,
102
+ key_cache_ptr + tgt_idx_k,
103
key_tile,
104
mask=tile_pos < (num_heads * head_size),
105
)
106
tl.store(
81
- value_cache_ptr + tgt_idx + tile_pos,
107
+ value_cache_ptr + tgt_idx_v,
108
value_tile,
109
mask=tile_pos < (num_heads * head_size),
110
)
@@ -99,17 +125,26 @@ def triton_reshape_and_cache_flash(
125
):
126
num_heads = key.shape[1]
127
head_size = key.shape[2]
102
- block_size = key_cache.shape[1]
103
- n = num_heads * head_size
128
129
+ use_head_major_layout = key_cache.ndim == 5
130
+ if use_head_major_layout:
131
+ block_size = key_cache.shape[3]
132
+ x = key_cache.shape[4]
133
+ head_stride = key_cache.stride(1)
134
+ dim_stride_k = key_cache.stride(2)
135
+ dim_stride_v = value_cache.stride(2)
136
+ else:
137
+ block_size = key_cache.shape[1]
138
+ x = 1
139
+ dim_stride_k = 0
140
+ dim_stride_v = 0
141
+ head_stride = key_cache.stride()[2]
142
+ n = num_heads * head_size
143
key_stride = key.stride()[0]
144
value_stride = value.stride()[0]
145
block_stride = key_cache.stride()[0]
146
page_stride = key_cache.stride()[1]
147
110
- head_stride = key_cache.stride()[2]
111
- assert head_stride == head_size, "only continous heads are supported"
112
-
148
assert kv_cache_dtype == "auto" or kv_cache_dtype.startswith("fp8"), (
149
f"unsupported kv_cache_dtype (str), got {kv_cache_dtype}."
150
)
@@ -171,10 +206,15 @@ def triton_reshape_and_cache_flash(
206
key_stride=key_stride,
207
value_stride=value_stride,
208
block_stride=block_stride,
209
+ head_stride=head_stride,
210
+ dim_stride_k=dim_stride_k,
211
+ dim_stride_v=dim_stride_v,
212
page_stride=page_stride,
213
num_heads=num_heads,
214
head_size=head_size,
215
block_size=block_size,
216
+ x=x,
217
+ USE_HEAD_MAJOR_LAYOUT=use_head_major_layout,
218
# FP8 flags
219
FP8_KV_CACHE=FP8_KV_CACHE,
220
# autotune parameters
vllm/v1/attention/backends/rocm_attn.py
+35
-10
@@ -15,6 +15,9 @@ from vllm.attention.backends.abstract import (
15
)
16
from vllm.attention.ops.chunked_prefill_paged_decode import chunked_prefill_paged_decode
17
from vllm.attention.ops.paged_attn import PagedAttention
18
+from vllm.attention.ops.triton_reshape_and_cache_flash import (
19
+ triton_reshape_and_cache_flash,
20
+)
21
from vllm.config import VllmConfig
22
from vllm.logger import init_logger
23
from vllm.model_executor.layers.quantization.utils.quant_utils import (
@@ -321,16 +324,38 @@ class RocmAttentionImpl(AttentionImpl):
324
if self.kv_sharing_target_layer_name is None:
325
# Reshape the input keys and values and store them in the cache.
326
# Skip this if sharing KV cache with an earlier attention layer.
324
- PagedAttention.write_to_paged_cache(
325
- key,
326
- value,
327
- key_cache,
328
- value_cache,
329
- attn_metadata.slot_mapping,
330
- self.kv_cache_dtype,
331
- layer._k_scale,
332
- layer._v_scale,
333
- )
327
+
328
+ # Get the actual block_size from value_cache
329
+ # value_cache shape: [num_blocks, num_heads, head_size, block_size]
330
+ block_size = value_cache.shape[3]
331
+ # Determine if it is a power of 2
332
+ is_pow2 = block_size > 0 and (block_size & (block_size - 1) == 0)
333
+
334
+ if is_pow2:
335
+ # Normal 16, 32, 64, etc., use vLLM native HIP C++ logic
336
+ PagedAttention.write_to_paged_cache(
337
+ key,
338
+ value,
339
+ key_cache,
340
+ value_cache,
341
+ attn_metadata.slot_mapping,
342
+ self.kv_cache_dtype,
343
+ layer._k_scale,
344
+ layer._v_scale,
345
+ )
346
+ else:
347
+ # Case B: Non-standard blocks (e.g., 544 in Qwen3),
348
+ # force using our modified Triton logic
349
+ triton_reshape_and_cache_flash(
350
+ key,
351
+ value,
352
+ key_cache,
353
+ value_cache,
354
+ attn_metadata.slot_mapping,
355
+ self.kv_cache_dtype,
356
+ layer._k_scale,
357
+ layer._v_scale,
358
+ )
359
360
if self.kv_cache_dtype.startswith("fp8"):
361
key_cache = key_cache.view(self.fp8_dtype)