# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations from dataclasses import dataclass from functools import cached_property, lru_cache from typing import TYPE_CHECKING import paddle from paddle import _C_ops from paddle.base.log_helper import get_logger from paddle.nn.attention.sdpa import ( SDPBackend, _get_backend_priority, _get_enabled_backends, ) _logger = get_logger( __name__, "INFO", fmt='%(asctime)s-%(levelname)s: %(message)s' ) from paddle.nn.functional.flash_attention import _math_attention if TYPE_CHECKING: from paddle import Tensor, dtype from paddle.base.core import Place _config = {} def init_config(): global _config _config = { "flash_attn": { "MINIMUM_SM_VERSION": (8, 0), "MAXIMUM_SM_VERSION": (12, 1), "support_dtypes": (paddle.float16, paddle.bfloat16) if paddle.device.is_bf16_supported(including_emulation=False) else (paddle.float16,), }, "mem_efficient_attn": { "MINIMUM_SM_VERSION": (5, 0), "MAXIMUM_SM_VERSION": (12, 1), "support_dtypes": ( paddle.float16, paddle.bfloat16, paddle.float, ) if paddle.device.is_bf16_supported(including_emulation=False) else (paddle.float16, paddle.float), }, } def _repeat_kv(key: Tensor, value: Tensor, num_repeats: int): """ Repeat key and value tensors along the num_heads(3) dimension. The layout of key and value should be [batch_size, seq_len, num_heads, head_dim]. """ if num_repeats == 1: return key, value # repeat_interleave does not support float16 on GPU, so we manually expand the tensor key, value = key.unsqueeze(3), value.unsqueeze(3) key, value = ( key.expand([-1, -1, -1, num_repeats, -1]), value.expand([-1, -1, -1, num_repeats, -1]), ) key, value = ( key.flatten(2, 3).contiguous(), value.flatten(2, 3).contiguous(), ) return key, value @dataclass class SDPParams: query_shape: paddle.Size key_shape: paddle.Size value_shape: paddle.Size attn_mask_shape: paddle.Size | None dropout: float is_causal: bool scale: float | None query_stop_gradient: bool dtype: tuple[dtype, dtype, dtype] place: tuple[Place, Place, Place] @cached_property def batch_size(self) -> tuple[int, int, int]: return self.query_shape[0], self.key_shape[0], self.value_shape[0] @cached_property def seq_len(self) -> tuple[int, int, int]: return self.query_shape[1], self.key_shape[1], self.value_shape[1] @cached_property def num_heads(self) -> tuple[int, int, int]: return self.query_shape[2], self.key_shape[2], self.value_shape[2] @cached_property def head_dim(self) -> tuple[int, int, int]: return self.query_shape[-1], self.key_shape[-1], self.value_shape[-1] @cached_property def device_id(self) -> tuple[int, ...]: ret = tuple( pl.gpu_device_id() if pl.is_gpu_place() else -1 for pl in self.place ) return ret @lru_cache(maxsize=8) def get_device_capability(device_id: int) -> tuple[int, int]: if device_id < 0: return (0, 0) return paddle.device.cuda.get_device_capability(device_id) @lru_cache(maxsize=32) def check_sm_version( min_sm: tuple[int, int], max_sm: tuple[int, int], device_id: int = 0 ) -> bool: major, minor = get_device_capability(device_id) current = (major, minor) return min_sm <= current <= max_sm @lru_cache(maxsize=1) def check_cuda_is_available() -> bool: return paddle.is_compiled_with_cuda() and paddle.cuda.is_available() def check_all_tensors_on_device(params: SDPParams): """ Check all input tensors are placed on the GPU device. """ if not ( params.place[0].is_gpu_place() or params.place[0].is_custom_place() ): _logger.debug( "All input tensors should be placed on GPU or custom place, but " f"query place: {params.place[0]}, key place: " f"{params.place[1]}, value place: {params.place[2]}" ) return False return True def check_head_dim_size_flash(params: SDPParams): """ Check the dimension of head in query, key, and value should be equal and all less than 256. """ q_head_dim, k_head_dim, v_head_dim = params.head_dim if q_head_dim > 256 or q_head_dim != k_head_dim or k_head_dim != v_head_dim: _logger.debug( "The dimension of head in query, key, and value should be equal and all less than 256, " f"but q_head_dim: {q_head_dim}, k_head_dim: {k_head_dim}, v_head_dim: {v_head_dim}" ) return False if q_head_dim % 8 != 0: _logger.debug( "The dimension of head in query, key, and value should be a multiple of 8, " f"but q_head_dim: {q_head_dim}" ) return False return True @lru_cache(maxsize=8) def check_flash_attention_hardware_support(device_id: int): """ Check flash attention requires CUDA support and SM between 8.0 and 12.1. """ if SDPBackend.FLASH_ATTENTION and paddle.is_compiled_with_custom_device( paddle.device.get_all_device_type()[0] ): return True if not check_cuda_is_available(): _logger.debug("Flash attention requires CUDA support.") return False if not check_sm_version( _config["flash_attn"]["MINIMUM_SM_VERSION"], _config["flash_attn"]["MAXIMUM_SM_VERSION"], device_id, ): _logger.debug( f"Flash attention requires SM between {_config['flash_attn']['MINIMUM_SM_VERSION']}" f"and {_config['flash_attn']['MAXIMUM_SM_VERSION']}, but found SM " f"{get_device_capability(device_id)}" ) return False return True def check_flash_causal_non_square_seqlens(params: SDPParams): """ Check flash attention only supports causal attention when the sequence length of query and key are equal. """ if not params.is_causal: return True q_len, k_len, _ = params.seq_len if q_len == k_len: return True _logger.debug( f"Flash attention only supports causal attention when the sequence" f"length of query and key are equal, but got query shape: " f"{params.query_shape}, key shape: {params.key_shape}" ) return False def check_dtypes_low_precision_fa(params: SDPParams): """ check QKV share the same dtype and are supported dtype. """ q_dtype, k_dtype, v_dtype = params.dtype if ( q_dtype != k_dtype or v_dtype != k_dtype or q_dtype not in _config["flash_attn"]["support_dtypes"] ): _logger.debug( f"Flash attention requires query, key, and value " f"to be of the same dtype and support dtype, but " f"got query dtype: {q_dtype}, key dtype: {k_dtype}" f", value dtype: {v_dtype}. Supported dtypes are: " f"{_config['flash_attn']['support_dtypes']}" ) return False return True def check_dtypes_low_precision_mem_efficient_attn(params: SDPParams): """ check QKV share the same dtype and are supported dtype. """ q_dtype, k_dtype, v_dtype = params.dtype if ( q_dtype != k_dtype or v_dtype != k_dtype or q_dtype not in _config["mem_efficient_attn"]["support_dtypes"] ): _logger.debug( f"Mem_efficient_attn requires query, key, and value " f"to be of the same dtype and support dtype, but " f"got query dtype: {q_dtype}, key dtype: {k_dtype}" f", value dtype: {v_dtype}. Supported dtypes are: " f"{_config['mem_efficient_attn']['support_dtypes']}" ) return False return True @lru_cache(maxsize=2) def use_tensor_cores(is_half: bool, device_id: int) -> bool: major, _ = get_device_capability(device_id) if major >= 8: return True if major == 7: return is_half return False @lru_cache(maxsize=32) def minimum_gemm_alignment(dtype: dtype, device_id: int): is_half = dtype in (paddle.float16, paddle.bfloat16) use_tc = use_tensor_cores(is_half, device_id) major, _ = get_device_capability(device_id) matmul_alignment_mn = 4 if major > 8 else 1 bits_per_scalar = 16 if is_half else 32 if use_tc: matmul_alignment_mn = max(matmul_alignment_mn, 128 / bits_per_scalar) return matmul_alignment_mn @lru_cache(maxsize=8) def check_mem_efficient_hardware_support(device_id: int): """ Check mem_efficient attention requires CUDA support and SM between 5.0 and 12.1. """ if not check_cuda_is_available(): _logger.debug("Mem efficient attention requires CUDA support.") return False if not check_sm_version( _config["mem_efficient_attn"]["MINIMUM_SM_VERSION"], _config["mem_efficient_attn"]["MAXIMUM_SM_VERSION"], device_id, ): _logger.debug( f"Mem efficient attention requires SM between {_config['mem_efficient_attn']['MINIMUM_SM_VERSION']}" f"and {_config['mem_efficient_attn']['MAXIMUM_SM_VERSION']}, but found SM " f"{get_device_capability(device_id)}" ) return False return True def check_head_dim_size_mem_efficient(params: SDPParams): q_head_dim, k_head_dim, v_head_dim = ( params.query_shape[-1], params.key_shape[-1], params.value_shape[-1], ) alignment = minimum_gemm_alignment(params.dtype[0], params.device_id[0]) if ( q_head_dim % alignment != 0 or k_head_dim % alignment != 0 or v_head_dim % alignment != 0 ): _logger.debug( f"Mem efficient attention requires head dim size aligned to {alignment}, " f"but found q_head_dim: {q_head_dim}, k_head_dim: {k_head_dim}, v_head_dim: {v_head_dim}" ) return False return True def check_attn_mask_alignment(params: SDPParams) -> bool: if params.is_causal: return True if params.attn_mask_shape is None: return True last_dim = params.attn_mask_shape[-1] if last_dim % 8 != 0: _logger.debug( f"Mem efficient attention requires attn_mask last dimension to be divisible by 8 " f"to satisfy vector alignment, but got {last_dim}. " "Falling back to other backends." ) return False return True def check_scale_is_None(params: SDPParams) -> bool: if params.scale is None: return True _logger.debug("Paddle's FAV2 does not support scale parameter.") return False def can_use_flash_attention(params: SDPParams = False) -> bool: general_constraints = [ check_all_tensors_on_device, check_head_dim_size_flash, check_flash_causal_non_square_seqlens, check_dtypes_low_precision_fa, check_scale_is_None, ] for constraint in general_constraints: if not constraint(params): return False if not check_flash_attention_hardware_support(params.device_id[0]): return False return True def can_use_mem_efficient_attention(params: SDPParams = False) -> bool: constraints = [ check_all_tensors_on_device, check_head_dim_size_mem_efficient, check_attn_mask_alignment, check_dtypes_low_precision_mem_efficient_attn, ] for constraint in constraints: if not constraint(params): return False if not check_mem_efficient_hardware_support(params.device_id[0]): return False return True def select_sdp_for_sdpa(param: SDPParams) -> str: # Note: This API is designed for nn.functional.scaled_dot_product_attention, # and is **NOT** expected to be called by others. Some promises should be guaranteed # by caller to skip some rarely unmet constraints: # 1. The input dim is 4, layout is (batch, seq_len, num_heads, head_dim) # 2. The batch_size and num_heads of each input should be the same place = paddle.get_device() if "xpu" in place: return "flash_attn" enabled_backends = _get_enabled_backends() priority_order = _get_backend_priority() for backend in priority_order: if backend not in enabled_backends: continue if backend == SDPBackend.FLASH_ATTENTION: if can_use_flash_attention(param): return "flash_attn" elif backend == SDPBackend.EFFICIENT_ATTENTION: if can_use_mem_efficient_attention(param): return "mem_efficient" elif backend == SDPBackend.MATH: return "math" raise RuntimeError( "No available backend for scaled_dot_product_attention was found." ) def scaled_dot_product_attention( query: Tensor, key: Tensor, value: Tensor, attn_mask: Tensor | None = None, dropout_p: float = 0.0, is_causal: bool = False, training: bool = True, backend: str | None = None, scale: float | None = None, enable_gqa: bool = True, name: str | None = None, ) -> Tensor: r""" The equation is: .. math:: result=softmax(\frac{ Q * K^T }{\sqrt{d}}) * V where : ``Q``, ``K``, and ``V`` represent the three input parameters of the attention module. The dimensions of the three parameters are the same. ``d`` represents the size of the last dimension of the three parameters. Warning: This API only verifies inputs with dtype float16 and bfloat16, other dtypes may fall back to math implementation, which is less optimized. Warning: If is_causal is set to True, the causal mask should not be provided, otherwise the provided mask will be ignored. Note: This API differs from :ref:`api_paddle_compat_nn_functional_scaled_dot_product_attention` in that: 1. The QKV layout of this API is [batch_size, seq_len, num_heads, head_dim] or [seq_len, num_heads, head_dim]. If you need num_heads before seq_len layout, please use ``paddle.compat.nn.functional.scaled_dot_product_attention``. Args: query(Tensor): The query tensor in the Attention module. 4-D tensor with shape: [batch_size, seq_len_key, num_heads, head_dim]. 3-D tensor with shape: [seq_len_key, num_heads, head_dim]. The dtype can be float16 or bfloat16. key(Tensor): The key tensor in the Attention module. 4-D tensor with shape: [batch_size, seq_len_key, num_heads, head_dim]. 3-D tensor with shape: [seq_len_key, num_heads, head_dim]. The dtype can be float16 or bfloat16. value(Tensor): The value tensor in the Attention module. 4-D tensor with shape: [batch_size, seq_len_value, num_heads, head_dim]. 3-D tensor with shape: [seq_len_value, num_heads, head_dim]. The dtype can be float16 or bfloat16. attn_mask(Tensor, optional): The attention mask tensor. The shape should be broadcastable to [batch_size, num_heads, seq_len_key, seq_len_query]. The dtype can be bool or same type of query. The bool mask indicates the positions should take part in attention. The non-bool mask will be added to attention score. dropout_p(float, optional): The dropout ratio. is_causal(bool, optional): Whether enable causal mode. training(bool, optional): Whether it is in the training phase. backend(str, optional): Specify which backend to compute scaled dot product attention. Currently only support "p2p" for distribution usage. scale(float, optional): The scaling factor used in the calculation of attention weights. If None, scale = 1 / sqrt(head_dim). enable_gqa(bool, optional): Whether enable GQA(Group Query Attention) mode. Default is True. name(str|None, optional): The default value is None. Normally there is no need for user to set this property. For more information, please refer to :ref:`api_guide_Name`. Returns: out(Tensor): The attention tensor. 4-D tensor with shape: [batch_size, seq_len, num_heads, head_dim]. 3-D tensor with shape: [seq_len, num_heads, head_dim]. The dtype can be float16 or bfloat16. Examples: .. code-block:: pycon >>> # doctest: +SKIP('bfloat need V100 compile') >>> import paddle >>> q = paddle.rand((1, 128, 2, 16), dtype=paddle.bfloat16) >>> output = paddle.nn.functional.scaled_dot_product_attention(q, q, q, None, 0.9, False) >>> print(output) >>> # doctest: -SKIP """ is_batched = query.dim() == 4 if not is_batched: # FlashAttention backend does not support unbatched input, # we add batch dim here and will skip check input dim when selecting FA backend. query = query.unsqueeze(0) key = key.unsqueeze(0) value = value.unsqueeze(0) k_heads, q_heads, v_heads = ( key.shape[2], query.shape[2], value.shape[2], ) if enable_gqa: assert k_heads == 0 or q_heads % k_heads == 0, ( f"The number of groups in query({q_heads}) must be divisible by the number of groups in key({k_heads}) if GQA enabled." ) assert k_heads == v_heads, ( f"The number of groups in key({k_heads}) must be equal to the number of groups in value({v_heads}) if GQA enabled." ) else: assert q_heads == k_heads == v_heads, ( f"The number of groups in query({q_heads}) must be equal to the number of groups in key({k_heads}) " f"and the number of groups in value({v_heads}) if GQA disabled." ) bs, seq_len_q, num_heads_q, head_dim_q = query.shape _, seq_len_k, num_heads_k, head_dim_k = key.shape if ( backend == 'p2p' and query.is_dist() and key.is_dist() and value.is_dist() ): # ring attention for auto_parallel mode assert scale is None, f"Backend {backend} not support scale parameter." out = paddle.distributed.auto_parallel.ring_attention.RingFlashAttention.apply( query, key, value, attn_mask, dropout_p, is_causal, ) return out if not paddle.base.in_dygraph_mode(): qkv_place = (paddle.framework._current_expected_place_(),) * 3 else: qkv_place = (query.place, key.place, value.place) param = SDPParams( query_shape=query.shape, key_shape=key.shape, value_shape=value.shape, attn_mask_shape=attn_mask.shape if attn_mask is not None else None, dropout=dropout_p, is_causal=is_causal, scale=scale, query_stop_gradient=query.stop_gradient, dtype=(query.dtype, key.dtype, value.dtype), place=qkv_place, ) if len(_config) == 0: init_config() is_zero_size = ( query.shape.numel() == 0 or key.shape.numel() == 0 or value.shape.numel() == 0 ) if attn_mask is not None: if attn_mask.dtype == paddle.bool: attn_mask = paddle.where( attn_mask, paddle.to_tensor(0.0, dtype=query.dtype), paddle.to_tensor(-float('inf'), dtype=query.dtype), ) if is_zero_size: sdp_func_name = "math" else: sdp_func_name = select_sdp_for_sdpa(param) _logger.debug("Selected backend:" + sdp_func_name) if sdp_func_name == "flash_attn": fixed_seed_offset = None return_softmax = False rng_name = "" if attn_mask is not None: if attn_mask.ndim == 2: attn_mask = attn_mask.expand([bs, 1, *attn_mask.shape]) elif attn_mask.ndim == 3: attn_mask = paddle.unsqueeze(attn_mask, axis=1) out, _, _, _ = _C_ops.flash_attn( query, key, value, fixed_seed_offset, attn_mask, dropout_p, is_causal, return_softmax, not training, rng_name, ) elif sdp_func_name == "mem_efficient": from paddle.incubate.nn.memory_efficient_attention import ( LowerTriangularMask, memory_efficient_attention, ) repeats = q_heads // k_heads key, value = _repeat_kv(key, value, repeats) if is_causal: attn_mask = LowerTriangularMask() elif attn_mask is not None: # if need broadcast, memory_efficient_attention requires to # broadcast first two dim simultaneously if attn_mask.dim() == 3: attn_mask = attn_mask.unsqueeze(axis=1) if attn_mask.dim() == 4 and ( attn_mask.shape[0] != bs ^ attn_mask.shape[1] != num_heads_q ): attn_mask = attn_mask.expand( [ bs, num_heads_q, attn_mask.shape[2], attn_mask.shape[3], ] ) out = memory_efficient_attention( query, key, value, attn_bias=attn_mask, p=dropout_p, scale=scale, training=training, ) elif sdp_func_name == "math": repeats = q_heads // k_heads if k_heads != 0 else 1 key, value = _repeat_kv(key, value, repeats) if attn_mask is not None and attn_mask.dim() == 3: attn_mask = attn_mask.unsqueeze(axis=1) out = _math_attention( query, key, value, attn_mask, dropout_p, is_causal, False, training, scale, )[0] else: raise ValueError(f"Invalid backend {backend}") if not is_batched: out = paddle.squeeze(out, axis=0) return out