返回博客

2026.10.13

别人说三张卡跑不起来,我算出是显存不够:为什么两个配置崩在同一行,反而说明跟显存无关?

一个社区仓库说 DeepSeek-V4-Flash 在三张 64GB 卡上加载就崩,只有四张能跑。我按显存账推出「提高显存比例就行」,两天后读源码发现那个参数在加载阶段根本不生效。他测的两档比例差 3.2 GiB,崩在同一行,这本来就是资源无关的证据,我读反了。真凶是一个环境变量,它比那条「三卡不行」的结论晚 22 小时才进仓库。

GPULLM推理调试方法方法论

两个配置,显存预算差了 3.2 GiB,崩在同一个文件的同一行。

我当时的解读是:你看,两次都撞在同一堵显存墙上,再往上提一点就过了。

说反了。资源不够的时候,多给 3.2 GiB,崩溃点至少该挪一挪。纹丝不动,说明它压根跟资源没关系。这篇讲的就是我怎么把一条反证当成了佐证,以及真凶为什么藏得那么深。

想直接拿判据的看第 3 节,那是一个减法;想看三张卡最后怎么跑起来、层该怎么分,看第 6 节。

1. 一份「三张卡不行」的报告

我手上有三张挖矿版的数据中心卡,原厂 8GB,软件解锁到每张 64GB,每张只接了 PCIe 2.0 x4,通道窄得可怜。

想跑的模型是 DeepSeek-V4-Flash,284B 总参数,43 层,权重大约 155 GiB。单张卡装不下,得用流水线并行:把 43 层切成三段,一张卡扛一段,算完把中间结果递给下一张。

社区里有人在四张同款卡上跑通了,写了一份很扎实的仓库。里面有一张表,我照着意思转一下:

配置 结果
4 张卡 能跑
3 张卡 + 投机解码,显存比例 0.90 加载时崩在专家权重重排那一步
3 张卡,不开投机解码,显存比例 0.85 同样的失败,同样的位置
2 张卡 155 GiB 装不进 128 GiB,没戏

作者的归因是:崩在模型加载期,所以既跟投机解码无关,也跟显存比例这个参数无关。

我那会儿正纠结要不要买第四张卡。看到这张表,我第一反应是:他归因错了。

2. 我的第一版账:显存挤爆,提到 0.95 就行

崩溃那一步叫专家重排。这个模型每层有 256 个「专家」子网络,每次只叫醒其中几个干活。加载时要把每个专家的权重按 GPU 内核喜欢的格式重新排一遍。

我去读了那段代码。它重排完第一个专家,就一次性开出全部 256 个专家的输出缓冲,而这时候原始权重还没释放。我按张量形状算了一下单层瞬时峰值,当时得出约 6 GiB(后来按真实张量重算,最重的那一下是 4 GiB:输入 2 GiB、输出 2 GiB 同时在场)。

然后算显存账。vLLM 有个参数叫显存比例,意思是「这张卡你最多用百分之多少」:

默认均分,最重那张扛 15 层 ≈ 53.3 GiB
比例 0.85:63.5 × 0.85 = 54.0 GiB,剩 0.7 GiB
比例 0.90:63.5 × 0.90 = 57.1 GiB,剩 3.9 GiB
重排峰值要 6 GiB,两档都不够

账算得严丝合缝。我写下的结论是:把比例提到 0.95,预算 63.5 × 0.95 = 60.3 GiB,剩 7.0 GiB,够了。主板别换,第四张卡先别买。

我还顺手把作者那句「同样的失败,同样的位置」读成了自己的佐证:两次撞的是同一堵墙嘛。

3. 那个参数在加载阶段根本不管事

两天后被人反问了一句:「你不是说改个比例就行?」我回去从 worker 的启动顺序开始,一行一行读。

vLLM 的启动是三步:初始化设备 → 加载模型 → 算剩多少显存给 KV 缓存。显存比例这个参数只在第一步和第三步出现:

  • 第一步,拿「总显存 × 比例」跟当前空闲比一下,够就放行。这时候权重还一个字节都没加载,空闲接近 63.5 GiB,必然放行,之后不留任何约束。
  • 第三步,用「总显存 × 比例 − 模型已经占掉的」决定 KV 缓存池给多大。这一步必须等模型加载完才能算,所以顺序是钉死的。

专家重排发生在第二步。那个时候它能用的是整张卡的物理空闲,没有任何上限。比例从 0.85 调到 0.95,对重排那一刻一个字节都不改变。

