「这张卡能不能跑这个模型」有两种回答方式。一种是查表——把常见组合的实测值记下来,遇到就翻。另一种是建一个估算器,对任意组合都能算。
查表的问题是覆盖不了组合爆炸:变体、量化格式、上下文长度、卡数,四个维度乘起来几百种。真要做成一个能用的计算器,就得把显存占用建模出来。
这篇是那次建模的记录:公式长什么样、其中一个查不到的量怎么测出来、以及两个只看显存会漏掉的约束。
三条已知的机制,先回顾
建模之前有三个坑要先绕过,它们的现象和成因我在另一篇讲变体选型的文章里详细写过,这里只列结论,因为公式的每一项都对应其中一条:
一、参数量算不出体积。厂商标注的参数量口径不一(全部参数 / 激活参数 / 有效参数),磁盘上的字节数才是唯一确定的。所以估算的输入必须是查来的真实文件大小,不是参数量。
二、量化覆盖不均匀。常见的量化配置只压线性层,embedding 保留原精度。对使用逐层 embedding 的架构,那张没被压的查找表可能比主干还大——于是「参数量小」反而对应「体积大」。所以公式必须把未量化部分单独按原精度计算。
三、文件体积不等于显存占用。有些推理引擎把稀疏访问的 embedding 表留在系统内存里。同一个文件,不同引擎放进显存的部分不一样。所以估算的是「引擎实际加载的部分」,不是文件大小。
这三条决定了公式的输入该取什么。剩下的是公式本身。
公式的三个部分
拟合到对四档实测配置误差在 0.15 GiB 以内。三项相加:
权重项,扣掉常驻系统内存的部分。只对有逐层 embedding 的架构、且只在会做这个优化的格式下扣减:
CPU 常驻 = (词表 × 层数 × 每层维度 + 词表 × 隐藏维度) × 每参数字节数
显存权重 = 文件体积 - CPU 常驻
KV cache 项。现代模型常混用全局注意力和滑动窗口注意力:全局层的 KV 随上下文线性增长,滑窗层增长到窗口大小就封顶。所以这一项不能按总层数算,只能按全局层数算——而全局层占比是个查不到的量,下一节讲怎么测。
运行开销项。纯经验拟合,包含 CUDA 上下文、激活值、显存碎片:
开销 = max(0.7 × 卡数, 权重体积 × 0.10) + 上下文长度/131072 × 0.35
max 里的两项对应两种主导情形:小模型多卡时,每张卡的固定上下文开销占大头;大模型单卡时,按权重比例的激活占大头。取 max 而不是相加,是因为这两种开销在实测里不叠加——它们在不同规模下分别成为瓶颈。
查不到的量:怎么测全局层占比
全局注意力层的比例,厂商文档不一定写,配置文件里也未必有。但它可以测出来,而且方法很通用。
原理是两类层的 KV cache 随上下文的增长方式不同:全局层线性增长,滑窗层封顶。所以总显存对上下文长度的导数,只由全局层贡献。测出这个导数,就能反推全局层数。
做法是固定其它一切,只改上下文长度,每档记录稳定后的显存:
for ctx in 8192 16384 32768 65536; do
启动引擎 --ctx-size $ctx --parallel 1
等待模型加载完成并跑一次推理
nvidia-smi --query-gpu=memory.used --format=csv,noheader
停止引擎
done
拿相邻两档的显存差除以上下文差,得到每 token 的 KV 字节数。再除以单层单 token 的理论 KV 大小(2 × KV头数 × 头维度 × 每元素字节数),得到等效全局层数;除以总层数就是比例。这条线上反推出来约是非共享层的 3/8。
三个必须注意的地方,每一个我都踩过:
并发数固定为 1。多数引擎为每个并发槽分配独立的 KV cache。并发数没固定,等于在改乘数,测出来的斜率没有意义。这个坑特别隐蔽,因为默认并发数往往不是 1。
等模型完全加载并跑过一次推理再读数。加载过程中读到的是分配中途的值,而且不同档位读到的进度不一样,噪声会盖过信号。
至少取三档做线性检验。两点永远能连成直线,看不出问题。三个点如果不共线,说明还有别的机制在起作用——比如某档触发了不同的内存分配策略——这时候斜率本身就不可信,要先弄清那个拐点是什么。
这个方法不限于注意力层。任何随一个你能控制的参数单调变化的量,都可以用「固定其它、只动一个、看斜率」测出来,完全不需要知道内部实现。查不到就测,不要猜。
被忘掉的第四个维度:并发数
前面说输入是「变体 × 格式 × 上下文长度 × 卡数」四个维度。实际上还有第五个,而且它是个乘数——并发槽数。
推理引擎为了同时服务多个请求,会预分配多份 KV cache,一个并发槽一份。也就是说:
实际 KV cache = 单份 KV cache × 并发槽数
问题在于默认值通常不是 1。有的引擎默认 4。于是你写 --ctx-size 16384,以为按 16K 上下文算显存,实际分配的是 16384 × 4 = 65536 的量。
这个错误的失败方式很有欺骗性:它不会报「显存不足」,因为引擎会把装不下的模型层挤到系统内存里继续跑。表现是能启动、能出结果,但慢一个到两个数量级——我遇到的一次是 prompt 处理从上千 token/s 掉到 1.3 token/s。如果不知道有这个乘数,会一路去查驱动、查显卡、查模型格式,而根本原因只是一个默认参数。
所以估算器有两个必须做的事。把并发数作为显式输入,不要假设 1。以及在生成建议的命令行时显式写出并发参数,哪怕值就是默认值——显式写出来的参数,下一个人读命令时能看见;靠默认值的参数,只有踩坑之后才会知道它存在。
这一条也是前面测全局层比例时必须固定并发为 1 的原因:它是个乘数,不固定的话你测的斜率里混着两个变量。
只看显存会漏掉的约束一:整除
算完显存够不够,还有一个独立的门槛:张量并行要求 KV 头数能被卡数整除。不满足就直接起不来——这不是性能下降,是启动失败。
一个模型系列内部的 KV 头数往往差异很大,从 1 到 16 都可能有。这带来两个后果:
KV 头数为 1 的变体永远用不了张量并行。无论你有几张卡,它只能单卡跑。这类变体通常是系列里最小的那个,本来也不太需要多卡——但如果你的调度逻辑假设「显存不够就加卡」,遇到它会一直失败。
某些卡数下整个系列可能没有一个变体能用。卡数取 3、5、6 这类数字时,很容易所有变体的 KV 头数都不能整除。四卡、八卡这类 2 的幂反而安全得多——这解释了为什么多卡配置几乎总是 2 的幂。
按层切分的引擎没有这个限制,但要清楚它解决的是另一个问题:它让你装下单卡装不下的模型,但不会因为多卡而变快——层是串行执行的,多卡只是把它们分开存放。想要吞吐提升还是得靠张量并行,也就还是要满足整除。
所以计算器必须回答两个问题:显存够不够,以及这个卡数下能不能并行。两个都过才是真的能跑。
只看显存会漏掉的约束二:排序
最后一个设计决策,是「装得下的组合有好几个,推荐哪一个」。
最省事的写法是按权重体积降序——体积越大能力越强。实测下来这个假设会出错:在某个显存档位上,它推荐了一个小变体的高精度格式(16.8 GB),而不是一个明显更强的大变体的量化格式(16.3 GB)。两者体积几乎一样,能力差距不小。
正确的排序是两级的:先在装得下的候选里选能力最强的变体,再在这个变体内部选精度最高的格式。变体的强弱来自架构和评测,不来自体积。
背后是个更一般的道理:用一个容易测量的量去代理一个真正关心的量,在两者相关性不稳定的区间会给出错误答案。体积和能力在同一个变体内部是单调相关的(同一个模型,精度越高越强),跨变体就不是了(不同模型,体积不代表能力)。排序必须在相关性成立的范围内做——这里就是「先分组,组内排序」。
这个模式在别处也常见:用文件大小代理内容质量、用代码行数代理复杂度、用响应时间代理系统健康。都是在某个范围内成立、跨范围失效。
什么时候这套公式会失效
拟合出来的东西都有适用范围,写清楚边界比公式本身更重要——否则下一个人会拿它去算一个它根本不覆盖的场景,然后以为是自己配错了。
换引擎必须重测。「哪些张量留在系统内存」完全是引擎的实现选择,不是模型属性。同一个权重换个引擎,那个扣减项可能整个消失。这一项应该按引擎开关,不能无条件生效。
换架构族必须重测。全局层比例是对特定架构反推出来的常数。另一个模型族的注意力设计不同,这个 3/8 毫无意义。更糟的是它会给出一个看起来合理的数字——不报错,只是错。
量化方案升级要重看配置。厂商随时可能把 embedding 也纳入量化范围。一旦纳入,第二条机制整个翻转——「小的比大的还大」的反常现象会消失,而按旧公式算会高估。
开销项的系数是经验值。0.7 × 卡数 和 × 0.10 没有理论依据,是在特定驱动和引擎版本上拟合的。驱动大版本更新后值得复测——CUDA 上下文的固定开销确实会随版本变化。
处理办法不是让公式更复杂去覆盖所有情况,而是把每一项的适用条件和拟合来源写进代码注释,并保留那几组实测数据作为回归基线。下次改动之后跑同样的输入,误差超出 0.15 GiB 就说明动到了不该动的地方。
留下的
估算器的误差通常来自缺失的机制,不是错误的系数。这次三次修正没有一次是公式写错,全都是模型里少了一个真实存在的东西。所以估算不准时,调系数往往没用——要找的是还没建模的那件事。
查不到的量优先测,不要猜。全局层比例文档里没有,但它随上下文单调变化,于是可以测。凡是「随某个可控参数单调变化」的量,都有这条路。
「够用」往往不止一个条件。显存够只是其中一条,整除约束是另一条,而且它的失败方式完全不同——不是变慢,是起不来。设计一个判定器时,先把所有硬约束列全,再考虑排序和推荐。