第 11 章 评测与成本:把时间换成钱
前十章讲的都是"怎么更快"。但一个服务是否成立,取决于另外两个问题:它能接多少请求,每个 token 花多少钱。这两个问题都不能靠感觉回答,只能靠测量。
这一章讲三件事:测什么、怎么测、怎么把测量结果换成成本。先说清楚两个指标和它们背后的分位数,再说压测的方法,最后算钱。
两个指标:首字与出字
用户感知的延迟可以拆成两段,对应两个指标。
首字延迟(TTFT)是从请求发出到看见第一个字的时间。它由预填充决定,也包含排队时间。这个指标决定了用户觉得"这个产品反不反应"。
出字速度(TPS)是第一个字之后,每秒产生多少个 token。它由逐字生成决定,瓶颈在带宽。这个指标决定了用户觉得"读起来顺不顺"。
TPS 有三个容易混淆的版本。单用户视角的每秒 token 数,是延迟指标;整个服务每秒产出的 token 总数,是吞吐指标;相邻两个 token 之间的间隔叫 ITL,10 毫秒对应每秒 100 个。三个数说的不是一回事,讨论性能时必须说明是哪一个。
还有一个常被忽略的事实:这两个指标由不同阶段决定,所以优化手段不通用。量化、投机解码改善 TPS;切块预填充、前缀缓存改善 TTFT;批处理同时影响两者,但方向相反。
平均值会骗人
延迟不是一个对称分布。大多数请求集中在某个值附近,少数请求会拖得很长:缓存没命中、赶上扩容、被一条长请求堵住。平均值会被这些长尾拉高,也会掩盖长尾本身。
所以看分位数。
| 分位数 | 含义 |
|---|---|
| P50 | 中位数,一半请求比它慢 |
| P90 | 百分之十的请求比它慢 |
| P95 | 百分之五的请求比它慢 |
| P99 | 百分之一的请求比它慢 |
P50 好看不代表体验好。如果每一百次交互就有一次卡好几秒,用户会记住那一次。Inference Engineering 的说法是,好的性能工作要把 P90 和 P99 一起压下去,而不是只优化平均。
分位数的另一个作用是定位问题。P50 正常、P99 很差,说明大部分请求没问题,少数请求撞上了某种资源竞争——缓存淘汰、队列抖动、跨机通信重试。P50 本身就差,说明是系统性的容量不足。
端到端不等于推理
推理时间只是链路的一段。请求还要经过网关、鉴权、预处理、后处理、网络传输。
如果推理很快而端到端很慢,问题不在模型,在基础设施。这时候继续调引擎参数是白费力气,该看的是网关延迟、序列化开销、跨区域传输。
Inference Engineering 把这个区分说得很直接:推理时间告诉你模型优化有没有效,端到端指标告诉用户实际感受。两个都要测,而且要分开测。
怎么压测
压测最容易犯的错是用不真实的负载。
用固定长度的输入、均匀的到达间隔去压,得到的数字很好看,但和真实流量没有关系。真实流量是抖动的:请求随机到达,长度参差不齐,偶尔来一批长文档。这种抖动会让队列忽长忽短,延迟分布和均匀负载完全不同。
所以压测要满足四条。第一,ISL 和 OSL 按真实分布采样,不是取平均值。第二,到达时间加抖动,模拟突发。第三,先预热,让引擎把缓存、编译、连接都准备好,再开始计时。第四,测多组配置,批大小、并行度、量化各变一次,看趋势而不是看单点。
工具方面,NVIDIA 的 genai-perf、vLLM 和 SGLang 自带的 benchmark 脚本都能做,性能剖析用 PyTorch Profiler 或者 Nsight。工具不是难点,难点是让负载像真的。
并发该按多少算
有一个简单的公式可以估并发需求,叫利特尔法则:系统中的平均请求数,等于到达率乘以平均停留时间。
举例。假设每秒来 10 条请求,每条从进入到结束平均 3.9 秒(第 2 章算过的那条 8K 输入、512 输出的请求)。那么在系统里的平均请求数就是 39 条。
这 39 条就是你需要的同时处理能力。如果一张卡只能并 12 条,就需要四张卡。如果四张卡还不够,要么加卡,要么缩短停留时间,要么在入口限流。
这个公式的价值在于它把"要几台机器"变成了一个除法。很多容量规划靠拍脑袋,其实只需要三个数:到达率、单条耗时、单机并发。
它也有前提:系统稳定,请求不会无限堆积。过载时队列会一直涨,利特尔法则算出来的并发数没有意义,这时候该做的是限流。
成本怎么算
算成本的基本单位是 GPU 小时,不是 token。
一张卡每小时多少钱,乘以卡数,乘以运行小时数,就是总成本。把它除以这段时间产出的 token 数,得到每百万 token 的成本。
关键是分母。分母取决于吞吐,吞吐取决于批大小、量化、缓存命中率。同样一张卡,优化前后每百万 token 的成本可以差三倍。
举个例子,数字只为说明方法。假设一张 A100 每小时两美元,跑 7B 模型,批大小 32,每步 6.9 毫秒。一步产出 32 个 token,一秒约 4600 个,一小时约 1670 万个。成本是两美元除以 16.7,约每百万 token 一毛二。
如果实际利用率只有一半,成本翻倍到两毛四。如果上了 FP8 和前缀缓存,吞吐翻倍,成本回到一毛出头。如果并发上不去、卡空转,成本可能到一块钱以上。
所以"每百万 token 多少钱"这个报价,必须附三个条件:什么卡、什么模型、什么负载。缺一个就没法比较。公开 API 的价格看起来便宜,往往是因为批量大、利用率高、模型小;自建看起来贵,往往是因为利用率低。
扩容与冷启动
流量不是恒定的。白天高、夜里低,活动期间会突然涨几倍。容量按峰值准备,大部分时间在浪费;按均值准备,峰值会崩。
自动扩缩是折中:按当前负载动态调整副本数。触发条件通常不是 CPU 使用率,而是队列长度或者首字延迟——这两个才直接反映用户感受。
难点在冷启动。一个新副本从启动到能服务,要加载权重、初始化引擎、预热缓存,几分钟很正常。等队列变长再扩,用户已经等了几分钟。所以扩缩策略要提前:按预测扩,或者保留一定的缓冲容量。
多区域部署是另一层。跨区域能降低网络延迟、提高可用性,但模型权重要在每个区域各放一份,成本翻倍。只有用户分布广、延迟敏感的场景才值得。
一份压测报告该有什么
压测结果经常不可比,因为报告里缺信息。一份能用的报告至少要写清五样。
第一是硬件和模型:什么卡、几张、什么精度、什么引擎和版本。同一张卡,引擎版本不同,数字能差一成。
第二是负载分布:ISL 和 OSL 的中位数、P90、最大值,以及到达率曲线。只写"平均 8K"没有意义,因为分布的形状决定了批的构成。
第三是配置:批大小上限、并行度、量化方案、缓存策略。缺一个,别人就复现不了。
第四是分位数:TTFT 和 TPOT 各自的 P50、P90、P99,而不是平均值。平均值会把长尾藏起来。
第五是持续时间:跑了多久,有没有预热,有没有出现队列持续增长。跑三十秒的峰值数字和跑一小时的稳态数字,不是一回事。
有这五样,两组数字才能比较。缺一样,就只能当参考,不能当结论。
成本的边界
三件事不能靠优化解决。
第一,模型本身的成本。2350 亿参数的模型,权重 440 GiB,无论怎么优化,都要六张卡起步。这部分是固定的。
第二,需求的不确定性。输出长度不可预测,成本就不可预测。设最大长度和超时是唯一的控制手段。
第三,峰谷差。按峰值准备容量,利用率必然低;按均值准备,峰值必然排队。这个矛盾只能靠定价或者排队策略缓解,不能靠技术消除。
能做的三件事是:把吞吐压到硬件上限附近,把缓存命中率提上去,把扩容做在队列变长之前。这三件事做完,成本通常能降一半以上,剩下的就是模型和需求本身决定的。
下一章把前面十一章全部用一遍,走一个完整的案例:从需求到选型,从上线到故障排查。