作者那句「跟显存比例无关」是对的。我推翻错了。

回头看,最该让我警觉的就是开头那句:

两次实验的资源差 = 63.5 GiB × (0.90 − 0.85) ≈ 3.2 GiB
崩溃位置的变化 = 0

资源不够通常是渐变的。多给 3.2 GiB,要么能多走几层,要么崩在别处。两个不同配置崩在同一个文件同一行,这是「跟这个资源无关」的证据。我把它读成了「撞同一堵墙」,方向整个反了。

纯物理容量账也站不住:

每卡权重 ≈ 54 GiB + 重排峰值 4 GiB ≈ 58 GiB
单卡物理可用 ≈ 63.5 GiB
余量 ≈ 5.5 GiB

装得下。容量这条路走到头了。

4. 一张卡排掉七个嫌疑

那会儿我只有两张卡,第三张还在路上。但我想明白了一件事:专家重排是每张卡自己对自己手里那些专家做的位重排,跟一共几张卡没关系。所以一张卡就能复现「三卡里那张卡」会遇到的一切内核级问题。

我做了两层探针:

  1. 把重排内核原样从 vLLM 源码里抠出来,一行不改,用合成数据按生产规格(256 个专家)在真卡上跑。全过。
  2. 拿真实的权重文件喂进去,走完整的准备流程。全过。

为了确认喂进去的真是权重而不是填充值,我加了一道门:取一小块看有多少种不同的值。只有一种,就说明我在测假数据,结论作废。真权重有几百种。

下面这七条假说,全部排除:

假说 结果
显存比例太低 排除,加载阶段不生效
三卡物理装不下 排除,余量约 5.5 GiB
这代卡共享内存更大导致越界 排除,偏移全是编译期常量
输出缓冲越界 排除,索引严丝合缝
重排内核在这代卡上有 bug 排除,真卡 256 专家规格全过
真实权重的某种数值触发 排除,真权重实测通过
专家数量从配置读错 不适用,这条路径直接从张量形状推

中间还差点再栽一次。我在一个社区分支里看到一处修改,把「专家总数」改成「本卡专家数」,注释写得极有说服力,我当场就想说「就是它」。去 grep 调用链才发现,那是另一种量化格式走的函数,我这个模型根本不经过它。

排完七条,我写下的结论是「剩余嫌疑只在多卡分片交互,单卡测不了」。

这一条也猜错了。

5. 真凶是一个环境变量

答案来自两份独立的复现报告,都指向同一个东西:PyTorch 显存分配器的一个开关,expandable_segments:True。

这个开关打开之后,PyTorch 每次释放显存都把物理页直接还给驱动,不留在自己的缓存池里。平时没什么,但专家重排是每个专家开一个临时张量、用完就扔。最后那张卡上大约要来回申请释放一万四千次。

这批卡的显存清零引擎跟不上。驱动日志会刷出清零超时,然后出现一个很诡异的状态:

nvidia-smi        → 三张卡容量正常、温度正常
显存读数          → 空的
申请显存          → 失败
cuInit()          → 返回 3

卡看着是空的,就是分不出来,只有冷重启能救。

我前面七条假说全是在问「容量够不够」「索引对不对」,真凶在第三个维度:分配器释放显存的方式。账算得通却照样失败的时候,该看的是分配和释放的频率、粒度、还不还给驱动,光看峰值大小没用。

那为什么那份 README 到今天还写着「三张卡不行」?我翻了仓库的提交历史:

第 1 天 00:57  首版结果文档写下「三卡失败」,那时这个开关是开着的
第 1 天 22:43  作者在四卡上独立撞到同一个问题,把这个开关加进禁用清单
               之后没有回头重测三卡

结论比发现真凶早 22 个小时。那条「三卡不行」,归因给了卡数,实际该归因给一个当时还没被识别出来的环境变量。

这就是可以拿去拆别人结论的判据:

看到「N 张卡不行」「A 配置不行」:
  结论写进仓库的时间 < 已知坑写进仓库的时间
  且之后没有重测记录
  ⇒ 这条结论只算线索,不算证据

查起来也不费事,git log 加上 git show <commit>:文件名,看那一版的文档里有没有提到那个坑。

6. 第三张卡到了

第三张卡装上之后,我关掉那个开关,按两份报告给的分层跑:

