main
py 483 lines 16.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Attention layer with PagedAttention and Triton prefix prefill."""
4
5 from typing import ClassVar
6
7 import torch
8
9 from vllm import _custom_ops as ops
10 from vllm._aiter_ops import IS_AITER_FOUND, rocm_aiter_ops
11 from vllm.config import get_current_vllm_config_or_none
12 from vllm.config.cache import CacheDType
13 from vllm.logger import init_logger
14 from vllm.model_executor.layers.quantization.utils.quant_utils import (
15 QuantKey,
16 kFp8StaticTensorSym,
17 )
18 from vllm.utils.torch_utils import is_quantized_kv_cache
19 from vllm.v1.attention.backend import (
20 AttentionLayer,
21 AttentionType,
22 CommonAttentionMetadata,
23 MultipleOf,
24 )
25 from vllm.v1.attention.backends.rocm_attn import (
26 RocmAttentionBackend,
27 RocmAttentionImpl,
28 RocmAttentionMetadata,
29 RocmAttentionMetadataBuilder,
30 )
31 from vllm.v1.kv_cache_interface import AttentionSpec, KVCacheLayout
32
33 logger = init_logger(__name__)
34
35
36 def _kv_connector_enabled() -> bool:
37 vllm_config = get_current_vllm_config_or_none()
38 if vllm_config is None:
39 return False
40 kv_transfer_config = vllm_config.kv_transfer_config
41 return kv_transfer_config is not None and kv_transfer_config.is_kv_transfer_instance
42
43
44 class RocmAiterUnifiedAttentionMetadataBuilder(RocmAttentionMetadataBuilder):
45 def build_for_cudagraph_capture(
46 self, common_attn_metadata: CommonAttentionMetadata
47 ) -> RocmAttentionMetadata:
48 attn_metadata = self.build(0, common_attn_metadata)
49 # Avoid capturing the real maximum sequence length while preserving
50 # query_start_loc, which unified attention consumes during replay.
51 attn_metadata.seq_lens.fill_(1)
52 return attn_metadata
53
54
55 class RocmAiterUnifiedAttentionBackend(RocmAttentionBackend):
56 supported_dtypes: ClassVar[list[torch.dtype]] = [torch.float16, torch.bfloat16]
57 supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
58 "auto",
59 "float16",
60 "bfloat16",
61 "fp8",
62 "fp8_e4m3",
63 "fp8_e5m2",
64 ]
65
66 @staticmethod
67 def get_supported_kernel_block_sizes(kv_cache_spec=None) -> list[int | MultipleOf]:
68 return [MultipleOf(16)]
69
70 @classmethod
71 def get_preferred_block_size(cls, default_block_size: int) -> int:
72 return 64
73
74 @classmethod
75 def supports_block_size(cls, block_size: int | None) -> bool:
76 if block_size is None:
77 return True
78 return block_size % 16 == 0
79
80 @classmethod
81 def supports_head_size(cls, head_size: int) -> bool:
82 return head_size >= 32
83
84 @classmethod
85 def supports_mm_prefix(cls) -> bool:
86 # Not implemented
87 return False
88
89 @classmethod
90 def supports_sink(cls) -> bool:
91 return True
92
93 @classmethod
94 def supports_non_causal(cls) -> bool:
95 return False
96
97 @classmethod
98 def supports_kv_connector(cls) -> bool:
99 return True
100
101 forward_includes_kv_cache_update: bool = False
102
103 @staticmethod
104 def get_name() -> str:
105 return "ROCM_AITER_UNIFIED_ATTN"
106
107 @staticmethod
108 def get_impl_cls() -> type["RocmAiterUnifiedAttentionImpl"]:
109 return RocmAiterUnifiedAttentionImpl
110
111 @classmethod
112 def customize_spec(cls, spec: AttentionSpec) -> AttentionSpec:
113 """Keep K and V packed in the content dim, unlike the native HIP
114 kernels the base class targets."""
115 # block_size == 1 is the per-token page-size probe (see
116 # Platform.get_page_size_bytes); real blocks must be gatherable in
117 # 16-token units by the ROCm kernel.
118 if spec.block_size != 1 and spec.block_size % 16 != 0:
119 raise ValueError("Block size must be a multiple of 16.")
120 return spec
121
122 @classmethod
123 def supported_kv_cache_layouts(cls) -> tuple[KVCacheLayout, ...]:
124 # K and V come out of the content dim as transposed views rather than
125 # copies, so the head dim may sit on either side of the block dim, but
126 # the layer must stay outermost.
127 if _kv_connector_enabled():
128 # Connectors like MoRI assume contiguous blocks
129 return (KVCacheLayout.LBHNC,)
130 return (KVCacheLayout.LBHNC, KVCacheLayout.LHBNC)
131
132 @staticmethod
133 def use_cascade_attention(*args, **kwargs) -> bool:
134 return False
135
136 @staticmethod
137 def get_builder_cls() -> type["RocmAiterUnifiedAttentionMetadataBuilder"]:
138 return RocmAiterUnifiedAttentionMetadataBuilder
139
140 @classmethod
141 def supports_attn_type(cls, attn_type: str) -> bool:
142 """RocmAiterUnifiedAttention supports all attention types."""
143 return attn_type in (
144 AttentionType.DECODER,
145 AttentionType.ENCODER,
146 AttentionType.ENCODER_ONLY,
147 AttentionType.ENCODER_DECODER,
148 )
149
150
151 class RocmAiterUnifiedAttentionImpl(RocmAttentionImpl):
152 def fused_output_quant_supported(self, quant_key: QuantKey):
153 return quant_key == kFp8StaticTensorSym
154
155 def __init__(
156 self,
157 num_heads: int,
158 head_size: int,
159 scale: float,
160 num_kv_heads: int,
161 alibi_slopes: list[float] | None,
162 sliding_window: int | None,
163 kv_cache_dtype: str,
164 logits_soft_cap: float | None = None,
165 attn_type: AttentionType = AttentionType.DECODER,
166 kv_sharing_target_layer_name: int | None = None,
167 sinks: torch.Tensor | None = None,
168 ) -> None:
169 super().__init__(
170 num_heads,
171 head_size,
172 scale,
173 num_kv_heads,
174 alibi_slopes,
175 sliding_window,
176 kv_cache_dtype,
177 logits_soft_cap,
178 attn_type,
179 kv_sharing_target_layer_name,
180 sinks,
181 )
182 logger.info_once(
183 "Using aiter unified attention for RocmAiterUnifiedAttentionImpl"
184 )
185 from aiter.ops.triton.unified_attention import unified_attention
186
187 self.unified_attention = unified_attention
188 self.supports_quant_query_input = True
189
190 def _split_kv_cache(
191 self, kv_cache: torch.Tensor
192 ) -> tuple[torch.Tensor, torch.Tensor]:
193 # (B, H, N, 2*hs) -> ((B, N, H, hs), (B, N, H, hs))
194 return kv_cache.transpose(1, 2).split(self.head_size, dim=-1)
195
196 def forward(
197 self,
198 layer: torch.nn.Module,
199 query: torch.Tensor,
200 key: torch.Tensor,
201 value: torch.Tensor,
202 kv_cache: torch.Tensor,
203 attn_metadata: RocmAttentionMetadata,
204 output: torch.Tensor,
205 output_scale: torch.Tensor | None = None,
206 output_block_scale: torch.Tensor | None = None,
207 ) -> torch.Tensor:
208 """Forward pass with FlashAttention.
209
210 Args:
211 layer: The attention layer, providing the q/k/v quantization scales.
212 query: shape = [num_tokens, num_heads, head_size]
213 key: shape = [num_tokens, num_kv_heads, head_size]
214 value: shape = [num_tokens, num_kv_heads, head_size]
215 kv_cache: shape =
216 [num_blocks, 2, block_size, num_kv_heads, head_size]
217 attn_metadata: Metadata for attention.
218 output: Tensor that the attention result is written into.
219 output_scale: Scale for fused output quantization.
220 output_block_scale: Block scale for fused output quantization;
221 not supported by this backend.
222
223 Returns:
224 shape = [num_tokens, num_heads * head_size]
225
226 """
227 if output_block_scale is not None:
228 raise NotImplementedError(
229 "fused block_scale output quantization is not yet supported"
230 " for RocmAttentionImpl"
231 )
232
233 if attn_metadata is None:
234 # Profiling run.
235 return output.fill_(0)
236
237 assert attn_metadata.use_cascade is False
238
239 # IMPORTANT!
240 # NOTE(woosuk): With piece-wise CUDA graphs, this method is executed in
241 # eager-mode PyTorch. Thus, we need to be careful about any CPU overhead
242 # in this method. For example, `view` and `slice` (or `[:n]`) operations
243 # are surprisingly slow even in the case they do not invoke any GPU ops.
244 # Minimize the PyTorch ops in this method as much as possible.
245 # Whenever making a change in this method, please benchmark the
246 # performance to make sure it does not introduce any overhead.
247
248 num_actual_tokens = attn_metadata.num_actual_tokens
249
250 # Handle encoder attention differently - no KV cache needed
251 if self.attn_type in (AttentionType.ENCODER_ONLY, AttentionType.ENCODER):
252 # For encoder attention,
253 # we use direct Q, K, V tensors without caching
254 return self._forward_encoder_attention(
255 query[:num_actual_tokens],
256 key[:num_actual_tokens],
257 value[:num_actual_tokens],
258 output[:num_actual_tokens],
259 attn_metadata,
260 layer,
261 )
262
263 key_cache, value_cache = self._split_kv_cache(kv_cache)
264
265 softmax_scale = self.scale
266 if is_quantized_kv_cache(self.kv_cache_dtype):
267 key_cache = key_cache.view(self.fp8_dtype)
268 value_cache = value_cache.view(self.fp8_dtype)
269
270 cu_seqlens_q = attn_metadata.query_start_loc
271 seqused_k = attn_metadata.seq_lens
272 max_seqlen_q = attn_metadata.max_query_len
273 max_seqlen_k = attn_metadata.max_seq_len
274 block_table = attn_metadata.block_table
275
276 if attn_metadata.causal:
277 self.unified_attention(
278 q=query[:num_actual_tokens],
279 k=key_cache,
280 v=value_cache,
281 out=output[:num_actual_tokens],
282 cu_seqlens_q=cu_seqlens_q,
283 max_seqlen_q=max_seqlen_q,
284 seqused_k=seqused_k,
285 max_seqlen_k=max_seqlen_k,
286 softmax_scale=softmax_scale,
287 causal=True,
288 alibi_slopes=self.alibi_slopes,
289 window_size=self.sliding_window,
290 block_table=block_table,
291 softcap=self.logits_soft_cap,
292 q_descale=layer._q_scale if query.dtype == self.fp8_dtype else None,
293 k_descale=layer._k_scale,
294 v_descale=layer._v_scale,
295 sinks=self.sinks,
296 output_scale=output_scale,
297 )
298 else:
299 # The aiter kernel is causal-only. Non-causal cross-attention
300 # (ENCODER_DECODER, e.g. Whisper) falls back to the vLLM Triton
301 # unified kernel, which shares this layout and honors the flag.
302 from vllm.v1.attention.ops.triton_unified_attention import (
303 unified_attention as triton_unified_attention,
304 )
305
306 descale_shape = (cu_seqlens_q.shape[0] - 1, key_cache.shape[2])
307 triton_unified_attention(
308 q=query[:num_actual_tokens],
309 k=key_cache,
310 v=value_cache,
311 out=output[:num_actual_tokens],
312 cu_seqlens_q=cu_seqlens_q,
313 max_seqlen_q=max_seqlen_q,
314 seqused_k=seqused_k,
315 max_seqlen_k=max_seqlen_k,
316 softmax_scale=softmax_scale,
317 causal=attn_metadata.causal,
318 alibi_slopes=self.alibi_slopes,
319 window_size=self.sliding_window,
320 block_table=block_table,
321 softcap=self.logits_soft_cap,
322 q_descale=layer._q_scale if query.dtype == self.fp8_dtype else None,
323 k_descale=layer._k_scale.expand(descale_shape),
324 v_descale=layer._v_scale.expand(descale_shape),
325 sinks=self.sinks,
326 output_scale=output_scale,
327 )
328
329 return output
330
331 def do_kv_cache_update(
332 self,
333 layer: AttentionLayer,
334 key: torch.Tensor,
335 value: torch.Tensor,
336 kv_cache: torch.Tensor,
337 slot_mapping: torch.Tensor,
338 ):
339 if self.attn_type in (AttentionType.ENCODER_ONLY, AttentionType.ENCODER):
340 # For encoder attention,
341 # we use direct Q, K, V tensors without caching
342 return
343 key_cache, value_cache = self._split_kv_cache(kv_cache)
344
345 # Reshape the input keys and values and store them in the cache.
346 ops.reshape_and_cache_flash(
347 key,
348 value,
349 key_cache,
350 value_cache,
351 slot_mapping,
352 self.kv_cache_dtype,
353 layer._k_scale,
354 layer._v_scale,
355 )
356
357 def fused_rope_kvcache_supported(self):
358 return rocm_aiter_ops.is_enabled()
359
360 def fused_qk_norm_rope_kvcache_supported(self):
361 return rocm_aiter_ops.is_enabled()
362
363 def fused_qk_norm_mrope_kvcache_supported(self) -> bool:
364 return IS_AITER_FOUND
365
366 def do_qk_norm_rope_kvcache_update(
367 self,
368 layer: AttentionLayer,
369 qkv: torch.Tensor,
370 q_out: torch.Tensor,
371 k_out: torch.Tensor,
372 positions: torch.Tensor,
373 q_weight: torch.Tensor,
374 k_weight: torch.Tensor,
375 rms_norm_eps: float,
376 cos_sin_cache: torch.Tensor,
377 is_neox: bool,
378 kv_cache: torch.Tensor,
379 layer_slot_mapping: torch.Tensor,
380 ):
381 key_cache, value_cache = self._split_kv_cache(kv_cache)
382 rocm_aiter_ops.do_qk_norm_rope_kvcache_update(
383 qkv=qkv,
384 q_weight=q_weight,
385 k_weight=k_weight,
386 cos_sin_cache=cos_sin_cache,
387 positions=positions,
388 num_heads_q=self.num_heads,
389 num_heads_k=self.num_kv_heads,
390 head_dim=self.head_size,
391 is_neox=is_neox,
392 rms_norm_eps=rms_norm_eps,
393 q_out=q_out,
394 k_out=k_out,
395 key_cache=key_cache,
396 value_cache=value_cache,
397 slot_mapping=layer_slot_mapping,
398 k_scale=layer._k_scale_cpu,
399 v_scale=layer._v_scale_cpu,
400 kv_cache_dtype=self.kv_cache_dtype,
401 use_shuffle_layout=False,
402 )
403
404 def do_qk_norm_mrope_kvcache_update(
405 self,
406 layer: AttentionLayer,
407 qkv: torch.Tensor,
408 q_out: torch.Tensor,
409 positions: torch.Tensor,
410 q_weight: torch.Tensor,
411 k_weight: torch.Tensor,
412 rms_norm_eps: float,
413 cos_sin_cache: torch.Tensor,
414 is_neox: bool,
415 mrope_section: tuple[int, int, int],
416 is_interleaved: bool,
417 rotary_dim: int,
418 kv_cache: torch.Tensor,
419 layer_slot_mapping: torch.Tensor,
420 ):
421 key_cache, value_cache = self._split_kv_cache(kv_cache)
422 rocm_aiter_ops.do_qk_norm_mrope_kvcache_update(
423 qkv=qkv,
424 q_weight=q_weight,
425 k_weight=k_weight,
426 cos_sin_cache=cos_sin_cache,
427 positions=positions,
428 num_heads_q=self.num_heads,
429 num_heads_k=self.num_kv_heads,
430 head_dim=self.head_size,
431 is_neox=is_neox,
432 mrope_section=list(mrope_section),
433 is_interleaved=is_interleaved,
434 rms_norm_eps=rms_norm_eps,
435 q_out=q_out,
436 key_cache=key_cache,
437 value_cache=value_cache,
438 slot_mapping=layer_slot_mapping,
439 k_scale=layer._k_scale_cpu,
440 v_scale=layer._v_scale_cpu,
441 kv_cache_dtype=self.kv_cache_dtype,
442 rotary_dim=rotary_dim,
443 )
444
445 def do_rope_and_kv_cache_update(
446 self,
447 layer: AttentionLayer,
448 query: torch.Tensor,
449 key: torch.Tensor,
450 value: torch.Tensor,
451 positions: torch.Tensor,
452 cos_sin_cache: torch.Tensor,
453 is_neox: bool,
454 kv_cache: torch.Tensor,
455 layer_slot_mapping: torch.Tensor,
456 ):
457 if self.attn_type in (AttentionType.ENCODER_ONLY, AttentionType.ENCODER):
458 # For encoder attention,
459 # we use direct Q, K, V tensors without caching
460 return
461 key_cache, value_cache = self._split_kv_cache(kv_cache)
462 flash_layout = True
463
464 is_fp8_kv_cache = is_quantized_kv_cache(self.kv_cache_dtype)
465 if is_fp8_kv_cache:
466 key_cache = key_cache.view(self.fp8_dtype)
467 value_cache = value_cache.view(self.fp8_dtype)
468
469 rocm_aiter_ops.triton_rope_and_cache(
470 query,
471 key,
472 value,
473 positions,
474 cos_sin_cache,
475 is_neox,
476 key_cache,
477 value_cache,
478 layer_slot_mapping,
479 layer._k_scale,
480 layer._v_scale,
481 flash_layout,
482 is_fp8_kv_cache,
483 )