I·Infra Notes AI Infra 工程实践指南 ← 返回主博客
模型与缓存 / READING NOTES

5.1 从朴素 Attention 到分块执行

GQA/MLA、在线 Softmax 与 FlashAttention

令 Q:[B,Hq,Sq,D]、K/V:[B,Hkv,Sk,D],GQA 要求 Hq % Hkv == 0。数学参考实现先构造 Sq×Sk scores;这是正确性 oracle,却会把大矩阵写入显存。FlashAttention 通过 Q/K/V tile 与 online softmax 在片上更新状态。

Q tile → 加载 K/V tile → S=QKᵀ×scale + mask
       → 行最大值 m_new → p=exp(S-m_new)
       → l_new=exp(m_old-m_new)×l_old+sum(p)
       → acc_new=exp(m_old-m_new)×acc_old+pV
       → 下一 tile;最终 O=acc/l
import torch

def online_attention(q, k, v, block=64):
    # q:[Q,D], k/v:[K,D];教学版,先用 float32 验证
    scale = q.shape[-1] ** -0.5
    out = []
    for qs in range(0, q.shape[0], block):
        qb = q[qs:qs + block].float()
        m = torch.full((qb.shape[0], 1), -float('inf'), device=q.device)
        l = torch.zeros_like(m)
        acc = torch.zeros((qb.shape[0], v.shape[-1]), device=q.device)
        for ks in range(0, k.shape[0], block):
            score = qb @ k[ks:ks + block].float().T * scale
            # causal: 全局 q 位置与 k 位置比较;空 tile 不参加更新
            qi = torch.arange(qs, qs + qb.shape[0], device=q.device)[:, None]
            ki = torch.arange(ks, ks + score.shape[1], device=q.device)[None, :]
            score = score.masked_fill(ki > qi, -float('inf'))
            new_m = torch.maximum(m, score.max(-1, keepdim=True).values)
            safe_m = torch.where(torch.isfinite(new_m), new_m, torch.zeros_like(new_m))
            alpha = torch.where(torch.isfinite(m), torch.exp(m - safe_m), 0)
            p = torch.exp(score - safe_m)
            acc = acc * alpha + p @ v[ks:ks + block].float()
            l = l * alpha + p.sum(-1, keepdim=True)
            m = new_m
        out.append(acc / l.clamp_min(1e-20))
    return torch.cat(out, dim=0)

上面针对整段 causal self-attention;Decode 时 Q 只有当前 token,但 qi 必须取其绝对位置,不能沿用局部行号。变长批还需 cu_seqlens、每请求长度和 mask;全 mask 行要定义输出,不可让 -inf - -inf 产生 NaN。工业实现可从 FlashAttention 的 API 与 CUDA/Triton 实现 先找入口和布局,再跟踪 tile 循环及 online softmax 状态。