☰
图解显存:模型不大,为什么一跑就爆显存
2026/10/2 5:53:27 网站建设 项目流程

版权与内容来源声明
本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容,均在附表 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 里的数据类型位数每个参数占的字节数
FP32torch.float3232 位4 字节
FP16torch.float1616 位2 字节
BF16torch.bfloat1616 位2 字节
INT8 / FP8torch.int8 / torch.float8_e4m3fn8 位1 字节
INT4torch.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 Cache2 × 层数 × 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」,优先通过。

资料按「先路线、再动手、最后查漏」的顺序整理好了,建议先看学习路线那一份,照着它挑一条适合自己当前基础的路径再往下看。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询