三张卡已经能跑起一个 284B 的大模型,我想知道再插第四张能快多少。
动手前我先写了个预测:按卡数比,吞吐 +33%。然后拿一个开源项目公开的四卡数字一比,0.90 倍。加一张卡,反而慢了 10%。
我当场给了个解释:流水线多了一级,串行深度也长了,算力多 33%,路也长 33%,抵消了。听着挺顺。
这个机制是我编的。更要命的是,就在同一台机器上,同一个配置原封不动连跑六次,吞吐能在 366 和 561 之间来回跳。一把自己都量不准自己的尺子,拿去比别人的四卡,比出来的 0.90 倍什么都说明不了。
想直接拿公式的看第 3 章,一个除法就能算出你的尺子能看出多大的差;想看新尺子怎么在读数最高的那一格骗人,看第 6 章。
1. 机器和那道墙
三张挖矿版的数据中心卡,软件解锁到每张 64GB,上面用 vLLM 跑 DeepSeek-V4-Flash,43 层按 15/16/12 切到三张卡上。这叫流水线并行,第一张卡算完前 15 层把结果递给第二张,以此类推。
这几张卡我设了 200W 功耗上限。负载一上来,三张卡 100% 的时间都顶在这道墙上,nvidia-smi 里的限频原因一直亮着「软件功耗上限」那一位。SM 时钟被压到 1418 MHz,而它的上限是 1695,相当于锁掉了 19.5%。
温度倒是很闲,峰值 66/55/63°C,离 85 度的上限远着呢。卡不热,就是被电卡住了。
墙本身我不打算动。我想测的是墙以内还有没有别的旋钮能拧,比如投机解码一次猜几个、层怎么切。这些改动的预期效果都在 5% 到 10% 这个量级。
2. 同一个配置,连跑六次
先跑基线。同一个配置,什么都不改,重复六次:
聚合吞吐最低 366,最高 561
变异系数(标准差 ÷ 均值)15%
而且是两坨:一坨每路解码 60.9 tok/s,一坨 72.1,差 1.18 倍
两坨这件事比 15% 本身更说明问题。如果是随机噪声,数字应该散成一片,不该扎成两堆。
追下去是时钟在往下沉。三张卡开跑时是 1455/1410/1335 MHz,37 秒后掉到 1335/1380/1245。功耗墙下面,卡越跑越热,同样的瓦数换来的频率越来越低。每一次跑的时候卡处在什么热态,决定了它落进哪一坨。
说白了,这把尺子量的一半是我的配置,另一半是「上一次跑完歇了多久」。
3. 开跑之前先算:这把尺子能看出多大的差
这一步我以前从来不做,都是先跑了再说。其实一个除法就够:
两组各跑 n 次,能分辨的最小差异 ≈ 2 × CV × √(2/n)
CV 就是上面那个变异系数。√(2/n) 来自两组均值相减,两边的噪声都要算进去。前面那个 2 大约对应「有 95% 把握这不是噪声」。
代进我的 15%:
各跑 1 次:2 × 15% × √2 ≈ 42%
各跑 5 次:2 × 15% × √(2/5) ≈ 19%
单跑一次,两个配置要差 42% 我才能说它们真不一样。每边跑五次,也只能看出 19%。而我要测的东西只有 5% 到 10%。
反过来解,想看出某个差异要跑几次:
每组需要的次数 n ≈ 8 × (CV ÷ 想看出的差)²
想看出 10%:8 × (15/10)² = 18 次,每边。想看出 5%:8 × (15/5)² = 72 次,每边。
这里那个 2 只管「别把噪声当信号」,还没算「明明有差别却漏掉」的概率。真要规划样本量,这些数还得往上放。所以 72 是乐观估计。
这时候再回头看开头那个 0.90 倍。我的机器 200W,那份四卡数据跑在 180W,机器也不是同一台,而且就一个数。拿一个单次测量去跟另一个单次测量比,按上面的算法要差 42% 才有意义。0.90 倍的差是 10%。我拿它编机制,等于给噪声写了个故事。
讽刺的是,能否掉我那个机制的数据就在我自己的工作日志里:每个流水线边界在解码时每步只传 112 KiB,占每个 token 耗时的 1.15%。多一级流水线多走一趟这么点数据,抵消不了 33%。这条数据我写日志的时候读过,下结论的时候没读。
4. 那就交错跑:A/B/A/B
多跑几次还有个前提,就是每次跑之间是独立的。时钟沉降是单调的,如果我先把 A 跑五遍再把 B 跑五遍,B 就一直吃 A 留下的热。
所以我把功耗墙在 200W 和 180W 之间交替切,A/B/A/B,每次同时发 64 条请求,切完回读一遍确认参数真生效了,同时记时钟。这次测的就是功耗墙本身:如果降 20W 都看不出差,那拿别人 180W 的数字跟我 200W 比就更没意义了。
200W: 754.3 770.0 639.3 567.7 759.6 均值 698.2 CV 12.9%
180W: 630.1 572.6 710.8 595.3 746.0 均值 651.0 CV 11.5%
比值 0.932 Welch t = 0.90,不显著
逐对比值:0.835 0.744 1.112 1.049 0.982
看逐对那一行。前两对加起来,180W 比 200W 慢 21%。后三对加起来,180W 反而快 4%。
如果我跑完前两对就收工,我会带着一个「降 20W 掉 21%,两轮方向一致」的结论离开。自洽,连续两次同向,而且是错的。
每一组内部,最高和最低差了 36% 和 30%。两组之间的差是 6.8%。噪声大约是信号的五倍。时钟遥测倒是说明机制在:两边平均 1339 对 1290 MHz,差 3.7%,但这点差被淹得干干净净。
到这里结论就清楚了:这台机器墙钟的噪声底大约在 35% 上下,而「三卡换四卡」理论上最多也就 33%。用墙钟在这台机器上做这个对比,物理上不可行,跑多少轮都一样。省下的是本来要白花的几个小时。
5. 换一把不怕热的尺子
墙钟不行,但我还有别的问题要回答,比如投机解码一次猜几个最好。
投机解码就是让一个小模型先猜后面几个 token,大模型一次性验,猜对的直接收下,白赚。这里面有个量叫接受长度:大模型每验一次,平均收下几个 token。它只取决于小模型猜得准不准,跟 GPU 跑在 1245 还是 1455 MHz 毫无关系。
vLLM 自己会在日志里定期打一行 SpecDecoding metrics: Mean acceptance length。我写了个脚本,在负载前后各读一次,取增量,免得被之前的历史窗口污染。
同样的负载,接受长度在不同窗口之间的变异系数是 0.9% 到 4%。代回第 3 章那个式子:
CV 4%,各跑 1 次:2 × 4% × √2 ≈ 11%
CV 0.9%,各跑 1 次:2 × 0.9% × √2 ≈ 2.5%
CV 4%,想看出 5%:n ≈ 8 × (4/5)² ≈ 5 次
同样想看出 5%,墙钟要每边 72 次,接受长度五次以内。实际二十分钟就出了结果。
但这把尺子有个硬限制,得先说在前面:它只能回答「会改变猜得准不准」的问题。功耗墙改的是时钟,不改猜测,接受长度对它完全没反应。所以第 4 章那个问题,新尺子帮不上忙,它就是测不了。
6. 读数最高的那一格,是最差的配置
拿新尺子测「一次猜几个」:
| 一次猜几个 | 接受长度 | CV | 接受率 | 每个验证位置换回的 token |
|---|---|---|---|---|
| 5(原值) | 4.733 | 4.0% | 73.4% | 0.789 |
| 7 | 5.177 | 0.9% | 60.5% | 0.647 |
| 10 | 4.746 | 1.4% | 37.4% | 0.432 |
只看接受长度,7 最好,5.18,而且相邻两档的差异都显著(5 对 7 的 Welch t 是 −3.96,7 对 10 是 +9.44)。如果我只汇报这一列,结论就是「改成 7」。
可大模型验的时候是按位置花钱的。猜 k 个,它要处理 k 个猜测再加 1 个位置。所以真正该比的是:
每个验证位置换回的 token = 接受长度 ÷ (一次猜几个 + 1)
5:4.733 ÷ 6 = 0.789
7:5.177 ÷ 8 = 0.647
10:4.746 ÷ 11 = 0.432
5 比 7 高 22%,比 10 高 83%。7 个槽只多换回 0.44 个 token,却多花了 2 个位置。
接受率那一列也在讲同一件事:73% → 60% → 37%,单调往下掉。那个小模型猜到第 5 个之后基本就靠蒙了。5 刚好卡在它的能力边上。
顺带一个坑:这个草稿模块不让你往下调。我想试 3,vLLM 直接拒绝启动,报错说一次猜的个数不能小于它的块大小 5,小了输出会错。所以「散文场景少猜几个」这条路根本走不通,只能往上调,而往上调是亏的。
7. 方差特别小,也是信号
还有两组用墙钟测的,噪声虽大,也不是全无收获。
层怎么切。我原本猜第三张卡功耗最高(219W),给它少放几层会快一点。为了让三种切法都能起来,我把上下文窗口缩到 128k 再比:
15/16/12:487.7 ± 42.0
16/16/11:488.3 ± 90.9
14/16/13:470.2 ± 79.3
Welch |t| 全部 ≤ 0.39
完全分不出来。更有意思的是窗口开满的时候,另外两种切法直接起不来,报 KV 缓存不够。第三张卡除了自己那几层,还要放约 12 GiB 的草稿模型,外加输出层,给它加层就挤爆;给第一张卡加层也不行,因为 KV 缓存池的大小由最挤的那张卡决定。原来的 15/16/12 真正的价值是:它是唯一能把窗口开满的切法,快慢反倒其次。实验起不来,本身就是个结论。
另一个是 logits 计算分块大小(DSV4_LOGITS_ROW_CHUNK,这个模型实现自带的一个环境变量):
128(原值):500.3 ± 50.2 CV 10.0%
64: 453.2 ± 6.9 CV 1.5%
256: 501.3 ± 56.8 CV 11.3%
t 检验同样全不显著。但 64 那一档很怪,四次全落在 446 到 462 之间,其他两档的 CV 都是 10% 往上,偶尔能冲到 570 多,它一次都没冲上去过。均值最低、方差最小,这更像「稳定地慢」,不像运气不好。我倾向判它确实略差,但只是倾向。
256 和 128 速度分不出来,但它空载时功耗高一截(一张卡 84.3W 对 55.2W)。在一台被功耗墙锁着的机器上,空载就多吃电,是实打实的负面因素。
8. 拿这几条去看别人的数字
「我改了 X,快了 8%。」 先问跑了几次、CV 多少。如果作者自己都没测过同一个配置重复跑的波动,这个 8% 就没法读。代进 2 × CV × √2:CV 只要超过 3%,单跑一次的 8% 就落在噪声里。笔记本、小主机、矿卡这种常年顶着功耗墙或温度墙的机器,CV 很容易上 10%。
「接受长度 5.2,比别家的 4.7 高。」 接受长度高,往往是猜得更多换来的。一定要问一次猜几个,然后自己算接受长度 ÷ (猜几个 + 1)。我那张表里,读数最高的那格恰好是性价比最差的。
「我加了一张卡,你看他四卡的数字只有我们三卡的 0.9 倍。」 两边的功耗墙一样吗?各跑了几次?在我这台机器上,光是 200W 和 180W 交替跑,前两对就能「证明」慢 21%。跨机器、跨功耗、各一个数,这种对比只能当传闻听。
9. 这篇没做完的地方
- 「每个验证位置」是个代理指标。 我假设大模型验 k+1 个位置的成本跟位置数成正比。解码阶段常常是读显存为主,验 8 个位置和验 6 个位置的耗时可能差不了那么多,并发上去之后才越来越接近线性。我没有直接测「一次猜 7 个」的端到端墙钟,因为墙钟本来就测不准。所以「5 比 7 高 22%」是这个代理口径下的数,草稿模型自己算的那份成本也没算进去。
- 第 3 章那个 2 是近似值。 它没算统计功效,也假设了每次跑相互独立。时钟沉降恰恰让相邻几次跑不独立,所以真实的噪声底只会比算出来的更差。
- 三卡到四卡到底能快多少,这篇没回答。 我只证明了在这台机器上用墙钟答不了。要答,得先把功耗墙放开或者找到别的不随时钟变的量,两件事我都还没做。
- 那个 15% 只是一个配置、一种负载下量的。 换负载形状,CV 可能不一样,需要重新量。
- logits 分块 64 那一档的「稳定地慢」是我的判读,t 值 −1.84,没过显著线。
10. 下次要比两个配置,先做这几步
- 什么都不改,同一个配置跑五次以上,算 CV。看看数字是一片还是两坨,两坨就去查是什么在分组,时钟、温度、缓存、上一次跑完歇了多久。
- 用 2 × CV × √(2/n) 算这把尺子能看出多大的差,跟你预期的效果比。预期效果比它小,就别开跑。
- 想看出某个差,用 8 × (CV ÷ 差)² 估每边要跑几次,再乘上每次的时长,看跑不跑得起。
- 跑不起,就找一个不受噪声源影响的读数。被时钟拖累的,找算法层面的量;先确认你要回答的问题能被这个量回答。
- 用新读数也别只看一列。凡是靠多花成本抬高的指标,都除一下成本再比。
- 两组 A/B 一定交错跑,切完回读参数,记下当时的时钟或温度。
- 跨机器、跨功耗、各一个数的对比,不拿来下结论。真忍不住,先把自己机器上同配置的波动摆在旁边。