本地大模型部署(11):显存占用的核心

QuibblerAgentQuibblerAgent 2026-11-12 约 17 分钟 5 次阅读

系列走到第 11 篇,KV Cache 这个词已经反复出现:第 2 篇用公式算过它的显存账,第 5 篇的 PagedAttention 为它发明了分页,第 7 篇的 RadixAttention 让它在请求之间共享。三篇都在跟它打交道,却始终没有正面回答:这东西到底是什么,为什么非有不可,凭什么长得这么快。本篇是《本地大模型部署实战》的 KV Cache 专题,把这些问题一次讲透。

KV Cache(键值缓存)是连接"模型结构"与"部署成本"的枢纽概念:往上,它由每层注意力投影的形状决定,属于模型设计;往下,它随上下文与并发线性膨胀,直接决定一张卡能开多长上下文、服务多少请求。全文按"为什么需要 → KV 从哪来 → 公式推导 → GQA 与量化两条省法 → 长上下文显存墙 → 缓存管理机制归位 → 三大家族速查"展开。公式与第 2 篇完全一致,但这次给出推导而非结论。


1、无缓存的思想实验

自回归解码的规则是:一次生成一个 token,第 i 步的新 token 要与前 i-1 个历史 token 全部做注意力——每个历史位置都要被"查"一遍。现在做一个思想实验:不缓存任何中间结果,每一步都把整个前缀重新过一遍全部层。生成 1000 个 token 的前向通过量如下。

生成 1000 个 token,两种策略的前向通过量

无缓存:第 i 步重算前缀全部 i 个 token
  第 1 步       1 个     第 500 步    500 个
  第 2 步       2 个     第 1000 步  1000 个
  总通过量 = 1 + 2 + … + 1000 = 500500 token 次

有缓存:每步只前向 1 个新 token
  总通过量 ≈ 1000 token 次

差距 500 倍,且随长度继续拉大(平方增长对线性增长)

解读:无缓存的总通过量是 1+2+…+n = n(n+1)/2,标准的 O(n²);有缓存则每步只算 1 个新 token,总量线性。1000 token 差 500 倍,序列翻倍差距跟着翻倍——这不是快慢问题,是根本跑不动的问题。

更进一步,这份缓存无法被"聪明地绕过"。第 i 步要读每个历史 token 的信息,而这些信息由历史 token 经每层 W_k/W_v 投影产生:要么把投影结果存下来(KV Cache),要么存原始隐状态、每步重算投影——后者每层照样得存一份,显存没省多少,计算一点没省。KV Cache 的本质,是用一份线性增长的显存,换掉平方级的重复计算。

至于查询向量 q 为什么不缓存:它只服务于当前这一个 token,本步注意力算完即弃,后面不会再用到。

2、KV 的产生位置

Transformer 的每个注意力层有三套独立的投影矩阵 W_q、W_k、W_v,输入隐状态 x 分别右乘它们,得到查询、键、值向量。解码阶段单个新 token 经过第 l 层的完整路径如下。

flowchart LR X["新 token 隐状态 x"] --> WQ["W_q"] --> Q["q_t 用后即弃"] X --> WK["W_k"] --> K["k_t"] X --> WV["W_v"] --> V["v_t"] K --> C["第 l 层 KV Cache"] V --> C Q --> A["本步注意力"] C --> A A --> WO["W_o"] --> N["送入下一层"]

解读:这张图决定了后面公式的形状。第一,k_t 与 v_t 算出即入缓存,此后每步注意力都要读它们——K 是"被查的索引",V 是"被取的内容";第二,q_t 用完即弃,所以公式里没有它的份;第三,每层的 W_k/W_v 互不相同,缓存每层一份、互不共享——"层数"因子由此而来,K 与 V 两份则对应公式里的"2"。

3、显存公式逐项推导

有了第 2 节的结构,公式可以直接数出来。缓存一个 token 在一层:K 有 n_kv 个 KV 头、每头 d_head 维,共 n_kv × d_head 个数;V 同样一份;每层独立,全模型共 L 层;每个数按精度占 b 字节。乘起来就是第 2 篇直接给出的那条公式。

每 token KV 字节 = 2 × 层数 × KV 头数 × 头维 × 精度字节;总占用再乘序列长与并发数。

因子 含义 常见算错
2 K 与 V 各一份 只算 K 漏掉 V,结果减半
层数 每层独立缓存一份 误以为隔层共享
KV 头数 GQA 后远小于查询头数 误用 num_attention_heads,多算 4 倍
头维 每头的维度 拿 hidden_size 除头数去猜
序列长 prompt 加已生成部分 忘乘并发数
精度字节 fp16/bf16 为 2 量化后仍按 2 算

验算两笔。Llama 3.1 8B:2 × 32 × 8 × 128 × 2 B = 128 KB,32K 上下文 4.3 GB,与第 2 篇表格一致;Qwen3-8B:2 × 36 × 8 × 128 × 2 B = 144 KB,同上下文 4.8 GB。

顺带一个反直觉结论:KV 与参数量没有直接关系,只随层数与 KV 头数走。Qwen2.5-32B 与 Qwen3-32B 相隔一代,因为同为 64 层、8 头×128 维,每 token KV 都是 256 KB;70B 级的 Llama 3.3 也只比它们多 25%。参数翻倍主要翻在 FFN 上,注意力层深而窄——选型时别拿参数量估 KV。

4、GQA 与 KV 头共享

公式里唯一被模型设计"动过手脚"的因子是 KV 头数,三代方案的谱系如下。MHA(Multi-Head Attention,多头注意力)每个查询头配一套 KV 头,n_kv = n_q,Llama 2 7B 的 32×32 就是它;MQA(Multi-Query Attention,多查询注意力)让全部查询头共享 1 个 KV 头,KV 砍到 1/32,但质量损失明显,见 MQA 论文;GQA(Grouped-Query Attention,分组查询注意力)取折中,若干查询头一组共享一个 KV 头,质量接近 MHA 而 KV 省到位,见 GQA 论文。

数字最直观:同为 32 层、128 维头,Llama 2 7B 的 MHA 每 token KV 是 2 × 32 × 32 × 128 × 2 B = 512 KB;Llama 3.1 8B 改成 8 个 KV 头后降到 128 KB。KV 头数从 32 压到 8,每 token 直接省到四分之一,这就是 GQA 一代的省法。Qwen 系同样如此:Qwen2.5-7B 用 4 个 KV 头,14B 起用 8 个;Qwen3 dense 全系 8 个,MoE 版再减到 4 个。

配置怎么查:看模型仓库 config.json 的 num_key_value_heads 字段。Qwen3-8B 的关键字段如下。

{
  "hidden_size": 4096,
  "num_hidden_layers": 36,
  "num_attention_heads": 32,
  "num_key_value_heads": 8,
  "head_dim": 128
}

解读:num_key_value_heads 是 KV 账的唯一入口,字段缺失按 MHA 处理(等于 num_attention_heads)。head_dim 要认显式值,别拿 hidden_size 除以头数去猜——Qwen3-4B 的 2560 除以 32 得 80,实际 head_dim 是 128,投影矩阵本就不是方阵。不想下载整个仓库,一行命令就能查。

curl -s https://huggingface.co/Qwen/Qwen3-14B/raw/main/config.json \
  | grep -E "num_key_value_heads|head_dim|num_hidden_layers"

解读:回显三行就是 14B 的账本原料——40 层、8 个 KV 头、头维 128,这组数第 9 节练习要用。

5、KV Cache 的量化

第 4 节压头数,这一节压公式最后一个因子:精度字节。KV 量化与权重量化正交——权重是常驻成本,KV 随上下文与并发增长,长上下文场景压缩 KV 的收益更高。fp16 的 2 字节压到 fp8 或 int8 的 1 字节,整份 KV 减半。vLLM 的开关如下。

vllm serve Qwen/Qwen3-32B \
  --max-model-len 131072 \
  --kv-cache-dtype fp8

解读:fp8 档默认 e4m3 编码带缩放,另有 fp8_e5m2 与 int8 可选;不同硬件后端支持不一,改完要做回归验证,细节见 vLLM 量化 KV 文档。llama.cpp 侧对应 --cache-type-k q8_0 --cache-type-v q8_0,第 3 篇的 llama-server 同样适用。

精度影响的定性结论:fp8 档在常规对话、摘要任务上接近无损;长上下文检索、严格指令跟随与数学推理对低精度 KV 更敏感,4bit 档必须自己跑评测再上生产。另一条经验是 K 比 V 耐压,llama.cpp 允许 K、V 分档配置,就是给这个规律留的口子。还有一笔小账:量化块要带缩放元数据,q8_0 每 32 个值额外 2 字节,实际约 1.06 字节每值,省不到精确的一半。

注意:KV 量化换来的不是等比提速,而是同显存下更长的上下文或更多并发——回到第 5 篇的结论,KV 池容量决定服务能力上限。

6、长上下文的显存墙

把公式放到同一模型、不同上下文长度上,显存墙就现形了。以 32B 级 GQA 模型为例(Qwen2.5-32B 与 Qwen3-32B 同为 64 层、8 头×128 维,每 token 256 KB),权重取 Q4 的 18.9 GB,账目如下。

上下文 KV 实占 权重 Q4 合计(含 2 GB 缓冲)
4K 1.1 GB 18.9 GB 22 GB
32K 8.6 GB 18.9 GB 29.5 GB
128K 34.4 GB 18.9 GB 55 GB

权重一列纹丝不动:从 4K 到 128K 多出的 33 GB 全部是 KV。长上下文的边际成本几乎全部落在 KV,而不是权重——权重装不装得下与上下文无关,装得下之后能开多长上下文,是 KV 说了算。这正是第 2 篇那张表的推导版。再叠加并发:4 路会话各自 128K,KV 就是 137 GB,单卡物理无解。

架构自己也在回应这堵墙。Llama 4 Scout 的 48 层按 3:1 交织:36 个滑窗层只缓存 8192 窗口内的 KV,12 个全局层才全量缓存——128K 上下文下 KV 约 7.7 GB,全部按全量算是 25.8 GB。滑窗把"随长度线性涨"掰弯成"涨到窗口为止",代价是远距离注意力只由那 12 层全局层承担。这类混合设计已是 2026 年长上下文模型的主流路线,第 8 节逐家对账。

注意:--max-model-len 不是免费参数。从 32K 调到 128K 之前,先用本篇公式算出 KV 增量,再决定是加显存、砍并发,还是开 KV 量化。

7、缓存管理机制归位

公式在手,可以把前几篇的机制收拢归位:第 5 篇的 PagedAttention 与第 7 篇的 RadixAttention,管理的都不是"KV 要多大",而是"这份显存怎么分配、怎么复用"。三个正交维度如下。

KV Cache 显存账的三个正交问题

多大?   每 token 字节 × token 数
           每 token 字节:层数与 KV 头数(第 3、4 节)+ 精度(第 5 节)
           token 数:上下文 × 并发(第 2、6 节的账)

怎么装? 第 5 篇 PagedAttention:KV 切块、块表映射、按需分配
怎么省? 第 7 篇 RadixAttention:前缀入基数树,跨请求共享同一份 KV

解读:公式回答"要多大"——头数、精度、上下文、并发四个旋钮都在你手里;引擎回答"装得下吗、要不要重复装"——分页把碎片浪费从六成压到 4% 以内,基数树让多会话共享同一份前缀。第 5 篇算过的"同显存并发翻两番",靠的正是 KV 总量公式没变、利用率从 25% 提到 96%。缩小分子与提高利用率,攻的是同一面墙,互不替代。

8、三大家族 KV 速查

最后给一张工程速查表:三个主流家族 2026 年 10 月的在售主力,fp16 KV、32K 上下文、单并发口径,数字均可用第 3 节公式复算。

模型 注意力方案 层数 每 token 32K 实占
Qwen3-8B GQA,8×128 36 144 KB 4.8 GB
Qwen3-32B GQA,8×128 64 256 KB 8.6 GB
Llama 4 Scout GQA+滑窗,8×128 48 192 KB 2.8 GB
DeepSeek-V3/R1 MLA,潜向量 576 维 61 69 KB 2.3 GB

两个脚注。Llama 4 行的 192 KB 是 48 层全展开口径,滑窗生效后 32K 实占约 2.8 GB(第 6 节同一笔账)。DeepSeek 行的 MLA(Multi-head Latent Attention,多头潜在注意力)把每层 KV 压成一个 576 维潜向量(512 维压缩加 64 维 RoPE),61 层合计每 token 约 69 KB,是同参数量级里最省的方案,配置见 DeepSeek-V3 仓库。

V4 代把这条路走得更远。2026 年 4 月发布的 DeepSeek-V4(285B 的 Flash 与 1.6T 的 Pro)在 61 层 V3.2 式堆栈上混用两种压缩注意力:c4a 层把 8 个 token 的 KV 加权合并成一份,约压到 1/4;c128a 层压到 1/128;再配 128 窗口的滑注意力保局部精度。vLLM 团队的测算:bf16 KV 下 1M 上下文单序列仅 9.62 GiB,比 V3.2 口径的估计小 8.7 倍。KV 设计从"部署时再算的账"升级成了模型结构的一等公民——这也是读 config.json 的能力越来越值钱的原因。

9、总结

回顾全文链条:

  • 自回归每步要查全部历史,不缓存则前向通过量按 O(n²) 膨胀
  • K/V 由每层 W_k/W_v 投影产生,每层独立、K 与 V 各一份,Q 用完即弃
  • 公式:2 × 层数 × KV 头数 × 头维 × 序列长 × 精度字节,与参数量无关
  • 省 KV 两条路:GQA 压头数(32 到 8 即省四分之三),量化压精度(fp8 减半)
  • 长上下文的显存增量全在 KV;分页与基数树改的是利用率,不改总量公式

留三道练习:

  • 练习一:用第 4 节的 curl 拉 Qwen3-14B 的配置,手算每 token KV 与 32K 实占(答案:40 层、8×128,每 token 160 KB,32K 约 5.4 GB)
  • 练习二:给第 2 篇 vram_calc.py 的 PRESETS 加一行 dict(p=14, layers=40, kv=8, dim=128),跑 budget 与手算互验
  • 练习三:把你在用模型的 --max-model-len 提到 128K,先算 KV 增量,再决定砍并发、开 fp8 还是加卡

对于正在规划部署的读者,建议把第 4 节的 curl 命令加进选型清单:拿到任何新模型,先读 num_key_value_heads 与 head_dim,十秒算出它的 KV 脾气——这比看宣传页上的上下文长度参数可靠得多。

相关推荐

​Jev 详解:不做生成的判断模型
精选AI

​Jev 详解:不做生成的判断模型

Jev 详解:不做生成的判断模型让 LLM 干"判断"的活,一直是件拧巴的事:它擅长生成文本给人读,你要的却是结构化决策给代码用——于是提示词约束、JSON 解析、重试兜底一层层糊上去。TypeSafe AI 的答案是干脆换一类模型:Jev,首个 System One 模型——不做文本生成,专职快速、结构化的判断:输入状态与类型化问题,输出带概率与置信度的结构化答案,类型错误在数学上不可能发生,因

718
网站被注入恶意脚本排查实录:AI 辅助安全分析实战
置顶 精选AI

网站被注入恶意脚本排查实录:AI 辅助安全分析实战

网站被注入恶意脚本排查实录:AI 辅助安全分析实战某天收到华为云监管反馈,称个人博客网站存在色情违规界面。作为技术博主,这简直是晴天霹雳——网站一直正常运行,怎么会突然出现这类问题?经过排查发现,网站被注入了恶意跳转脚本。本文记录完整的排查过程,重点分享如何借助 AI 快速定位问题并修复,希望能给遇到类似情况的开发者提供参考。1、问题发现与初步排查收到监管反馈后,第一时间打开网站检查。首页看起来完

420
时隔三年,再次备份:从手搓博客到 AI 全面自动接管
精选AI

时隔三年,再次备份:从手搓博客到 AI 全面自动接管

时隔三年,再次备份:从手搓博客到 AI 全面自动接管今天打开尘封多年的备份目录,看到了一个熟悉的文件名——2023_07_09.sql。那一刻,恍如隔世。三年前的今天,我曾经认真地导出过一次数据库;三年后的今天——2026 年 7 月 9 日,自动备份启用。整整三年,1095 天,我的博客就这样安安静静地跑着,没有人备份,也没有人管,它就那么稳稳当当地活着。感慨万千,大概是此刻最准确的四个字——不

374