全部文章
Attention计算进化史deep-learning · ai-infra

Attention计算进化史(1)--Attention 与 MHA 的计算原理及实现

《Attention计算进化史》系列的第一篇,关于Scaled Dot-product Attention、Causal Mask、MHA的流程和计算。

《Attention is All You Need》提出了Transformer架构及Attention机制,引发了人工智能领域的浪潮。站在ChatGPT-6 Astra、DeepSeek V4.1等新模型、新架构不断迭代更新的时间点上,我决定从头学习一遍Attention机制、架构、计算方式的演进,以对大模型发展有一个更加全面的了解。

苏剑林大佬在他的博客有过一个《Transformer升级之路》系列,主要聚焦了Transformer架构中位置编码的内容及其数学原理和推导。此外,还有一篇缓存与效果的极限拉扯:从MHA、MQA、GQA到MLA。受该系列博客启发,本篇就将作为《Attention计算进化史》的第一篇,从Full Attention讲起,开启对于Attention机制发展的学习与梳理,更加侧重于具体流程并伴有部分代码实现,希望能帮到和我一样没有那么强数学直觉的人。

什么是Attention?

缩放点积注意力

对于Attention,大家的第一反应应该都是《Attention is All You Need》论文中的经典公式:

Attention(Q,K,V)=Softmax(QKTdk)V(1.1)\mathrm{Attention}(Q, K, V) = \mathrm{Softmax} ( \dfrac{QK^T}{\sqrt{d_k}})V \tag{1.1}

这没有什么问题。但是单从这个公式来看,其实很难了解到背后的实现细节,Q,K,VQ,K,V是如何被计算出来的,整个流程如何通过代码实现的等等。

下面,我们从Attention计算的输入序列开始,假设输入是一个批次大小为 BB 的序列数据,第 bb 个输入数据可以表示为:

X(b)=[x1(b),x2(b),…,xL(b)]∈RL×dmodel(1.2)X^{(b)} = [x_1^{(b)}, x_2^{(b)}, \dots,x_L^{(b)}] \in \mathbb{R}^{L \times d_{model}} \tag{1.2}

其中,LL 是序列长度,dmodeld_{model} 是特征维度,xi(b)x_i^{(b)} 即为第 bb 个序列中第 ii 个Token的嵌入向量,最终输入即为:

X=Stack(X(1),X(2),…,X(B))∈RB×L×dmodel(1.3)X = Stack(X^{(1)},X^{(2)}, \dots, X^{(B)}) \in \mathbb{R}^{B \times L \times d_{model}} \tag{1.3}

输入 XX 不是直接作为 Q,K,VQ,K,V 进行注意力计算,对于自注意力而言,是经过三个不同的、可学习的权重矩阵进行线性变换后,分别得到 Q,K,VQ,K,V 矩阵。

Q=XWQ∈RB×L×dk,  WQ∈Rdmodel×dkK=XWK∈RB×L×dk,  WK∈Rdmodel×dkV=XWV∈RB×L×dv,  WV∈Rdmodel×dv(1.4)\begin{gather} Q = XW_Q \in \mathbb{R}^{B \times L \times d_k} ,\space\space W_{Q} \in \mathbb{R}^{d_{model} \times d_{k}} \\ K = XW_K \in \mathbb{R}^{B \times L \times d_k} ,\space\space W_{K} \in \mathbb{R}^{d_{model} \times d_{k}} \\ V = XW_V \in \mathbb{R}^{B \times L \times d_v} ,\space\space W_{V} \in \mathbb{R}^{d_{model} \times d_{v}} \end{gather} \tag{1.4}

然后,才对得到的 Q,K,VQ,K,V 进行式 1.1 的计算,下面进一步拆解其中的计算过程。

第一步,计算注意力分数,表示查询 ii 和键 jj 的相似度(通过点积的方式)

(QKT)bij=qi(b)⋅kj(b)(1.5)(QK^T)_{bij} = q_{i}^{(b)} \cdot k_{j}^{(b)} \tag{1.5}

经过缩放后,对每个 Query,在最后一个维度,也就是 Key 维度上进行 softmax\mathrm{softmax}:

S=QKTdk∈RB×L×LA=Softmax(QKTdk)∈RB×L×LAbij=eSbij∑j′=1LeSbij′∈R1(1.6)\begin{gather} \\ S = \dfrac{QK^T}{\sqrt{ d_{k} }} \in \mathbb{R}^{B \times L \times L} \\ A = \mathrm{Softmax} ( \dfrac{QK^T}{\sqrt{d_k}}) \in \mathbb{R}^{B \times L \times L} \\ \\ A_{bij} = \dfrac{e^{S_{bij}}}{\sum_{j'=1}^{L} e^{S_{bij'}}} \in \mathbb{R}^1 \end{gather} \tag{1.6}

将归一化的注意力权重再与值矩阵进行张量乘法,对位置 ii 的Token计算出新的表示:

O=AV∈RB×L×dvoib=∑j=1LAbij⋅vjb∈Rdv(1.7)\begin{gather} O = AV \in \mathbb{R}^{B \times L \times d_{v}} \\ o_{i}^{b} = \sum_{j=1}^{L} A_{bij} \cdot v_{j}^{b} \in \mathbb{R}^{d_{v}} \end{gather} \tag{1.7}

在 Next Token Prediction 的自回归建模范式下,位置 ii 在预测下一个 Token 时不能利用未来位置的信息。因此,Attention 需要满足因果性约束:位置 ii 只能关注位置 1,…,i1,\dots,i。训练阶段虽然一次性输入完整序列,但通过 Causal Mask 屏蔽未来 Token,就可以在不泄露未来信息的前提下并行计算所有位置。具体而言,生成一个掩码矩阵 M∈RL×LM \in \mathbb{R}^{L \times L},对于矩阵位置索引 ii (查询 QQ 的索引)和索引 jj (键 KK 的索引)的定义如下:

Mij={0,if i≥j−∞,if i<j(1.8)M_{i j} = \begin{cases} 0, &\text{if }i\geq j\\ - \infty, & \text{if } i < j \end{cases} \tag{1.8}

再将该掩码矩阵直接与缩放后的注意力分数相加,从而带有掩码的注意力计算为:

Attention=softmax(QKTdk+M)V(1.9)\mathrm{Attention} = \mathrm{softmax}\left( \frac{QK^T}{\sqrt{ d_{k} }} + M \right) V \tag{1.9}

经过 softmax\mathrm{softmax} 后,对应未来位置的权重会变为零,从而实现因果性。

用代码实现以上注意力计算如下:

from math import sqrt
from einops import einsum
import torch.nn.functional as F

mask = torch.tril(
    torch.ones(L, L, dtype=torch.bool)
)

def scaled_dot_attention(q, k, v, mask=None):
    """
    q, k: [B, L, d_k]
    v:    [B, L, d_v]
    mask: [L, L]
    """
    d_k = q.size(-1)

    # 1. Query-Key 相似度
    scores = einsum(q, k, "... i d, ... j d -> ... i j")

    # 2. Scale
    scores = scores / sqrt(d_k)

    # 3. Causal Mask
    if mask is not None:
        scores = scores.masked_fill(~mask, float("-inf"))

    # 4. Normalize over all Key positions
    attn_weights = F.softmax(scores, dim=-1)

    # 5. Weighted sum of Values
    output = einsum(attn_weights, v, "... i j, ... j d -> ... i d")
    return output

通过 einsum 可以方便地利用 pattern 字符串的方式说明张量维度的变化,从而执行批张量乘法,此处的mask为上三角为0,下三角(包括对角线)为1的布尔张量,从而实现 1.9 中加性掩码的效果(与其等价)。

Multi-head Attention

以上为单头的自注意力,多头注意力通过不同的可学习投影矩阵,将输入映射到多个不同的 Query、Key、Value 表示空间,使不同注意力头能够学习不同的注意力模式,并从不同的表示子空间聚合信息。对同样的输入,会计算多遍自注意力,每一组的可学习矩阵权重是不同的,然后再把 hh 个头的计算结果拼接,最终经过一个输出矩阵进行线性变换后输出。

仍然考虑 1.3 式作为输入,假设有 hh 个注意力头,每个头的Query/Key的维度为 dkd_{k} ,value的维度为 dvd_{v},通常取:

dk=dv=dmodel/h(1.10)d_{k} = d_{v} = d_{model} / h \tag{1.10}

对于第 rr 个头,其 Q、K、VQ、K、V 计算如下:

Q(r)=XWQ(r)∈RB×L×dk,  WQ(r)∈Rdmodel×dkK(r)=XWK(r)∈RB×L×dk,  WK(r)∈Rdmodel×dkV(r)=XWV(r)∈RB×L×dv,  WV(r)∈Rdmodel×dv(1.11)\begin{gather} Q^{(r)} = XW_{Q}^{(r)} \in \mathbb{R}^{B \times L \times d_{k}}, \space\space W_{Q}^{(r)} \in \mathbb{R}^{d_{model}\times d_{k}} \\ K^{(r)} = XW_{K}^{(r)} \in \mathbb{R}^{B \times L \times d_{k}}, \space\space W_{K}^{(r)} \in \mathbb{R}^{d_{model}\times d_{k}} \\ V^{(r)} = XW_{V}^{(r)} \in \mathbb{R}^{B \times L \times d_{v}}, \space\space W_{V}^{(r)} \in \mathbb{R}^{d_{model}\times d_{v}} \end{gather} \tag{1.11}

按照一样的计算逻辑,每个头的输出为:

headr=softmax(Q(r)(K(r))Tdk+M)V(r)(1.12)\text{head}_{r} = \mathrm{softmax}\left( \frac{Q^{(r)}(K^{(r)})^T}{\sqrt{d_{k}}} + M\right)V^{(r)} \tag{1.12}

最终将所有注意力头的输出结果在特征维度拼接起来,并经过一个线性变换,使得不同的注意力头之间能相互交互:

MultiHead(X)=Concat(head1,head2,…,headh)WO(1.13)\text{MultiHead}(X) = \mathrm{Concat}(\text{head}_{1}, \text{head}_{2}, \dots, \text{head}_{h}) W_{O} \tag{1.13}

其中,拼接后的张量的维度为 RB×L×hdv\mathbb{R}^{B \times L \times hd_{v}} ,WO∈Rhdv×dmodelW_{O} \in \mathbb{R}^{hd_{v}\times d_{model}}。

但在实际工程使用中,计算不会按照每个头单独计算再Concat的方式进行,而是直接通过并行化的方式,将多个头原本独立的矩阵运算,组织成包含 Head 维度的批量矩阵运算。Head 维度在这里相当于额外的批次维度,使不同头的注意力计算可以通过批量矩阵乘法并行完成。不同头之间并不会在 Attention 权重计算阶段相互混合。具体计算过程如下:

首先,将每个头各自的 Q、K、VQ、K、V 投影矩阵沿输出特征维度分别拼接,形成三个大的投影矩阵,通过矩阵乘法一次性生成所有注意力头对应的 Q、K、VQ、K、V。

Q=XWQ∈RB×L×hdk,  WQ∈Rdmodel×hdkK=XWK∈RB×L×hdk,  WK∈Rdmodel×hdkV=XWV∈RB×L×hdv,  WV∈Rdmodel×hdv(1.14)\begin{gather} Q = XW_{Q} \in \mathbb{R}^{B \times L \times hd_{k}}, \space\space W_{Q} \in \mathbb{R}^{d_{model} \times hd_{k}} \\ K = XW_{K} \in \mathbb{R}^{B \times L \times hd_{k}}, \space\space W_{K} \in \mathbb{R}^{d_{model} \times hd_{k}} \\ V = XW_{V} \in \mathbb{R}^{B \times L \times hd_{v}}, \space\space W_{V} \in \mathbb{R}^{d_{model} \times hd_{v}} \\ \end{gather} \tag{1.14}

在进行注意力计算时,将整个的 Q,K,VQ,K,V 矩阵进行 reshape 和 transpose,将每个头的特征拆分开来:

Q′=transpose(reshape(Q)):RB×L×hdk→RB×L×h×dk→RB×h×L×dkK′=transpose(reshape(K)):RB×L×hdk→RB×L×h×dk→RB×h×L×dkV′=transpose(reshape(V)):RB×L×hdv→RB×L×h×dv→RB×h×L×dv(1.15)\begin{gather} Q' = \text{transpose}(\text{reshape}(Q)): \mathbb{R}^{B \times L \times hd_{k}} \to \mathbb{R}^{B \times L \times h \times d_{k}} \to \mathbb{R}^{B \times h \times L \times d_{k}} \\ K' = \text{transpose}(\text{reshape}(K)): \mathbb{R}^{B \times L \times hd_{k}} \to \mathbb{R}^{B \times L \times h \times d_{k}} \to \mathbb{R}^{B \times h \times L \times d_{k}} \\ V' = \text{transpose}(\text{reshape}(V)): \mathbb{R}^{B \times L \times hd_{v}} \to \mathbb{R}^{B \times L \times h \times d_{v}} \to \mathbb{R}^{B \times h \times L \times d_{v}} \end{gather} \tag{1.15}

然后进行注意力分数计算:

S=Q′K′Tdk∈RB×h×L×L(1.16)S = \frac{Q'{K'}^T}{\sqrt{ d_{k} }} \in \mathbb{R}^{B \times h \times L \times L} \tag{1.16}