分层(三段层数) 末段层数 结果
14, 15, 14 14 崩,卡被锁死(报告数据)
15, 15, 13 13 起不来,KV 只剩 0.76 GiB,需要 1.24(报告数据)
15, 16, 12 12 能跑(报告数据,我本机复现)
16, 16, 11 11 能跑(报告数据)

为什么最后一段要少扛几层?投机解码的草稿模块和输出层都只放在最后一张卡上。投机解码就是让一个小模型先猜后面几个词,大模型一次性验,猜对白赚。这个草稿模块大约 12 GiB,每层大约 3.33 GiB,折合约 3.6 层。

让三张卡总负担差不多,就是一个方程:

前两段各 L 层,末段 L − 3.6 层
2L + (L − 3.6) = 43
L ≈ 15.5,末段 ≈ 11.9
⇒ 末段 ≤ 12 层

15, 16, 12 和 16, 16, 11 都落在这条线上。另外还有一条硬约束:草稿模块要读第 40–42 层的中间结果,所以这三层必须在最后一段。

我本机跑出来的数:

  • 冷启动 180 秒,三张卡的模型占用 51.64 / 53.76 / 52.86 GiB
  • 加载、专家重排、CUDA graph 捕获全过,内核日志里没有清零超时
  • KV 缓存池 1,000,964 个 token
  • 199,957 token 的长文里埋 5 根针,找到 5 根,顺序全对,预填充 3,005.5 tok/s
  • 四类 512 token 输出的单流生成速度,中位数 138.23 tok/s。代码 152.22,中文散文 87.15。内容类型差出将近一倍,因为投机解码在代码上猜得准得多

顺带把那四档显存比例的账重新摆一遍。比例真正管的是 KV 池:63.5 × 0.85 ≈ 54.0 GiB,扣掉 53 GiB 上下的权重,只剩 1 GiB 左右给 KV,三卡起不来。三卡要用 0.94–0.95。所以「三卡要调高比例」这句话本身是对的,只是它救的是 KV 池,救不了加载期的崩溃。我当初对了结论的一半,理由全错。

vLLM 给这三张卡算出来的最大窗口是 959,488 token。那份上游报告自己测过长文检索:150K 准确率 100%,300K 87%,450K 60%,750K 50%,900K 30%。所以「装得下 959K」和「959K 里找得到东西」是两回事。我那个 5/5 是在 20 万 token 测的,往上我没测。

7. 这次哪里不够硬

  • 我没有在三卡上把那个开关打开、亲手复现一次崩溃。 一崩就得冷重启,我没舍得。因果链是:两份独立报告指认它,加上我关掉它之后三卡跑通。证据是「去掉它就好了」,严格说还差一次「加回来就坏」。
  • 一万四千次申请释放、清零引擎跟不上,这两条机制描述来自外部报告,我没逐次计数。
  • 分层表里四行,只有 15, 16, 12 是我本机跑的,其余三行是转述别人的。
  • 第 4 节那两层探针只覆盖单卡。单卡能排除内核和数据,排除不了分片之后的行为。事实证明我用它下的「剩余嫌疑在分片交互」也是错的。
  • 跑通当天晚上,服务在完全空闲时自己崩过一次,崩溃时在跑和排队的请求都是 0。还没查清。我前两次报「零硬件报错」看的是 nvidia-smi,后来改读系统的内核日志,才抓到之前漏掉的一条 Xid。
  • 生成速度只测了单流、512 token、四类内容。

8. 下次看到「N 张卡跑不起来」

  1. 先查结论是哪次提交写的,再查作者后来加的禁用项、已知坑是哪次提交加的。坑晚于结论又没重测,结论作废。
  2. 两个不同资源配置崩在同一行,先当成「跟这个资源无关」处理。资源不足很少一个字节都不挪。
  3. 怀疑某个参数之前,先确认它在出事的那个阶段生效。vLLM 的显存比例只管启动前检查和 KV 池大小,管不到加载。
  4. 容量账算得通却仍然失败,去看分配器:释放频率、粒度、是否把物理页还给驱动。PYTORCH_CUDA_ALLOC_CONF 先打出来看一眼。
  5. nvidia-smi 说卡好好的,cuInit() 返回非零,信后者。
  6. 看到一个说得通的修改,先 grep 调用链,确认你的路径真的经过它。
  7. 流水线并行分层,把只放在末段的东西(草稿模块、输出层)折算成层数,解 2L + (L − 折算层数) = 总层数。
  8. 推算完的结论,标上「静态推算,没跑过」,等真跑完再摘掉。我当初写了这行,所以后来改口的时候没那么难看。