1. 注意力机制的数学基础

1.1 从信息检索到注意力

注意力机制源于信息检索的直觉:给定查询 Query,在键值对 (Key-Value) 集合中检索相关信息。注意力是软检索——不是返回最匹配的一项,而是对所有项加权求和。

1.2 Scaled Dot-Product Attention 推导

给定 $Q \in \mathbb{R}^{n \times d_k}$, $K \in \mathbb{R}^{m \times d_k}$, $V \in \mathbb{R}^{m \times d_v}$:

$$\text{Attention}(Q,K,V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$

逐步分解:

  1. 相似度计算:$S = QK^T \in \mathbb{R}^{n \times m}$,每个元素 $S_{ij} = \sum_{l=1}^{d_k} Q_{il} K_{jl}$ 是点积相似度
  2. 缩放:$\hat{S}{ij} = S{ij} / \sqrt{d_k}$,控制方差
  3. 归一化:$A_{ij} = \frac{e^{\hat{S}{ij}}}{\sum{j’} e^{\hat{S}_{ij’}}}$,每行 softmax
  4. 加权聚合:$O = AV \in \mathbb{R}^{n \times d_v}$

方差分析:假设 $Q, K$ 各分量独立同分布,均值为 0,方差为 1,则 $S_{ij} = \sum_{l=1}^{d_k} Q_{il}K_{jl}$ 的均值为 0,方差为 $d_k$。当 $d_k=64$ 时,$S_{ij}$ 标准差为 8,softmax 输入范围 $[-24, 24]$,梯度几乎为零。除以 $\sqrt{d_k}=8$ 后方差恢复为 1。

1.3 Softmax 的数值稳定实现

def stable_softmax(x, dim=-1):
    """数值稳定的 softmax,避免 exp 溢出"""
    x_max = x.max(dim=dim, keepdim=True)
    exp_x = torch.exp(x - x_max)
    return exp_x / exp_x.sum(dim=dim, keepdim=True)

减去最大值不改变 softmax 输出(因为 $\text{softmax}(x) = \text{softmax}(x - c)$),但防止了 $e^x$ 溢出。

2. Multi-Head Attention

2.1 为什么需要多头

单头注意力只能学习一种关联模式(如语法依存)。多头让模型同时关注不同子空间的不同关系:某些头关注语法、某些头关注语义、某些头关注位置相近的词。

2.2 分头策略

$d_{model} = 512$, $h = 8$ 头,每头 $d_k = 64$。关键设计:总参数量与单头相同,因为 $W^Q \in \mathbb{R}^{512 \times 512}$ 等价于 8 个 $W_i^Q \in \mathbb{R}^{512 \times 64}$。

2.3 Grouped-Query Attention (GQA)

标准 MHA 每个头有独立的 K/V,参数量和 KV Cache 内存大。GQA 让多个 Query 头共享一组 K/V:

方式Q 头数K/V 头数KV Cache质量
MHAhh最高
MQAh11/h最低
GQAhg (1<g<h)g/h中间

LLaMA-2 70B 使用 GQA(8 个 KV 组),推理速度显著提升且质量损失极小。

class GroupedQueryAttention(nn.Module):
    def __init__(self, d_model, num_heads, num_kv_heads):
        super().__init__()
        self.num_heads = num_heads
        self.num_kv_heads = num_kv_heads
        self.num_groups = num_heads // num_kv_heads  # 每组共享 KV 的 Q 头数
        self.d_k = d_model // num_heads
        
        self.W_q = nn.Linear(d_model, num_heads * self.d_k, bias=False)
        self.W_k = nn.Linear(d_model, num_kv_heads * self.d_k, bias=False)
        self.W_v = nn.Linear(d_model, num_kv_heads * self.d_k, bias=False)
        self.W_o = nn.Linear(d_model, d_model, bias=False)
        
    def forward(self, x, mask=None):
        B, n, _ = x.shape
        Q = self.W_q(x).view(B, n, self.num_heads, self.d_k).transpose(1, 2)
        K = self.W_k(x).view(B, n, self.num_kv_heads, self.d_k).transpose(1, 2)
        V = self.W_v(x).view(B, n, self.num_kv_heads, self.d_k).transpose(1, 2)
        
        # 扩展 KV 到与 Q 相同的头数
        K = K.repeat_interleave(self.num_groups, dim=1)
        V = V.repeat_interleave(self.num_groups, dim=1)
        
        scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
        if mask is not None:
            scores = scores.masked_fill(mask == 0, float('-inf'))
        attn = F.softmax(scores, dim=-1)
        context = torch.matmul(attn, V)
        context = context.transpose(1, 2).contiguous().view(B, n, -1)
        return self.W_o(context)

3. Causal Mask(因果掩码)

自回归生成中,位置 $i$ 不能看到未来位置。通过上三角掩码实现:

$$M_{ij} = \begin{cases} 0 & \text{if } j \leq i \ -\infty & \text{if } j > i \end{cases}$$

def causal_mask(seq_len, device='cpu'):
    """生成因果掩码: 上三角为 -inf"""
    mask = torch.full((seq_len, seq_len), float('-inf'), device=device)
    mask = torch.triu(mask, diagonal=1)
    return mask

掩码后 softmax 将未来位置的权重归零,实现自回归约束。

4. Flash Attention:IO 感知的注意力优化

4.1 问题分析

标准注意力的瓶颈不在 FLOPS,而在内存 IO

  • $QK^T$ 产生 $n \times n$ 矩阵,对 $n=8192$ 需 256MB(FP32)
  • 中间矩阵在 HBM(高带宽内存)和 SRAM 之间反复搬运
  • HBM 带宽 ~2TB/s,SRAM 带宽 ~19TB/s,差距近 10 倍

4.2 Flash Attention 核心思想

分块计算(Tiling)+ 在线 Softmax,避免物化 $n \times n$ 注意力矩阵:

  1. 将 Q, K, V 分块加载到 SRAM
  2. 在 SRAM 内计算局部注意力
  3. 使用在线 Softmax 增量更新全局结果

4.3 在线 Softmax

标准 softmax 需要遍历两次:第一次求 max,第二次求 exp 和归一化。在线 softmax 只需一次遍历:

def online_softmax(rows):
    """在线 softmax: 单次遍历计算"""
    m = float('-inf')  # 当前最大值
    s = 0.0            # 当前 exp 和
    result = []
    
    for x in rows:
        m_new = max(m, x)
        s = s * math.exp(m - m_new) + math.exp(x - m_new)
        m = m_new
        result.append(math.exp(x - m) / s)  # 增量输出
    
    # 最终归一化
    return [math.exp(x - m) / s for x in rows]

4.4 Flash Attention 性能对比

指标标准 AttentionFlash Attention v2
HBM 读写$O(n^2)$$O(n^2 d / M)$,$M$ 为 SRAM 大小
额外内存$O(n^2)$$O(n)$
FLOPS$2n^2 d$$2n^2 d$(相同,但更接近峰值)
实际加速2-4×

Flash Attention v2 进一步优化了并行度:沿序列维度切分,充分利用 GPU SM 并行。

5. 稀疏注意力

5.1 动机

标准注意力是稠密的——每个 token 关注所有 token。实际上大部分注意力权重很小,可以安全跳过。

5.2 主要方法

方法策略复杂度代表模型
Local Attention只关注窗口内 $w$ 个 token$O(nw)$Longformer
Strided Attention等间距采样$O(n\sqrt{n})$Sparse Transformer
Global Attention指定全局 token$O(n \cdot g)$BigBird
Block-sparse块级稀疏模式$O(n^2/s)$GPT-4 (推测)

5.3 Sliding Window Attention

Mistral/LLaMA-2 采用滑动窗口注意力,窗口大小 $w$:

def sliding_window_attention(Q, K, V, window_size=4096):
    """滑动窗口注意力: 每个 token 只看窗口内"""
    n = Q.shape[2]
    # 构造带状掩码
    mask = torch.zeros(n, n, device=Q.device)
    for i in range(n):
        start = max(0, i - window_size + 1)
        mask[i, start:i+1] = 1
    mask = mask.masked_fill(mask == 0, float('-inf'))
    scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(Q.shape[-1])
    scores = scores + mask.unsqueeze(0).unsqueeze(0)
    return torch.matmul(F.softmax(scores, dim=-1), V)

通过层级堆叠,$L$ 层滑动窗口的有效感受野为 $L \times w$,以 $O(L \cdot n \cdot w)$ 的代价实现长序列建模。

6. 线性注意力

将注意力复杂度从 $O(n^2)$ 降到 $O(n)$:

$$\text{Attention}(Q,K,V) = \phi(Q)(\phi(K)^T V)$$

其中 $\phi$ 是非线性特征映射,利用矩阵乘法结合律先算 $\phi(K)^T V$($d \times d$),再算 $\phi(Q) \times (\cdot)$($n \times d$)。

代价是近似精度低,表达力不如标准 softmax 注意力。代表工作:Linear Transformer、Performer、RWKV。

7. 注意力可视化与模式分析

训练后的注意力头展现出可解释的模式:

模式描述典型层位置
Diagonal关注自身位置浅层
Previous-token关注前一个 token浅层
Syntax关注语法依存中层
Rare token关注稀有词(如专有名词)中层
Aggregation关注句首 [BOS] 或分隔符深层

8. 总结

注意力机制是 Transformer 的核心,其数学形式简洁但工程优化空间巨大。从 Scaled Dot-Product 的方差分析,到多头分组的参数效率,再到 Flash Attention 的 IO 感知设计,每一步优化都深刻影响 LLM 的训练和推理效率。当前趋势:GQA 成为默认配置、Flash Attention-3 支持最新 GPU、滑动窗口与稀疏模式结合实现超长上下文。理解这些优化原理,是掌握 LLM 系统优化的前提。

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。