想象你经营着一家繁忙的图书馆。每天都有几十位读者(查询头 Q)来查找资料,每位读者都配有一位专属的索引员(键头 K)和资料员(值头 V)。书越读越多,每翻开一页新书,都要给每位资料员增加一个工作台。图书馆的空间(显存)渐渐被工作台占满:能同时接待的读者(批处理量)越来越少,一本书能翻阅的页码(上下文长度)也被卡死。 于是你想出一个主意:读者们要查的资料其实有很多重叠,何必每人配一位资料员?不如把读者分成几个小组,每组共用一位资料员。读者依然能高效查资料(注意力计算),而工作台的数量一下子减少了 75% 甚至 87.5%——省出的空间,让图书馆能接待更多读者、翻阅更厚的书。 这就是 GQA(Grouped-Query Attention,分组查询注意力) 的智慧。Llama 2 与 Llama 3 都采用这个方案,让大模型在有限显存下支持更长上下文与更大批处理量,成为开源大模型的标配设计。这篇文章,我们就来聊聊这套“分组共享资料员”的方案是如何运转的。
掌握 GQA,你将看透大模型推理阶段显存优化的核心手段——这是部署 Llama 级模型、设计高性能推理服务时绕不开的关键技术。
目录
可视化演示
展示Llama 2/3中GQA(Grouped-Query Attention)的核心机制:从MHA到MQA再到GQA的架构演进,以及KV Cache显存占用的量化对比,直观呈现分组共享KV头如何实现推理加速。
本节配有交互动效(进入视口后自动加载,约 3KB)。
GQA架构演进 MHA: H个Q头对应H个KV头 GQA: H个Q头共享G个KV头 KV显存=G/H 显存降至G/H倍
注意力机制:读者与资料员的协作
在 GQA 的故事开始前,先认识一下图书馆的基本运营方式。Transformer 的注意力机制,本质上就是一场“精准的资料检索”。每个词元会派出自己的读者代表(Query 查询头),向其他词元发出咨询:“谁和我最相关?”而被咨询的词元则会亮出索引标签(Key 键头)和具体资料(Value 值头)。这三者协作,决定了每个词元的上下文表示。
多头注意力(MHA)是这场协作的标准流程:不只派一名读者代表,而是并行派出多组代表,也就是多个“头”。每个头在不同的子空间中独立检索,最后把结果汇总。这样模型能同时关注语法、语义、指代等不同层面的线索,表达力更强。
在 MHA 中,Q、K、V 分别由输入乘以三个可学习的投影矩阵得到。每个头有独立的 Q、K、V,注意力得分通过缩放点积计算,再对 V 加权求和:
在多头注意力的图书馆里,一个查询头就是一位读者代表。MHA 给每位代表配了专属索引员与资料员,检索精度高,但代价是——每位代表的工作台都要占用不小的空间。随着模型越来越大、读者代表越来越多,图书馆的空间危机开始浮现。
- 查询矩阵 Q当前词元的查询向量组成的矩阵,每一行代表一个查询头的检索需求,行数等于序列长度。
- 键矩阵 K所有词元的键向量组成的矩阵,相当于资料索引标签,用来与查询向量做匹配。
- 点积 QKᵀ计算查询与所有键的相似度得分,结果是一个 n×n 的注意力分数矩阵。
- 缩放因子 √d_k除以头维度的平方根,防止点积过大导致 softmax 梯度消失。
- softmax 归一化把分数转化为概率分布,表示对每个词元的关注权重。
- 加权求和 V用注意力权重对值矩阵 V 加权求和,得到当前词元的上下文表示。
KV缓存与显存瓶颈
图书馆一旦开始接待长期读者(生成任务),问题就更严重了。自回归生成就像读者逐页阅读一本长书:每翻到新的一页,读者代表都会重新思考:“之前看过的内容里,哪些跟我现在有关?”但早已翻阅过的每一页的索引(K)和摘录(V)其实是固定的——没有必要每翻一页就去库房重新取一次。
聪明的做法是把这些索引和摘录放在一个专门的书架上随取随用。这个书架在 Transformer 推理中就叫 KV Cache。它把历史词元的 K 向量和 V 向量缓存下来,避免每一步重复计算整段序列的 K/V 投影。但代价是:书架上的资料会随生成长度持续增加。
KV Cache 的显存占用可以精确计算。设批大小为 b、序列长度为 s、KV 头数为 h、每头维度为 d,fp16 精度下每个参数占 2 字节,则 K 与 V 两组缓存的占用为:
在 MHA 图书馆里,h 就等于读者代表的数量。读者越多、书翻得越厚,书架占的空间就越大——而且这个空间随翻页数线性增长。于是两个直接后果出现:能同时接待的长期读者(batch)变少,一本书最多能读到的页码(上下文长度)也被卡死。这就是推理阶段最头疼的“空间危机”。
- 批大小与序列长度缓存在 batch 和序列两个维度上都要逐一存放,两者线性相乘。
- 头数与头维度每组头都有自己的 K、V 向量,头数越多缓存越大。
- K 与 V 各一份键缓存与值缓存各占一份空间,因此最前面有一个系数 2。
- fp16 字节数每个参数按 2 字节存储,最后再乘 2。
MQA:一位资料员服务所有人
面对空间危机,一个极端的方案浮出水面:只保留一位全能资料员。2019 年提出的 MQA(Multi-Query Attention,多查询注意力) 正是这个思路——所有查询头共享同一组 K/V 头。不管你有 32 个还是 64 个查询头,键和值都只有一份。
MQA 把 KV 头数压缩到 1。这样的好处立竿见影:KV Cache 显存公式中的 h 直接从几十降为 1,占用瞬间减少到原来的 1/32 甚至 1/64。在追求极致显存压缩的早期推理加速方案中,MQA 是一个极具吸引力的选择。
用符号表示,MQA 是 KV 头数为 1 的特例:
但很快,图书馆长发现一位资料员要同时响应几十位读者的咨询,忙得不可开交。模型评测显示,MQA 在多项任务上的精度明显下滑——特别是模型规模变大后,共享一组 K/V 成为信息瓶颈,读者们得到的答复质量大打折扣。省了空间,却丢了服务质量。
- KV 头数固定为 1全部查询头共享唯一一组键值投影。
- 查询头数量保持查询头数不变,多个查询头并行工作但共享同一组键值。
GQA核心:分组共享的折中智慧
那有没有“既要空间、又要质量”的方案?当然有——把读者分个组。GQA(Grouped-Query Attention,分组查询注意力) 的设想是:把 h_q 个查询头分成若干组,每组共享一组 K/V 头。组内共享、组间独立,既有空间压缩,又保留多样性。
GQA 是 MHA 与 MQA 的完美折中。它引入分组比例:假如查询头有 h_q 个、KV 头有 h_kv 个,那么每个 KV 头被 \(\frac{h_q}{h_kv}\) 个查询头共享。当这个比例等于 1 时,GQA 退化为 MHA;当 KV 头数为 1 时,退化为 MQA。
头数关系用公式表达非常简洁:
在 GQA 图书馆里,读者按照兴趣方向分成若干小组,每组配一位专属资料员。资料员只需熟悉本组读者的查阅偏好,服务效率高;不同小组各司其职,检索角度依然多样。空间与质量,第一次握手言和。
- g 为分组大小每个 KV 头负责服务的查询头数量。
- KV 头数为分组结果查询头数除以分组大小,得到实际需要保留的 KV 头数。
Llama的GQA实践
当管理方案成熟后,开源社区最有影响力的 Llama 系列把它写进了运营章程。Llama 2 与 Llama 3 都采用 GQA,使其成为开源大模型的标配设计。具体来看,Llama 2 的 7B 和 13B 版本使用 MHA,34B 和 70B 版本转向 GQA;Llama 3 则全系列采用 GQA。
以 Llama 2 70B 为例:它有 64 个查询头,但 KV 头只有 8 个——每个 KV 头被 8 个查询头共享。Llama 3 8B 则是 32 个查询头配 8 个 KV 头,每组 4 个查询头共享一组 KV 头。配置灵活而统一。
Llama 的 GQA 配置可用分组比例描述:
为什么 Llama 如此青睐 GQA?因为训练时它几乎不损失模型能力,推理时却能带来数倍的显存收益。从 Llama 2 到 Llama 3,从旗舰到小参数版本,GQA 成为开源模型推理优化的事实标准。
- 查询头数 N_qLlama 2 70B 的注意力层拥有 64 个查询头。
- KV 头数 N_kvK/V 投影只保留 8 个头,投影矩阵输出维度为 8×头维度。
- 分组比例 G每组 8 个查询头共享一个 KV 头,KV Cache 相应压缩到 1/8。
显存收益:量化分析与加速效果
空间省下来,图书馆的接待能力就有了质的飞跃。GQA 的显存收益是立竿见影的——在 KV Cache 显存公式中,头数从查询头数降到 KV 头数,缓存占用直接乘以二者的比值。
Llama 2 70B 的 Q/KV 头数比为 64:8,KV Cache 降为原来的 1/8,节省 87.5%;Llama 3 8B 的 32:8 比例下,节省 75%。举个具体数字:batch=1、seq=2048、head_dim=128、fp16 时,Llama 3 8B 的 MHA 模式 KV Cache 约 32MB,GQA 模式仅 8MB。
节省比例的通用公式为:
书架腾出的空间意味着:同样的显存预算下,可以同时接待更多读者(更大的 batch),或者允许读者翻阅更厚的长书(更长的上下文)。这正是 Llama 推理服务吞吐量提升的关键来源,也是 GQA 被称为“推理加速方案”的根本原因。
- KV 头占比\(\frac{N_{kv}}{N_q}\) 表示 GQA 缓存相对 MHA 缓存的比例。
- 节省比例用 1 减去占比,得到显存节省率。
质量对比:注意力多样性的保持
分组共享资料员,读者们的咨询质量到底有没有打折扣?这是 GQA 方案能否成立的关键问题。论文实验给出的结论是:GQA 的质量非常接近 MHA,显著优于 MQA。原因在于注意力的多样性。
MHA 中 64 个查询头提供 64 种独立角度的信息;GQA 将查询头分成 8 组,虽然每组共享 K/V,但 Q 的投影仍然独立,每组内的查询头依然能检索出不同侧重点的信息。而 MQA 所有查询头共享一套 K/V,角度单一,成为短板。
实验常用困惑度(PPL)衡量生成质量,PPL 越低越好。公式为:
在图书馆的读者满意度调查中,分组资料员方案的满意度与每位读者配专属资料员几乎无异。因为各组虽然共享了“资料索引”(K/V),但每位读者代表仍用自己的方式提问,检索角度的多样性得以保持——这就是 GQA“省空间不丢质量”的秘密。
- 对数概率模型对第 i 个真实词预测概率的对数。
- 平均负对数似然对所有位置取平均再加负号,得到平均损失。
- 指数还原取指数将损失映射为可读的困惑度值。
工程实现:分组共享的落地细节
方案再好,也要落地到图书馆管理系统里。GQA 的工程实现并不复杂,核心改造有两处:投影矩阵的维度调整与头的共享复制。第一处,K/V 投影矩阵的输出维度从“查询头数×头维度”改为“KV 头数×头维度”;第二处,在注意力计算前把 K/V 头重复扩展到查询头数,让矩阵形状对齐。
在 PyTorch 中,repeat_interleave 可以轻松实现 KV 头的分组复制。设 K 的形状为 (batch, n_kv, seq, dim),每组 g 个查询头共享一个 KV 头,则扩展后:
落地时还有一个关键技巧:KV Cache 本身仍然按 n_kv 个 KV 头存储,只有在计算注意力分数时才展开复制。缓存保持精简,计算照常工作——相当于资料员的席位还是那几个,只是响应查询时一人扮演组内所有读者代表的“接线员”。这是 GQA 工程实现的精髓,也是部署推理引擎时性能与显存兼顾的关键。
repeat_interleave 不是拷贝数据到显存缓存中,而是在计算时按组展开视图,底层可做内存复用。- 重复插值将每个 KV 头沿头维度连续复制 g 份。
- 形状对齐复制后 KV 头数变为 \(n_{kv} \times g = n_q\),与查询头数量一致,可直接做矩阵乘法。
AI 应用场景
云端大模型推理服务
阶段:推理部署与性能优化
解决的问题:在云 GPU 上部署 Llama 2 70B 时,KV Cache 往往占据大量显存,导致单卡能承载的并发请求数极其有限。使用 GQA 后,KV Cache 降至原来的 1/8,同样的 A100 80GB 可以支持更大的 batch 和更多的并发请求,单位成本大幅下降。
具体案例:某公司用 8×A100 部署 Llama 2 70B 提供 API 服务,切换到基于 GQA 的推理后端后,吞吐量提升约 2 倍,首 token 延迟同步下降,客户满意度明显提升。
超长上下文处理
阶段:长文本数据预处理与推理
解决的问题:法律、金融文档分析需要一次性处理数万字的上下文,而 KV Cache 与序列长度成正比,长上下文往往超出单卡显存。GQA 将 KV Cache 压缩到 1/4 甚至 1/8,使同一块显卡能支撑的上下文长度扩大 4 到 8 倍。
具体案例:使用 Llama 3 8B 分析一份 10 万字的企业财报,在 KV Cache 降为 MHA 的 1/4 后,模型可以一次性读完整份文档并稳定生成摘要,无需切片处理。
端侧轻量化部署
阶段:端侧模型推理优化
解决的问题:手机、PC 端运行小模型时显存和内存都极其有限,KV Cache 优化是端侧推理能否落地的关键一环。GQA 让 Llama 3 8B 这类模型在消费级设备上支持更长的对话历史。
具体案例:在 MacBook M2 上运行 Llama 3 8B 的量化版,GQA 使 KV Cache 减少 75%,对话窗口从 4K 扩展到 16K,离线助手体验接近云端水平。
Python 演示
import mathimport torchimport torch.nn as nn class GQAAttention(nn.Module): """Grouped-Query Attention 分组共享KV头""" def __init__(self, hidden_dim, num_q_heads, num_kv_heads): super().__init__() self.num_q_heads = num_q_heads self.num_kv_heads = num_kv_heads self.head_dim = hidden_dim // num_q_heads self.num_groups = num_q_heads // num_kv_heads kv_dim = num_kv_heads * self.head_dim self.q_proj = nn.Linear(hidden_dim, hidden_dim) self.k_proj = nn.Linear(hidden_dim, kv_dim) self.v_proj = nn.Linear(hidden_dim, kv_dim) self.o_proj = nn.Linear(hidden_dim, hidden_dim) def forward(self, x): batch, seq_len, _ = x.shape q = self.q_proj(x).view( batch, seq_len, self.num_q_heads, self.head_dim ).transpose(1, 2) k = self.k_proj(x).view( batch, seq_len, self.num_kv_heads, self.head_dim ).transpose(1, 2) v = self.v_proj(x).view( batch, seq_len, self.num_kv_heads, self.head_dim ).transpose(1, 2) 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.head_dim) attn = torch.softmax(scores, dim=-1) out = torch.matmul(attn, v) out = out.transpose(1, 2).contiguous().view(batch, seq_len, -1) return self.o_proj(out) def kv_cache_bytes(batch, seq_len, num_kv_heads, head_dim, dtype_bytes=2): return 2 * batch * seq_len * num_kv_heads * head_dim * dtype_bytes if __name__ == "__main__": hidden_dim = 4096 num_q_heads = 32 num_kv_heads = 8 head_dim = hidden_dim // num_q_heads batch, seq_len = 2, 2048 model = GQAAttention(hidden_dim, num_q_heads, num_kv_heads) x = torch.randn(batch, seq_len, hidden_dim) out = model(x) print("输出形状:", tuple(out.shape)) mha_mem = kv_cache_bytes(batch, seq_len, num_q_heads, head_dim) gqa_mem = kv_cache_bytes(batch, seq_len, num_kv_heads, head_dim) ratio = 1 - gqa_mem / mha_mem print(f"MHA KV Cache: {mha_mem / 1024**2:.1f} MB") print(f"GQA KV Cache: {gqa_mem / 1024**2:.1f} MB") print(f"显存节省: {ratio * 100:.1f}%")
kv_dim = num_kv_heads * self.head_dimK/V 投影输出维度只按 KV 头数计算,这是显存压缩的起点。self.k_proj = nn.Linear(hidden_dim, kv_dim)键投影矩阵比标准 MHA 小得多,参数也更少。k = k.repeat_interleave(self.num_groups, dim=1)沿头维度把每个 KV 头复制 g 份,使头数与查询头对齐。scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim)缩放点积计算注意力分数。out = out.transpose(1, 2).contiguous().view(batch, seq_len, -1)把多头输出拼接回隐藏维度。kv_cache_bytes(...)按 KV 头数计算缓存字节数,头数越少占用越低。ratio = 1 - gqa_mem / mha_mem量化节省比例,32 头降到 8 头时节省 75%。
常见误区
误区减少 KV 头等于减少信息,模型质量肯定会大幅下降。很多初学者看到“共享”二字就本能地排斥,认为这是偷工减料。
正解GQA 在 Llama 2/3 的实验中的确让 KV 头数大减,但困惑度与 MHA 几乎持平,远好于 MQA。原因是同一组内的查询头高度相关,它们共享一套键值不会损失太多信息;而不同组之间仍然保持独立,注意力的多样性得以保留。
为何常见:直觉上“少了就是差了”,忽略了注意力头之间存在大量冗余这一事实。MQA 的教训让人误以为凡是共享都会掉点,但 GQA 的分组粒度恰恰避开了这个坑。
误区两者都减少了 KV 头数,很多人以为 GQA 只是 MQA 换了个名字,或 MQA 是 GQA 的进阶版。
正解MQA 是 GQA 的特例——当所有查询头归为一组、共享唯一一组 KV 头时,就是 MQA。GQA 支持任意分组比例,例如 Llama 3 8B 用 4 个查询头共享 1 个 KV 头,Llama 2 70B 用 8 个查询头共享 1 个 KV 头,灵活性远高于 MQA。
为何常见:两者在“减少 KV 头”这件事上方向一致,初学者只看到表面相似,忽略了分组这一关键维度。
误区以为 KV Cache 只是显存占用问题,跟推理快慢无关,优化它只是为了省显存而已。
正解KV Cache 占用的显存减少后,同一块 GPU 可以塞进更大的 batch,计算吞吐随之提升;同时访问更小的缓存也降低了内存带宽压力,解码速度同样受益。GQA 之所以被称为“推理加速方案”,正是因为它通过显存优化间接加速了整个推理过程。
为何常见:人们习惯把“显存”和“速度”当作两个独立维度,忽略了显存约束会直接卡住 batch 大小,而 batch 大小是吞吐的决定性因素。
误区认为 GQA 是一种推理加速技巧,训练时仍然使用 MHA,推理时才切换到 GQA。
正解GQA 是模型架构的一部分,Llama 2/3 从预训练开始就使用 GQA 结构,训练和推理完全一致。如果训练用 MHA、推理用 GQA,权重不匹配会导致模型完全无法运行。
为何常见:GQA 的宣传重点在推理优化,教程也常从 KV Cache 讲起,容易让人误以为训练和推理可以拆分使用不同的注意力结构。
误区既然 KV 头越少显存越省,那把 KV 头压到 1 个(MQA)不就是最优解?何必搞分组?
正解MQA 的显存最省,但实验表明其生成质量明显下滑,尤其是在大规模模型上共享一组键值会成为信息瓶颈。GQA 的价值在于找到最佳平衡点——Llama 系列用 8 个 KV 头换来接近 MHA 的质量,同时省下 75% 以上的缓存。
为何常见:把显存收益当作唯一目标,忽略了质量与效率的权衡。只盯着“少”而忘了“够用”才是工程决策的本质。
面试题
Q1. GQA的全称是?
- A.分组查询注意力← 正确答案
- B.全局查询注意力
- C.生成查询注意力
- D.梯度查询注意力
答案:A。GQA 是 Grouped-Query Attention 的缩写,即分组查询注意力。它把查询头分成若干组,每组共享一组 KV 头,是介于 MHA 与 MQA 之间的折中方案。
Q2. GQA相对于MHA的主要优势是?
- A.提高模型精度
- B.减少训练数据
- C.减少KV缓存显存← 正确答案
- D.消除过拟合
答案:C。GQA 通过分组共享 KV 头,将 KV 头数减少为查询头数的若干分之一,直接压缩了 KV Cache 的显存占用。它并不提升精度,也不减少训练数据或消除过拟合。
Q3. KV Cache中缓存的内容是?
- A.查询向量和值向量
- B.键向量和值向量← 正确答案
- C.查询向量和键向量
- D.注意力权重矩阵
答案:B。自回归推理时,历史词元的 K 和 V 在生成过程中保持不变,因此缓存 K 和 V 供后续步骤复用。Q 是每步重新计算的,而注意力权重矩阵是动态结果,不需要缓存。
Q4. 查询头64个KV头8个,每组共享多少查询头?
- A.4个查询头
- B.8个查询头← 正确答案
- C.16个查询头
- D.32个查询头
答案:B。每组共享的查询头数 = 查询头数 ÷ KV 头数 = 64 ÷ 8 = 8。这是 Llama 2 70B 的实际配置,8 个查询头共享一组 KV 头。
Q5. Llama 2 70B中KV Cache显存减少比例?
- A.百分之五十
- B.百分之七十五
- C.百分之八十七点五← 正确答案
- D.百分之九十五
答案:C。节省比例 = 1 - KV头数/查询头数 = 1 - 8/64 = 87.5%。KV Cache 从 64 组头降至 8 组头,显存压缩到原来的 1/8。
Q6. GQA与MQA之间的关系是?
- A.两者是完全无关机制
- B.MQA是GQA的特例情况← 正确答案
- C.GQA是MQA的特例情况
- D.两者是完全相同的概念
答案:B。当 GQA 的所有查询头归为一组、共享唯一一组 KV 头时,GQA 就退化为 MQA。因此 MQA 是 GQA 在分组数为 1 时的特例。
Q7. GQA中K和V的投影维度是?
- A.查询头数量乘头维度
- B.KV头数量乘头维度← 正确答案
- C.模型隐藏层维度
- D.词汇表总维度
答案:B。GQA 的 K/V 投影只输出 KV 头数对应的维度,即 KV头数 × 头维度,比查询头的维度小。这从投影层就减少了参数和缓存,是显存优化在工程上的起点。
Q8. hidden=4096, q=32, kv=8, b=1, s=2048, fp16。KV Cache多少MB?
- A.约16MB← 正确答案
- B.约32MB
- C.约64MB
- D.约128MB
答案:A。head_dim = 4096 / 32 = 128。KV Cache = 2 × 1 × 2048 × 8 × 128 × 2 字节 = 16,777,216 字节 ≈ 16MB。若用 MHA 的 32 个 KV 头,则占用 64MB,GQA 恰好节省 75%。
Q9. 关于GQA分组说法正确的有?
- A.每组查询头共享KV头← 正确答案
- B.不同组之间也共享KV头
- C.KV头数等于Q头数时是MHA← 正确答案
- D.KV头数为1时是MQA← 正确答案
答案:ACD。GQA 的核心是组内共享、组间独立,因此 B 错误。当 KV 头数等于查询头数时,每组只有一个查询头,退化为 MHA;当 KV 头数为 1 时,所有查询头一组,退化为 MQA。
Q10. GQA相比MQA的优势包括?
- A.保持注意力多样性← 正确答案
- B.训练和推理效果更好← 正确答案
- C.KV缓存占用更小
- D.模型参数大幅减少
答案:AB。GQA 将查询头分组,每组保留独立的 KV 头,注意力多样性优于 MQA 的单组共享,生成质量更高。C 错误:MQA 的 KV 头数更少,缓存占用更小。D 错误:GQA 参数略多,但差距不大,称不上“大幅减少”。