Skip to content

约 6 分钟阅读

05. 衡量快慢:TTFT、TPOT 与 Goodput ​

前几篇讲了推理怎样进行、显存花在哪里。这一篇回答一个看似简单的问题:推理服务“快”,到底指什么?用户、运维和财务关心的“快”并不相同,混在一起谈,优化就会跑偏。读完你应该能分清首 token 延迟、每 token 延迟、端到端延迟、吞吐、尾延迟和 Goodput,并用一个公式判断自己的业务该先优化哪一项。

一个请求的时间线 ​

先把一次流式请求的时间画出来。用户点下发送,请求经过网络到达服务,可能先排一会儿队,然后 Prefill,生成第一个 token 并返回;之后 Decode 一步步生成,每生成一个就往回推送,直到结束。

一次流式请求的时间线:排队、Prefill、首 token、逐个 token 间隔和完整结束

这条时间线上有三个最常用的延迟指标。

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 token400 + 20 × 19 = 780 ms约 51%
写报告、写代码800 token400 + 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 模型。

别急,先让缓存热一下。