Files
2026-07-13 12:40:42 +08:00

564 lines
21 KiB
Python

# 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 typing import TYPE_CHECKING
import paddle
import paddle.nn.functional as F
from paddle import nn
from paddle.nn.initializer import XavierNormal, XavierUniform
if TYPE_CHECKING:
from paddle import Tensor
from paddle._typing import DTypeLike, PlaceLike
class MultiheadAttention(nn.Layer):
r"""
Allows the model to jointly attend to information from different representation subspaces.
Multi-Head Attention is defined as:
.. math::
\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1,\dots,\text{head}_h)W^O
where :math:`\text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)`.
Please refer to `Attention Is All You Need <https://arxiv.org/pdf/1706.03762.pdf>`_
for more details.
.. note::
This layer will use the optimized implementation
:func:`paddle.nn.functional.scaled_dot_product_attention` if no need to return the attention weights.
Parameters:
embed_dim (int): Total dimension of the model.
num_heads (int): The number of heads in multi-head attention.
dropout (float, optional): The dropout probability used on attention
weights to drop some attention targets. 0 for no dropout. Default 0.0.
bias (bool, optional): If specified, adds bias to input / output projection layers.
Default: True.
add_bias_kv (bool, optional): If specified, adds bias to the key and value sequences
at axis=0. Default: False.
add_zero_attn (bool, optional): If specified, adds a new batch of zeros to the
key and value sequences at axis=1. Default: False.
kdim (int, optional): Total number of features for keys. If None, assumed equal to
`embed_dim`. Default: None.
vdim (int, optional): Total number of features for values. If None, assumed equal to
`embed_dim`. Default: None.
batch_first (bool, optional): If True, then the input and output tensors are provided
as [batch, seq, feature]. Default: False.
device (PlaceLike|None, optional): The device to initialize parameters on. Default: None.
dtype (DTypeLike|None, optional): The data type of the parameters. Default: None.
Examples:
.. code-block:: pycon
>>> import paddle
>>> from paddle.compat import nn
>>> # Example with batch_first=True
>>> embed_dim, num_heads = 128, 8
>>> multihead_attn = nn.MultiheadAttention(embed_dim, num_heads, batch_first=True)
>>> # query: [batch_size, target_seq_len, embed_dim]
>>> query = paddle.randn([32, 10, embed_dim])
>>> # key, value: [batch_size, source_seq_len, embed_dim]
>>> key = paddle.randn([32, 20, embed_dim])
>>> value = paddle.randn([32, 20, embed_dim])
>>> attn_output, attn_output_weights = multihead_attn(query, key, value)
>>> print(attn_output.shape)
paddle.Size([32, 10, 128])
"""
def __init__(
self,
embed_dim: int,
num_heads: int,
dropout: float = 0.0,
bias: bool = True,
add_bias_kv: bool = False,
add_zero_attn: bool = False,
kdim: int | None = None,
vdim: int | None = None,
batch_first: bool = False,
device: PlaceLike | None = None,
dtype: DTypeLike | None = None,
) -> None:
if dtype:
super().__init__(dtype=dtype)
else:
super().__init__()
self.embed_dim = embed_dim
self.kdim = kdim if kdim is not None else embed_dim
self.vdim = vdim if vdim is not None else embed_dim
self._qkv_same_embed_dim = (
self.kdim == embed_dim and self.vdim == embed_dim
)
self.num_heads = num_heads
self.dropout = dropout
self.batch_first = batch_first
self.head_dim = embed_dim // num_heads
assert self.head_dim * num_heads == self.embed_dim
self.in_proj_bias = None
self.q_proj_bias = None
self.k_proj_bias = None
self.v_proj_bias = None
if self._qkv_same_embed_dim:
self.in_proj_weight = self.create_parameter(
shape=[3 * embed_dim, embed_dim],
dtype=self._dtype,
is_bias=False,
device=device,
default_initializer=XavierUniform(),
)
self.q_proj_weight = None
self.k_proj_weight = None
self.v_proj_weight = None
if bias:
self.in_proj_bias = self.create_parameter(
shape=[3 * embed_dim],
dtype=self._dtype,
is_bias=True,
device=device,
)
else:
self.q_proj_weight = self.create_parameter(
shape=[embed_dim, embed_dim],
dtype=self._dtype,
is_bias=False,
device=device,
default_initializer=XavierUniform(),
)
self.k_proj_weight = self.create_parameter(
shape=[embed_dim, self.kdim],
dtype=self._dtype,
is_bias=False,
device=device,
default_initializer=XavierUniform(),
)
self.v_proj_weight = self.create_parameter(
shape=[embed_dim, self.vdim],
dtype=self._dtype,
is_bias=False,
device=device,
default_initializer=XavierUniform(),
)
self.in_proj_weight = None
if bias:
self.q_proj_bias = self.create_parameter(
shape=[embed_dim],
dtype=self._dtype,
is_bias=True,
device=device,
)
self.k_proj_bias = self.create_parameter(
shape=[embed_dim],
dtype=self._dtype,
is_bias=True,
device=device,
)
self.v_proj_bias = self.create_parameter(
shape=[embed_dim],
dtype=self._dtype,
is_bias=True,
device=device,
)
self.out_proj = paddle.compat.nn.Linear(
embed_dim, embed_dim, bias=bias, dtype=self._dtype
)
self.add_bias_kv = add_bias_kv
self.add_zero_attn = add_zero_attn
if add_bias_kv:
self.bias_k = self.create_parameter(
shape=[1, 1, embed_dim],
dtype=self._dtype,
is_bias=True,
device=device,
default_initializer=XavierNormal(),
)
self.bias_v = self.create_parameter(
shape=[1, 1, embed_dim],
dtype=self._dtype,
is_bias=True,
device=device,
default_initializer=XavierNormal(),
)
else:
self.bias_k = self.bias_v = None
def _convert_bool_mask_to_float(
self, mask: paddle.Tensor, dtype: DTypeLike
) -> paddle.Tensor:
"""
Convert boolean mask to float mask. True -> -inf, False -> 0.0
Args:
mask (paddle.Tensor): boolean mask
dtype (DTypeLike): float dtype
Returns:
paddle.Tensor: float mask
"""
assert mask.dtype == paddle.bool, (
f"mask must be boolean, but got {mask.dtype}"
)
filler = paddle.to_tensor(paddle.finfo(dtype).min, dtype=dtype)
return paddle.where(mask, filler, paddle.zeros_like(mask, dtype=dtype))
def _combine_masks(
self, mask1: paddle.Tensor, mask2: paddle.Tensor, dtype: DTypeLike
) -> paddle.Tensor:
"""
Safely combine two masks, mask can be bool or float.
If both mask are bool, this function equals to
paddle.logical_or(mask1, mask2) and return boolean mask.
Otherwise, the boolean mask will be converted to float and combined with
the float mask using addition.
Args:
mask1 (paddle.Tensor): mask1
mask2 (paddle.Tensor): mask2
Returns:
paddle.Tensor: combined mask
"""
if mask1.dtype == paddle.bool and mask2.dtype == paddle.bool:
return mask1 | mask2
if mask1.dtype == paddle.bool:
mask1 = self._convert_bool_mask_to_float(mask1, dtype=dtype)
if mask2.dtype == paddle.bool:
mask2 = self._convert_bool_mask_to_float(mask2, dtype=dtype)
return mask1 + mask2
def _pad_mask(self, mask: Tensor, pad_amt: int = 1) -> Tensor:
shape = mask.shape
pad_shape = [*shape[:-1], pad_amt]
pad_tensor = paddle.zeros(pad_shape, dtype=mask.dtype)
return paddle.concat([mask, pad_tensor], axis=-1)
def _project_qkv(
self, query: Tensor, key: Tensor, value: Tensor
) -> tuple[Tensor, Tensor, Tensor]:
# in: [batch, seq_len, embed]
# out: [batch, seq_len, embed]
if self._qkv_same_embed_dim:
if id(query) == id(key) and id(key) == id(value):
qkv = F.linear(query, self.in_proj_weight.T, self.in_proj_bias)
q, k, v = qkv.split(3, axis=-1)
else:
q_w, k_w, v_w = self.in_proj_weight.chunk(3, axis=0)
q_b, k_b, v_b = (
self.in_proj_bias.chunk(3, axis=0)
if self.in_proj_bias is not None
else (None,) * 3
)
q = F.linear(query, q_w.T, q_b)
k = F.linear(key, k_w.T, k_b)
v = F.linear(value, v_w.T, v_b)
else:
q = F.linear(query, self.q_proj_weight.T, self.q_proj_bias)
k = F.linear(key, self.k_proj_weight.T, self.k_proj_bias)
v = F.linear(value, self.v_proj_weight.T, self.v_proj_bias)
return q, k, v
def _prepare_qkv_heads(
self,
q: Tensor,
k: Tensor,
v: Tensor,
batch_size: int,
target_seq_len: int,
) -> tuple[Tensor, Tensor, Tensor]:
# in: [batch, seq_len, num_head * dim]
# out: [batch, num_head, seq_len, dim]
if self.add_bias_kv:
k = paddle.concat(
[k, self.bias_k.expand([batch_size, -1, -1])], axis=1
)
v = paddle.concat(
[v, self.bias_v.expand([batch_size, -1, -1])], axis=1
)
q = q.reshape(
[batch_size, target_seq_len, self.num_heads, self.head_dim]
).transpose([0, 2, 1, 3])
current_src_len = k.shape[1]
k = k.reshape(
[batch_size, current_src_len, self.num_heads, self.head_dim]
).transpose([0, 2, 1, 3])
v = v.reshape(
[batch_size, current_src_len, self.num_heads, self.head_dim]
).transpose([0, 2, 1, 3])
if self.add_zero_attn:
zeros = paddle.zeros(
[batch_size, self.num_heads, 1, self.head_dim], dtype=k.dtype
)
k = paddle.concat([k, zeros], axis=2)
v = paddle.concat([v, zeros], axis=2)
return q, k, v
def _prepare_attn_mask(
self,
attn_mask: Tensor | None,
key_padding_mask: Tensor | None,
target_seq_len: int,
src_len_before_bias: int,
dtype: DTypeLike,
batch_size: int,
is_causal: bool,
need_weights: bool,
) -> Tensor | None:
# Do not generate attn_mask if is_causal is True and add_bias_kv is False
# and add_zero_attn is False. In such case, we pass attn_mask as None to
# select efficient implementation backend of sdpa.
if (
is_causal
and not self.add_bias_kv
and not self.add_zero_attn
and key_padding_mask is None
and not need_weights
):
return None
if attn_mask is None and not is_causal and key_padding_mask is None:
return None
if attn_mask is None:
if is_causal:
attn_mask = paddle.triu(
paddle.ones(
[target_seq_len, src_len_before_bias], dtype=paddle.bool
),
diagonal=1,
)
else:
attn_mask = paddle.zeros(
[target_seq_len, src_len_before_bias], dtype=dtype
)
pad_count = int(self.add_zero_attn + self.add_bias_kv)
if pad_count > 0:
attn_mask = self._pad_mask(attn_mask, pad_amt=pad_count)
if key_padding_mask is not None:
key_padding_mask = self._pad_mask(
key_padding_mask, pad_amt=pad_count
)
if attn_mask.dim() == 2:
attn_mask = attn_mask.expand(
[batch_size * self.num_heads, *attn_mask.shape]
)
if attn_mask.dim() == 3:
attn_mask = attn_mask.reshape(
[batch_size, self.num_heads, target_seq_len, -1]
)
if key_padding_mask is not None:
# [N, len_k+pad_count] -> [N, 1, 1, len_k+pad_count]
key_padding_mask = key_padding_mask.unsqueeze(axis=[1, 2])
key_padding_mask = key_padding_mask.repeat(
[1, *attn_mask.shape[1:3], 1]
)
attn_mask = self._combine_masks(attn_mask, key_padding_mask, dtype)
if attn_mask.dtype != dtype:
if attn_mask.dtype == paddle.bool:
attn_mask = self._convert_bool_mask_to_float(attn_mask, dtype)
else:
attn_mask = attn_mask.astype(dtype)
return attn_mask
def _attention_core(
self,
q: Tensor,
k: Tensor,
v: Tensor,
final_mask: Tensor | None,
need_weights: bool,
is_causal: bool,
) -> tuple[Tensor, Tensor | None]:
# in: [batch, num_head, seq_len, head_dim]
# out: [batch, num_head, seq_len, head_dim]
batch_size, _, target_seq_len, _ = q.shape
is_causal = is_causal and final_mask is None
if not need_weights:
attn_output = (
paddle.compat.nn.functional.scaled_dot_product_attention(
q,
k,
v,
attn_mask=final_mask,
dropout_p=self.dropout if self.training else 0.0,
is_causal=is_causal,
)
)
attn_output = attn_output.transpose([0, 2, 1, 3])
attn_output = attn_output.reshape(
[batch_size, target_seq_len, self.embed_dim]
)
return attn_output, None
else:
scores = paddle.matmul(q, k, transpose_y=True)
scores = scores / (self.head_dim**0.5)
if final_mask is not None:
if final_mask.dtype == paddle.bool:
final_mask = self._convert_bool_mask_to_float(
final_mask, scores.dtype
)
scores = scores + final_mask
weights = F.softmax(scores, axis=-1)
weights = F.dropout(weights, self.dropout, training=self.training)
ctx = paddle.matmul(weights, v)
attn_output = ctx.transpose([0, 2, 1, 3]).reshape(
[batch_size, target_seq_len, self.embed_dim]
)
return attn_output, weights if need_weights else None
def forward(
self,
query: paddle.Tensor,
key: paddle.Tensor,
value: paddle.Tensor,
key_padding_mask: paddle.Tensor | None = None,
need_weights: bool = True,
attn_mask: paddle.Tensor | None = None,
average_attn_weights: bool = True,
is_causal: bool = False,
) -> tuple[paddle.Tensor, paddle.Tensor | None]:
r"""
Forward pass of the MultiheadAttention layer.
.. note::
If ``need_weights`` is ``False``, this api will fallback to native math implementation,
otherwise it will call ``paddle.compat.nn.functional.scaled_dot_product_attention`` to
compute the attention score.
To achieve better performance, explicitly set ``need_weights=False``,
and set ``is_causal=True`` if the attn_mask is the causal mask.
Parameters:
query (Tensor): The query embeddings. Shape depends on `batch_first`.
If `batch_first` is False, shape is `[target_seq_len, batch_size, embed_dim]`.
If `batch_first` is True, shape is `[batch_size, target_seq_len, embed_dim]`.
key (Tensor): The key embeddings. Shape depends on `batch_first`.
If `batch_first` is False, shape is `[source_seq_len, batch_size, kdim]`.
If `batch_first` is True, shape is `[batch_size, source_seq_len, kdim]`.
value (Tensor): The value embeddings. Shape depends on `batch_first`.
If `batch_first` is False, shape is `[source_seq_len, batch_size, vdim]`.
If `batch_first` is True, shape is `[batch_size, source_seq_len, vdim]`.
key_padding_mask (Tensor, optional): If specified, a mask indicating which
elements within `key` to ignore for the purpose of attention (i.e. treat as "padding").
Can be a boolean mask (True indicates padding) or a float mask.
Shape is `[batch_size, source_seq_len]`. Default: None.
need_weights (bool, optional): Indicate whether to return the attention
weights. Default: True.
attn_mask (Tensor, optional): 2D or 3D mask that prevents attention to certain positions.
A 2D mask will be broadcasted for all batches while a 3D mask allows different masks
for the entries in the batch. Shape is `[target_seq_len, source_seq_len]` or
`[batch_size * num_heads, target_seq_len, source_seq_len]`. Default: None.
average_attn_weights (bool, optional): If True, indicates that the returned
`attn_weights` should be averaged across heads. Default: True.
is_causal (bool, optional): If True, implies that a causal mask is applied to
the attention implementation. If attn_mask is None and is_causal is True,
a causal mask is automatically created and used in the attention computation.
Default: False.
Returns:
tuple[Tensor, Tensor|None]:
- **attn_output** (Tensor): The output of the attention mechanism.
Shape matches `query` (based on `batch_first`).
- **attn_output_weights** (Tensor|None): The attention weights. Returns None if
`need_weights` is False. Shape is `[batch_size, target_seq_len, source_seq_len]`
if `average_attn_weights` is True.
If `average_attn_weights` is False, shape is
`[batch_size, num_heads, target_seq_len, source_seq_len]`.
"""
is_batched = query.dim() == 3
if not is_batched:
query = query.unsqueeze(0 if self.batch_first else 1)
key = key.unsqueeze(0 if self.batch_first else 1)
value = value.unsqueeze(0 if self.batch_first else 1)
if key_padding_mask is not None and key_padding_mask.dim() != 2:
key_padding_mask = key_padding_mask.unsqueeze(0)
if not self.batch_first:
query = query.transpose([1, 0, 2])
key = key.transpose([1, 0, 2])
value = value.transpose([1, 0, 2])
batch_size, target_seq_len, _ = query.shape
src_len_before_bias = key.shape[1]
if key_padding_mask is not None:
assert key_padding_mask.shape == (batch_size, src_len_before_bias)
q, k, v = self._project_qkv(query, key, value)
q, k, v = self._prepare_qkv_heads(q, k, v, batch_size, target_seq_len)
final_mask = self._prepare_attn_mask(
attn_mask=attn_mask,
key_padding_mask=key_padding_mask,
target_seq_len=target_seq_len,
src_len_before_bias=src_len_before_bias,
dtype=q.dtype,
batch_size=batch_size,
is_causal=is_causal,
need_weights=need_weights,
)
attn_output, attn_weights = self._attention_core(
q, k, v, final_mask, need_weights, is_causal
)
attn_output = self.out_proj(attn_output)
if not self.batch_first:
attn_output = attn_output.transpose([1, 0, 2])
if need_weights and attn_weights is not None:
if average_attn_weights:
attn_weights = attn_weights.mean(axis=1)
if not is_batched:
attn_output = attn_output.squeeze(0 if self.batch_first else 1)
if attn_weights is not None:
attn_weights = attn_weights.squeeze(0)
return attn_output, attn_weights