开测之前我给自己写了一条线:三张卡串起来跑一个大模型,如果 8 条请求并发的聚合吞吐放大不到单路的 3 倍,就说明这套拼法不划算,回去重新评估。
第一次测出来:单路 60.3 tok/s,8 路 171.2 tok/s,放大 2.84 倍。差 0.16,没过线。
按那条线,它该判死刑了。可我心里一直别扭:流水线并发不就是为了填空转吗,怎么会 8 路和 16 路一模一样?回头换了一种测法,同一台机器、同一个模型、同样 8 路,聚合吞吐 606 tok/s,放大 4.31 倍。中间一行配置都没改。
再往后我又从同一台机器上测出过 72 和 359。四个数全是真测的,全是 8 条请求并发。这篇讲它们分别量到了什么。
想直接拿公式的看第 2 章;想知道一个跑分里「内容」能占多大份量,看第 4 章,那个结果我自己也没想到。
1. 那个差点判死刑的 2.84 倍
先交代机器。三张挖矿版的数据中心卡,软件解锁到每张 64GB,每张只接了 PCIe 2.0 x4 的通道,窄得可怜。上面跑 DeepSeek-V4-Flash,284B 总参数、每个 token 激活 13B,43 层按 15/16/12 切到三张卡上,这叫流水线并行:第一张卡算完前 15 层把结果递给第二张,第二张递给第三张。
只有一条请求的时候,这条流水线大部分时间是空的。卡 1 在算的时候卡 2、卡 3 在等,这些空转的时间叫「气泡」。并发的用处就是往气泡里塞别的请求,三张卡都有活干。所以我对它的预期是:并发上去,放大倍数至少该接近 3。
第一版测量我写得很随意:每条请求带一段 352 token 的提示,让模型正常回答,max_tokens=256,答完自己停。8 条同时发,总输出 token 除以墙钟。
N=1 输出 76 token 墙钟 1.26s 聚合 60.3 tok/s
N=8 输出 486 token 墙钟 2.84s 聚合 171.2 tok/s
N=16 输出 906 token 墙钟 5.27s 聚合 171.9 tok/s
8 路到 16 路纹丝不动,171 就像一堵墙。我当时的结论是「聚合吞吐 170 左右就到顶了」。
问题就在那个 486 上。8 条请求加起来只吐了 486 个 token,平均每条 61 个。模型说完一句短话就停了,墙钟 2.84 秒里有一大截时间根本没在出字。
2. 一个除法:墙钟里到底装了什么
聚合吞吐就是一个除法,分子是所有请求的输出 token 总数,分母是墙钟。毛病全出在分母里。把墙钟拆开:
墙钟 ≈ 所有 prompt token ÷ 预填充速度 + 最长那条的输出 token ÷ 单条解码速度
聚合吞吐 = N × 每条输出 token ÷ 墙钟
预填充(prefill)就是模型先把整段提示读一遍、还没开始吐字的那个阶段。前一项是读题时间,后一项是写答案时间。你想量的「解码吞吐」只住在后一项里。前一项越大、后一项越短,你量到的就越不是解码。
这台机器的预填充速度我单独测过:一条 20 万 token 的长请求,3,005 tok/s。拿它代进去对账:
171 那一组(短提示、自然停):8 × 352 = 2,816 个 prompt token,÷ 3,005 = 0.94 秒。实测首字延迟中位 0.974 秒,对得上。墙钟 2.84 秒里有三分之一在读题,剩下的时间里每条也只写了平均 61 个字。
72 那一组(同一轮里的长提示组,每条 5,634 token,也是自然停):8 × 5,634 = 45,072 个 prompt token,÷ 3,005 = 15.0 秒。墙钟 17.77 秒,84% 在读题。这组的聚合 72.03 tok/s,量的基本就是预填充,跟解码没啥关系。
606 那一组:每条提示只有一句话,加上 ignore_eos 和 min_tokens=512,强制每条不许提前停、正好吐满 512 个 token。预填充几乎为零,首字延迟 0.23 秒。8 路时每条解码中位 79.45 tok/s:
预测墙钟 = 0.23 + 512 ÷ 79.45 = 6.67 秒
预测聚合 = 8 × 512 ÷ 6.67 = 614 tok/s
实测 = 4096 ÷ 6.76 = 606 tok/s
差 1.3%。墙钟 95% 在写答案,这才是我想量的那个东西。
单路也一样。强制 512 token 时 N=1 是 140.6 tok/s,8 路 606,放大 4.31 倍,过线。同一条线,换一种测法,结论从「不划算」翻成「划算」。
说白了,第一版测的是「这堆请求多快说完」,而「多快说完」主要取决于它们打算说多少。
3. 171 → 359 → 606,拆成两刀
不过 171 到 606 这 3.54 倍,不全是停止方式的锅。我回头检查才发现,第一版和第三版之间其实变了两样东西:停止方式,以及内容。第一版是一段知识问答式的中文回答,第三版是「写几个 Python 小函数」。
好在我当天还跑了一组强制 512 token 的中文散文(讲 Transformer 的注意力机制),正好卡在中间:
自然停 · 中文问答 8 路聚合 171.2 每条解码中位 40.95
强制 512 · 中文散文 8 路聚合 359.3 每条解码中位 47.25
强制 512 · 代码 8 路聚合 606.1 每条解码中位 79.45
中间那行和第一行,每条的解码速度只差 15%,内容的味道差不多。聚合却差 2.10 倍,这一刀基本归停止方式。
中间那行和最后一行,停止方式一样,聚合差 1.69 倍,这一刀全归内容。
2.10 × 1.69 = 3.54,正好是 606 ÷ 171。
我原本以为停止方式是唯一的病,结果它只占一半。另一半藏在一个我压根没当成变量的地方:模型在写什么。
4. 写代码比写散文快 1.7 倍,为什么
同一个模型、同一张卡,写代码凭什么快?
因为我开着投机解码。投机解码就是让一个小模型先猜后面几个 token,大模型一次性验,猜对的直接收下,猜错的从错处重来。猜对就是白赚。
代码好猜。缩进、括号、def、return、类型注解,套路很重。中文散文难猜,同一个意思有十种说法。我同一天另外测过接受率:代码和 JSON 约 68.9%,开放式中文散文约 30.5%。
最能说明问题的是把投机解码关掉再测一遍:
单路解码 关投机 单路解码 开投机
代码 54.04 143.01
中文散文 52.66 115.30
关掉投机,代码和散文几乎一样快,差 2.6%。打开之后差 24%。内容对速度的影响,全部是通过投机解码的猜中率进来的。 底下那个大模型本身,算代码和算散文一样累。
加上并发之后这个差距还在放大:
聚合 tok/s 关投机 开投机 投机的倍数
代码 N=1 53.0 140.6 2.65x
代码 N=8 199.8 606.1 3.03x
散文 N=1 52.3 113.8 2.18x
散文 N=8 228.3 359.3 1.57x
代码的投机收益随并发变大,散文的反而缩水。这张表顺便把「批处理」和「投机解码」两件事拆开了:关掉投机,代码从 1 路到 8 路是 53.0 → 199.8,纯批处理放大 3.77 倍;单路时投机单独贡献 2.65 倍。两样基本各管各的。
3.77 倍比我预期的 3 倍还多。我猜除了填气泡,8 条请求共用一次权重读取也省了不少,这一条没单独验证。
回到开头那条线。如果当初我测的是散文、开着投机、强制 512,8 路是 359.3 ÷ 113.8 = 3.16 倍,勉强过。如果测的是自然停的短回答,2.84 倍,不过。同一台机器,判决取决于我那天随手挑了什么提示词。
5. 拿到一个并发跑分,先问这几句
别人贴出来「8 条请求并发聚合 X tok/s」,我现在会先问:
输出是强制定长,还是自然停? 自然停的数字,量的是「这批题目的答案有多长」。答案越短,首字延迟和收尾越占便宜,聚合越低。反过来,有人挑一批长答案题,数字就好看。这一条不说清楚,数字没法比。
prompt 多长? 我这台机器上,一句话提示 + 强制定长,8 路聚合 606;5,634 token 提示 + 自然停,8 路只剩 72,差 8.4 倍。这里同时变了提示长度和停止方式,可按第 2 章的账,72 那组 84% 的墙钟在读题,大头是提示长度。拿一句话的提示测出来的吞吐,去估一个每次塞 5K 上下文的 agent,会差出一个量级。
内容是什么,开没开投机? 开了投机的数字必须带内容类型。代码 606、散文 359,同一个配置差 1.69 倍。只报一个数,大概率报的是好看的那个。
聚合是怎么算的? 有一种算法是把每条请求自报的解码速度加起来。我这边散文 16 路一组,这样加出来是 840.1 tok/s,用总 token 除墙钟是 401.3,差 2.09 倍。原因是服务端一次只收 8 条,后 8 条在排队,首字延迟中位涨到 4.72 秒,可排队的时间不算进「每条自己的解码速度」里。各算各的满速,加起来就虚高。这个坑我在另一篇里专门讲过,这里只提醒一句:只认「总输出 ÷ 墙钟」。
6. 我这组数哪里不硬
- 每个点基本只跑了一次。唯一重复过的是代码单路:两次聚合 140.55 和 133.27,差约 5%,主要因为首字延迟一次 0.062 秒、一次 0.316 秒;解码本身 143.01 对 145.24,只差 1.6%。所以单路的聚合数天然比较抖,看倍数时要打个折扣。
- 第 3 章「两刀」的拆法有个缺口:自然停那组是中文问答,强制定长那组是中文散文,不是同一批提示。我拿每条解码速度相近(40.95 对 47.25)来论证内容差不多,算间接证据,严格做法是同一批提示各测一次自然停和强制定长。
- 预填充速度 3,005 tok/s 来自一条 20 万 token 的单请求,拿它去估 8 条并发短提示的读题时间,形状不一样。171 那组对上了首字延迟,72 那组我没有独立的首字证据。
- 接受率 68.9% / 30.5% 是同一天另一组提示测的,跟本文这几条提示不完全是同一批。
- 关投机那组只测了代码 1/2/4/8 路和散文 1/8 路,散文的中间档是空的。
- 第 4 章「共用权重读取」那个解释是猜的。
- 一台机器、一个模型、temperature=0。换成 PCIe 带宽正常的卡,流水线气泡的比例会变,放大倍数不能照搬。
7. 下次测并发吞吐,按这个顺序
- 先跑一组强制定长的:
ignore_eos+min_tokens+ 短提示。这个数是解码上限,别的数都拿来跟它比。 - 算一下墙钟里读题占多少:
N × prompt 长度 ÷ 预填充速度。超过三成,你量的就不只是解码了。 - 再按你真实的负载跑一遍:真实的提示长度、真实的停止方式。这个数是你实际能拿到的,它和第 1 步的差距就是你该优化的地方。
- 开着投机解码的,至少分代码和散文两类各测一次,并且关掉投机再测一次,看差距是不是全从猜中率来的。
- 聚合只用「总输出 token ÷ 墙钟」,不加每条的自报速度。
- 测之前定好的判决线,要写明是用哪种口径的吞吐。我那条「3 倍线」当初没写,差一点就用最不合适的那种口径把整套方案判死了。