大模型推理性能优化:拆解TTFT与ITL,告别总延迟误区

大模型推理性能优化:拆解TTFT与ITL,告别总延迟误区 1. 两个模型的对比实验为什么总延迟会骗人1.1 一次让我印象深刻的模型选型经历前阵子给一个内部知识库问答系统做模型选型测试了两个口碑都不错的模型 A 和 B。用传统评测方法跑一遍看总耗时A 平均 8 秒出完整回答B 平均 6 秒B 明显更好。但我把两个模型接到实际聊天界面里让同事试了试几乎所有人一致认为 A 的体验更好反馈是A 好像更聪明、反应更快B 半天憋不出一个字。这里就有意思了。我仔细抓了下两个模型的时间线A 的 TTFTTime To First Token首令牌延迟只有 0.8 秒但生成速度偏慢ITLInter-Token Latency令牌间延迟大约 65ms/tokenB 的 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 跟输出长度关系不大但跟并发数、批处理大小、模型大小、量化方式直接挂钩。如果你只测总延迟等于把两个物理上不同性质的阶段混在一起。模型 A8 秒可能是 0.8 秒 prefill 7.2 秒 decode模型 B6 秒可能是 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的时间戳 t1TTFT 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可能动辄两三千 tokenprefill 耗时直接翻倍。把 system prompt 从 800 token 精简到 300 tokenTTFT 可以有肉眼可见的改善。但真正排查线上 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 之间的时间间隔。业界还有个叫法 TPOTTime Per Output Token每输出 token 耗时二者基本等价只是统计口径略有不同TPOT 通常算的是 decode 阶段每个 token 的平均耗时ITL 更多指相邻 token 的间隔是一个更细粒度的指标。计算公式可以通过总生成耗时反推[ \text{ITL} \approx \frac{\text{总生成耗时} - \text{TTFT}}{\text{输出token数} - 1} ]举个例子一个请求输出了 500 个 tokenTTFT1.2s总耗时 18s。那么 ITL ≈ (18 - 1.2) / 499 ≈ 33.7ms/token也就是大约每秒生成 30 个 token。注意这里要减掉 TTFT因为总耗时里包含了 prefill 阶段的时间。如果简单用 18 / 500 36ms/token误差不大但做精细化优化时还是按标准的来。同样耗时 18 秒如果输出只有 200 tokenITL 就变成 84ms/token用户直观感受就是字蹦得很慢如果输出 800 tokenITL 只有 21ms/token看起来就非常流畅几乎像打字机跟手输出。3.2 为什么 decode 阶段卡在显存带宽而不是算力ITL 的物理瓶颈几乎都在显存带宽这是个让很多人意外的结论。7B 参数的模型在 FP16 精度下模型权重占约 14GB 显存。decode 阶段每生成一个 token都要把整个模型的权重从显存读一遍参与计算。假设你的 GPU 是 A100显存带宽大约 2TB/sHBM 带宽理论值那么理论上的单用户最大生成速度就是[ \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/sITL 约 71ms/token。跟 7B 模型差了整整一个数量级。这跟脑筋急转弯无关就是显存带宽的物理限制。这也是为什么量化收益巨大把模型从 FP16 量化到 INT8权重体积减半显存读取量减半ITL 理论上能提升约一倍前提是显存带宽是唯一瓶颈。INT4 量化在此基础上还能再翻倍。代价是精度损失现在的 AWQ、GPTQ 等量化方案在 4-bit 上损失已经很小工程上很值得做。3.3 并发请求对 ITL 的影响以及 KV cache 读取量随上下文增长的隐藏税ITL 还有个容易被忽略的特点它跟输出长度并不是固定的而是会随着生成变长而逐渐升高。原因在于 KV cache 的体积随生成的 token 数线性增长。每多生成一个 tokendecode 时读取的 KV cache 就多一份。模型权重是固定读取量KV cache 是递增读取量两者加起来越往后每个 token 的实际显存读取越多。这就出现了一个现象同一个请求刚开始生成时 ITL 可能只有 30ms生成到 1000 个 token 以后可能涨到 50-60ms。用户会感觉打了几百个字之后明显变钝了。这不是幻觉是 KV cache 带来的真实开销。多用户并发时这个效应更明显。连续批处理会让多个序列共享一次 decode 前向过程每个 token 生成都要读取所有并发序列的 KV cache。并发数从 1 涨到 10ITL 不一定是线性增长但一定显著变差。而且由于每个序列的上下文长度不同长上下文请求会拖慢整个 batch 的速度。Post-training 阶段可以通过 Grouped Query AttentionGQA、Multi-Query AttentionMQA压缩 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, jsonpayload, headersheaders, streamTrue) 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指标平均值P95TTFT350ms1200msITL45ms80ms平均 TTFT 350ms 看着还行但 P95 到 1.2 秒说明有相当一部分用户要等一秒以上才看到首字。这种波动通常来自排队和 preemption。分位线的价值在于暴露长尾。我自己遇到过一个奇怪问题——平均 ITL 只有 38ms但用户隔三岔五反馈卡一下。后来发现是 P99 的 ITL 到了 2 秒这种偶发的大间隔让流式体验出现停顿感。光看平均分永远定位不到这种问题。4.3 监控与工具链选型如果你已经把模型部署到了生产环境靠临时脚本测量是不够的。需要一套长期的观测体系推理框架内置 metricsvLLM 有 Prometheus 格式的 metricsTTFT 和 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/token10 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 动辄上百 tokenITL30ms 和 ITL80ms 意味着多等 5 秒的差距。TTFT 反而可以稍宽松一点因为 Agent 经常在后台静默调用用户看不到流式输出只看最终结果。更稳的做法是让 Agent 的 LLM 输出走结构化约束格式比如 Guidance、Outlines、JSON mode、function calling避免生成非法 JSON 导致二次重试那比 ITL 慢更致命。还有一个容易被忽略的点Agent 多轮工具调用时每轮都要把之前的工具调用结果加进 promptprompt 越来越长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 不代表有意义的 tokenTTFT 是收到第一个 token的时间但第一个 token 经常是好的、这是一个之类的寒暄或开头词。对于真实用户体验来说模型开始输出有信息量的内容才是关键。这会导致 TTFT 指标很好看但用户感觉一上来废话连篇。我做摘要类应用时发现模型偶发先输出好的我来帮你总结一下这种句式白白浪费 10 个 token 的时间和展示空间。解决办法有两个一是做输出约束强制跳过固定开场白二是在体验侧把寒暄 token 的延迟并入 TTFT 内让产品决策者知道假的快不是快。6.2 并发压测时的 ITL 与单请求 ITL 完全不同很多人优化的第一步是测单请求哇 ITL 只有 25ms完美。结果上线一测试8 并发下 ITL 变成 90ms。这是正常的。decode 是显存带宽瓶颈并发数翻倍每个请求能分到的有效带宽几乎等比例下降。不要拿单请求的数据跟并发压测的数据比它们不是同一个物理过程。真要判断一个模型的并发承载能力我习惯直接压测并观察ITL 开始显著拐弯的并发数阈值。比如 7B INT4 模型在同一张 A100 上4 并发 ITL 45ms8 并发 65ms16 并发直接 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-500msITL 约 25-50ms/token对应 20-40 token/s在 A100 上部署 FP16 7B 模型短 prompt 下 TTFT 约 150-300msITL 约 15-30ms/token对应 33-66 token/s。如果你的数据跟这个范围差出一个数量级大概率是配置有问题不是硬件问题。这些数字只能做参考不同实现差异很大。真正重要的是把 TTFT 和 ITL 拆开监控、拆开优化别让总延迟淹没真正的问题。在我做过的所有实际项目里凡是抱怨模型反应慢的情况最后几乎都能定位到这两个指标之一上——找到它优化就完成了一半。