@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 8c363ed666
Fork 0

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 12, 07:15 UTC

[ROCm][Attention] Sliding window support for `AiterFlashAttentionBackend` (#29234)

Signed-off-by: ganyi <ygan@amd.com>

Pleaplusone committed Nov 30, 2025 at 19:31 UTC 8c363ed6663f69b97c9f34b0be0091d8135f958c
1 file changed +224 -49
vllm/v1/attention/backends/rocm_aiter_fa.py
+224 -49
@@ -13,8 +13,9 @@ from vllm.attention.backends.abstract import (
13 AttentionType,
14 MultipleOf,
15 )
16 +from vllm.attention.layer import Attention
17 from vllm.attention.ops.merge_attn_states import merge_attn_states
17 -from vllm.config import VllmConfig
18 +from vllm.config import VllmConfig, get_layers_from_vllm_config
19 from vllm.logger import init_logger
20 from vllm.platforms import current_platform
21 from vllm.utils.math_utils import cdiv
@@ -57,58 +58,55 @@ if current_platform.is_rocm():
58 head_size,
59 x,
60 max_block_num,
60 - num_tokens,
61 - num_programs,
61 DEQUANT: tl.constexpr,
62 PAGE_SIZE: tl.constexpr,
63 CACHE_FORMAT: tl.constexpr,
64 BLOCK_SIZE: tl.constexpr,
65 ):
67 - bid = tl.program_id(0)
66 + token_id = tl.program_id(0)
67 col_offsets = tl.arange(0, BLOCK_SIZE)
68 if DEQUANT:
69 k_scale = tl.load(k_scale_ptr)
70 v_scale = tl.load(v_scale_ptr)
71
73 - for token_id in tl.range(bid, num_tokens, num_programs):
74 - key_ptr_offset = key_ptr + token_id * head_size * num_heads
75 - value_ptr_offset = value_ptr + token_id * head_size * num_heads
76 - batch_idx = tl.load(token_to_batch_ptr + token_id)
77 - batch_start = tl.load(seq_start_ptr + batch_idx)
78 - token_start = tl.load(cu_seqlens_kv_ptr + batch_idx)
79 - batch_offset = token_id - token_start + batch_start
80 - block_offset = batch_offset // PAGE_SIZE
81 - block_id = tl.load(
82 - block_table_ptr + max_block_num * batch_idx + block_offset
72 + key_ptr_offset = key_ptr + token_id * head_size * num_heads
73 + value_ptr_offset = value_ptr + token_id * head_size * num_heads
74 + batch_idx = tl.load(token_to_batch_ptr + token_id)
75 + batch_start = tl.load(seq_start_ptr + batch_idx)
76 + token_start = tl.load(cu_seqlens_kv_ptr + batch_idx)
77 + batch_offset = token_id - token_start + batch_start
78 + block_offset = batch_offset // PAGE_SIZE
79 + block_id = tl.load(
80 + block_table_ptr + max_block_num * batch_idx + block_offset
81 + ).to(tl.int64)
82 + slot_id = batch_offset % PAGE_SIZE
83 +
84 + if CACHE_FORMAT == "NHD":
85 + # for kv cache layout as
86 + # K: [num_blocks, page_size, num_head, head_dim]
87 + # V: [num_blocks, page_size, num_head, head_dim]
88 + key_cache_ptr_offset = (
89 + key_cache_ptr
90 + + block_id * num_heads * head_size * PAGE_SIZE
91 + + slot_id * num_heads * head_size
92 + )
93 + value_cache_ptr_offset = (
94 + value_cache_ptr
95 + + block_id * num_heads * head_size * PAGE_SIZE
96 + + slot_id * num_heads * head_size
97 )
84 - slot_id = batch_offset % PAGE_SIZE
85 -
86 - if CACHE_FORMAT == "NHD":
87 - # for kv cache layout as
88 - # K: [num_blocks, page_size, num_head, head_dim]
89 - # V: [num_blocks, page_size, num_head, head_dim]
90 - key_cache_ptr_offset = (
91 - key_cache_ptr
92 - + block_id * num_heads * head_size * PAGE_SIZE
93 - + slot_id * num_heads * head_size
94 - )
95 - value_cache_ptr_offset = (
96 - value_cache_ptr
97 - + block_id * num_heads * head_size * PAGE_SIZE
98 - + slot_id * num_heads * head_size
99 - )
98
101 - for i in tl.range(0, head_size * num_heads, BLOCK_SIZE):
102 - mask = (col_offsets + i) < head_size * num_heads
103 - k_reg = tl.load(key_cache_ptr_offset + col_offsets + i, mask=mask)
104 - v_reg = tl.load(value_cache_ptr_offset + col_offsets + i, mask=mask)
105 - if DEQUANT:
106 - k_dtype = k_reg.dtype
107 - v_dtype = v_reg.dtype
108 - k_reg = (k_reg.to(tl.float32) * k_scale).to(k_dtype)
109 - v_reg = (v_reg.to(tl.float32) * v_scale).to(v_dtype)
110 - tl.store(key_ptr_offset + col_offsets + i, k_reg, mask=mask)
111 - tl.store(value_ptr_offset + col_offsets + i, v_reg, mask=mask)
99 + for i in tl.range(0, head_size * num_heads, BLOCK_SIZE):
100 + mask = (col_offsets + i) < head_size * num_heads
101 + k_reg = tl.load(key_cache_ptr_offset + col_offsets + i, mask=mask)
102 + v_reg = tl.load(value_cache_ptr_offset + col_offsets + i, mask=mask)
103 + if DEQUANT:
104 + k_dtype = k_reg.dtype
105 + v_dtype = v_reg.dtype
106 + k_reg = (k_reg.to(tl.float32) * k_scale).to(k_dtype)
107 + v_reg = (v_reg.to(tl.float32) * v_scale).to(v_dtype)
108 + tl.store(key_ptr_offset + col_offsets + i, k_reg, mask=mask)
109 + tl.store(value_ptr_offset + col_offsets + i, v_reg, mask=mask)
110
111 def cp_mha_gather_cache(
112 key_cache: torch.Tensor,
@@ -143,9 +141,7 @@ if current_platform.is_rocm():
141 page_size = key_cache.shape[1]
142 num_heads = key_cache.shape[2]
143
146 - NUM_PRGMS = num_programs(total_tokens)
147 - BLOCK_SIZE = block_size(key_cache, head_dim)
148 - grid = lambda meta: (NUM_PRGMS,)
144 + grid = lambda meta: (total_tokens,)
145 cp_mha_gather_cache_kernel[grid](
146 key_cache,
147 value_cache,
@@ -161,12 +157,10 @@ if current_platform.is_rocm():
157 head_dim,
158 x,
159 block_tables.size(1),
164 - total_tokens,
165 - NUM_PRGMS,
160 DEQUANT=dequant,
161 PAGE_SIZE=page_size,
162 CACHE_FORMAT=kv_cache_layout,
169 - BLOCK_SIZE=BLOCK_SIZE,
163 + BLOCK_SIZE=head_dim,
164 )
165
166
@@ -189,6 +183,17 @@ class AiterFlashAttentionPrefillMetadata:
183 query_start_loc: torch.Tensor
184
185
186 +@dataclass
187 +class AiterChunkSlidingWindowMetadata:
188 + swa_seqlens: torch.Tensor
189 + swa_cu_seqlens: torch.Tensor
190 + swa_seq_starts: torch.Tensor
191 + swa_token_to_batch: torch.Tensor
192 + swa_max_seqlens: int
193 + swa_total_tokens: int
194 + swa_workspace: torch.Tensor
195 +
196 +
197 @dataclass
198 class AiterChunkContextMetadata:
199 workspace: torch.Tensor
@@ -200,6 +205,7 @@ class AiterChunkContextMetadata:
205 seq_lens: torch.Tensor
206 num_chunks: int
207 total_token_per_batch: list[int]
208 + swa_metadata: AiterChunkSlidingWindowMetadata | None
209
210
211 @dataclass
@@ -278,6 +284,20 @@ class AiterFlashAttentionMetadataBuilder(
284 self.aot_sliding_window: tuple[int, int] | None = None
285 self.total_tokens: int = 0
286
287 + sliding_window_configs: set[tuple[int, int] | None] = set()
288 + layers = get_layers_from_vllm_config(self.vllm_config, Attention)
289 + for layer in layers.values():
290 + assert isinstance(layer.impl, AiterFlashAttentionImpl)
291 + sliding_window_configs.add(layer.impl.sliding_window)
292 +
293 + while len(sliding_window_configs) > 0:
294 + sliding_window_config = sliding_window_configs.pop()
295 + if sliding_window_config is not None and sliding_window_config[0] != -1:
296 + assert self.aot_sliding_window is None, (
297 + "Aiter Flash ATTENTION can only support one valid sliding window!"
298 + )
299 + self.aot_sliding_window = sliding_window_config
300 +
301 self.extend_workspace = torch.empty(
302 [2, _CP_TOKENS_PER_ITER_ROCM, self.num_heads_kv, self.headdim],
303 dtype=self.model_config.dtype,
@@ -349,6 +369,55 @@ class AiterFlashAttentionMetadataBuilder(
369 query_lens_for_extend = query_lens_cpu[num_extends_slice]
370 seq_lens_for_extend = common_attn_metadata.seq_lens_cpu[num_extends_slice]
371 computed_kv_lens = seq_lens_for_extend - query_lens_for_extend
372 + swa_metadata = None
373 + if self.aot_sliding_window is not None:
374 + swa_seqlen_for_extend = torch.minimum(
375 + seq_lens_for_extend,
376 + query_lens_for_extend + self.aot_sliding_window[0] + 1,
377 + )
378 + cu_seq_lens = torch.zeros(
379 + num_extends + 1,
380 + dtype=torch.int32,
381 + device=seq_lens_for_extend.device,
382 + )
383 + torch.cumsum(
384 + swa_seqlen_for_extend,
385 + dim=0,
386 + dtype=cu_seq_lens.dtype,
387 + out=cu_seq_lens[1:],
388 + )
389 + token_to_seq = torch.arange(
390 + 0,
391 + num_extends,
392 + dtype=torch.int32,
393 + device=seq_lens_for_extend.device,
394 + )
395 + token_to_seq = torch.repeat_interleave(
396 + token_to_seq, swa_seqlen_for_extend
397 + )
398 + fetched_shape = cu_seq_lens[-1].item()
399 + # TODO(ganyi): Maybe reuse these 2 buffer from extend_workspace
400 + swa_workspace = torch.empty(
401 + (2, fetched_shape, self.num_heads_kv, self.headdim),
402 + dtype=self.vllm_config.model_config.dtype,
403 + device=self.device,
404 + )
405 +
406 + seq_starts = seq_lens_for_extend - swa_seqlen_for_extend
407 + max_seqlen_k = swa_seqlen_for_extend.max().item()
408 + total_tokens = cu_seq_lens[-1].item()
409 +
410 + swa_metadata = AiterChunkSlidingWindowMetadata(
411 + swa_seqlens=swa_seqlen_for_extend.to(
412 + self.device, non_blocking=True
413 + ),
414 + swa_cu_seqlens=cu_seq_lens.to(self.device, non_blocking=True),
415 + swa_seq_starts=seq_starts.to(self.device, non_blocking=True),
416 + swa_token_to_batch=token_to_seq.to(self.device, non_blocking=True),
417 + swa_max_seqlens=max_seqlen_k,
418 + swa_total_tokens=total_tokens,
419 + swa_workspace=swa_workspace,
420 + )
421
422 # allocate the equal amount of workspace for
423 # each chunk prefill request
@@ -392,6 +461,7 @@ class AiterFlashAttentionMetadataBuilder(
461 token_to_batch=token_to_batch_tensor.to(self.device, non_blocking=True),
462 num_chunks=num_chunks,
463 total_token_per_batch=cu_seq_lens_cpu[:, -1].tolist(),
464 + swa_metadata=swa_metadata,
465 )
466
467 query_start_loc_device = common_attn_metadata.query_start_loc[
@@ -504,9 +574,9 @@ class AiterFlashAttentionImpl(AttentionImpl):
574 alibi_slopes = torch.tensor(alibi_slopes, dtype=torch.float32)
575 self.alibi_slopes = alibi_slopes
576 if sliding_window is None:
507 - self.sliding_window = [-1, -1]
577 + self.sliding_window = (-1, -1)
578 else:
509 - self.sliding_window = [sliding_window - 1, 0]
579 + self.sliding_window = (sliding_window - 1, 0)
580 self.kv_cache_dtype = kv_cache_dtype
581 if logits_soft_cap is None:
582 # In flash-attn, setting logits_soft_cap as 0 means no soft cap.
@@ -522,6 +592,67 @@ class AiterFlashAttentionImpl(AttentionImpl):
592 "Encoder self-attention is not implemented for FlashAttentionImpl"
593 )
594
595 + def extend_for_sliding_window(
596 + self,
597 + attn_metadata: AiterFlashAttentionMetadata,
598 + query: torch.Tensor,
599 + key_cache,
600 + value_cache,
601 + output: torch.Tensor,
602 + cu_seqlens_q: torch.Tensor,
603 + max_seqlen_q: int,
604 + block_table: torch.Tensor,
605 + k_scale: float,
606 + v_scale: float,
607 + ):
608 + assert attn_metadata.extend_metadata is not None
609 + assert attn_metadata.extend_metadata.chunk_context_metadata is not None
610 + chunked_metadata = attn_metadata.extend_metadata.chunk_context_metadata
611 + swa_metadata = chunked_metadata.swa_metadata
612 + assert swa_metadata is not None
613 + swa_cu_seqlens = swa_metadata.swa_cu_seqlens
614 + swa_seq_starts = swa_metadata.swa_seq_starts
615 + swa_token_to_batch = swa_metadata.swa_token_to_batch
616 + swa_max_seqlens = swa_metadata.swa_max_seqlens
617 + swa_total_tokens = swa_metadata.swa_total_tokens
618 + key_fetched, value_fetched = (
619 + swa_metadata.swa_workspace[0],
620 + swa_metadata.swa_workspace[1],
621 + )
622 + cp_mha_gather_cache(
623 + key_cache=key_cache,
624 + value_cache=value_cache,
625 + key=key_fetched,
626 + value=value_fetched,
627 + block_tables=block_table,
628 + k_scales=k_scale,
629 + v_scales=v_scale,
630 + cu_seqlens_kv=swa_cu_seqlens,
631 + token_to_batch=swa_token_to_batch,
632 + seq_starts=swa_seq_starts,
633 + dequant=False,
634 + kv_cache_layout="NHD",
635 + total_tokens=swa_total_tokens,
636 + )
637 +
638 + aiter.flash_attn_varlen_func(
639 + q=query,
640 + k=key_fetched,
641 + v=value_fetched,
642 + cu_seqlens_q=cu_seqlens_q,
643 + cu_seqlens_k=swa_cu_seqlens,
644 + max_seqlen_q=max_seqlen_q,
645 + max_seqlen_k=swa_max_seqlens,
646 + min_seqlen_q=1,
647 + dropout_p=0.0,
648 + softmax_scale=self.scale,
649 + causal=True,
650 + window_size=self.sliding_window,
651 + alibi_slopes=self.alibi_slopes,
652 + return_lse=False,
653 + out=output,
654 + )
655 +
656 def extend_forward(
657 self,
658 attn_metadata: AiterFlashAttentionMetadata,
@@ -540,6 +671,20 @@ class AiterFlashAttentionImpl(AttentionImpl):
671 k_scale: float,
672 v_scale: float,
673 ):
674 + if self.sliding_window[0] != -1:
675 + self.extend_for_sliding_window(
676 + attn_metadata,
677 + query,
678 + key_cache,
679 + value_cache,
680 + output,
681 + cu_seqlens_q,
682 + max_seqlen_q,
683 + block_table,
684 + k_scale,
685 + v_scale,
686 + )
687 + return
688 out, lse = aiter.flash_attn_varlen_func(
689 q=query,
690 k=key,
@@ -782,6 +927,36 @@ class AiterFlashAttentionImpl(AttentionImpl):
927 # calculate for decodes
928 if num_decodes > 0:
929 assert attn_metadata.decode_metadata is not None
930 + if self.sliding_window[0] != -1:
931 + from aiter.ops.triton.unified_attention import (
932 + unified_attention,
933 + )
934 +
935 + descale_shape = (
936 + attn_metadata.query_start_loc[:num_decodes].shape[0] - 1,
937 + key_cache.shape[2],
938 + )
939 + unified_attention(
940 + q=query[:num_decode_tokens],
941 + k=key_cache,
942 + v=value_cache,
943 + out=output[:num_decode_tokens],
944 + cu_seqlens_q=attn_metadata.query_start_loc[:num_decodes],
945 + max_seqlen_q=1, # optimize this
946 + seqused_k=attn_metadata.seq_lens[:num_decodes],
947 + max_seqlen_k=attn_metadata.max_seq_len,
948 + softmax_scale=self.scale,
949 + causal=True,
950 + alibi_slopes=self.alibi_slopes,
951 + window_size=self.sliding_window,
952 + block_table=attn_metadata.block_table[:num_decodes],
953 + softcap=self.logits_soft_cap,
954 + q_descale=None,
955 + k_descale=layer._k_scale.expand(descale_shape),
956 + v_descale=layer._v_scale.expand(descale_shape),
957 + )
958 + return
959 + assert attn_metadata.decode_metadata is not None
960 _, num_heads, head_size = query.shape
961 nbytes_per_qo_elem = torch.finfo(query.dtype).bits // 8
962 num_seqs = attn_metadata.seq_lens.shape[0]