文章目录
- [Attention MHA GQA(多头注意力)](#Attention MHA GQA(多头注意力))
-
- [一、 为什么传统的 MHA 成了推理瓶颈?](#一、 为什么传统的 MHA 成了推理瓶颈?)
-
- [显存暴涨的元凶:KV Cache](#显存暴涨的元凶:KV Cache)
- [二、 三大注意力机制对比:MHA vs MQA vs GQA](#二、 三大注意力机制对比:MHA vs MQA vs GQA)
-
- [1. MHA (Multi-Head Attention) --- 传统多头](#1. MHA (Multi-Head Attention) — 传统多头)
- [2. MQA (Multi-Query Attention) --- 极端单头](#2. MQA (Multi-Query Attention) — 极端单头)
- [3. GQA (Grouped-Query Attention) --- 黄金折中方案](#3. GQA (Grouped-Query Attention) — 黄金折中方案)
- [三、 数学维度与计算对比](#三、 数学维度与计算对比)
- [FlashAttention 模拟](#FlashAttention 模拟)
-
- [一、 核心痛点:GPU 的"计算快,访存慢"](#一、 核心痛点:GPU 的“计算快,访存慢”)
-
- [1. GPU 内存层级](#1. GPU 内存层级)
- [2. 传统 Attention 的"读写噩梦"](#2. 传统 Attention 的“读写噩梦”)
- [二、 FlashAttention 的两大核心魔法](#二、 FlashAttention 的两大核心魔法)
-
- [1. 平铺(Tiling):分块计算与 Softmax 局部更新](#1. 平铺(Tiling):分块计算与 Softmax 局部更新)
-
- [💡 解决方案:在线 Softmax(Online Softmax)](#💡 解决方案:在线 Softmax(Online Softmax))
- [2. 重计算(Recomputation / Gradient Checkpointing)](#2. 重计算(Recomputation / Gradient Checkpointing))
- [三、 FlashAttention 的演进之路 (V1 vs V2 vs V3)](#三、 FlashAttention 的演进之路 (V1 vs V2 vs V3))
-
- [1. FlashAttention-1 (2022年)](#1. FlashAttention-1 (2022年))
- [2. FlashAttention-2 (2023年)](#2. FlashAttention-2 (2023年))
- [3. FlashAttention-3 (2024年及以后)](#3. FlashAttention-3 (2024年及以后))
- [四、 为什么它是大模型时代的基石?](#四、 为什么它是大模型时代的基石?)
Attention MHA GQA(多头注意力)
可以先把 Attention 记成"当前 token 去回看历史 token",KV Cache 是把历史信息存起来避免重复算,GQA 则是在效果和显存之间做折中。
在大语言模型(LLM)的演进过程中,如何在高并发、长文本场景下降低显存占用并提升推理速度,一直是研究的重中之重。
在 Self-Attention 机制中,传统的 MHA(Multi-Head Attention,多头注意力) 带来了极大的计算和显存负担。为了解决这个问题,研究者们相继提出了 MQA(Multi-Query Attention) 和目前最主流的 GQA(Grouped-Query Attention,分组查询注意力)。
下面我们由浅入深,对这三种注意力机制进行全方位的对比与深度拆解。
一、 为什么传统的 MHA 成了推理瓶颈?
在 Transformer 的自回归解码(Generation)阶段,模型需要逐字生成 Token。为了避免重复计算,我们需要把之前生成过的所有 Token 的 K K K 和 V V V 向量缓存下来,这被称为 KV Cache。
显存暴涨的元凶:KV Cache
在传统的 MHA 中:
- 每个 Attention Head(注意力头)都有自己独一份的 Q Q Q、 K K K、 V V V 投影矩阵。
- 假设模型有 H H H 个头,每个头的维度是 d d d,那么每个 Token 都需要存储 H × d H \times d H×d 大小的 K K K 向量和 V V V 向量。
- 随着序列长度(Context Length)和并发请求数(Batch Size)的增加,KV Cache 占用的显存会呈线性暴涨,甚至远超模型权重本身,导致 GPU 显存轻易 OOM(Out of Memory)。
- 访存瓶颈(Memory-Bound):在 GPU 计算时,从全局显存读取这些庞大 KV Cache 的速度(带宽)跟不上计算单元(Tensor Cores)的速度,导致 GPU 算力利用率极低。
二、 三大注意力机制对比:MHA vs MQA vs GQA
为了解决 KV Cache 的痛点,架构演进经历了以下三个阶段:
1. MHA (Multi-Head Attention) --- 传统多头
- 结构 : N N N 个 Query 头、 N N N 个 Key 头、 N N N 个 Value 头(一配一)。
- 特点 :每个 Q Q Q 头都有自己专属的 K K K 和 V V V 头。
- 优缺点:
- 优点:表达能力最强,模型效果最好。
- 缺点:KV Cache 极其庞大,推理速度慢,显存开销巨大。
2. MQA (Multi-Query Attention) --- 极端单头
- 结构 : N N N 个 Query 头,但只有 1 个 Key 头和 1 个 Value 头(多配一)。
- 特点 :所有的 Q Q Q 头共享同一组 K K K 和 V V V 向量。
- 优缺点:
- 优点:KV Cache 骤降为原来的 1 / N 1/N 1/N,极大地减少了显存占用和访存带宽,推理速度(Throughput)飞跃式提升。
- 缺点:由于所有头强行共享同一组 KV,模型理解复杂语义的能力有所下降,容易出现性能退化。
3. GQA (Grouped-Query Attention) --- 黄金折中方案
-
结构 :将 N N N 个 Query 头均分成 G G G 个组(Group),每个组内的 Query 头共享 1 个 Key 头和 1 个 Value 头。
-
特点:介于 MHA 和 MQA 之间的折中方案。
-
当 G = N G = N G=N 时,GQA 退化为 MHA。
-
当 G = 1 G = 1 G=1 时,GQA 变为 MQA。
-
优缺点:
-
优点:完美平衡了速度与效果。它在保持接近 MHA 的模型精度(Accuracy)的同时,获得了接近 MQA 的极快推理速度和超低显存占用。
三、 数学维度与计算对比
为了更直观地理解,我们来看一下这三种机制在维度 上的差异。
假设:
- 输入特征维度为
hidden_size = 4096 - 注意力头数 H Q = 32 H_{Q} = 32 HQ=32,每个头的维度
head_dim = 128
| 机制 | Query 投影输出形状 | Key/Value 投影输出形状 | KV 显存占用对比 | 适用场景 |
|---|---|---|---|---|
| MHA | [B, S, 32, 128] |
[B, S, 32, 128] |
100% (基准) | 早期模型 (GPT-3, LLaMA-1) |
| MQA | [B, S, 32, 128] |
[B, S, 1, 128] |
3.125% ( 1 / 32 1/32 1/32) | 极长上下文、轻量级模型 (Falcon) |
| GQA (以 8 组为例) | [B, S, 32, 128] |
[B, S, 8, 128] |
25% ( 8 / 32 8/32 8/32) | 现代主流大模型 (LLaMA-3, Mistral, Qwen) |
代码实现:
python
import torch
import torch.nn as nn
import math
python
def repeat_kv(hidden_states:torch.Tensor,n_rep:int) -> torch.Tensor:
#将KV头复制n_rep次,以匹配Query头的数量(GQA/MQA需要)
batch,num_kv_heads,slen,head_dim = hidden_states.shape
if n_req == 1:
return hidden_states
hidden_states = hidden_states[:,:,Node,:,:].expand(batch,num_kv_heads,n_rep,slen,head_dim)
return hidden_states.reshape(batch,num_kv_heads * n_rep,slen,head_dim)
class GroupedQueryAttention(nn.Module):
def __init__(self,hidden_dim:int,num_heads:int,num_kv_heads:int = None):
super().__init__()
self.hidden_dim = hidden_dim
self.num_heads = num_heads
self.num_kv_heads = num_kv_heads if num_kv_heads is not None else num_heads
self.num_queries_per_kv = self.num_heads // self.num_kv_heads
self.head_dim = hidden_dim // num_heads
#定义投影矩阵
self.q_proj = nn.Linear(hidden_dim,num_heads * self.head_dim,bias = False)
self.k_proj = nn.Linear(hidden_dim,self.num_kv_heads * self.head_dim,bias = False)
self.v_proj = nn.Linear(hidden_dim,self.num_kv_heads * self.head_dim,bias = False)
self.o_proj = nn.Linear(num_heads * self.head_dim,hidden_dim,bias = False)
def forward(
self,
x:torch.Tensor,
attention_mask:torch.Tensor = None,
kv_cache:tuple[torch.Tensor,torch.Tensor] = None
):
batch_size,seq_len,_ = x.shape
#1、线性投影
xq,xk,xv = self.q_proj(x),self.k_proj(x),self.v_proj(x)
#TODO1:Reshape xq,xk,xv 以适配多头注意力计算
xq = xq.view(batch_size,seq_len,self.num_heads,self.head_dim).transpose(1,2)
xk = xk.view(batch_size,seq_len,self.num_kv_heads,self.head_dim).transpose(1,2)
xv = xv.view(batch_size,seq_len,self.num_kv_heads,self.head_dim).transpose(1,2)
#TODO2:处理KV Cache
if kv_cache is not None:
k_cache,v_cache = kv_cache
xk = torch.cat([k_cache,xk],dim = 2)
xv = torch.cat([v_cache,xv],dim = 2)
new_kv_cache = (xk,xv)
#通过repeat_kv 把GQA的KV头数扩充到和Query数量一致
xk = repeat_kv(xk,self.num_queries_per_kv)
xv = repeat_kv(xv,self.num_queries_per_kv)
#TODO3:计算注意力分数
scores = torch.matmul(xq,xk.transpose(2,3))/math.sqrt(self.head_dim)
if attention_mask is not Node:
scores = scores + attention_mask
probs = torch.nn.functional.softmax(scores,dim=-1)
output = torch.matmul(probs,xv)
#TODO4:恢复形状并输出
output = output.transpose(1,2).contiguous().view(batch_size,seq_len,-1)
return self.o_proj(output),new_kv_cache
结果:

FlashAttention 模拟
在了解了自注意力机制(Self-Attention)和 GQA 之后,你可能会发现,大模型的计算瓶颈除了参数量和 KV Cache 之外,还有一个非常致命的痛点:内存带宽(Memory Bandwidth)。
传统的 Attention 计算在处理长文本时,速度会急剧下降,甚至导致显存溢出(OOM)。为了彻底解决这一问题,斯坦福大学的 Tri Dao 等人提出了 FlashAttention。
FlashAttention 没有改变 Attention 的数学逻辑(其输出与标准 Attention 完全一致) ,它是一项纯粹的硬件加速与访存优化技术。
一、 核心痛点:GPU 的"计算快,访存慢"
要理解 FlashAttention 的伟大之处,首先需要了解 GPU 的存储架构。
1. GPU 内存层级
在 GPU 内部,存储介质主要分为两种:
- HBM(High Bandwidth Memory,高带宽显存) :也就是我们平时说的"显存"(如 A100 的 80GB 显存)。它的容量大,但是读写速度相对较慢(类似于电脑的内存)。
- SRAM(Static Random-Access Memory,静态随机存取内存) :也叫 Shared Memory(共享内存) 。它集成在 GPU 芯片内部,容量极小(通常每块只有 100KB 左右),但读写速度极其恐怖(类似于 CPU 的 L1/L2 缓存)。
2. 传统 Attention 的"读写噩梦"
在计算标准 Attention 时,公式为:
Attention ( Q , K , V ) = softmax ( Q K T d k ) V \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V Attention(Q,K,V)=softmax(dk QKT)V
传统的 PyTorch 实现(通常称为 Baseline)是逐个算子(Kernel)调用 HBM 进行读写的:
- 从 HBM 读取 Q Q Q 和 K K K,计算 S = Q K T S = QK^T S=QKT,将大小为 N × N N \times N N×N( N N N 为序列长度)的中间矩阵 S S S 写回 HBM。
- 从 HBM 读取 S S S,计算 P = softmax ( S ) P = \text{softmax}(S) P=softmax(S),再将 P P P 写回 HBM。
- 从 HBM 读取 P P P 和 V V V,计算 O = P V O = PV O=PV,将结果 O O O 写回 HBM。
❌ 致命问题 :
当序列长度 N N N 很大时(例如 100k), N × N N \times N N×N 的中间矩阵 S S S 和 P P P 会变得超级巨大,频繁在慢速的 HBM 中进行读写(I/O 交互)消耗了 GPU 80% 以上的时间。这种瓶颈被称为访存受限(Memory-Bound)。
二、 FlashAttention 的两大核心魔法
FlashAttention 的核心思路是:绝不往 HBM 中写入任何 N × N N \times N N×N 的中间矩阵!所有计算尽可能在超高速的 SRAM 中完成。
为了实现这一点,它引入了两个革命性的数学与工程设计:平铺(Tiling) 与 重计算(Recomputation)。
1. 平铺(Tiling):分块计算与 Softmax 局部更新
因为 SRAM 的容量非常小(仅约 100KB),我们无法一次性把整个 Q , K , V Q, K, V Q,K,V 塞进去。FlashAttention 采用平铺(Tiling)技术,将 Q , K , V Q, K, V Q,K,V 拆分成许多个小分块(Blocks),每次只加载一小块到 SRAM 中计算。
但这里有一个巨大的数学障碍:Softmax 是全局关联的。
softmax ( x i ) = e x i ∑ e x j \text{softmax}(x_i) = \frac{e^{x_i}}{\sum e^{x_j}} softmax(xi)=∑exjexi
为了防止数值溢出,实际计算时需要减去全局最大值 m m m:
softmax ( x i ) = e x i − m ∑ e x j − m \text{softmax}(x_i) = \frac{e^{x_i - m}}{\sum e^{x_j - m}} softmax(xi)=∑exj−mexi−m
如果把矩阵分块,我们怎么在不知道全局最大值 m m m 和全局分母和(归一化因子) d d d 的情况下,计算出正确的 Softmax 呢?
💡 解决方案:在线 Softmax(Online Softmax)
FlashAttention 利用了数学上的递推性质,可以动态更新 Softmax 的值。
假设我们已经计算了前一小块的局部最大值 m ( 1 ) m^{(1)} m(1) 和分母和 d ( 1 ) d^{(1)} d(1),现在迎来了新的一小块,其局部最大值为 m ( 2 ) m^{(2)} m(2)。
我们可以通过以下公式,无缝地将它们融合成新的全局指标:
m new = max ( m ( 1 ) , m ( 2 ) ) m^{\text{new}} = \max(m^{(1)}, m^{(2)}) mnew=max(m(1),m(2))
d new = d ( 1 ) ⋅ e m ( 1 ) − m new + d ( 2 ) ⋅ e m ( 2 ) − m new d^{\text{new}} = d^{(1)} \cdot e^{m^{(1)} - m^{\text{new}}} + d^{(2)} \cdot e^{m^{(2)} - m^{\text{new}}} dnew=d(1)⋅em(1)−mnew+d(2)⋅em(2)−mnew
通过这种缩放更新机制,FlashAttention 能够一边分块读取数据,一边动态修正之前的 Attention 输出 ,从而彻底避免了保存全局 N × N N \times N N×N 矩阵的需要。
2. 重计算(Recomputation / Gradient Checkpointing)
在反向传播(Backward Pass)中,通常需要用到正向传播时产生的激活值(即那两个大矩阵 S S S 和 P P P)。如果我们在正向传播中不保存它们,反向传播该怎么办?
FlashAttention 采用了一种"以计算换空间"的激进策略:
- 在正向传播时,直接扔掉中间的 Attention 矩阵。
- 在反向传播需要用到它们时,直接利用 SRAM 里保留的 Q , K , V Q, K, V Q,K,V 分块现场重新计算一次。
虽然这增加了一点点计算量,但由于不用读写慢速的 HBM,省下来的 I/O 时间远远超过了重计算的时间,整体速度反而得到了成倍的提升!
三、 FlashAttention 的演进之路 (V1 vs V2 vs V3)
随着显卡架构(如 Ampere, Hopper)的升级,FlashAttention 也经历了多次迭代:
1. FlashAttention-1 (2022年)
- 奠定了 Tiling 和 Recomputation 的框架。
- 相比于标准 Attention,实现了 2~4倍 的速度提升,显存占用从 O ( N 2 ) O(N^2) O(N2) 骤降到 O ( N ) O(N) O(N)。
2. FlashAttention-2 (2023年)
- 减少了非矩阵乘法(Non-matmul)计算:将不适合 Tensor Core 的运算(如 Softmax 缩放)尽可能减少,让 90% 以上的计算都跑在极快的 Tensor Core 上。
- 更高效的多头并行 :在 Thread Block(线程块)维度上进行了更好的切分与调度,GPU 算力利用率(TFLOPs/s)从 V1 的 30~40% 提升到了 50~75%,速度相比 V1 再次翻倍。
3. FlashAttention-3 (2024年及以后)
- 针对 NVIDIA Hopper 架构(H100 / H200 / B200)进行了底层硬件级榨干。
- 异步执行(Asynchronous WGMMA) :利用 H100 的 Tensor Memory Accelerator (TMA),让"数据从 HBM 搬运到 SRAM"和"Tensor Core 进行矩阵乘法计算"同时异步进行,消除了等待延迟。
- 支持 FP8(8位浮点数)低精度训练与推理,速度相比 V2 再次提升 1.5~2倍。
四、 为什么它是大模型时代的基石?
| 特性 | 标准 Attention | FlashAttention |
|---|---|---|
| 显存占用(存储复杂度) | O ( N 2 ) O(N^2) O(N2) (随长度呈平方级暴涨) | O ( N ) O(N) O(N) (随长度呈线性增长) |
| 访存特征 | 频繁读写 HBM(Memory-Bound) | 内部高效利用 SRAM(Compute-Bound) |
| 硬件利用率 | 极低(GPU 经常在"等数据") | 极高(Tensor Core 全速运转) |
| 支持的上下文长度 | 极难突破 8K | 轻松支持 32K / 128K / 1M 级别 |
一句话总结:
FlashAttention 通过数学公式的巧妙重构(在线 Softmax 递推),把原本需要写在显存里的超大临时表,变成了在 GPU 缓存里闪电般完成的局部计算。它直接拉开了大模型"长文本时代(Long Context)"的序幕!
代码实现:
python
import torch
import math
python
def flash_attention_forward_sim(q,k,v,block_size = 2):
#纯PyTorch模拟FlashAttention前向传播,假设没有Batch和Head维度,q,k,v的形状都是(seq_len,dim)
seq_len,dim = q.shape
#TODO1:初始化输出O,全局最大值m,全局指数和1
out = torch.zeros((seq_len,dim),device=q.device)
m = torch.full((seq_len,1),-float('inf'),device = q.device)
l = torch.zeros((seq_len,1),device=q.device)
scale = 1.0 / math.sqrt(dim)
#外层循环:遍历Q的分块
for i in range(0,seq_len,block_size):
q_block = q[i:i+block_size]*scale
m_i = m[i:i+block_size]
l_i = l[i:i+block_size]
out_i = out[i:i+block_size]
#内层循坏:遍历K,V的分块
for j in range(0,seq_len,block_size):
k_block = k[j:j+block_size]
v_block = v[j:j+block_size]
#TODO2:计算当前块的未归一化分数S_ij
S_ij = q_block @ k_block.transpose(-2,-1)
#TODO3:计算当前块的局部最大值m_block,并求出新的全局最大值m_new
m_block = torch.max(S_ij,dim = -1,keepdim=True)[0]
m_new = torch.maximum(m_i,m_block)
#TODO4:计算P_ij = exp(S_ij - m_new)
P_ij = torch.exp(S_ij - m_new)
#TODO5:计算当前块的局部指数和l_block,并更新全局指数和l_new
l_block = torch.sum(P_ij,dim = -1,keepdim=True)
l_new = l_i * torch.exp(m_i - m_new) + l_block
#TODO6:更新输出O_i(使用Online Softmax的修正公式)
out_i = out_i * (l_i * torch.exp(m_i - m_new) / l_new) + (P_ij @ v_block) / l_new
#更新全局状态
m_i = m_new
l_i = l_new
pass
#写回全局变量
out[i:i+block_size] = out_i
m[i:i+block_size] = m_i
l[i:i+block_size] = l_i
return out
python
# 测试你的实现
def test_flash_attention_sim():
try:
import math
def run_case(seq_len, dim, block_size, seed):
torch.manual_seed(seed)
q = torch.randn(seq_len, dim)
k = torch.randn(seq_len, dim)
v = torch.randn(seq_len, dim)
scale = 1.0 / math.sqrt(dim)
scores = (q @ k.transpose(-2, -1)) * scale
attn = torch.nn.functional.softmax(scores, dim=-1)
out_ref = attn @ v
out_sim = flash_attention_forward_sim(q, k, v, block_size=block_size)
diff = torch.max(torch.abs(out_ref - out_sim))
print(f"[seq={seq_len}, dim={dim}, block={block_size}] 最大误差: {diff.item():.6e}")
assert diff < 1e-5, f"计算结果与标准 Attention 不一致!(seq={seq_len}, dim={dim}, block={block_size})"
run_case(seq_len=8, dim=4, block_size=2, seed=42)
run_case(seq_len=5, dim=3, block_size=3, seed=7)
run_case(seq_len=3, dim=2, block_size=1, seed=123)
print("✅ Online Softmax 与分块计算逻辑正确!")
print("\n FlashAttention 分块计算逻辑验证通过。")
except NotImplementedError:
print("请先完成 TODO 部分的代码!")
raise
except (AttributeError, NameError, TypeError, ValueError, AssertionError, RuntimeError) as e:
if isinstance(e, AttributeError):
print("代码未完成,无法找到必要的属性")
elif isinstance(e, NameError):
print("代码可能未完成,导致了变量未定义")
elif isinstance(e, TypeError):
print("代码可能未完成,导致了操作错误")
elif isinstance(e, ValueError):
print("代码可能未完成,导致了张量维度错误")
elif isinstance(e, RuntimeError):
print("代码可能未完成,导致了运行时错误")
else:
print("代码可能未完成,导致了断言失败")
raise NotImplementedError("请先完成 TODO 部分的代码!") from e
except Exception as e:
print(f"❌ 测试失败: {e}")
raise
test_flash_attention_sim()
结果:
