上周给 Qwen3-8B 估 KV 缓存,我按 32 个注意力头去算,得出的数字是实际值的 4 倍。翻 config 才发现,num_key_value_heads 写的是 8,不是 32。差的这几倍来自 GQA。这篇文章把账重算一遍,顺便记下部署时真正会绊住人的地方。
GQA 是什么:MHA 和 MQA 之间的折中
标准多头注意力(MHA)里,有几个查询头就配几份 K 和 V。Llama-3.1-8B 有 32 个查询头,按老规矩解码时就得缓存 32 份 K/V。
2019 年 11 月,Noam Shazeer 发了篇只有三页纸的论文,提出多查询注意力(MQA):所有查询头共享同一份 K/V。加载缓存的带宽开销直接砍到头数分之一,解码快了很多,代价是模型质量可能往下掉。
2023 年 5 月,Google 的团队在 GQA 论文(arXiv 2305.13245)里给出折中方案:查询头分组,每组共享一份 K/V。论文里有两处值得记:把训好的 MHA 模型「升级训练」成 MQA/GQA,只需要原始预训练计算量的 5%;升级后的 GQA 质量接近 MHA,速度接近 MQA。摘要原话是 quality close to multi-head attention with comparable speed to MQA。
论文自己的实验基于 T5:XXL 版本里 MHA 解码要 1.51 秒,8 个 KV 组的 GQA 压到 0.28 秒,五份摘要任务的质量分数几乎没动。数字摆在面前,主流模型跟进了。这几个 config 都是我在 Hugging Face 现拉的:
| 模型 | 查询头 | KV 头 | 每组共享 |
|---|---|---|---|
| Llama-3.1-8B | 32 | 8 | 4 个 |
| Qwen3-8B | 32 | 8 | 4 个 |
| Falcon3-7B | 12 | 4 | 3 个 |
| Llama-2-70B | 64 | 8 | 8 个 |
| MiniMax-M2 | 48 | 8 | 6 个 |
清一色 GQA。现在还坚持 MHA 的开源模型反而是少数。
分组可以直接想成值班表。32 个查询头按顺序 4 个一组,第 1 到 4 号共用 1 号 KV 头,第 5 到 8 号共用 2 号,往后类推。算注意力时,每个查询头自己的查询向量不变,学到什么、关注哪里也不受影响,变的是组内大家共用同一份 K/V。组越大省得越狠:MQA 相当于全模型一组,MHA 是一人一组,GQA 卡在中间。
显存账:省出来的 4 倍在哪
KV 缓存每 token 的字节数有一个公式:2 × 层数 × KV 头数 × head_dim × 每参数字节数。开头的 2 是 K 和 V 各存一份。注意公式里用的是 KV 头数,不是查询头数,GQA 省的就是这一项。
拿 Llama-3.1-8B 算,32 层、head_dim 128、fp16 两字节:
- 假如它是 MHA,32 个 KV 头:每 token 512 KB
- GQA 实际,8 个 KV 头:每 token 128 KB
32K 上下文,GQA 要 4 GB,不砍 KV 头的写法要 16 GB。8B 模型的权重 fp16 也才 16 GB 左右,KV 这笔账不省,长上下文服务根本摆不开。上下文拉到 128K,差距变成 64 GB 对 16 GB。
MiniMax-M2 的账我也顺带算了:62 层、8 个 KV 头、head_dim 128,每 token 248 KB。32K 上下文约 7.75 GB。它 config 里 max_position_embeddings 是 196608,真开满单 KV 缓存就要 46.5 GB(fp16),还没算权重。模型卡里写它 230B 总参数、10B 激活,MoE 省的是权重和算力,KV 依旧老老实实线性涨。
和 MLA 对照一下更有感觉。DeepSeek V3 是 61 层,MLA 把 KV 压到每 token 约 68.6 KB(我上一篇 MLA 文章里算过细账),只有 M2 这类 GQA 的三成不到。GQA 是「少存几份」,MLA 是「换一种存法」。两条路线的分布也核实过:Kimi K3 用的是 KDA,MiniMax-M2 是 GQA,MLA 目前基本就是 DeepSeek 系在用。
部署的坑:并行切法被 KV 头数卡住
显存账算完是好消息,坏消息在部署侧。vLLM 源码(vllm/config/model.py)里有硬校验:模型注意力头总数必须能被张量并行数整除,报错原文是 must be divisible by tensor parallel size。GQA 模型还有隐含的一条:TP 切分的是查询头,每张卡分到的查询头组里得带完整的 KV 头,所以 TP 数同时被 KV 头数卡着。
落到 8 个 KV 头的模型上,TP 只能选 1、2、4、8。想拿 3 张卡凑 TP=3,vLLM 直接拒绝启动。这不是 bug,是结构决定的:8 份 KV 切不成 3 等份。
Ollama 那边是另一个习惯问题。官方 FAQ 写明默认上下文只有 4096 token,很多人跑长文档不调 num_ctx,GQA 省出来的显存一直闲着。按 128 KB/token 的账,把 8B 模型开到 32K 只多吃 4 GB,16 GB 显存的卡装完权重和激活还有余量。先算账再调参,心里有底。
transformers 的实现细节也值得看一眼。modeling_llama.py 里有个 repeat_kv 函数,在算注意力之前把每组共享的 K/V 沿头维度广播展开成 32 份。这份展开是计算时的临时量,持久化的 KV 缓存还是按 8 份存。这也解释了一个容易误会的事:KV 头数影响缓存大小,不改变注意力本身的计算量。
真缺显存时,动哪里
算完发现显存不够,手段从粗到细有三种。最粗的是并行切法,上一节说过,TP 被 KV 头数卡死,别硬凑不整除的组合。
细一层是压 KV 本身。vLLM 启动参数里有个 --kv-cache-dtype,能把 KV 缓存从 fp16 换成 fp8 一类的低精度,字节数直接砍半,前面那笔账全部乘 0.5,质量有轻微损失,长上下文场景一般划算,具体取值以你那个版本的 --help 为准。上下文该砍也得砍:Qwen3-8B 的 max_position_embeddings 是 40960,真开满光 KV 就要 5 GB,业务用不到 32K 就别开 32K。
最细的是让每一字节都花在刀刃上,这是 vLLM PagedAttention 的地盘:把整块连续分配改成分页,碎片浪费从百分之几十压到几乎为零,细节在我之前那篇 vLLM 里。
收尾:一个能带走的顺序
拿到新模型,我的顺序是:先看 config 里的 num_key_value_heads,套公式算出每 token 的 KV 字节,乘上目标上下文长度,对照显卡显存余量,最后才决定 TP 怎么切、要不要量化。拿 24 GB 的卡举例:fp16 的 8B 模型权重要 16 GB,剩 8 GB 给 KV 和激活。按每 token 128 KB 算,8 GB 大约撑 6.5 万 token 的缓存,扣掉激活和框架开销,32K 上下文就是安全线。这笔账两分钟算完,比上线后试错快得多。
次序反过来的话,经常是模型 load 得进去、跑长上下文时 OOM,查了半天查到 KV 头数上。你的显卡跑 8B 模型时开过多长的上下文?评论区聊聊各自的显存账。
MLA 路线的实际收益和代价,我计划用实测数据单独写一篇 vLLM 上 GQA 与 MLA 的吞吐对比,写好会挂在下面这篇 MLA 入门的评论里,感兴趣可以蹲一下。
延伸阅读: