05. 衡量快慢:TTFT、TPOT 与 Goodput
前几篇讲了推理怎样进行、显存花在哪里。这一篇回答一个看似简单的问题:推理服务“快”,到底指什么?用户、运维和财务关心的“快”并不相同,混在一起谈,优化就会跑偏。读完你应该能分清首 token 延迟、每 token 延迟、端到端延迟、吞吐、尾延迟和 Goodput,并用一个公式判断自己的业务该先优化哪一项。
一个请求的时间线
先把一次流式请求的时间画出来。用户点下发送,请求经过网络到达服务,可能先排一会儿队,然后 Prefill,生成第一个 token 并返回;之后 Decode 一步步生成,每生成一个就往回推送,直到结束。
这条时间线上有三个最常用的延迟指标。
TTFT(Time To First Token,首 token 延迟):从发出请求到收到第一个输出 token 的时间。它由网络、排队、Prefill 三部分组成。输入越长,Prefill 越久,TTFT 越大;服务越忙,排队越久,TTFT 也越大。聊天类产品里,TTFT 决定了用户对“响应快不快”的第一印象。
TPOT(Time Per Output Token,每 token 延迟):开始输出以后,平均每个 token 花的时间,也常叫 ITL(Inter-Token Latency,token 间延迟)。它决定字“往外蹦”的速度:TPOT 为 20 毫秒,用户看到的就是每秒约 50 个 token。
两者在多数场合可以混用,但不同工具的口径不完全一样。NVIDIA 的推理基准文档把它们当作同一个指标;vLLM 的压测工具则分开报告:TPOT 按每个请求计算,排除第一个 token:
text
TPOT = (端到端延迟 − TTFT) / (输出 token 数 − 1)ITL 则是相邻两次返回之间的每一个间隔,把所有请求的所有间隔汇总成分布,能看出偶发的长停顿,而不只是平均值。比较不同报告时,先确认它们用的是哪个口径。
端到端延迟(E2E Latency):从发出请求到收到完整回答的总时间。把上面两个指标组合起来:
text
端到端延迟 ≈ TTFT + TPOT × (输出 token 数 − 1)短回答看首字,长回答看出字速度
这个公式很实用,它告诉你业务的瓶颈落在哪一侧。设 TTFT 为 400 毫秒、TPOT 为 20 毫秒,对比两种场景:
| 场景 | 输出长度 | 端到端延迟 | TTFT 占比 |
|---|---|---|---|
| 分类、抽取、短答 | 20 token | 400 + 20 × 19 = 780 ms | 约 51% |
| 写报告、写代码 | 800 token | 400 + 20 × 799 = 16,380 ms | 约 2.4% |
短输出场景里,一半时间花在首字之前,优化 Prefill、减少排队最有效;长输出场景里,首字几乎可以忽略,每一毫秒的 TPOT 都会被放大八百倍。同一套系统,业务形态不同,优化重点完全不同。
所以谈优化之前,先统计自己业务的输入长度和输出长度分布。资料助手大部分请求输入很长(带着制度原文)、输出较短,TTFT 是主要矛盾;代码助手输入输出都长,两者都要管。
吞吐:一套机器能扛多少
延迟是单个用户的体验,吞吐是整套系统的产能。常见三种口径:
| 指标 | 含义 | 适合回答的问题 |
|---|---|---|
| 系统输出 token/s | 全系统每秒生成的输出 token 总数 | 这批机器的产能和单位成本 |
| 单用户 token/s | 一个用户感受到的出字速度,约等于 1 / TPOT | 用户读起来流不流畅 |
| RPS(请求/秒) | 每秒成功完成的请求数 | 能承载多大的业务量 |
这三个数字不能互相换算。同样的机器,请求都是“输入 30 token、输出 1 token”的分类任务时,RPS 会非常高;换成长文生成,RPS 会降一两个数量级,系统 token/s 却可能更高。看到一个吞吐数字,先问清输入输出长度。
吞吐和延迟通常是一对矛盾。 把更多请求放进同一批计算,GPU 利用得更充分,系统吞吐上升;但每一步要处理的数据更多,每个请求的 TPOT 会变长,排队也会变久。随着并发增加,吞吐先快速上升,然后趋于平缓,而延迟在某个点之后会急剧变差。找到这个拐点,是压测的主要目标之一(第 20 篇)。
平均值会骗人:看尾延迟
假设 100 个请求里,99 个在 1 秒内完成,1 个花了 30 秒。平均延迟约 1.3 秒,看起来不错,但那位等了 30 秒的用户已经在投诉了。
所以生产环境更关注分位数:
- P50(中位数):一半请求比它快。
- P95 / P99:95% 或 99% 的请求比它快,剩下的 5% 或 1% 更慢。
P99 才是用户投诉的来源。造成尾延迟的常见原因有:恰好碰上一个超长输入在做 Prefill(第 11 篇),显存不足导致请求被抢占重算(第 9 篇),或者排队。只看平均值,这些问题全都看不见。
需要注意样本量:只跑了 50 个请求,P99 就只由最慢的那一个决定,几乎没有统计意义。报告分位数时,要同时报告样本数。
Goodput:只算达标的那部分
把吞吐和延迟结合起来,就得到 Goodput(有效吞吐):只统计满足服务目标(SLO)的请求所贡献的吞吐。
例如资料助手的 SLO 定为“TTFT 小于 1 秒,且 TPOT 小于 50 毫秒”。一个系统每秒能完成 20 个请求,但其中一半首字超过了 1 秒,它的 Goodput 只有每秒 10 个。另一个系统每秒只完成 15 个请求,但全部达标,它的 Goodput 更高,对用户来说也更好用。
Goodput 的意义在于防止“刷吞吐”:把批次塞得很大、让每个请求都慢一点,原始吞吐数字会很好看,Goodput 却会下降。
它有两种常见的算法。压测工具(比如第 20 篇的 vllm bench serve)在给定负载下统计“每秒有多少个达标的请求”,就是上面例子的算法。学术论文(比如第 17 篇介绍的 DistServe)更常用另一种:在至少 90% 的请求达标的前提下,每张 GPU 最多能承载多大的请求速率。前者描述一次测试的结果,后者描述一套部署的容量,第 17 篇的 P/D 分离以后者为优化目标。
测量时容易踩的坑
指标定义清楚了,测出来的数字也可能不可信。常见的几个坑:
- 把 chunk 当成 token。 流式接口的一次推送可能包含多个 token,用推送次数算 TPOT 会偏大。输出 token 数应以服务返回的 usage 统计为准。
- 思考内容算不算首字。 带推理过程的模型会先输出一段思考内容。TTFT 算到第一个思考 token 还是第一个正文 token,结论差别很大,要写清楚。
- 输出长度不一致。 两次对比中,一边因为模板或停止条件不同少生成了一半内容,“速度提升”就是假的。
- 冷启动混进结果。 服务刚启动时要编译、预热、填充缓存,前几个请求会特别慢。正式测量前要预热,并单独记录冷启动时间。
- 客户端和服务端口径不同。 客户端测到的 TTFT 包含网络,服务端日志里的不包含。定位问题时两边都要看。
推理服务的“快”至少有三层:用户等第一个字的 TTFT,出字速度 TPOT,以及整体完成时间。短输出看 TTFT,长输出看 TPOT;系统产能看吞吐,用户体验看 P95/P99,两者结合看 Goodput。
第一部分到这里结束,推理的坐标系已经立好了:Prefill 与 Decode 两个阶段,KV Cache 和显存账本,以及衡量快慢的指标。第二部分回答第 2 篇留下的问题:Decode 每步只算一个 token,为什么反而更慢、更浪费?下一篇从 GPU 的结构讲起:GPU 与 Roofline 模型。
