版权与内容来源声明
本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容,均在附表 A 中标注来源;引用官方原文保持原样,不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准,标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式,也不对任何收益结果作承诺。转载请注明出处。
第 1 章 显存不是一个数,是四笔账
第一次把模型拉到本地跑的人,多半都经历过同一个瞬间:看一眼参数规模,觉得卡够用;把上下文调长一点、或者同时来了几个请求,程序直接被系统杀掉。这个现象叫显存溢出(OOM,显存不够用了,运行中的进程被强制终止)。
问题出在一个默认假设上——很多人把「模型多大」当成了「显存占多少」。这两件事差得很远。
显存其实是被四笔账分掉的:权重、KV Cache、激活值与临时缓冲、框架与运行时开销。前三笔随你的用法变化,第四笔几乎完全不受你控制,但一直在那儿。
1.1 四块账各自由谁决定
先把四笔账摆到一张表里,后面每一章再拆开讲。
| 这一块账 | 主要由谁决定 | 随上下文长度涨吗 | 随并发 / 批量涨吗 | 最容易在哪里成为瓶颈 |
|---|---|---|---|---|
| 权重 | 参数量 × 每个参数的字节数 | 不涨 | 不涨 | 模型刚加载完就快占满 |
| KV Cache | 层数、KV 头数、头维度、序列长度、精度 | 线性涨 | 线性涨 | 上下文一拉长就崩 |
| 激活值与临时缓冲 | 批量大小、序列长度、模型深度、隐藏层维度 | 涨 | 涨 | 加大批量或跑长序列时崩 |
| 框架与运行时开销 | CUDA 上下文、CUDA 图、预分配余量、显存碎片 | 不涨 | 不涨 | 「算着能跑、实际跑不了」 |
1.2 为什么「装得下」是个错觉
把模型权重加载进去、还没开始处理任何请求时,占用确实只有第一笔账。这时候很多人会下一个结论:「还有一半显存空着,稳了。」
但剩下那三笔账是在工作过程中才长出来的:KV Cache 随着上下文变长和并发数增加而增长,激活值与临时缓冲在每一层计算时起起落落,框架的预分配和碎片则从头到尾都在那儿占着位置。
所以「装得下」只证明了第一笔账能付,没证明四笔账都付得起。
第 2 章 第一笔账:权重,最死板的那一块
权重就是模型训练完存下来的那一堆数字,推理时它们不变。它是四笔账里最好算的,因为公式只有一步乘法。
2.1 粗算口径:参数量 × 每个参数的字节数
每个参数占多少字节,取决于用什么数值精度存它。Hugging Face 的官方文档《Model memory anatomy》里写得很直接:混合精度训练时权重需要两份拷贝,一份 FP16(16 位浮点,占 2 字节)用于前向和反向,一份 FP32(32 位浮点,占 4 字节)作为稳定更新的主拷贝,合计6 字节/参数。
推理场景通常只保留低精度那一份,所以口径退化成一句乘法:权重字节数 ≈ 参数量 × 每个参数的字节数。
参数量一般在模型名或模型卡里就有,比如模型名里的 7B 表示约 70 亿个参数。每个参数多少字节,则由精度决定。
| 常见叫法 | PyTorch 里的数据类型 | 位数 | 每个参数占的字节数 |
|---|---|---|---|
| FP32 | torch.float32 | 32 位 | 4 字节 |
| FP16 | torch.float16 | 16 位 | 2 字节 |
| BF16 | torch.bfloat16 | 16 位 | 2 字节 |
| INT8 / FP8 | torch.int8 / torch.float8_e4m3fn | 8 位 | 1 字节 |
| INT4 | torch.float4_e2m1fn_x2(打包存放) | 4 位 | 0.5 字节 |
这张表的位数来自 PyTorch 官方文档《Tensor Attributes》(页面更新于 2026-05-08),它逐个列出了各数据类型的位宽;字节数就是位数除以 8。
有了口径和精度表,粗算就是一次乘法。7B 的模型,用 FP32 存大约是 28 GB,用 FP16 或 BF16 存大约是 14 GB,用 INT8 存大约是 7 GB;换成 70B,同样三种精度分别大约是 280 GB、140 GB、70 GB。这些数按 1 GB ≈ 10 亿字节 粗算,只算权重、不含其它三笔账,真实值还会因嵌入层大小、是否绑定词表而略有出入。
2.2 为什么这笔账最「死」
权重这一块的特点是不随你的输入变化:上下文再长、并发再多,它都不动。所以它只会造成两种结果——要么一开始就装不下,要么装下之后就稳定占着,不再是你后面崩掉的原因。
也正因为它是静态的,它才是第一个该被算清楚的数:如果连权重都装不下,后面三笔账不用讨论了,直接换精度或分片。减少这笔账的手段就是把参数存得更省(比如用低精度存权重),但这属于另一个话题,本文只把它算进账里,不展开。
第 3 章 第二笔账:KV Cache,随上下文线性增长的那一块
模型「能装下」却「跑不动」,绝大多数时候是这一笔账在作祟。它是四笔账里唯一会随着上下文长度线性上涨的一块。
3.1 KV Cache 是什么
大模型生成文本是一个 token 一个 token 往外吐的。每生成一个新 token,都要拿它去和前面所有 token 做注意力计算。这种「靠注意力直接建立全局依赖」的思路,源头是 Transformer 架构的原始论文《Attention Is All You Need》(arXiv:1706.03762,2017-06-12 首次提交,最新修订版为 2023-08-02 的 v7)。
如果每生成一步都把前面所有 token 的 key 和 value 向量重算一遍,代价会随长度不断累积。
工程上的做法是:把每个 token 算出来的 key 和 value 向量缓存起来,下一步直接取用。缓存下来的这一大块,就叫KV Cache(键值缓存,模型为避免重复计算而缓存下来的注意力键值向量)。
vLLM 官方博客对这块的描述很直白,原文是:
The KV cache isLarge:Takes up to 1.7GB for a single sequence in LLaMA-13B.Dynamic:Its size depends on the sequence length, which is highly variable and unpredictable.
意思是:一个序列的 KV Cache 就能到 1.7GB 这个体量,而且它的大小取决于序列长度,变化非常剧烈、难以预测。vLLM 论文的摘要里也是同样的定性——每个请求的 KV cache 占用很大,而且是动态涨落的。
注意「随序列长度变化」这几个字。权重是固定的一张账单,KV Cache 是一张跟着你输入变长而不断加长的账单。
3.2 它到底随什么涨
把影响因子拆开,一共六个。前五个决定「每个 token 存多少」,第六个决定「存多少个 token」。
| 影响因子 | 为什么它会推高 KV Cache |
|---|---|
| 层数 | 每一层都各自存一份 K 和 V,层数越多份数越多 |
| KV 头数 | 用分组查询的模型里,KV 头数少于查询头数,按 KV 头数算 |
| 头维度 | 每个注意力头向量的元素个数,维度越大每个向量越长 |
| 序列长度 | 提示词加生成内容,每一个 token 都要存一份 |
| 并发序列数 | 每个同时在跑的请求各存一份,互不共用 |
| 精度字节数 | 每个元素占几个字节,用 FP16 就是 2 |
写成一句话:单个序列、单个 token 的 KV 占用 ≈ 2 × 层数 × KV 头数 × 头维度 × 每个元素的字节数。
开头那个 2 是「键 + 值」两份,不是经验系数——KV Cache 这个定义本身就是把 key 和 value 一起缓存下来,所以必然是两份。这个式子是按定义推导出来的,不是官方文档里的原句,附表 A 里注明了推导依据。
3.3 为什么上下文一长就崩
拿一组常见的配置代入:32 层、8 个 KV 头、头维度 128、用 FP16 存(2 字节)。
单 token 占用 = 2 × 32 × 8 × 128 × 2 = 131072 字节,正好 128 KiB。
| 上下文长度(单个序列) | KV Cache 占用 |
|---|---|
| 8K | 约 1.07 GB |
| 32K | 约 4.29 GB |
| 128K | 约 17.2 GB |
关键在最后一行。一个 7B 模型用 FP16 存权重是 14 GB 左右,而单条128K 上下文的 KV Cache 就要 17 GB——比权重还大。这还没算并发:同时有 4 条这样的序列,KV 就是 68 GB 的量。
所以「模型能装下,长上下文跑不了」不是玄学,是算术。你只付了第一笔账,第二笔账翻倍地涨上来了。
第 4 章 第三笔账:激活值与临时缓冲
这一块最不显眼,但它是「一加大批量就崩」的常见原因。
4.1 激活值是什么
激活值(activation)是模型在每一层计算过程中产生的中间结果。训练时这些中间结果要留着给反向传播用,所以必须驻留显存;推理时大部分可以即算即弃,但仍然会有一批临时张量同时存在。
Hugging Face 官方文档对这种开销的说明是:前向激活的大小随批量大小、序列长度、模型深度和隐藏层维度变化,并且「即使模型本身放得下,批量大小或序列长度也足以把显存耗光」。这句话正好解释了第 1 章里那个错觉——装得下和跑得动是两回事。
4.2 临时尖峰:短命但能致命
同一份官方文档还提到另一类内存:临时张量。它们由 softmax、矩阵乘法这类算子产生,算完就释放,生命周期很短,但原文明确说了——如果单个算子的峰值过于密集,就会形成一次临时内存尖峰,把显存顶爆。
这解释了一种很难查的现象:显存监控上看平均占用不高,但程序就是会在某一步挂掉。因为压垮它的不是平均值,是那一瞬间的峰值。
对排查的启示很明确:如果你已经算过权重、也算过 KV,两边都还宽裕,但只要把批量调大就崩,那么嫌疑基本就落在激活值与临时缓冲这一块。
第 5 章 第四笔账:框架与运行时开销
前三笔账都和你的模型、你的输入有关,这一笔账几乎和你无关,但它一直在扣钱。
5.1 固定的那部分:CUDA 上下文与 CUDA 图
CUDA 上下文是运行时为显卡建立的一套执行环境,它本身就要常驻一部分显存,模型还没开始算就已经被占掉一块。
更值得注意的是优化带来的额外占用。CUDA 图是一种把一串计算调用提前固化下来、减少逐次调度开销的加速手段。vLLM 官方文档《Conserving Memory》(页面日期 2026-07-09)里写着:默认情况下会用 CUDA 图来优化推理,而 CUDA 图本身会额外占用 GPU 显存;文档同时给出了compilation_config和enforce_eager两个开关来权衡速度与显存。
也就是说,性能优化不是免费的。你把某种加速打开,它是拿显存换来的。这部分开销在纸面估算里常常被漏掉。
5.2 预分配与碎片:为什么「算出来能跑、实际跑不了」
预分配是框架为了减少运行中反复申请释放的开销,一次性预留一块显存的做法。它按上限来预留——你要支持 32K 上下文,它就按 32K 的规模留位置。于是「其实用不到」的那部分,也被占住了。
显存碎片是另一个方向的浪费:显存被切成一堆大小不一、互不相邻的小块之后,即使空闲总量够,也可能拼不出一个足够大的连续块给下一个请求。
这两件事加起来的破坏力有多大,vLLM 官方博客给过一个数:在这套机制出现之前,现有系统由于碎片化和过量预留,会浪费掉 60% – 80% 的显存。这就是「按公式算完全够、实际就是起不来」的根源——你算的是需要多少,框架面对的是能凑出多少连续空间。
5.3 PagedAttention 解决的是哪一个问题
PagedAttention是 vLLM 提出的一种注意力算法,它要解决的不是「KV Cache 太大」,而是「KV Cache 被浪费」。
它的做法借用了操作系统的虚拟内存分页思想:把 KV Cache 切成固定大小的块,逻辑上连续、物理上可以不连续,再用一张块表把逻辑块映射到物理块。vLLM 官方博客里对这个类比的原文是:
one can think of blocks as pages, tokens as bytes, and sequences as processes.
即把块看作内存页、把 token 看作字节、把序列看作进程。vLLM 官方设计文档里对「块」的定义是——KV Cache 被切分成块,每个块存放固定数量 token 在一个头上的数据。
效果是:显存浪费只可能发生在一条序列的最后一块,官方给出的说法是浪费低于 4%。这块改进让同样的显存能容纳更多并发序列,从而把吞吐拉上去。
一篇讲显存账的文章,最该配一份能照着算的估算清单。我把「参数量 → 权重、序列长度 → KV Cache」的估算口径和查显存用的命令整理在了一起,和上面的推导是同一套口径。放在资料包里,扫码即可获取:
第 6 章 可执行的排查顺序
账拆完了,接下来是顺序问题。顺序错了,你会花很多时间在错误的那块账上找原因。
正确的顺序是:先看权重,再看 KV Cache,再看激活与批量,最后看框架预分配。理由是前三笔按「改动难度」从小到大、按「是否随输入变化」从静到动排列,最后一笔你基本改不动,所以放最后兜底。
6.1 四步排查顺序表
| 步骤 | 先算什么 | 看什么指标 | 什么现象说明就是这一块 | 常规下一步 |
|---|---|---|---|---|
| 1. 权重 | 参数量 × 每参数字节数 | 模型加载完、还没有请求时的占用 | 此时占用就已接近上限 | 换更省的精度,或做分片 |
| 2. KV Cache | 2 × 层数 × KV 头数 × 头维度 × 序列长度 × 字节数 | 最大上下文长度与最大并发数 | 短上下文正常、长上下文直接崩 | 缩小最大上下文或最大并发 |
| 3. 激活与批量 | 批量 × 序列长度 × 模型规模 | 不同批量下的峰值占用 | 小批量正常、大批量崩 | 降批量,或开梯度检查点(仅训练) |
| 4. 框架预分配 | 预分配比例与碎片 | 启动时预留了多少 | 平均占用不高但会挂 | 调低预留比例,或换分页式 KV 管理 |
6.2 每一步具体看什么
第一步看权重,只需要一条命令就能看到静态占用。
⚠️代码待验证
nvidia-smi --query-gpu=memory.total,memory.used,memory.free--format=csv关键动作是:在模型刚加载完、还没有任何请求进来时执行一次。如果这一下memory.used就接近memory.total,说明第一笔账本身就超了,后面几步都不用做。
第二步和第三步可以合起来算,不需要真正加载模型,只要模型目录里有配置文件就行。
⚠️代码待验证
importjson cfg=json.load(open("模型目录/config.json",encoding="utf-8"))num_params=7_000_000_000# 从模型名或模型卡获取的参数量bytes_per_param=2# FP16 或 BF16:16 位 ÷ 8 = 2 字节weight_gb=num_params*bytes_per_param/(10**9)num_layers=cfg["num_hidden_layers"]num_kv_heads=cfg.get("num_key_value_heads",cfg["num_attention_heads"])head_dim=cfg["hidden_size"]//cfg["num_attention_heads"]seq_len=8192batch=1kv_bytes=2*num_layers*num_kv_heads*head_dim*seq_len*batch*bytes_per_param kv_gb=kv_bytes/(10**9)print(f"权重约{weight_gb:.2f}GB")print(f"KV Cache 约{kv_gb:.2f}GB")字段名以你本地那份config.json为准,不同模型可能缺省其中某几个键,取不到时按模型卡的说明补齐。
第四步是核对精度与字节数的换算关系,确认自己没有把位数当字节数用。
⚠️代码待验证
importtorchforname,dtypein[("FP32",torch.float32),("FP16",torch.float16),("BF16",torch.bfloat16)]:t=torch.zeros(1024,dtype=dtype)print(name,t.element_size(),"字节/元素")最后是把嫌疑落到参数上。如果用服务框架部署,先把最大上下文长度和最大并发数调小,看还崩不崩——这两个参数正好是 KV Cache 公式里的两个乘数,能帮你把第二笔账和第三笔账区分开。
⚠️代码待验证
vllm serve 你的模型目录 --max-model-len8192--max-num-seqs4调小之后如果恢复正常,说明瓶颈在 KV Cache;如果照旧崩,再往激活值与临时缓冲那一层找。确认口径之前,先回头看框架层:启动时预分配了多少、有没有开 CUDA 图这类吃显存的优化,以及是不是在「平均占用不高」的情况下挂掉——那就是碎片或临时尖峰的典型特征。
第 7 章 把四笔账串起来
前面拆开讲,这一章合起来算一遍,然后说清楚什么时候该停手。
7.1 一个完整的手算例子
场景:7B 模型,FP16 存权重,头维度按第 3 章的配置,想跑 8K 上下文、单序列。
| 账目 | 手算过程 | 结果 |
|---|---|---|
| 权重 | 70 亿 × 2 字节 | 约 14 GB |
| KV Cache | 单 token 128 KiB × 8192 | 约 1.07 GB |
| 激活与临时缓冲 | 随批量与序列长度浮动,单序列推理时是临时峰值 | 远小于权重,但会尖峰 |
| 框架与运行时 | CUDA 上下文、CUDA 图、预分配余量 | 通常还要留出一块余量 |
结论:一张 24GB 的消费级卡,跑这个组合是够的,还能留出余量。但如果把上下文拉到 128K,KV Cache 一项就从 1 GB 涨到约 17 GB,和权重同量级——总账一下子翻倍,原来够用的卡就不够了。
7.2 什么时候该停手、该换方向
判断标准其实只有一条:看哪笔账在变。
如果权重是最大项,而 KV 很小,那你的瓶颈是静态的,减少权重(用更省的精度)是有效方向。反过来,如果 KV 已经和权重同量级甚至更大,那么再怎么压缩权重,收益也有限——因为真正随你的用法膨胀的是 KV 那一块,这时候该动的是上下文长度、并发数,或者用分页与共享前缀这类减少浪费的机制。
对着现象倒推嫌疑,对应关系其实很好记:模型刚加载完就快占满,是权重;短上下文正常、长上下文直接崩,是 KV Cache;单条请求正常、多来几个请求就崩,是 KV Cache 里的并发那一份;批量调大就崩、调小就正常,是激活值与临时缓冲;平均占用不高却某一步莫名挂掉,是临时尖峰或显存碎片;按公式算够用、实际就是起不来,则是框架预分配与碎片。
排查的顺序清单、估算口径和查显存用的命令,我整理成了一份可以直接照着走的版本,和正文用的是同一套推导。放在资料包里,扫码即可获取:
附表 A:本文引用事实与出处对照表
| 事实 | 出处(文档名 + 发布方 + 链接) | 本文位置 |
|---|---|---|
| KV Cache 体量大且动态,单序列可到 1.7GB,大小取决于序列长度 | 《vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention》,vLLM 项目官方博客,2023-06-20,https://blog.vllm.ai/2023/06/20/vllm.html | 第 3 章 |
| 早期系统因碎片化与过量预留浪费 60%–80% 显存 | 同上 | 第 5 章 |
| PagedAttention 把 KV Cache 切成固定大小的块;浪费只发生在最后一块,官方称为低于 4% | 同上 | 第 5 章 |
| 块/页/进程的类比原文「blocks as pages, tokens as bytes, and sequences as processes」 | 同上 | 第 5 章 |
| 块的定义:KV Cache 被切成块,每块存放固定数量 token 在一个头上的数据 | 《Paged Attention》设计文档,vLLM 官方文档(页面日期 2026-06-24),https://docs.vllm.ai/en/latest/design/paged_attention.html | 第 5 章 |
| 每个请求的 KV cache 占用巨大且动态涨落;目标是把浪费降到接近零 | 《Efficient Memory Management for Large Language Model Serving with PagedAttention》,Kwon 等,SOSP 2023,arXiv:2309.06180,https://arxiv.org/abs/2309.06180 | 第 3 章 |
| 混合精度训练权重为 6 字节/参数(FP16 副本 2 字节 + FP32 副本 4 字节) | 《Model memory anatomy》,Hugging Face 官方文档,https://huggingface.co/docs/transformers/en/model_memory_anatomy | 第 2 章 |
| 前向激活大小随批量大小、序列长度、模型深度、隐藏层维度变化;模型放得下也可能因批量或序列长度耗尽显存 | 同上 | 第 4 章 |
| 临时张量由 softmax、矩阵乘法等算子产生,单算子峰值过密会造成临时尖峰并导致显存不足 | 同上 | 第 4 章 |
| 各精度位宽:float32 为 32 位、float16 为 16 位、bfloat16 为 16 位、float8 为 8 位、float4 为打包 4 位 | 《Tensor Attributes》,PyTorch 官方文档(页面更新于 2026-05-08),https://docs.pytorch.org/docs/stable/tensor_attributes.html | 第 2 章 |
| 默认使用 CUDA 图优化推理,CUDA 图会额外占用 GPU 显存;可用配置权衡速度与显存 | 《Conserving Memory》,vLLM 官方文档(页面日期 2026-07-09),https://docs.vllm.ai/en/latest/configuration/conserving_memory.html | 第 5、6 章 |
| 限制最大上下文长度与最大并发数可进一步降低显存占用 | 同上 | 第 6 章 |
| Transformer 架构与注意力机制的原始论文 | 《Attention Is All You Need》,Vaswani 等,arXiv:1706.03762(v1 提交于 2017-06-12,最新修订 v7 于 2023-08-02),https://arxiv.org/abs/1706.03762 | 第 3 章 |
| 单 token KV 字节数 ≈ 2 × 层数 × KV 头数 × 头维度 × 精度字节数(本文按定义推导,非官方原文) | 推导依据:vLLM 官方博客对 KV Cache 的定义 + vLLM 设计文档对「块」的定义 | 第 3、6 章 |
| 第 2、3 章各表的 GB 与 KV 数值均为本文按上述口径自行手算,非官方给出的数字 | 计算口径见第 2 章与第 3 章 | 第 2、3、7 章 |
框架预分配比例参数(如 vLLM 的gpu_memory_utilization)的具体默认值 | 待验证:本次未核到该默认值的官方文档原文,请以你本地安装版本的官方文档为准 | 第 5 章 |
附表 B:术语速查表
| 术语 | 一句话解释 | 在本文哪里用到 |
|---|---|---|
| 显存溢出(OOM) | 显存不够用,运行中的进程被系统强制终止 | 第 1 章 |
| 权重 | 模型训练完存下来、推理时不再变化的那一堆数字 | 第 1、2 章 |
| KV Cache | 为避免重复计算,把每个 token 的注意力键值向量缓存下来的那块显存 | 第 1、3 章 |
| 激活值 | 模型每层计算过程中产生的中间结果,训练时要留着,推理时多为临时 | 第 1、4 章 |
| 临时张量 | softmax、矩阵乘法等算子产生的短命中间数据,峰值可能顶爆显存 | 第 4 章 |
| CUDA 上下文 | 运行时为显卡建立的执行环境,本身要常驻一部分显存 | 第 5 章 |
| CUDA 图 | 一种把计算调用序列固化下来以加速的优化手段,会额外占用显存 | 第 5 章 |
| 预分配 | 框架为省去反复申请释放,按上限一次性预留一块显存的做法 | 第 5 章 |
| 显存碎片 | 空闲显存被切成互不相邻的小块,总量够却拼不出连续大块 | 第 5 章 |
| PagedAttention | 把 KV Cache 切块、逻辑连续物理可不连续的分页式注意力算法 | 第 5 章 |
| KV 头数 | 存放键值的注意力头数量,分组查询下少于查询头数 | 第 3 章 |
| 数值精度 | 每个数字占多少位,直接决定每个参数占多少字节 | 第 2 章 |
写在最后:这篇用到的资料
写这篇文章时,把相关的官方文档和源码又翻了一遍,顺手也整理了几份配套的东西:
- 大模型学习路线图:从零基础到能自己动手做 Agent,按阶段说明每一步该学什么、哪些可以先跳过
- 《LangChain + LangGraph + MCP 智能体开发实战》视频课:7 个模块,从私有化部署、Embedding+RAG 到 MCP+Agent 全流程
- AI 大模型知识库(在线可查):Agent Skills 从入门到落地、Claude Skills 完全指南等专题,按目录浏览即可
- 640 套 AI 大模型行业报告 + 经典 PDF 书籍:看行业落地案例和别人怎么做的时候用得上
- 大模型零基础到精通教学视频:跟着敲一遍,比只读文档快得多
资料是我自己整理的,放在下面这个码上,扫码即可获取:
添加时备注「AI」,优先通过。
资料按「先路线、再动手、最后查漏」的顺序整理好了,建议先看学习路线那一份,照着它挑一条适合自己当前基础的路径再往下看。