“显存又炸了。”
凌晨两点,终端里最后一行日志停在模型加载到 78% 的位置,风扇还在转,屏幕已经不动了。这种场景对一个人干完全部活的小团队来说太熟悉了——模型参数越来越大,档位越来越花,而手头能用的加速卡只有一张。Step 5 Preview 本地部署踩到显存 OOM之后,摆在面前的选择其实只有两条路:要么把占用压下去,要么掏钱换卡。前者靠的是对显存账本的精确拆解,后者靠的是一人公司低成本选型的算账能力,两件事都不难,难的是大多数人卡在中间,既没算清账,也没调对参数。
这篇内容我想聊的就是这两条路:先把 OOM 的真实原因一层层剥开,给出可以自己复算的显存预算公式,再把不改硬件就能把占用砍掉一半的实操开关摆出来,最后落到钱上——什么规模的活该自购,什么节奏该租用,盈亏平衡点怎么算。适合独立开发者、两三个人的小工作室,以及任何想在单机上把大模型跑起来的个人。文中涉及具体配置的部分,是基于常见部署实践的合理推演,不是某个固定环境的唯一解,你按自己的机器对照着改。
1. 先搞清楚 OOM 到底卡在哪:把显存账本摊开算
显存不够这件事,最怕的不是不够,而是不知道自己缺多少。很多人一看到报错就去换卡,换完发现还是炸,因为压根没定位到是哪一项吃掉了显存。模型加载失败和推理中途崩溃,原因可能完全不在一个地方。
1.1 显存不是被模型权重一个人吃掉的
绝大多数人对显存的直觉是“参数量乘个字节数就完事了”,7B 模型 fp16 大约 13GB,听起来 16GB 卡能塞下。这个算法只对了一半,它只覆盖了静态权重这一项。真正跑起来的时候,显存里同时住着至少五类东西。
第一类是模型权重,也就是参数量乘以每参数字节数。fp16 是 2 字节,int8 是 1 字节,4bit 大概 0.5 字节但要额外存量化缩放因子,实际开销通常比理论值高出 5% 到 10%。第二类是KV Cache,这是自回归生成绕不开的缓存,上下文越长、并发越多,它涨得越凶,而且它是动态增长的,启动时看着很宽裕,聊到第五轮就爆了。第三类是中间激活张量,推理阶段比训练小得多,但在高分辨率图像、长视频、长序列场景下会突然变成一个巨无霸。第四类是框架和驱动的基础开销,显卡上下文本身就要占几百 MB,加上通信缓冲区、算子工作区,这部分经常被忽略。第五类是碎片,也就是申请了但没用满的空洞。
我见过一个很典型的案例:某开发者用 24GB 卡跑一个 13B 的 fp16 模型,权重 26GB,理论上根本装不下,但他开了 4bit 量化,权重降到 7GB 左右,启动成功了。结果跑到第三轮对话就 OOM,因为他把上下文开到了 32K,KV Cache 一项就吃了 8GB 多。他以为是量化没生效,其实是缓存把剩下的空间挤爆了。
这里给一个可以直接套用的 KV Cache 估算公式,按 token 计算:
每 token 占用 = 2 × 层数 × KV 头数 × 头维度 × 每元素字节数拿一个 32 层、采用分组查询注意力(8 个 KV 头)、头维度 128、fp16 精度的模型举例:2 × 32 × 8 × 128 × 2 = 131072 字节,也就是 128KB 一个 token。听起来很小,但 8K 上下文就是 1GB,四个并发请求就是 4GB,32K 上下文单请求就是 4GB。这就解释了为什么“模型能加载”和“模型能对话”是两回事。
图像和视频类模型还有另一个隐形杀手:注意力的平方级复杂度。一张 1024×1024 的图,经过 8 倍下采样进入潜空间是 128×128,展平成序列就是 16384 个 token。注意力矩阵的大小是序列长度的平方,16384 的平方再乘 fp16 的 2 字节,单个头就是 512MB,多头叠加直接把卡撑爆。所以高分辨率出图炸显存,炸的往往不是权重,是这一步的中间矩阵。理解这一点,后面提到的分块处理才有意义。
1.2 一张可以自己复算的显存预算表
与其凭感觉估,不如把每一项都列出来。下面这张表是我自己每次部署前必填的,模型规模换了就把数字替换掉,五分钟能算完,比事后调试两小时划算得多。
| 组成项 | 估算方式 | 13B / fp16 示例 | 13B / 4bit 示例 |
|---|---|---|---|
| 模型权重 | 参数量 × 字节数 | 约 26GB | 约 7.5GB |
| KV Cache | 每 token 占用 × 上下文 × 并发 | 8K × 2 并发约 2GB | 同左,约 2GB |
| 中间激活 | 与批大小、序列长度强相关 | 1 到 3GB | 1 到 3GB |
| 框架基础开销 | 上下文 + 工作区 | 0.8 到 1.5GB | 0.8 到 1.5GB |
| 碎片余量 | 预留 10% 到 15% | 3 到 4GB | 1.5 到 2GB |
| 合计 | 各项相加 | 约 34GB | 约 14GB |
看这张表就能明白两件事。第一,同一个模型在不同精度下的总占用差距是倍数级的,量化不是可选项而是必选项。第二,权重之外的部分加起来可能占到总占用的三成以上,这就是为什么“权重刚好塞满显存”的方案一定会失败——你没有给缓存和碎片留任何活路。
我一般会留出至少 15% 的余量,这个比例不是拍脑袋。显存分配器在反复申请释放之后会产生外部碎片,尤其是在变长序列和变分辨率输入的场景下,碎片率很容易爬到 10% 以上。余量留少了,跑半小时没事,跑两小时就崩,排查起来极其痛苦。
1.3 为什么“显存够用”的判断经常翻车
有三个特别常见的误判,我几乎每次带新人都会遇到。
第一个是把均值当峰值。打开监控看一眼,显存占用 60%,觉得还有 40% 余量,结果峰值其实摸到 95%,只是采样间隔太长没抓到。显存 OOM 是瞬时事件,只要有一个瞬间超了就直接抛异常,跟你平均用了多少没关系。采样必须看峰值,不能看当前值。
第二个是忽略同卡上的其他进程。桌面环境本身就会占用一部分显存做图形加速,浏览器开着一堆标签页,硬件加速也在吃。一张标称 24GB 的卡,实际可用往往只有 22GB 出头。这个差额在紧张的预算里就是压死骆驼的那根稻草。
第三个是把“预留”当成“泄漏”。有些推理引擎启动时就会按比例预占显存,监控一看 90%,以为出问题了,其实是正常行为。真正要看的是实际分配量和预留量之间的差值,前者是真正在用的,后者是向驱动要来的池子。差值大说明碎片多,差值接近零说明利用充分。分不清这两个数,就会在错误的方向上优化很久。
2. 从报错到定位:三层排查路径
看到 OOM 报错就盲目调参,效率极低。我的习惯是先分层,再定位,最后动手。三层下来,八成的问题能在十分钟内锁定。
2.1 第一层:区分“启动即炸”还是“跑到一半炸”
这两种情况的病因完全不同,处理思路也相反。
启动阶段就炸,说明静态占用已经超了。可能的原因只有几个:精度选错了、权重加载时没有做分片、模型实际参数量比你以为的大、显卡被别的进程占了。这种情况下调上下文长度、调批大小都没用,因为它们还没开始消耗。要做的是把权重占用降下来——换更激进的量化、启用按层加载、或者干脆换更小的模型档位。
跑到一半炸,说明静态能过,动态超了。这时候要看是在哪个阶段炸的:如果是对话轮次增加后炸,指向 KV Cache 增长;如果是处理高分辨率输入时炸,指向中间激活;如果是生成结束前最后一步炸,指向解码器(图像模型的 VAE 解码是经典爆点,很多人卡在这一步)。定位到具体阶段,优化手段就非常明确了。
有一个很实用的判断技巧:把批大小和并发都设成 1,上下文砍到最短,如果还炸,就是静态问题;如果稳了,再逐项加回去,加到哪一项炸就是哪一项的问题。二分法在这里比任何工具都好用。
2.2 第二层:看曲线,而不是看瞬时值
监控要记录三个数:当前分配量、峰值分配量、预留量。主流框架都提供了接口,几行代码就能打出来。
import torch def report(tag=""): alloc = torch.cuda.memory_allocated() / 1024**3 reserved = torch.cuda.memory_reserved() / 1024**3 peak = torch.cuda.max_memory_allocated() / 1024**3 print(f"[{tag}] 已分配 {alloc:.2f}GB / 预留 {reserved:.2f}GB / 峰值 {peak:.2f}GB") report("启动后") # 在这里跑一次完整推理 report("推理结束")把这段插在推理前后,能立刻看出两件事:峰值比启动后高了多少(动态开销),预留比已分配高了多少(碎片程度)。我一般会把它挂在一个低频的定时器上,每秒采样一次,跑完整流程后把峰值打印出来。峰值才是你做容量规划的依据。
如果峰值比启动后的静态占用高出 50% 以上,说明动态部分的优化空间很大,先别急着买卡。如果峰值和静态几乎持平,但就是差那么一点点装不下,那就老老实实考虑量化或者换卡,没有太多腾挪余地。
2.3 第三层:碎片与预分配带来的隐藏开销
碎片这个问题值得单独说,因为它最反直觉。显存分配不是想给多少就给多少,而是从已经申请的大块里切出来。切的次数多了,大块被切碎,明明总量够,就是凑不出一块连续的。表现就是:报错说显存不足,但监控显示还有好几个 G 空着。
主流的缓解手段有两个。一是开启可扩展段分配,让分配器能合并碎片:
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True这个开关对变长输入的场景效果特别明显,我实测在长上下文对话里能把碎片率从 15% 压到 5% 以内。二是限制单次切分的最大块,避免一个大请求把池子切得太碎:
export PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:512这两个参数不建议同时无脑开,先试第一个,不够再考虑第二个。另外还有一个容易被忽略的点:如果业务里输入尺寸变化很大(比如图像分辨率忽大忽小),最好在启动时用最大尺寸预热一次,让分配器提前把大块留出来,避免运行中途再去申请。
3. 不改硬件的六个开关:把占用压下去
这一节是实操重点。我按“收益从大到小、代价从小到大”排序,你可以从上往下试,试到够用为止。
3.1 精度与量化:性价比最高的一刀
量化是显存优化里投入产出比最高的手段,没有之一。权重从 fp16 降到 4bit,占用直接砍到四分之一左右,而质量损失在大多数生成任务上肉眼难辨。关键是选对方案。
| 精度方案 | 相对显存 | 质量损失 | 速度影响 | 适用场景 |
|---|---|---|---|---|
| fp16 / bf16 | 100% | 无 | 基准 | 显存充裕、追求极致质量 |
| fp8 | 约 50% | 极小 | 略快 | 新架构卡,推荐优先尝试 |
| int8 | 约 50% | 很小 | 基本持平 | 通用场景,兼容性好 |
| 4bit(NF4 类) | 约 27% | 可感知但可接受 | 略慢 | 显存紧张的主力方案 |
| 4bit(分组量化文件) | 约 27% | 视分组粒度 | 较快 | 单机部署、追求加载速度 |
有一个坑必须提醒:不要把所有子模块一视同仁地量化。图像或多模态模型里,视觉编码器和解码器对精度非常敏感,量化之后出图会出现色块、边缘糊、细节丢失。我的做法是主干网络用 4bit,视觉部分保持 fp16,这样总占用能降六成以上,画质基本无损。全部量化的省事做法,最后往往要花更多时间返工。
另一个坑是量化后的显存不等于理论值。标称 4bit 的方案,实际占用因为缩放因子、分组元数据、反量化缓冲区,通常会比理论值高出 8% 到 12%。做容量规划的时候按 1.1 倍算,别按理论值算。
3.2 分块与切分:把大张量拆成小口吃
如果说量化是降总量,分块就是降峰值。前面提到高分辨率图像的注意力矩阵是平方级增长,分块处理就是针对这个的。
常见的有三种粒度。注意力分片是把序列切成若干段分别计算再拼接,显存峰值能降一半以上,代价是速度慢 10% 到 30%。解码分块是把解码器拆成小块逐块处理,这是高分辨率出图必开的开关,不开基本必炸。层切分是把模型按层拆开,一次只加载几层到显存,其余留在内存,这个手段比较重,一般作为兜底。
我一般建议的处理顺序是:先开解码分块(几乎无质量损失),再考虑注意力分片(速度有损但可接受),层切分放最后。实测下来,一张 1024 分辨率的图,开解码分块之后峰值能从 18GB 降到 9GB 左右,画质完全一致,只是单张耗时从 6 秒变成 9 秒。这个交换非常划算。
3.3 卸载与换页:拿内存和硬盘换显存
卸载的思路很直白:显存放不下的部分,临时挪到内存里,用的时候再搬回来。主流框架都提供了自动卸载的封装,几行配置就能开。
代价也很直白:速度会掉一个数量级。因为内存带宽比显存带宽低一个量级,每次搬运都是瓶颈。我实测过一个 13B 模型全量卸载到内存,单 token 生成时间从 40 毫秒涨到 800 毫秒,基本没法用于交互场景,只能跑批处理任务。
所以我的判断标准是:如果这个任务是离线批处理,比如夜里跑一批图或者批量生成文案,卸载完全可接受,睡一觉起来收结果就行。如果是交互式应用,卸载就是自欺欺人,用户等不了。这种情况下不如老老实实降模型档位,或者减少并发。
还有一个隐藏成本:卸载要求内存足够大。权重从显存搬到内存,内存占用会同步涨上去,16GB 内存的机器跑 13B 模型会直接卡死。做这个决定之前先看一眼内存余量。
3.4 框架层参数:那些不起眼但很要命的默认值
有几个默认参数,不改的话会白白浪费大量显存。
批大小。默认值经常是 4 或者更大,单机部署时改成 1,显存立省 30% 以上,而实际吞吐在单用户场景下几乎没差别。这是最容易被忽略又最容易改的一项。
上下文长度上限。很多部署配置默认给到模型支持的最大值,比如 32K 或者 128K,但你的实际业务可能只需要 4K。把上限压到够用就行,KV Cache 是按上限预留的,改小直接省下好几个 G。
并发请求数。服务化部署时,并发数直接乘在 KV Cache 上。一个人用的小服务,并发设成 2 到 4 足够了,设成 32 纯属浪费。
推理引擎的显存占用比例。有些引擎启动时会预占固定比例的显存(常见是 90%),这是为了减少运行期分配开销,不是泄漏。但如果你的机器上还要跑别的任务,把这个比例调到 0.7 到 0.8,留出余地。
把这些参数挨个过一遍,通常能省出 20% 到 40% 的显存,而且是零成本零质量损失。我每次部署新模型,第一件事就是把这四个参数重新核一遍,比调任何高级优化都实在。
4. 一人公司的硬件选型:先算账,再下单
参数调到头还是不够,就该谈钱了。一个人干活的特点很明显:现金流敏感、没有备用机、没有运维、时间比电费贵。这决定了选型逻辑跟大团队完全不一样。
4.1 显存容量的四档划分与适配场景
不谈具体型号,只谈显存容量,因为这个维度才是决定你能跑什么的关键。
| 显存档位 | 能跑的模型规模 | 典型场景 | 二手价格区间(参考) | 满载功耗 |
|---|---|---|---|---|
| 12 到 16GB | 7B 级 4bit、小分辨率图像模型 | 轻量文本、512 分辨率出图 | 1500 到 3000 元 | 150 到 250W |
| 24GB | 13B 到 32B 级 4bit、中等分辨率图像 | 主力开发机、日常推理 | 4000 到 7000 元 | 250 到 350W |
| 40 到 48GB | 32B 到 70B 级 4bit、高分辨率图像与短视频 | 小规模服务、批量生产 | 9000 到 16000 元 | 300 到 450W |
| 80GB 级 | 70B 级 fp16、长上下文、多模态全量 | 对质量有硬要求的生产环境 | 20000 元以上 | 350 到 600W |
看这张表要配合前面的显存预算表一起看,别只看模型规模那一列。同样标称能跑 13B,4bit 和 fp16 是两码事,16K 上下文和 4K 上下文也是两码事。我的建议是按你实际业务里 90 分位的需求往上取一档,别按平均值取。因为显存不够的时候没有优雅降级,只有直接崩溃。
对一人公司来说,24GB 这一档是甜点区。它覆盖了绝大多数日常开发和小批量生产需求,价格在可承受范围内,功耗可控,噪音也能忍。再往上,边际收益会快速衰减,除非你确实有 70B 级别的硬需求。
4.2 自购、租用、混合:把盈亏平衡点算出来
这是最该算清楚却最少人算的一笔账。我见过太多人凭感觉买了卡,结果一个月用不满二十小时,摊下来单价高得离谱。
先算自购的月度成本:
月成本 = (购机价 - 预期残值)÷ 使用月数 + 月电费 + 隐性损耗举个例子。整机一万元,用三年,三年后预期还能卖三千,那么折旧是(10000 - 3000)÷ 36 ≈ 195 元/月。电费按满载 450W、每天跑 8 小时、每度电 0.55 元算:450 × 8 × 30 ÷ 1000 × 0.55 ≈ 59 元/月。两项加起来约 254 元/月,再留一点维修和故障成本,按 280 元/月算。
再算租用。同档位的按时租用,市场价大致在每小时 2 到 5 元之间,取中位数 3 元。盈亏平衡点就是:
280 ÷ 3 ≈ 93 小时/月,也就是每天约 3 小时结论很清楚:每天实际用机超过三小时,自购划算;低于这个数,租用更省。而且这个计算还没算自购的隐性成本——调试驱动的时间、机器出故障时没有备用机的停机损失、占地方、噪音、二手卡可能踩到的坑。把这些折进去,平衡点还要往上抬,大概每天四小时。
所以我的建议分三种情况。验证期,也就是还在试模型、试业务方向,绝对租用,按小时开,用完就关,一个月花不了多少钱。稳定期,业务跑通了、每天固定要跑几小时,自购一台主力机。爆发期,有临时的批量任务,自购机器扛不住,临时租几小时补上,这是混合模式,也是最省钱的做法。一人公司最忌讳的是用“以后可能会用得多”来说服自己买贵卡,那个“以后”通常不会来。
4.3 整机搭配的隐性成本,比显卡本身更容易翻车
买卡只是第一步,配套翻车的人也不少。
电源。显卡的瞬时功耗可以飙到标称值的 1.5 倍以上,持续几百毫秒。电源按显卡标称功耗的两倍配,别按整机总功耗刚好配。功率不够的表现是训练跑一半机器重启,排查起来能耗掉一整天。
散热与噪音。高功耗卡满载时机箱内温度轻松上七十度,长期跑会降频甚至影响寿命。一人公司通常在居住空间里干活,噪音是实打实的体验问题。我的经验是机箱风道比风扇数量重要,进风出风要对流,别堆一堆风扇互相打架。
主板通道。多卡场景要注意通道分配,单卡跑推理不太受这个限制,但如果你打算以后加卡,主板和 CPU 的通道数要提前看,不然加卡之后带宽砍半,速度反而不如单卡。
电费的真实算法。很多人只算满载,实际上大部分时间是待机和轻载。但反过来说,跑批量任务时可以集中到电价的低谷时段,一天跑下来能省三成。这是小事,但对一人公司的现金流来说,每一笔都算数。
5. 我踩过的坑,和一份可以直接抄的速查表
5.1 六个高频误判
误判一:显存显示 90% 就以为出问题了。这是预分配池的正常表现,看实际分配量才准。
误判二:加大虚拟内存能解决 OOM。显存和内存是两套体系,虚拟内存只对卸载方案有用,对纯显存溢出无效。
误判三:量化越多越省。视觉模块量化过度会毁掉画质,主干和外围要区别对待。
误判四:换个更大的卡就一劳永逸。上下文和并发会跟着水涨船高,人的使用习惯会自动把新卡的余量吃满,最后还是在边界上跑。
误判五:并发开大点吞吐就高。显存不够时并发直接导致 OOM,实际吞吐可能还不如单并发稳定跑。
误判六:二手卡便宜就是赚。关键看显存颗粒是否原装、有没有长时间高负载运行的历史,买之前一定要跑一遍完整的压力测试,别只看点亮。
5.2 OOM 速查表
| 现象 | 最可能的原因 | 优先尝试的手段 |
|---|---|---|
| 启动加载到一半就崩 | 权重超出静态容量 | 降精度、换小档位、启用分片加载 |
| 加载成功但首次推理就崩 | 工作区或预热不足 | 减小批大小、降低分辨率、开启分块 |
| 对话到第 N 轮崩 | KV Cache 增长超限 | 压缩上下文上限、减少并发、量化缓存 |
| 高分辨率出图时崩 | 注意力矩阵平方级膨胀 | 开启注意力分片、解码分块 |
| 生成快结束时崩 | 解码器峰值 | 分块解码、分步释放 |
| 报错说不够但监控有空闲 | 显存碎片 | 开启可扩展段、预热最大尺寸 |
| 跑一段时间后越来越慢最后崩 | 缓存未释放或内存交换 | 检查是否泄漏、限制历史累积 |
5.3 部署前的一份自检清单
每次上新模型之前,我会按这个顺序过一遍,五分钟能省掉后面两小时的排查。
先填显存预算表,算出总需求和峰值预留,确认留了 15% 余量。然后确认精度方案,主干和外围分开定。接着核四个默认参数:批大小、上下文上限、并发数、显存占用比例。再检查同卡其他进程,把不必要的图形加速关掉。最后跑一次最大尺寸的预热,观察峰值和碎片率。
这套流程走下来,我最近半年部署的新模型里,只有两次真的需要换硬件,其余都是参数问题。这两次一次是要跑 70B 级,一次是要处理 4K 分辨率的视频帧序列,都属于硬需求,不是优化能解决的。
回过头说句个人的体会。显存优化这件事,最贵的成本从来不是硬件,是时间。我早期有个习惯,遇到 OOM 就先去网上找“一键解决”的配置,折腾半天发现跟自己的场景不匹配。后来改成先花十分钟算账、先看峰值曲线、先核默认参数,反而快得多。一个人干活没有分工,最该省的就是反复试错的时间——先把账算清楚,再决定是调参还是掏钱,这个顺序别反。