# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project """Attention layer with FlashAttention.""" import copy from dataclasses import dataclass from typing import ClassVar import numpy as np import torch from vllm.attention.layer import Attention from vllm.v1.attention.backend import ( AttentionBackend, AttentionImpl, AttentionType, MultipleOf, is_quantized_kv_cache, ) from vllm.v1.attention.backends.fa_utils import ( flash_attn_supports_fp8, get_flash_attn_version, is_flash_attn_varlen_func_available, ) from vllm.v1.attention.ops.common import cp_lse_ag_out_rs from vllm.v1.attention.ops.merge_attn_states import merge_attn_states from vllm.platforms import current_platform if is_flash_attn_varlen_func_available(): if current_platform.is_rocm(): from vllm.v1.attention.backends.fa_utils import ( flash_attn_supports_sinks, vllm_flash_attn_varlen_func, reshape_and_cache_cuda, ) from vllm.v1.attention.ops.triton_reshape_and_cache_flash import ( triton_reshape_and_cache_flash, ) try: from flash_attn import varlen_fwd_unified except Exception: varlen_fwd_unified = None else: from vllm.v1.attention.backends.fa_utils import ( flash_attn_supports_sinks, flash_attn_varlen_func, get_scheduler_metadata, reshape_and_cache_flash, ) from vllm.config import VllmConfig, get_current_vllm_config, get_layers_from_vllm_config from vllm.config.cache import CacheDType from vllm.distributed.parallel_state import get_dcp_group from vllm.logger import init_logger from vllm.model_executor.layers.batch_invariant import ( vllm_is_batch_invariant, ) from vllm.platforms.interface import DeviceCapability from vllm.utils.math_utils import cdiv from vllm.v1.attention.backend import ( AttentionCGSupport, AttentionMetadataBuilder, CommonAttentionMetadata, ) from vllm.v1.attention.backends.utils import ( get_dcp_local_seq_lens, get_kv_cache_layout, ) from vllm.v1.kv_cache_interface import AttentionSpec import vllm.envs as envs logger = init_logger(__name__) class FlashAttentionBackend(AttentionBackend): accept_output_buffer: bool = True supported_dtypes: ClassVar[list[torch.dtype]] = [torch.float16, torch.bfloat16] @staticmethod def get_supported_kernel_block_sizes() -> list[int | MultipleOf]: vllm_config = get_current_vllm_config() model_config = vllm_config.model_config cache_config = vllm_config.cache_config if ( model_config and model_config.is_hybrid and ( cache_config.mamba_ssm_cache_dtype == "float32" or cache_config.mamba_cache_dtype == "float32" ) ): # NOTE(tdoublep): while in principle, FA supports # MultipleOf(16), these are the block sizes that do not # suffer from the NaN propagation problem described here: # https://github.com/Dao-AILab/flash-attention/issues/1974 return [16, 32, 64] return [MultipleOf(16)] forward_includes_kv_cache_update: bool = False @staticmethod def get_name() -> str: return "FLASH_ATTN" @classmethod def supports_attn_type(cls, attn_type: str) -> bool: """FlashAttention supports all attention types.""" return attn_type in ( AttentionType.DECODER, AttentionType.ENCODER, AttentionType.ENCODER_ONLY, AttentionType.ENCODER_DECODER, ) @staticmethod def get_impl_cls() -> type["FlashAttentionImpl"]: return FlashAttentionImpl @staticmethod def get_builder_cls() -> type["FlashAttentionMetadataBuilder"]: return FlashAttentionMetadataBuilder @classmethod def supports_alibi_sqrt(cls) -> bool: return True @classmethod def supports_mm_prefix(cls) -> bool: return True @staticmethod def _use_rocm_unified_kv_layout( block_size: int | None = None, key_cache: torch.Tensor | None = None, value_cache: torch.Tensor | None = None, ) -> bool: if not current_platform.is_rocm(): return False if block_size is None: if key_cache is not None and value_cache is not None: if key_cache.ndim != 4 or value_cache.ndim != 4: return False if key_cache.shape != value_cache.shape: return False block_size = key_cache.shape[1] else: try: block_size = get_current_vllm_config().cache_config.block_size except Exception: return False return block_size is not None and block_size != 64 and block_size % 64 == 0 if current_platform.is_rocm(): @staticmethod def get_kv_cache_shape( num_blocks: int, block_size: int, num_kv_heads: int, head_size: int, cache_dtype_str: str = "auto", ) -> tuple[tuple[int, ...], tuple[int, ...]]: if block_size % 16 != 0: raise ValueError("Block size must be a multiple of 16.") if FlashAttentionBackend._use_rocm_unified_kv_layout(block_size): unified_shape = (num_blocks, block_size, num_kv_heads, head_size) return (unified_shape, unified_shape) return ( (num_blocks, num_kv_heads, block_size, head_size), (num_blocks, num_kv_heads, head_size, block_size), ) @staticmethod def get_kv_cache_stride_order( include_num_layers_dimension: bool = False, ) -> tuple[tuple[int, ...], tuple[int, ...]]: # `stride_order` indicates the permutation that gets # us from `get_kv_cache_shape` to the actual memory layout we want. cache_layout = get_kv_cache_layout() if FlashAttentionBackend._use_rocm_unified_kv_layout(): if cache_layout != "NHD": raise RuntimeError( "ROCm unified KV layout currently supports NHD only." ) if include_num_layers_dimension: # (num_blocks, num_layers, block_size, num_kv_heads, head_size) return (1, 0, 2, 3, 4), (1, 0, 2, 3, 4) key_stride_order = (0, 1, 2, 3) value_stride_order = (0, 1, 2, 3) else: if cache_layout == "NHD" and include_num_layers_dimension: # (num_blocks, num_layers, block_size, num_kv_heads, head_size) return (1, 0, 3, 2, 5), (1, 0, 4, 2, 3) elif cache_layout == "NHD": key_stride_order = (0, 1, 2, 3) value_stride_order = (0, 1, 2, 3) elif cache_layout == "HND" and include_num_layers_dimension: # (num_blocks, num_kv_heads, num_layers, block_size, head_size) return (1, 2, 0, 3, 4), (1, 2, 0, 4, 3) elif cache_layout == "HND": key_stride_order = (0, 1, 2, 3) value_stride_order = (0, 1, 3, 2) else: raise ValueError(f"Unknown cache layout format {cache_layout}.") return key_stride_order, value_stride_order else: @staticmethod def get_kv_cache_shape( num_blocks: int, block_size: int, num_kv_heads: int, head_size: int, cache_dtype_str: str = "auto", ) -> tuple[int, ...]: if block_size % 16 != 0: raise ValueError("Block size must be a multiple of 16.") return (2, num_blocks, block_size, num_kv_heads, head_size) @staticmethod def get_kv_cache_stride_order( include_num_layers_dimension: bool = False, ) -> tuple[int, ...]: # `stride_order` indicates the permutation that gets # us from `get_kv_cache_shape` to the actual memory layout we want. cache_layout = get_kv_cache_layout() if cache_layout == "NHD" and include_num_layers_dimension: # (num_blocks, num_layers, 2, block_size, num_kv_heads, head_size) return (2, 0, 1, 3, 4, 5) elif cache_layout == "NHD": stride_order = (0, 1, 2, 3, 4) elif cache_layout == "HND" and include_num_layers_dimension: # (num_blocks, num_kv_heads, num_layers, 2, block_size, head_size) return (2, 4, 0, 1, 3, 5) elif cache_layout == "HND": stride_order = (0, 1, 3, 2, 4) else: raise ValueError(f"Unknown cache layout format {cache_layout}.") return stride_order @staticmethod def get_fp8_dtype_for_flashattn(kv_cache_dtype: str) -> torch.dtype: if kv_cache_dtype in ("fp8", "fp8_e4m3"): if torch.cuda.get_device_properties("cuda").gcnArchName.split(':')[0] == "gfx938": return torch.float8_e4m3fn else: raise ValueError(f"{kv_cache_dtype} only supported on nmz") elif kv_cache_dtype in ("fp8_e5m2"): return torch.float8_e5m2 else: raise ValueError(f"Unrecognized FP8 dtype: {kv_cache_dtype}") @classmethod def supports_head_size(cls, head_size: int) -> bool: return head_size % 8 == 0 and head_size <= 256 @classmethod def supports_kv_cache_dtype(cls, kv_cache_dtype: CacheDType | None) -> bool: if kv_cache_dtype is None: return True if kv_cache_dtype.startswith("fp8"): return flash_attn_supports_fp8() return kv_cache_dtype in ["auto", "bfloat16"] @classmethod def supports_sink(cls) -> bool: if not is_flash_attn_varlen_func_available(): return False return flash_attn_supports_sinks() @classmethod def supports_compute_capability(cls, capability: DeviceCapability) -> bool: return capability >= DeviceCapability(8, 0) @classmethod def supports_combination( cls, head_size: int, dtype: torch.dtype, kv_cache_dtype: CacheDType | None, block_size: int | None, use_mla: bool, has_sink: bool, use_sparse: bool, device_capability: DeviceCapability, ) -> str | None: if has_sink and device_capability < DeviceCapability(9, 0): return "sink not supported on compute capability < 9.0" return None @dataclass class FlashAttentionMetadata: # NOTE(sang): Definition of context_len, query_len, and seq_len. # |---------- N-1 iteration --------| # |---------------- N iteration ---------------------| # |- tokenA -|......................|-- newTokens ---| # |---------- context_len ----------| # |-------------------- seq_len ---------------------| # |-- query_len ---| num_actual_tokens: int # Number of tokens excluding padding. max_query_len: int query_start_loc: torch.Tensor max_seq_len: int seq_lens: torch.Tensor block_table: torch.Tensor slot_mapping: torch.Tensor # For cascade attention. use_cascade: bool common_prefix_len: int cu_prefix_query_lens: torch.Tensor | None prefix_kv_lens: torch.Tensor | None suffix_kv_lens: torch.Tensor | None # For GQA DCP max_dcp_context_kv_len: int | None = None dcp_context_kv_lens: torch.Tensor | None = None # Optional aot scheduling scheduler_metadata: torch.Tensor | None = None prefix_scheduler_metadata: torch.Tensor | None = None max_num_splits: int = 0 mm_prefix_range: dict[int, list[tuple[int, int]]] | None = None qq_bias: torch.Tensor | None = None causal: bool = True @property def mm_prefix_range_tensor(self) -> torch.Tensor | None: if self.mm_prefix_range is None: return None num_seqs = self.seq_lens.shape[0] device = self.seq_lens.device range_lists = [ self.mm_prefix_range.get(i, [(0, 0)]) or [(0, 0)] for i in range(num_seqs) ] if all(r == [(0, 0)] for r in range_lists): return None range_tensors = [ torch.tensor(r, dtype=torch.int32, device=device).view(-1, 2) for r in range_lists ] return torch.nested.nested_tensor( range_tensors, layout=torch.jagged ).to_padded_tensor(0) def _get_sliding_window_configs( vllm_config: VllmConfig, ) -> set[tuple[int, int] | None]: """Get the set of all sliding window configs used in the model.""" sliding_window_configs: set[tuple[int, int] | None] = set() layers = get_layers_from_vllm_config(vllm_config, Attention) for layer in layers.values(): assert isinstance(layer.impl, FlashAttentionImpl) sliding_window_configs.add(layer.impl.sliding_window) return sliding_window_configs class FlashAttentionMetadataBuilder(AttentionMetadataBuilder[FlashAttentionMetadata]): # FA3: # Supports full cudagraphs for all cases. # # FA2: # For FA2, a graph is captured with max_query_len=1, (which is what we # capture by default for num_tokens <= max_num_seqs when there is no # spec-decode) then these graphs will not work for mixed prefill-decode # (unlike FA3). This is due to special max_query_len=1 packed-GQA handling # in FA2. # In summary if we are running with spec decodes the graphs would # work for mixed prefill-decode and uniform-decode. But for non-spec decodes # the graphs would not work for mixed prefill-decode; sorta the inverse # of UNIFORM_SINGLE_TOKEN_DECODE. # There's probably a better way to describe this using `AttentionCGSupport` # but for now just set it to `UNIFORM_BATCH` to get use to drop down # to FULL_AND_PIECEWISE. # TODO(luka, lucas): audit FA2 as part of: # https://github.com/vllm-project/vllm/issues/22945 _cudagraph_support = ( AttentionCGSupport.ALWAYS if get_flash_attn_version() == 3 or current_platform.is_rocm() else AttentionCGSupport.UNIFORM_BATCH ) supports_update_block_table: bool = True @classmethod def get_cudagraph_support( cls, vllm_config: "VllmConfig", kv_cache_spec: "AttentionSpec", ) -> AttentionCGSupport: return cls._cudagraph_support def __init__( self, kv_cache_spec: AttentionSpec, layer_names: list[str], vllm_config: VllmConfig, device: torch.device, ): super().__init__(kv_cache_spec, layer_names, vllm_config, device) self.model_config = vllm_config.model_config self.parallel_config = vllm_config.parallel_config self.cache_config = vllm_config.cache_config self.compilation_config = vllm_config.compilation_config self.attention_config = vllm_config.attention_config self.num_heads_q = self.model_config.get_num_attention_heads( self.parallel_config ) self.num_heads_kv = self.model_config.get_num_kv_heads(self.parallel_config) self.kv_cache_dtype = kv_cache_spec.dtype self.headdim = self.model_config.get_head_size() self.block_size = kv_cache_spec.block_size self.max_num_splits = 0 # No upper bound on the number of splits. self.aot_schedule = get_flash_attn_version() == 3 try: from vllm.distributed.parallel_state import get_dcp_group self.dcp_world_size = get_dcp_group().world_size self.dcp_rank = get_dcp_group().rank_in_group except AssertionError: # DCP might not be initialized in testing self.dcp_world_size = 1 self.dcp_rank = 0 self.cp_kv_cache_interleave_size = ( self.parallel_config.cp_kv_cache_interleave_size ) self.use_full_cuda_graph = ( self.compilation_config.cudagraph_mode.has_full_cudagraphs() ) self.max_cudagraph_size = self.compilation_config.max_cudagraph_capture_size if self.use_full_cuda_graph and self.aot_schedule: self.scheduler_metadata = torch.zeros( vllm_config.scheduler_config.max_num_seqs + 1, dtype=torch.int32, device=self.device, ) # When using cuda graph, we need to set the upper bound of the # number of splits so that large enough intermediate buffers are # pre-allocated during capture. self.max_num_splits = ( self.attention_config.flash_attn_max_num_splits_for_cuda_graph ) # Sliding window size to be used with the AOT scheduler will be # populated on first build() call. self.aot_sliding_window: tuple[int, int] | None = None def build( self, common_prefix_len: int, common_attn_metadata: CommonAttentionMetadata, fast_build: bool = False, ) -> FlashAttentionMetadata: """ fast_build disables AOT scheduling, used when there will be few iterations i.e. spec-decode """ num_reqs = common_attn_metadata.num_reqs num_actual_tokens = common_attn_metadata.num_actual_tokens max_query_len = common_attn_metadata.max_query_len max_seq_len = common_attn_metadata.max_seq_len query_start_loc = common_attn_metadata.query_start_loc seq_lens = common_attn_metadata.seq_lens block_table_tensor = common_attn_metadata.block_table_tensor slot_mapping = common_attn_metadata.slot_mapping causal = common_attn_metadata.causal # the overhead of the aot schedule is not worth it for spec-decode aot_schedule = self.aot_schedule and not fast_build if self.aot_sliding_window is None: self.aot_sliding_window = (-1, -1) # For the AOT scheduler we need the sliding window value to be # constant for all layers to. We have to populate this on the first # build() call so the layers are constructed (cannot populate) # in __init__. if aot_schedule: sliding_window_configs = _get_sliding_window_configs(self.vllm_config) if len(sliding_window_configs) == 1: sliding_window_config = sliding_window_configs.pop() if sliding_window_config is not None: self.aot_sliding_window = sliding_window_config elif len(sliding_window_configs) > 1: self.aot_schedule = False aot_schedule = False max_num_splits = 0 # 0 means use FA3's heuristics, not CG compatible if ( self.use_full_cuda_graph and self.max_cudagraph_size is not None and num_actual_tokens <= self.max_cudagraph_size ): # NOTE(woosuk): Setting num_splits > 1 may increase the memory # usage, because the intermediate buffers of size [num_splits, # num_heads, num_tokens, head_size] are allocated. Therefore, # we only set num_splits when using cuda graphs. max_num_splits = self.max_num_splits if vllm_is_batch_invariant(): max_num_splits = 1 def schedule( batch_size, cu_query_lens, max_query_len, seqlens, max_seq_len, causal ): cache_dtype = self.cache_config.cache_dtype if cache_dtype.startswith("fp8"): qkv_dtype = FlashAttentionBackend.get_fp8_dtype_for_flashattn( cache_dtype ) else: qkv_dtype = self.kv_cache_dtype if aot_schedule: return get_scheduler_metadata( batch_size=batch_size, max_seqlen_q=max_query_len, max_seqlen_k=max_seq_len, num_heads_q=self.num_heads_q * self.dcp_world_size, num_heads_kv=self.num_heads_kv, headdim=self.headdim, cache_seqlens=seqlens, qkv_dtype=qkv_dtype, cu_seqlens_q=cu_query_lens, page_size=self.block_size, causal=causal, window_size=self.aot_sliding_window, num_splits=max_num_splits, ) return None use_cascade = common_prefix_len > 0 max_dcp_context_kv_len = 0 dcp_context_kv_lens = None cu_prefix_query_lens = None prefix_kv_lens = None suffix_kv_lens = None prefix_scheduler_metadata = None if self.dcp_world_size > 1: query_kv_lens = query_start_loc[1:] - query_start_loc[:-1] dcp_context_kv_lens = seq_lens - query_kv_lens dcp_context_kv_lens = get_dcp_local_seq_lens( dcp_context_kv_lens, self.dcp_world_size, self.dcp_rank, self.cp_kv_cache_interleave_size, ) # After DCP distribution, the maximum number of tokens for any rank is # ceil(L / (N * I)) * I, where L is max_seq_len, N is dcp_world_size, # and I is cp_kv_cache_interleave_size. # This eliminates GPU->CPU sync while minimizing workspace over-allocation. num_partitions = self.dcp_world_size * self.cp_kv_cache_interleave_size max_dcp_context_kv_len = ( (max_seq_len + num_partitions - 1) // num_partitions ) * self.cp_kv_cache_interleave_size scheduler_metadata = schedule( batch_size=num_reqs, cu_query_lens=query_start_loc, max_query_len=max_query_len, seqlens=dcp_context_kv_lens, max_seq_len=max_dcp_context_kv_len, causal=False, ) elif use_cascade: cu_prefix_query_lens = torch.tensor( [0, num_actual_tokens], dtype=torch.int32, device=self.device ) prefix_kv_lens = torch.tensor( [common_prefix_len], dtype=torch.int32, device=self.device ) # Use GPU tensor directly - no CPU sync needed suffix_kv_lens = seq_lens[:num_reqs] - common_prefix_len prefix_scheduler_metadata = schedule( batch_size=1, cu_query_lens=cu_prefix_query_lens, max_query_len=num_actual_tokens, seqlens=prefix_kv_lens, max_seq_len=common_prefix_len, causal=False, ) scheduler_metadata = schedule( batch_size=num_reqs, cu_query_lens=query_start_loc, max_query_len=max_query_len, seqlens=suffix_kv_lens, max_seq_len=max_seq_len - common_prefix_len, causal=True, ) else: scheduler_metadata = schedule( batch_size=num_reqs, cu_query_lens=query_start_loc, max_query_len=max_query_len, seqlens=seq_lens, max_seq_len=max_seq_len, causal=causal, ) # For FA3 + full cudagraph if self.use_full_cuda_graph and scheduler_metadata is not None: n = scheduler_metadata.shape[0] self.scheduler_metadata[:n] = scheduler_metadata # NOTE(woosuk): We should zero out the rest of the scheduler # metadata to guarantee the correctness. Otherwise, some thread # blocks may use the invalid scheduler metadata and overwrite the # output buffer. self.scheduler_metadata[n:] = 0 scheduler_metadata = self.scheduler_metadata[:n] attn_metadata = FlashAttentionMetadata( num_actual_tokens=num_actual_tokens, max_query_len=max_query_len, query_start_loc=query_start_loc, max_seq_len=max_seq_len, seq_lens=seq_lens, block_table=block_table_tensor, slot_mapping=slot_mapping, max_dcp_context_kv_len=max_dcp_context_kv_len, dcp_context_kv_lens=dcp_context_kv_lens, use_cascade=use_cascade, common_prefix_len=common_prefix_len, scheduler_metadata=scheduler_metadata, cu_prefix_query_lens=cu_prefix_query_lens, prefix_kv_lens=prefix_kv_lens, suffix_kv_lens=suffix_kv_lens, prefix_scheduler_metadata=prefix_scheduler_metadata, max_num_splits=max_num_splits, causal=causal, ) return attn_metadata def update_block_table( self, metadata: FlashAttentionMetadata, blk_table: torch.Tensor, slot_mapping: torch.Tensor, ) -> FlashAttentionMetadata: new_metadata = copy.copy(metadata) new_metadata.block_table = blk_table new_metadata.slot_mapping = slot_mapping return new_metadata def use_cascade_attention(self, *args, **kwargs) -> bool: return use_cascade_attention(*args, **kwargs) class FlashAttentionImpl(AttentionImpl): can_return_lse_for_decode: bool = True def __init__( self, num_heads: int, head_size: int, scale: float, num_kv_heads: int, alibi_slopes: list[float] | None, sliding_window: int | None, kv_cache_dtype: str, logits_soft_cap: float | None = None, attn_type: AttentionType = AttentionType.DECODER, kv_sharing_target_layer_name: str | None = None, sinks: torch.Tensor | None = None, use_alibi_sqrt: bool = False, ) -> None: self.num_heads = num_heads self.head_size = head_size self.scale = float(scale) self.num_kv_heads = num_kv_heads if alibi_slopes is not None: alibi_slopes = torch.tensor(alibi_slopes, dtype=torch.float32) self.alibi_slopes = alibi_slopes if sliding_window is None: self.sliding_window = (-1, -1) elif attn_type == AttentionType.ENCODER_ONLY: self.sliding_window = (sliding_window - 1, sliding_window - 1) else: self.sliding_window = (sliding_window - 1, 0) self.kv_cache_dtype = kv_cache_dtype if logits_soft_cap is None: # In flash-attn, setting logits_soft_cap as 0 means no soft cap. logits_soft_cap = 0 self.logits_soft_cap = logits_soft_cap self.kv_sharing_target_layer_name = kv_sharing_target_layer_name self.num_queries_per_kv = self.num_heads // self.num_kv_heads self.attn_type = attn_type self.vllm_flash_attn_version = get_flash_attn_version() self.use_alibi_sqrt = use_alibi_sqrt # Cache the batch invariant result for use in forward passes self.batch_invariant_enabled = vllm_is_batch_invariant() if is_quantized_kv_cache(self.kv_cache_dtype) and not flash_attn_supports_fp8(): raise NotImplementedError( "FlashAttention does not support fp8 kv-cache on this device." ) self.sinks = sinks if self.sinks is not None: if not current_platform.is_rocm(): assert flash_attn_supports_sinks(), ( "Sinks are only supported in FlashAttention 3" ) assert self.sinks.shape[0] == num_heads, ( "Sinks must have the same number of heads as the number of " "heads in the layer" ) self.supports_quant_query_input = True self.supports_per_head_quant_scales = ( self.vllm_flash_attn_version >= 3 if self.vllm_flash_attn_version is not None else False ) def _get_unified_extras( self, attn_metadata: FlashAttentionMetadata, ) -> tuple[torch.Tensor | None, torch.Tensor | None]: mm_prefix_range_tensor = attn_metadata.mm_prefix_range_tensor qq_bias = attn_metadata.qq_bias return mm_prefix_range_tensor, qq_bias def forward( self, layer: torch.nn.Module, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor, kv_cache: torch.Tensor, attn_metadata: FlashAttentionMetadata, output: torch.Tensor | None = None, output_scale: torch.Tensor | None = None, output_block_scale: torch.Tensor | None = None, ) -> torch.Tensor: """Forward pass with FlashAttention. Args: query: shape = [num_tokens, num_heads, head_size] key: shape = [num_tokens, num_kv_heads, head_size] value: shape = [num_tokens, num_kv_heads, head_size] kv_cache: shape = [2, num_blocks, block_size, num_kv_heads, head_size] attn_metadata: Metadata for attention. Returns: shape = [num_tokens, num_heads * head_size] NOTE: FP8 quantization, flash-attn expect the size of {q,k,v}_descale to be (num_sequences, num_kv_heads). We use torch's .expand() to avoid duplicating values """ assert output is not None, "Output tensor must be provided." assert self.vllm_flash_attn_version is not None, ( "FlashAttention version not detected." ) if output_scale is not None or output_block_scale is not None: raise NotImplementedError( "fused output quantization is not yet supported for FlashAttentionImpl" ) if attn_metadata is None: # Profiling run. return output.fill_(0) attn_type = self.attn_type # IMPORTANT! # NOTE(woosuk): With piece-wise CUDA graphs, this method is executed in # eager-mode PyTorch. Thus, we need to be careful about any CPU overhead # in this method. For example, `view` and `slice` (or `[:n]`) operations # are surprisingly slow even in the case they do not invoke any GPU ops. # Minimize the PyTorch ops in this method as much as possible. # Whenever making a change in this method, please benchmark the # performance to make sure it does not introduce any overhead. num_actual_tokens = attn_metadata.num_actual_tokens # Handle encoder attention differently - no KV cache needed if attn_type in (AttentionType.ENCODER_ONLY, AttentionType.ENCODER): # For encoder attention, # we use direct Q, K, V tensors without caching return self._forward_encoder_attention( query[:num_actual_tokens], key[:num_actual_tokens], value[:num_actual_tokens], output[:num_actual_tokens], attn_metadata, layer, ) # For decoder and cross-attention, use KV cache as before if current_platform.is_rocm(): key_cache, value_cache = kv_cache else: key_cache, value_cache = kv_cache.unbind(0) if self.kv_cache_dtype.startswith("fp8"): # queries are quantized in the attention layer dtype = FlashAttentionBackend.get_fp8_dtype_for_flashattn( self.kv_cache_dtype ) key_cache = key_cache.view(dtype) value_cache = value_cache.view(dtype) if not attn_metadata.use_cascade: cu_seqlens_q = attn_metadata.query_start_loc seqused_k = attn_metadata.seq_lens max_seqlen_q = attn_metadata.max_query_len max_seqlen_k = attn_metadata.max_seq_len block_table = attn_metadata.block_table scheduler_metadata = attn_metadata.scheduler_metadata if current_platform.is_rocm(): q_descale = None k_descale = layer._k_scale v_descale = layer._v_scale else: descale_shape = (cu_seqlens_q.shape[0] - 1, self.num_kv_heads) q_descale = layer._q_scale.expand(descale_shape) k_descale = layer._k_scale.expand(descale_shape) v_descale = layer._v_scale.expand(descale_shape) if self.dcp_world_size > 1: self._forward_with_dcp( query[:num_actual_tokens], key[:num_actual_tokens], value[:num_actual_tokens], key_cache, value_cache, output[:num_actual_tokens], attn_metadata, q_descale=q_descale, k_descale=k_descale, v_descale=v_descale, ) return output else: sliding_window_size = ( list(self.sliding_window) if self.sliding_window is not None else None ) if current_platform.is_rocm(): if envs.VLLM_USE_PA_PRINT_PARAM: print("PA SIZE:") print(f"q.shape = {query[:num_actual_tokens].shape}, key_cache.shape = {key_cache.shape}, value_cache.shape = {value_cache.shape}") print(f"cu_seqlens_q.shape = {cu_seqlens_q.shape}, max_seqlen_q = {max_seqlen_q}, seqused_k.shape = {seqused_k.shape}, max_seqlen_k = {max_seqlen_k}") print(f"softmax_scale = {self.scale:.3f}, alibi_slopes = {self.alibi_slopes}, window_size = {self.sliding_window}, block_tables.shape = {block_table.shape}, softcap = {self.logits_soft_cap}, scheduler_metadata = {scheduler_metadata}") use_unified_kv_layout = ( FlashAttentionBackend._use_rocm_unified_kv_layout( key_cache=key_cache, value_cache=value_cache) ) if use_unified_kv_layout: mm_prefix_range_tensor, qq_bias = self._get_unified_extras( attn_metadata ) varlen_fwd_unified( q=query[:num_actual_tokens], k=key_cache, v=value_cache, cu_seqlens_q=cu_seqlens_q, seqused_k=seqused_k, block_table=block_table, max_seqlen_q=max_seqlen_q, max_seqlen_k=max_seqlen_k, softmax_scale=self.scale, causal=attn_metadata.causal, softcap=self.logits_soft_cap, window_size=tuple(self.sliding_window), alibi_slopes=self.alibi_slopes, use_alibi_sqrt=self.use_alibi_sqrt, qq_bias=qq_bias, s_aux=self.sinks, mm_prefix_range=mm_prefix_range_tensor, return_softmax_lse=False, out=output[:num_actual_tokens], ) else: vllm_flash_attn_varlen_func( q=query[:num_actual_tokens], k=key_cache, v=value_cache, out=output[:num_actual_tokens], cu_seqlens_q=cu_seqlens_q, max_seqlen_q=max_seqlen_q, seqused_k=seqused_k, max_seqlen_k=max_seqlen_k, softmax_scale=self.scale, causal=attn_metadata.causal, alibi_slopes=self.alibi_slopes, window_size=sliding_window_size, block_table=block_table, softcap=self.logits_soft_cap, scheduler_metadata=scheduler_metadata, fa_version=self.vllm_flash_attn_version, q_descale=q_descale, k_descale=k_descale, v_descale=v_descale, # num_splits=attn_metadata.max_num_splits, s_aux=self.sinks, is_prefix_cache=True, ) else: flash_attn_varlen_func( q=query[:num_actual_tokens], k=key_cache, v=value_cache, out=output[:num_actual_tokens], cu_seqlens_q=cu_seqlens_q, max_seqlen_q=max_seqlen_q, seqused_k=seqused_k, max_seqlen_k=max_seqlen_k, softmax_scale=self.scale, causal=attn_metadata.causal, alibi_slopes=self.alibi_slopes, window_size=sliding_window_size, block_table=block_table, softcap=self.logits_soft_cap, scheduler_metadata=scheduler_metadata, fa_version=self.vllm_flash_attn_version, q_descale=q_descale, k_descale=k_descale, v_descale=v_descale, num_splits=attn_metadata.max_num_splits, s_aux=self.sinks, ) return output # Cascade attention (rare case). cascade_attention( output[:num_actual_tokens], query[:num_actual_tokens], key_cache, value_cache, cu_query_lens=attn_metadata.query_start_loc, max_query_len=attn_metadata.max_query_len, cu_prefix_query_lens=attn_metadata.cu_prefix_query_lens, prefix_kv_lens=attn_metadata.prefix_kv_lens, suffix_kv_lens=attn_metadata.suffix_kv_lens, max_kv_len=attn_metadata.max_seq_len, softmax_scale=self.scale, alibi_slopes=self.alibi_slopes, sliding_window=self.sliding_window, logits_soft_cap=self.logits_soft_cap, block_table=attn_metadata.block_table, common_prefix_len=attn_metadata.common_prefix_len, max_num_splits=attn_metadata.max_num_splits, fa_version=self.vllm_flash_attn_version, prefix_scheduler_metadata=attn_metadata.prefix_scheduler_metadata, suffix_scheduler_metadata=attn_metadata.scheduler_metadata, q_descale=None if current_platform.is_rocm() else layer._q_scale, k_descale=layer._k_scale, v_descale=layer._v_scale, s_aux=self.sinks, ) return output def do_kv_cache_update( self, layer: torch.nn.Module, key: torch.Tensor, value: torch.Tensor, kv_cache: torch.Tensor, slot_mapping: torch.Tensor, ) -> None: if self.attn_type in (AttentionType.ENCODER_ONLY, AttentionType.ENCODER): # For encoder attention, # we use direct Q, K, V tensors without caching return # key and value may be None in the case of cross attention. They are # calculated once based on the output from the encoder and then cached # in KV cache. if ( self.kv_sharing_target_layer_name is not None or key is None or value is None ): return if current_platform.is_rocm(): key_cache, value_cache = kv_cache else: key_cache, value_cache = kv_cache.unbind(0) # Reshape the input keys and values and store them in the cache. # Skip this if sharing KV cache with an earlier attention layer. # NOTE(woosuk): Here, key and value are padded while slot_mapping is # not padded. However, we don't need to do key[:num_actual_tokens] # and value[:num_actual_tokens] because the reshape_and_cache_flash # op uses the slot_mapping's shape to determine the number of # actual tokens. if current_platform.is_rocm(): if FlashAttentionBackend._use_rocm_unified_kv_layout( key_cache=key_cache, value_cache=value_cache, ): triton_reshape_and_cache_flash( key, value, key_cache, value_cache, slot_mapping, self.kv_cache_dtype, layer._k_scale, layer._v_scale, ) else: if envs.VLLM_USE_OPT_RESHAPE_AND_CACHE: from lightop import reshape_and_cache_cuda reshape_and_cache_cuda( key, value, key_cache, value_cache, slot_mapping, self.kv_cache_dtype, layer._k_scale, layer._v_scale ) else: from vllm.v1.attention.backends.fa_utils import reshape_and_cache_cuda reshape_and_cache_cuda( key, value, key_cache, value_cache, slot_mapping, self.kv_cache_dtype, layer._k_scale, layer._v_scale, ) else: reshape_and_cache_flash( key, value, key_cache, value_cache, slot_mapping, self.kv_cache_dtype, layer._k_scale, layer._v_scale, ) def _forward_with_dcp( self, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor, key_cache: torch.Tensor, value_cache: torch.Tensor, output: torch.Tensor, attn_metadata: FlashAttentionMetadata, q_descale: torch.Tensor | None = None, k_descale: torch.Tensor | None = None, v_descale: torch.Tensor | None = None, ) -> torch.Tensor: assert self.vllm_flash_attn_version is not None, ( "FlashAttention version not detected." ) cu_seqlens_q = attn_metadata.query_start_loc max_seqlen_q = attn_metadata.max_query_len block_table = attn_metadata.block_table query = query.contiguous() query_across_dcp = get_dcp_group().all_gather(query, dim=1) sliding_window_size = ( list(self.sliding_window) if self.sliding_window is not None else None ) if current_platform.is_rocm(): context_attn_out, context_lse = vllm_flash_attn_varlen_func( q=query_across_dcp, k=key_cache, v=value_cache, out=None, cu_seqlens_q=cu_seqlens_q, max_seqlen_q=max_seqlen_q, seqused_k=attn_metadata.dcp_context_kv_lens, max_seqlen_k=attn_metadata.max_dcp_context_kv_len, softmax_scale=self.scale, causal=False, alibi_slopes=self.alibi_slopes, window_size=sliding_window_size, block_table=block_table, softcap=self.logits_soft_cap, return_softmax_lse=True, scheduler_metadata=attn_metadata.scheduler_metadata, fa_version=self.vllm_flash_attn_version, q_descale=q_descale, k_descale=k_descale, v_descale=v_descale, is_prefix_cache=True, ) else: context_attn_out, context_lse = flash_attn_varlen_func( q=query_across_dcp, k=key_cache, v=value_cache, out=None, cu_seqlens_q=cu_seqlens_q, max_seqlen_q=max_seqlen_q, seqused_k=attn_metadata.dcp_context_kv_lens, max_seqlen_k=attn_metadata.max_dcp_context_kv_len, softmax_scale=self.scale, causal=False, alibi_slopes=self.alibi_slopes, window_size=sliding_window_size, block_table=block_table, softcap=self.logits_soft_cap, return_softmax_lse=True, scheduler_metadata=attn_metadata.scheduler_metadata, fa_version=self.vllm_flash_attn_version, q_descale=q_descale, k_descale=k_descale, v_descale=v_descale, ) # FA returns LSE in shape [ H, B ] but cp_lse_ag_out_rs wants [ B, H ] context_attn_out_cor, context_lse_cor = cp_lse_ag_out_rs( context_attn_out, context_lse.transpose(0, 1), get_dcp_group(), return_lse=True, ) context_lse_cor = context_lse_cor.transpose(0, 1).contiguous() if current_platform.is_rocm(): query_attn_out, query_lse = vllm_flash_attn_varlen_func( q=query, k=key, v=value, out=None, cu_seqlens_q=cu_seqlens_q, max_seqlen_q=max_seqlen_q, cu_seqlens_k=cu_seqlens_q, max_seqlen_k=max_seqlen_q, softmax_scale=self.scale, causal=attn_metadata.causal, alibi_slopes=self.alibi_slopes, window_size=sliding_window_size, softcap=self.logits_soft_cap, return_softmax_lse=True, fa_version=self.vllm_flash_attn_version, q_descale=q_descale, k_descale=k_descale, v_descale=v_descale, ) else: query_attn_out, query_lse = flash_attn_varlen_func( q=query, k=key, v=value, out=None, cu_seqlens_q=cu_seqlens_q, max_seqlen_q=max_seqlen_q, cu_seqlens_k=cu_seqlens_q, max_seqlen_k=max_seqlen_q, softmax_scale=self.scale, causal=attn_metadata.causal, alibi_slopes=self.alibi_slopes, window_size=sliding_window_size, softcap=self.logits_soft_cap, return_softmax_lse=True, fa_version=self.vllm_flash_attn_version, q_descale=q_descale, k_descale=k_descale, v_descale=v_descale, ) assert context_attn_out_cor.shape == query_attn_out.shape assert context_lse_cor.shape == query_lse.shape merge_attn_states( output, context_attn_out_cor, context_lse_cor, query_attn_out, query_lse, ) def _forward_encoder_attention( self, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor, output: torch.Tensor, attn_metadata: FlashAttentionMetadata, layer: torch.nn.Module, ) -> torch.Tensor: """Forward pass for encoder attention without KV cache. Args: query: shape = [num_encoder_tokens, num_heads, head_size] key: shape = [num_encoder_tokens, num_kv_heads, head_size] value: shape = [num_encoder_tokens, num_kv_heads, head_size] output: shape = [num_encoder_tokens, num_heads, head_size] attn_metadata: Encoder attention metadata layer: The attention layer """ assert self.vllm_flash_attn_version is not None, ( "FlashAttention version not detected." ) # For encoder attention, process FP8 quantization if needed if self.kv_cache_dtype.startswith("fp8"): raise NotImplementedError( "quantization is not supported for encoder attention" ) # Use encoder-specific metadata for sequence information cu_seqlens_q = attn_metadata.query_start_loc cu_seqlens_k = attn_metadata.query_start_loc max_seqlen_q = attn_metadata.max_query_len max_seqlen_k = attn_metadata.max_query_len descale_shape = ( cu_seqlens_q.shape[0] - 1, # type: ignore[union-attr] self.num_kv_heads, ) # Call flash attention directly on Q, K, V tensors sliding_window_size = ( list(self.sliding_window) if self.sliding_window is not None else None ) if current_platform.is_rocm(): vllm_flash_attn_varlen_func( q=query, k=key, v=value, out=output, cu_seqlens_q=cu_seqlens_q, cu_seqlens_k=cu_seqlens_k, max_seqlen_q=max_seqlen_q, max_seqlen_k=max_seqlen_k, softmax_scale=self.scale, causal=False, # Encoder attention is bidirectional alibi_slopes=self.alibi_slopes, window_size=sliding_window_size, softcap=self.logits_soft_cap, # fa_version=self.vllm_flash_attn_version, # q_descale=layer._q_scale.expand(descale_shape), # k_descale=layer._k_scale.expand(descale_shape), # v_descale=layer._v_scale.expand(descale_shape), q_descale=None, k_descale=layer._k_scale, v_descale=layer._v_scale, # num_splits=1 if self.batch_invariant_enabled else 0, is_prefix_cache=False, ) else: flash_attn_varlen_func( q=query, k=key, v=value, out=output, cu_seqlens_q=cu_seqlens_q, cu_seqlens_k=cu_seqlens_k, max_seqlen_q=max_seqlen_q, max_seqlen_k=max_seqlen_k, softmax_scale=self.scale, causal=False, # Encoder attention is bidirectional alibi_slopes=self.alibi_slopes, window_size=sliding_window_size, softcap=self.logits_soft_cap, fa_version=self.vllm_flash_attn_version, q_descale=layer._q_scale.expand(descale_shape), k_descale=layer._k_scale.expand(descale_shape), v_descale=layer._v_scale.expand(descale_shape), num_splits=1 if self.batch_invariant_enabled else 0, ) return output def use_cascade_attention( common_prefix_len: int, query_lens: np.ndarray, num_query_heads: int, num_kv_heads: int, use_alibi: bool, use_sliding_window: bool, use_local_attention: bool, num_sms: int, dcp_world_size: int, ) -> bool: """Decide whether to use cascade attention. This function 1) checks whether cascade attention is supported with the given configuration, and 2) heuristically decides whether using cascade attention can improve performance. """ # Too short common prefix. Probably not worth using cascade attention. # We use an arbitrary threshold of 256 tokens. TODO: Tune this threshold. # NOTE(woosuk): This is the common case. We should return False as soon as # possible to avoid any unnecessary computation. if common_prefix_len < 256: return False # Cascade attention is currently not supported with these variants. if use_alibi or use_sliding_window or use_local_attention: return False # Too few queries. Probably not worth using cascade attention. # We use an arbitrary threshold of 8 queries. TODO: Tune this threshold. num_reqs = len(query_lens) if num_reqs < 8: return False # disable cascade attention for DCP if dcp_world_size > 1: return False # Heuristics to decide whether using cascade attention is beneficial. # 1. When FlashDecoding is not used for normal attention, cascade attention # is likely to be faster since it saves memory bandwidth. num_queries_per_kv = num_query_heads // num_kv_heads # The criteria for using FlashDecoding can be found in the following link: # https://github.com/vllm-project/flash-attention/blob/96266b1111111f3d11aabefaf3bacbab6a89d03c/csrc/flash_attn/flash_api.cpp#L535 use_flash_decoding = ( num_queries_per_kv > 1 and not use_sliding_window and not use_alibi and np.all(query_lens == 1) ) if not use_flash_decoding: # Use cascade attention. return True # 2. When FlashDecoding is used for normal attention, it is not clear # whether cascade attention is beneficial, because FlashDecoding can # launch more CTAs than cascade attention. # We use a simple performance model to compare the two methods. # NOTE(woosuk): The performance model is very rough and may not be # accurate. num_tokens = num_reqs # NOTE(woosuk): These are default tile sizes. flash-attn might use # different tile sizes (e.g., 64 or 256) depending on the configuration. q_tile_size = 128 kv_tile_size = 128 num_prefix_tiles = cdiv(common_prefix_len, kv_tile_size) cascade_ctas = num_query_heads * cdiv(num_tokens, q_tile_size) cascade_waves = cdiv(cascade_ctas, num_sms) cascade_time = cascade_waves * num_prefix_tiles flash_decoding_ctas = ( num_reqs * num_kv_heads * cdiv(num_queries_per_kv, q_tile_size) ) flash_decoding_ctas *= num_prefix_tiles flash_decoding_time = cdiv(flash_decoding_ctas, num_sms) # Use cascade attention if it is faster than FlashDecoding. return cascade_time < flash_decoding_time def cascade_attention( output: torch.Tensor, query: torch.Tensor, key_cache: torch.Tensor, value_cache: torch.Tensor, cu_query_lens: torch.Tensor, max_query_len: int, cu_prefix_query_lens: torch.Tensor, prefix_kv_lens: torch.Tensor, suffix_kv_lens: torch.Tensor, max_kv_len: int, softmax_scale: float, alibi_slopes: torch.Tensor | None, sliding_window: tuple[int, int], logits_soft_cap: float, block_table: torch.Tensor, common_prefix_len: int, max_num_splits: int, fa_version: int, prefix_scheduler_metadata: torch.Tensor | None = None, suffix_scheduler_metadata: torch.Tensor | None = None, q_descale: torch.Tensor | None = None, k_descale: torch.Tensor | None = None, v_descale: torch.Tensor | None = None, s_aux: torch.Tensor | None = None, ) -> torch.Tensor: assert alibi_slopes is None, "Cascade attention does not support ALiBi." # TODO: Support sliding window. assert sliding_window == (-1, -1), ( "Cascade attention does not support sliding window." ) num_tokens = query.shape[0] block_size = key_cache.shape[-3] assert common_prefix_len % block_size == 0 num_common_kv_blocks = common_prefix_len // block_size assert num_common_kv_blocks > 0 if not current_platform.is_rocm(): descale_shape = (cu_prefix_query_lens.shape[0] - 1, key_cache.shape[-2]) # Process shared prefix. if current_platform.is_rocm(): prefix_output, prefix_lse, _ = vllm_flash_attn_varlen_func( q=query, k=key_cache, v=value_cache, cu_seqlens_q=cu_prefix_query_lens, seqused_k=prefix_kv_lens, max_seqlen_q=num_tokens, max_seqlen_k=common_prefix_len, softmax_scale=softmax_scale, causal=False, window_size=list(sliding_window), block_table=block_table[:1], softcap=logits_soft_cap, return_softmax_lse=True, scheduler_metadata=prefix_scheduler_metadata, fa_version=fa_version, q_descale=q_descale if q_descale is not None else None, k_descale=k_descale if k_descale is not None else None, v_descale=v_descale if v_descale is not None else None, # s_aux is incorporated into prefix_lse inside the GPU kernel, # enabling its effect during the final attention merge. s_aux=s_aux, # num_splits=1 if vllm_is_batch_invariant() else max_num_splits, is_prefix_cache=True, ) else: prefix_output, prefix_lse = flash_attn_varlen_func( q=query, k=key_cache, v=value_cache, cu_seqlens_q=cu_prefix_query_lens, seqused_k=prefix_kv_lens, max_seqlen_q=num_tokens, max_seqlen_k=common_prefix_len, softmax_scale=softmax_scale, causal=False, window_size=list(sliding_window), block_table=block_table[:1], softcap=logits_soft_cap, return_softmax_lse=True, scheduler_metadata=prefix_scheduler_metadata, fa_version=fa_version, q_descale=q_descale.expand(descale_shape) if q_descale is not None else None, k_descale=k_descale.expand(descale_shape) if k_descale is not None else None, v_descale=v_descale.expand(descale_shape) if v_descale is not None else None, # s_aux is incorporated into prefix_lse inside the GPU kernel, # enabling its effect during the final attention merge. s_aux=s_aux, num_splits=1 if vllm_is_batch_invariant() else max_num_splits, ) descale_shape = (cu_query_lens.shape[0] - 1, key_cache.shape[-2]) # Process suffix per query. if current_platform.is_rocm(): suffix_output, suffix_lse, _ = vllm_flash_attn_varlen_func( q=query, k=key_cache, v=value_cache, cu_seqlens_q=cu_query_lens, seqused_k=suffix_kv_lens, max_seqlen_q=max_query_len, max_seqlen_k=max_kv_len - common_prefix_len, softmax_scale=softmax_scale, causal=True, window_size=list(sliding_window), block_table=block_table[:, num_common_kv_blocks:], softcap=logits_soft_cap, return_softmax_lse=True, scheduler_metadata=suffix_scheduler_metadata, fa_version=fa_version, q_descale=q_descale if q_descale is not None else None, k_descale=k_descale if k_descale is not None else None, v_descale=v_descale if v_descale is not None else None, # num_splits=1 if vllm_is_batch_invariant() else max_num_splits, is_prefix_cache=True, ) else: suffix_output, suffix_lse = flash_attn_varlen_func( q=query, k=key_cache, v=value_cache, cu_seqlens_q=cu_query_lens, seqused_k=suffix_kv_lens, max_seqlen_q=max_query_len, max_seqlen_k=max_kv_len - common_prefix_len, softmax_scale=softmax_scale, causal=True, window_size=list(sliding_window), block_table=block_table[:, num_common_kv_blocks:], softcap=logits_soft_cap, return_softmax_lse=True, scheduler_metadata=suffix_scheduler_metadata, fa_version=fa_version, q_descale=q_descale.expand(descale_shape) if q_descale is not None else None, k_descale=k_descale.expand(descale_shape) if k_descale is not None else None, v_descale=v_descale.expand(descale_shape) if v_descale is not None else None, num_splits=1 if vllm_is_batch_invariant() else max_num_splits, ) # Merge prefix and suffix outputs, and store the result in output. merge_attn_states(output, prefix_output, prefix_lse, suffix_output, suffix_lse)