1. 两个模型的对比实验:为什么总延迟会骗人
1.1 一次让我印象深刻的模型选型经历
前阵子给一个内部知识库问答系统做模型选型,测试了两个口碑都不错的模型 A 和 B。用传统评测方法跑一遍,看总耗时,A 平均 8 秒出完整回答,B 平均 6 秒,B 明显更好。但我把两个模型接到实际聊天界面里让同事试了试,几乎所有人一致认为 A 的体验更好,反馈是"A 好像更聪明、反应更快,B 半天憋不出一个字"。
这里就有意思了。我仔细抓了下两个模型的时间线:A 的 TTFT(Time To First Token,首令牌延迟)只有 0.8 秒,但生成速度偏慢,ITL(Inter-Token Latency,令牌间延迟)大约 65ms/token;B 的 TTFT 却要 3.5 秒,用户在那干等了三秒半界面才动起来,而 ITL 反而只有 40ms/token。
数据库里的平均响应时间统计的是"完整回答耗时",但这个指标完全没有反映用户真实感知。用户在打字机效果里看到第一个字出现在屏幕上之前,多等两秒半是极其煎熬的;而一旦开始输出,每 40ms 还是 65ms 出一个 token 的差别,肉眼几乎分不出来。
这次经历让我彻底改变了对 LLM 推理性能的评测方式。总延迟只适合做机器层面的横向对比,做不了用户体验层面的判断。要评价一个模型的"快"和"慢",必须把 TTFT 和 ITL 分开看。
1.2 总延迟这个指标到底掩盖了什么
总延迟 = 首令牌时间 + 后续所有令牌时间之和。它由两部分构成,而这两部分在系统里的瓶颈资源完全不同。
- TTFT主要由 prefill(预填充/输入处理)阶段决定。这个阶段计算强度大,吃的是 GPU 算力,并行度很高,模型把整段 prompt 一次性处理完,产出第一个输出 token。TTFT 跟 prompt 长度基本成正比,prompt 越长,第一个 token 出来得越慢。
- ITL主要由 decode(解码)阶段决定。这个阶段是逐 token 生成的,每一步只生成一个 token,依赖上一步的输出,串行程度高。decode 吃的是显存带宽,算力反而用不满。ITL 跟输出长度关系不大,但跟并发数、批处理大小、模型大小、量化方式直接挂钩。
如果你只测总延迟,等于把两个物理上不同性质的阶段混在一起。模型 A"8 秒"可能是 0.8 秒 prefill + 7.2 秒 decode,模型 B"6 秒"可能是 3.5 秒 prefill + 2.5 秒 decode。总延迟相差 2 秒,但用户体验完全反过来。
一个更麻烦的情况是:总延迟指标在流式接口下经常测不准。很多压测工具默认只拿到完整响应后计时,根本没考虑首字节时间,这会把 TTFT 的差异完全抹平。我见过不少团队拿着总延迟数据去优化系统,折腾半天发现用户还是说"卡",就是因为一开始就看错了指标。
所以,解析 TTFT 和 ITL,本质上是把 LLM 推理这条链路从"黑盒总时间"拆成"两段可定位、可优化、可取舍的独立指标"。下面我一个个拆开讲。
2. TTFT:用户等第一个字时的真实感受,到底由什么决定
2.1 TTFT 的工程定义与测量口径
TTFT 在工程上的标准定义是:从客户端发出完整请求、到客户端收到第一个输出 token 之间的时间差。但在实际生产环境里,这个时间戳远没有想象中干净。我一般把 TTFT 拆成四个组成部分来观察:
- 网络传输时间:请求从客户端到服务器的 RTT(往返时延)。跨地域调用时这一项可能就有几十到上百毫秒,不值得忽略但也不是大头。
- 调度排队时间:请求到达推理服务后,如果 GPU 正在处理其他请求,新请求会进入队列。这个时间在高并发时会暴涨,也是 TTFT 波动的主要来源。
- Prefill 计算时间:模型处理输入 prompt 的时间。这是 TTFT 里最核心的部分,跟 prompt 长度、模型参数量、硬件算力直接相关。
- 首 token 传输时间:第一个输出 token 通过网络传回客户端的时间。
如果你用自己的脚本测 TTFT,一定要明确从哪一刻开始计时。我见过有人从代码发起 HTTP 请求开始算,有人从服务器收到请求开始算,两者相差一个 RTT,在跨地域部署时差距可达几百毫秒,很容易误判优化效果。
我目前推荐的测量方式:在客户端记录发起请求的时间戳 t0,然后逐条解析流式响应,记录收到第一个数据块(第一个 token 或首批 token)的时间戳 t1,TTFT = t1 - t0。这样测到的是真实的用户体感时间,任何服务端优化最终都要落到这个口径上才有意义。
2.2 Prefill 阶段是 TTFT 的主角,但排队才是隐形炸弹
Prefill 在 GPU 上以很高的并行度一次性处理整个输入序列,这个阶段是 compute-bound(计算密集型)的。它的耗时近似等于:
[ \text{T}_{\text{prefill}} \approx \frac{\text{prompt_tokens} \times \text{模型每token计算量}}{\text{GPU算力利用率}} ]
所以减少 prefill 耗时最直接的办法就是缩短 prompt。很多人没意识到,一个塞满了各种系统提示词、few-shot 示例、历史对话记录的 prompt,可能动辄两三千 token,prefill 耗时直接翻倍。把 system prompt 从 800 token 精简到 300 token,TTFT 可以有肉眼可见的改善。
但真正排查线上 TTFT 变慢时,第一个要看的不是 prefill 计算时间,而是排队时间。在大并发场景下,推理框架(比如 vLLM、TensorRT-LLM、SGLang)的调度器会把多个请求拼到一个 batch 里做连续批处理。如果 GPU 已经满载,后来的请求就得等着。
排队时间的计算公式很简单:排队时间 ≈ 排在它前面的请求的剩余处理时间之和。只要前序请求的输出很长,后到的请求就会在队列里干等。这也是为什么"一个用户输出长文本时,其他用户的首字延迟暴涨"——并行计算让 GPU 算力被瓜分,prefill 本身也会变慢。
2.3 影响 TTFT 的几个隐藏因素,尤其是 prefill 和 decode 的资源竞争
如果你用的推理框架支持 prefill 和 decode 分离(也叫 PD 分离,prefill/decode disaggregation),那你大概率已经发现了:当 prefill 和 decode 在同一个 GPU 上共享资源时,prefill 会把 decode 的显存带宽抢走,decode 也会阻塞 prefill 的算力调用。
在共享模式下,只要有一个正在生成 2000 token 的长响应,其他请求的 TTFT 就会被拖慢。优化选择有几种:
- 限制单请求最大输出长度。既能防止"一个人写完一本书、其他人等到超时"这种极端情况,也能降低排队波动。
- 开启 PD 分离。prefill 实例专门处理输入,decode 实例专门生成输出,两者各忙各的。vLLM 的 disaggregated prefill 功能就是干这个的,代价是增加了部署复杂度和跨节点传输 KV cache 的额外开销。
- 给 API 网关配超时和排队上限。不要让你的推理服务在过载时还在硬扛,宁可返回 429 让客户端重试,也比拖着所有请求一起变慢要好。
另外还有一个很多人会忽略的:KV cache 的内存管理。当显存里的 KV cache 接近上限时,调度器会触发 preemption(抢占/回收),把某些序列的 KV cache 清掉,等轮到它们时重新 prefill。一次 preemption 会让那个请求的 TTFT 从 1 秒飙到好几秒。排查 TTFT 异常波动,要看日志里有没有 "preempted" 或 "swap" 的记录。
3. ITL:打字机速度背后的显存带宽游戏
3.1 ITL、TPOT 到底怎么算
ITL 是 Inter-Token Latency,相邻两个输出 token 之间的时间间隔。业界还有个叫法 TPOT(Time Per Output Token,每输出 token 耗时),二者基本等价,只是统计口径略有不同:TPOT 通常算的是 decode 阶段每个 token 的平均耗时,ITL 更多指相邻 token 的间隔,是一个更细粒度的指标。
计算公式可以通过总生成耗时反推:
[ \text{ITL} \approx \frac{\text{总生成耗时} - \text{TTFT}}{\text{输出token数} - 1} ]
举个例子:一个请求输出了 500 个 token,TTFT=1.2s,总耗时 18s。那么 ITL ≈ (18 - 1.2) / 499 ≈ 33.7ms/token,也就是大约每秒生成 30 个 token。
注意这里要减掉 TTFT,因为总耗时里包含了 prefill 阶段的时间。如果简单用 18 / 500 = 36ms/token,误差不大,但做精细化优化时还是按标准的来。
同样耗时 18 秒,如果输出只有 200 token,ITL 就变成 84ms/token,用户直观感受就是"字蹦得很慢";如果输出 800 token,ITL 只有 21ms/token,看起来就非常流畅,几乎像打字机跟手输出。
3.2 为什么 decode 阶段卡在显存带宽而不是算力
ITL 的物理瓶颈几乎都在显存带宽,这是个让很多人意外的结论。7B 参数的模型在 FP16 精度下,模型权重占约 14GB 显存。decode 阶段每生成一个 token,都要把整个模型的权重从显存读一遍,参与计算。
假设你的 GPU 是 A100,显存带宽大约 2TB/s(HBM 带宽理论值),那么理论上的单用户最大生成速度就是:
[ \frac{2\text{TB/s}}{14\text{GB}} \approx 142 \text{ tokens/s} ]
换算成 ITL 大约是 7ms/token。这是理想值,实际还要扣除 KV cache 读取、采样开销、框架 overhead、并发抢占等,真实值可能只有理论值的 1/3 到 1/2,也就是 20-50ms/token。
这就是为什么模型越大、速度越慢:70B 模型 FP16 权重有 140GB,同一个 A100 上单用户理论速度直接降到 14 tokens/s,ITL 约 71ms/token。跟 7B 模型差了整整一个数量级。这跟脑筋急转弯无关,就是显存带宽的物理限制。
这也是为什么量化收益巨大:把模型从 FP16 量化到 INT8,权重体积减半,显存读取量减半,ITL 理论上能提升约一倍(前提是显存带宽是唯一瓶颈)。INT4 量化在此基础上还能再翻倍。代价是精度损失,现在的 AWQ、GPTQ 等量化方案在 4-bit 上损失已经很小,工程上很值得做。
3.3 并发请求对 ITL 的影响,以及 KV cache 读取量随上下文增长的"隐藏税"
ITL 还有个容易被忽略的特点:它跟输出长度并不是固定的,而是会随着生成变长而逐渐升高。原因在于 KV cache 的体积随生成的 token 数线性增长。每多生成一个 token,decode 时读取的 KV cache 就多一份。模型权重是固定读取量,KV cache 是递增读取量,两者加起来,越往后每个 token 的实际显存读取越多。
这就出现了一个现象:同一个请求,刚开始生成时 ITL 可能只有 30ms,生成到 1000 个 token 以后可能涨到 50-60ms。用户会感觉"打了几百个字之后明显变钝了"。这不是幻觉,是 KV cache 带来的真实开销。
多用户并发时这个效应更明显。连续批处理会让多个序列共享一次 decode 前向过程,每个 token 生成都要读取所有并发序列的 KV cache。并发数从 1 涨到 10,ITL 不一定是线性增长,但一定显著变差。而且由于每个序列的上下文长度不同,长上下文请求会拖慢整个 batch 的速度。
Post-training 阶段可以通过 Grouped Query Attention(GQA)、Multi-Query Attention(MQA)压缩 KV cache 体积来缓解,推理阶段也可以给 KV cache 做 FP8 量化。这些都是能直接降低每步内存读取量的手段。真实世界里,量化后 ITL 改善幅度经常会超出纯理论预期,因为 KV cache 读取量也被额外压缩了。
4. 一套能落地的测量方案,帮你把 TTFT 和 ITL 真正测准
4.1 流式接口的实测脚本,直接可抄
现在大多数 LLM 服务都提供 OpenAI 兼容的流式接口。测量 TTFT 和 ITL 最基本的思路是:逐条解析流式返回的每个数据块,给每个块盖上客户端时间戳。下面是一段我自己在用的 Python 脚本核心逻辑:
import json import time import requests def measure_streaming_metrics(api_url, headers, payload): start = time.perf_counter() resp = requests.post(api_url, json=payload, headers=headers, stream=True) ttft = None token_timestamps = [] for line in resp.iter_lines(): if not line: continue line = line.decode("utf-8") if not line.startswith("data:"): continue data_str = line[5:].strip() if data_str == "[DONE]": break now = time.perf_counter() try: chunk = json.loads(data_str) delta = chunk["choices"][0].get("delta", {}) if "content" in delta and delta["content"]: if ttft is None: ttft = now - start # 第一个有效内容 token 的时间 token_timestamps.append(now) except json.JSONDecodeError: continue if not token_timestamps: return None # 计算 ITL:相邻 token 间隔的平均值 intervals = [ token_timestamps[i + 1] - token_timestamps[i] for i in range(len(token_timestamps) - 1) ] avg_itl = sum(intervals) / len(intervals) return { "ttft_ms": ttft * 1000, "avg_itl_ms": avg_itl * 1000, "total_tokens": len(token_timestamps), "tokens_per_second": 1.0 / avg_itl if avg_itl > 0 else None, }几个细节经验:
- 用
time.perf_counter()而不是time.time()。time.time()的精度在 Windows 上只有毫秒级,而且可能被系统校时影响,测量 ITL(经常是几十毫秒)时会得到一堆 0 或者跳变。 - 首 token 可能是一个空的角色消息(比如
"delta": {"role": "assistant"}),一定要用content字段来判断,否则会把空消息算进去。 - 有些服务端会累积多个 token 合并成一个 chunk 发送,这种时候算出来的 ITL 会偏大,但反映的是网络的真实传输节奏,不能简单采信。
4.2 并发测试:别只关注平均分,要看分位线
单请求测出来的 TTFT 和 ITL 只是理想值,真实线上是并发场景下的表现。并发压测时,我强烈建议除了平均分,还要看 P95 甚至 P99 分位线。
我自己压测的配置方法是:固定并发数(比如 8/16/32),每个请求单独记录 TTFT 和 ITL,最后统计分布。使用的工具可以直接写 Python 脚本,也可以用 Locust 自定义 client。负载生成器要注意一点:不要用线程池发请求然后算总体平均,因为内部的 GIL 和线程调度会引入额外噪声。
举例来说,压测结果可能长这样(8 并发,prompt 512 token,模型 7B INT8):
| 指标 | 平均值 | P95 |
|---|---|---|
| TTFT | 350ms | 1200ms |
| ITL | 45ms | 80ms |
平均 TTFT 350ms 看着还行,但 P95 到 1.2 秒说明有相当一部分用户要等一秒以上才看到首字。这种波动通常来自排队和 preemption。
分位线的价值在于暴露长尾。我自己遇到过一个奇怪问题——平均 ITL 只有 38ms,但用户隔三岔五反馈"卡一下"。后来发现是 P99 的 ITL 到了 2 秒,这种偶发的大间隔让流式体验出现停顿感。光看平均分永远定位不到这种问题。
4.3 监控与工具链选型
如果你已经把模型部署到了生产环境,靠临时脚本测量是不够的。需要一套长期的观测体系:
- 推理框架内置 metrics:vLLM 有 Prometheus 格式的 metrics,TTFT 和 ITL 可以直接从
/metrics拉出来做成 Grafana 面板。SGLang 也有类似接口。尽量不要自己埋点,框架层面的指标已经覆盖了 prefill/decode 的细粒度阶段。 - 链路追踪:对每个请求记录时间戳(收到请求、进入队列、开始 prefill、prefill 结束、每个 decode step 结束),可以用 OpenTelemetry 的 span 做一个 LLM 推理请求的全链路追踪。我建议至少记录
queue_start、prefill_start、prefill_end、first_token_sent这几个关键点。 - 业务层监控:在网关层记录业务视角的 TTFT(客户端发起到收到首块),这个数值跟用户在 UI 上感受到的最一致。服务端 metrics 和业务层 metrics 可以对比排查网络开销。
这三个层面都打通之后,才能快速回答"TTFT 变慢了,到底是网络问题、排队问题还是 prefill 本身变慢了"。
5. 从指标到体验:真实产品里怎么取舍和优化
5.1 流式 UI 的隐藏逻辑:TTFT 与 ITL 的配合
做前端流式展示时,很少人意识到 TTFT 和 ITL 需要一起配合。即使 TTFT 只有 500ms,但 ITL 是 100ms/token(10 token/s),用户仍然会觉得输出"一个字一个字往外蹦",像老式拨号上网。反过来,ITL 20ms/token 很快,但 TTFT 3 秒,用户会在空白框前干等三秒。
要优化"打字机效果"的流畅感,我试过的有效手段包括:
- 提前展示中间态:在等待第一个 token 期间就渲染"正在思考..."的 UI 动效,降低用户对延迟的敏感度。这招有效到离谱,纯前端就能让 TTFT 3 秒的模型感觉上快一倍。
- 前端做增量渲染:不要等整句话返回再渲染,后端流式 token 到达后前端直接拼接到文本节点上。现代框架里 React/Vue 都能做到,注意不要用
innerHTML +=这种会触发整个 DOM 重排的低效方式。 - TTFT 与 ITL 的取舍要根据交互场景来:问答式产品优先保证 TTFT 低,因为用户的问题是确定的,等久了就想走;代码补全类产品更重视 ITL 稳定,因为补全结果要一边输入一边被消费。
5.2 Agent/工具调用场景,更看重 ITL 还是 TTFT
如果你在做 Agent 应用(AI 智能体),这个判断题会更微妙。Agent 调用 LLM 的典型模式是:模型输出结构化 JSON(包含 tool_name 和 parameters),然后应用解析 JSON,去调外部工具,再把工具结果拼回 prompt,继续下一轮推理。
这种情况下,TTFT 和 ITL 的意义都发生了偏移。对 Agent 来说,用户真正在意的不是模型"开始输出"的时间,而是模型"完整输出合法 JSON"的时间。这个时间 = TTFT + 生成完整 JSON 所需的时间(约等于 JSON token 数 × ITL)。
所以优化方向跟聊天场景不太一样:
- 优先保证 ITL 不要太差,因为 JSON 动辄上百 token,ITL=30ms 和 ITL=80ms 意味着多等 5 秒的差距。
- TTFT 反而可以稍宽松一点,因为 Agent 经常在后台静默调用,用户看不到流式输出,只看最终结果。
- 更稳的做法是让 Agent 的 LLM 输出走结构化约束格式(比如 Guidance、Outlines、JSON mode、function calling),避免生成非法 JSON 导致二次重试,那比 ITL 慢更致命。
还有一个容易被忽略的点:Agent 多轮工具调用时,每轮都要把之前的工具调用结果加进 prompt,prompt 越来越长,TTFT 自然越来越高。在我自己的一个 Agent 项目上,第一轮 TTFT 500ms,第四轮已经涨到 2.8 秒。后来做了历史压缩(用摘要代替完整记录),TTFT 才降回 800ms。做 Agent 应用的团队一定要提前考虑这个"累积效应"。
5.3 TTFT 和 ITL 的系统级调优,优先级怎么排
当系统整体变慢时,第一反应不要是换模型。先看指标分布:
- 如果 P95 TTFT 暴涨,但 ITL 稳定:优先排查排队和调度。增加实例数量(水平扩容)、缩短最大输出长度限制、开启 PD 分离,或者用抢占优先级调度让短请求优先插队。
- 如果 ITL 普遍变差,但 TTFT 还行:优先做显存带宽优化。量化模型、量化 KV cache、减少并发数、换带宽更高的卡,或者换小模型。
- 如果两者都差:大概率是算力和带宽同时吃紧。这种时候先看是不是 prompt 太长(爆 TTFT),再看是不是 batch 太大(爆 ITL)。最后实在不行再考虑加卡。
还有一类"优化"容易被忽视:降低无效输出。如果历史对话记录、系统 prompt 塞了很多模型用不上的信息,不仅拉高 TTFT,还让模型更容易输出冗长废话,白白拉长总耗时。清理 prompt 杂物往往是最便宜的优化手段,但大家都习惯性忽略它。
6. 实测中踩过的几个真正的"坑"
6.1 首 token 不代表"有意义的 token"
TTFT 是"收到第一个 token"的时间,但第一个 token 经常是"好的"、"这是一个"之类的寒暄或开头词。对于真实用户体验来说,"模型开始输出有信息量的内容"才是关键。这会导致 TTFT 指标很好看,但用户感觉"一上来废话连篇"。
我做摘要类应用时发现,模型偶发先输出"好的,我来帮你总结一下:"这种句式,白白浪费 10 个 token 的时间和展示空间。解决办法有两个:一是做输出约束,强制跳过固定开场白;二是在体验侧把寒暄 token 的延迟并入 TTFT 内,让产品决策者知道"假的快"不是快。
6.2 并发压测时的 ITL 与单请求 ITL 完全不同
很多人优化的第一步是:测单请求,哇 ITL 只有 25ms,完美。结果上线一测试,8 并发下 ITL 变成 90ms。这是正常的。decode 是显存带宽瓶颈,并发数翻倍,每个请求能分到的有效带宽几乎等比例下降。不要拿单请求的数据跟并发压测的数据比,它们不是同一个物理过程。
真要判断一个模型的并发承载能力,我习惯直接压测并观察"ITL 开始显著拐弯"的并发数阈值。比如 7B INT4 模型在同一张 A100 上,4 并发 ITL 45ms,8 并发 65ms,16 并发直接 160ms。我会把服务的告警阈值设在 ITL > 120ms 或者 P95 TTFT > 2s 上,因为这两个点用户体验会急转直下。
6.3 量化对 TTFT 和 ITL 的影响方向不同,量化要谨慎
前面说的 INT4 量化能大幅改善 ITL,但它对 TTFT 不一定是正向的——有些量化格式在 prefill 阶段的矩阵乘法上反而更慢,因为反量化(dequantize)需要额外计算。刚上量化时我先看到的是 TTFT 从 300ms 涨到 420ms,但 ITL 从 55ms 降到了 28ms,总耗时还是大幅下降。如果只看 TTFT 指标,会误以为量化失败了。
所以量化的验收必须同时看两个指标,而且要根据场景权重做决策。偏重 TTFT 的场景,可能需要选择对 prefill 友好的量化方法(比如 FP8 而不是 INT4);偏重 ITL 的场景,INT4 的优势就非常明显。
另外不同推理框架(vLLM、TensorRT-LLM、llama.cpp)的量化实现有差异,同一个模型在不同框架下的 TTFT/ITL 表现可能完全不一样。如果遇到"换了框架速度变了",先别怀疑硬件,框架的算子实现和显存管理策略才是主因。
根据我个人实测的经验范围,给你一个粗略的参考基准:在消费级显卡(RTX 4090)上量化后的 7B 模型,短 prompt 下 TTFT 约 200-500ms,ITL 约 25-50ms/token(对应 20-40 token/s);在 A100 上部署 FP16 7B 模型,短 prompt 下 TTFT 约 150-300ms,ITL 约 15-30ms/token(对应 33-66 token/s)。如果你的数据跟这个范围差出一个数量级,大概率是配置有问题,不是硬件问题。
这些数字只能做参考,不同实现差异很大。真正重要的是:把 TTFT 和 ITL 拆开监控、拆开优化,别让总延迟淹没真正的问题。在我做过的所有实际项目里,凡是抱怨"模型反应慢"的情况,最后几乎都能定位到这两个指标之一上——找到它,优化就完成了一半。