这里的转置只作用在Key的最后两个维度,Head维度也不参与不同头之间的注意力分数计算,只是作为一个额外的批次维度,使得计算并行化。因此,对第 bb 个批次的第 rr 个注意力头,其注意力分数的元素计算为:

Sbrij=qbri⋅kbrjdk∈R1(1.17)S_{brij}= \frac{q_{bri} \cdot k_{brj}}{\sqrt{ d_{k} }} \in \mathbb{R}^1 \tag{1.17}

然后,和单头的自注意力一致,在最后一个维度(Key维度)上进行 Softmax\mathrm{Softmax} ,再与Value矩阵相乘:

A=Softmax(S+M)∈RB×h×L×LO=AV′∈RB×h×L×dv(1.18)\begin{gather} A = \mathrm{Softmax}(S + M) \in \mathbb{R}^{B \times h \times L \times L} \\ O = A V' \in \mathbb{R}^{B \times h \times L \times d_{v}} \\ \end{gather} \tag{1.18}

从逐行展开:

Obri=∑j=1LAbrijVbrj′∈Rdv(1.19)O_{bri} = \sum_{j=1}^{L} A_{brij} V'_{brj} \in \mathbb{R}^{d_{v}} \tag{1.19}

下面,再将各个注意力头的输出合并,再经过一个输出投影矩阵线性变换:

O′=Reshape(Transpose(O)):RB×h×L×dv→RB×L×h×dv→RB×L×hdvY=O′WO∈RB×L×dmodel,  WO∈Rhdv×dmodel(1.20)\begin{gather} O' = \text{Reshape}(\text{Transpose}(O)):\mathbb{R}^{B \times h \times L \times d_{v}} \to \mathbb{R}^{B \times L \times h \times d_{v}} \to \mathbb{R}^{B \times L \times hd_{v}} \\ Y = O'W_{O} \in \mathbb{R}^{B \times L \times d_{model}}, \space\space W_{{O}} \in \mathbb{R}^{hd_{v}\times d_{model}} \end{gather} \tag{1.20}

在各个 Head 内部进行注意力计算时,不同 Head 的注意力权重和 Value 加权求和相互独立。随后,将所有 Head 的输出沿特征维度拼接,再通过输出投影矩阵 WOW_O 对不同 Head 的输出特征进行线性组合。

用代码实现多头注意力计算如下:

class Multihead_self_attention(nn.Module):
    def __init__(
            self,
            d_model: int,
            num_heads: int,
            device: torch.device | None = None,
            dtype: torch.dtype | None = None
    ):
        super().__init__()
        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = self.d_model // self.num_heads
        self.device = device
        self.dtype = dtype

        self.W_q = Linear(self.d_model, self.d_model, device=device, dtype=dtype)
        self.W_k = Linear(self.d_model, self.d_model, device=device, dtype=dtype)
        self.W_v = Linear(self.d_model, self.d_model, device=device, dtype=dtype)
        self.W_o = Linear(self.d_model, self.d_model, device=device, dtype=dtype)

        self.attention = Scaled_dot_product_attention(device=device, dtype=dtype)
    
    def forward(self, x: torch.Tensor):
        q = self.W_q(x)
        k = self.W_k(x)
        v = self.W_v(x)

        seq_len = x.shape[-2]
        masks = torch.tril(torch.ones((seq_len, seq_len), dtype=torch.bool, device=x.device))

        attention_values = self.attention(
            rearrange(q, "... s (h d) -> ... h s d", h=self.num_heads),
            rearrange(k, "... s (h d) -> ... h s d", h=self.num_heads),
            rearrange(v, "... s (h d) -> ... h s d", h=self.num_heads),
            masks
        )

        return self.W_o(rearrange(attention_values, "... h s d -> ... s (h d)"))

以上,就是最基础的标准点积注意力和多头注意力的定义及其计算流程,下面将进一步从Attention走向一个完整的Transformer Block,再进而到近年来的演进。