延迟、吞吐与成本 Latency, Throughput & Cost
所属:01-fundamentals / 06-agent-infra
面试权重:★★★
来源:通用领域知识;未做一手考证
相关项目:Own — QueWI(vLLM 部署 + 模型路由 + 成本权衡)→ 见02-interview/projects/quewi-chatbot.md
相关知识点:01-fundamentals/06-agent-infra/vllm-pagedattention.md、batching-parallelism.md、gateway-routing.md
一句话#
LLM 服务的性能要拆成两个延迟指标分别看 —— TTFT(首 token 时间,决定"多久看到第一个字")和 TPOT(每 token 输出时间,决定"读起来顺不顺");吞吐和延迟是取舍关系不是同向的,而成本归结为「单价 ÷ 有效率」这条公式。
为什么重要(面试角度)#
这是把「工程能力」变成「商业结果」的那道题。面试官想听:
- 你会拆指标:TTFT、TPOT、端到端延迟、吞吐、P99 —— 而不是笼统说"快"
- 你知道批处理同时抬高吞吐和延迟,能说清取舍而不是"优化了就都变好"
- 你算得清每 token 成本:API 计价的输入/输出拆分,以及自建时的「GPU 小时价 ÷ 每小时产出」
- 你知道 prompt caching 为什么同时省成本和降 TTFT
你在 QueWI 讲"路由把简单请求分流、降低单位成本"时,背后的账就是这套。能算数、敢说"这是估算不是实测",比背结论可信得多。
核心内容#
三个延迟指标#
| 指标 | 全称 | 由什么决定 | 用户感知 | 优化手段 |
|---|---|---|---|---|
| TTFT | Time To First Token | 排队时间 + prefill 时间 + 网络 | "多久开始有反应" —— 决定体感是否流畅 | 前缀缓存(省 prefill)、chunked prefill、分流到小模型、减少排队(扩容/调度)、流式输出 |
| TPOT / ITL | Time Per Output Token / Inter-Token Latency | decode 每步耗时 + 网络 | "输出读起来顺不顺",人工阅读速度大致每秒几个词,显著慢于此就会卡顿 | ⚠️ 不是「提高 batch」 —— 那是吞吐的手段,batch 越大每步要算的 token 越多,TPOT 反而变差(实测:把每副本并发从 32 降到 16,每 token 时间从 25.3 ms 降到 16.9 ms)。降 TPOT 的正解是:降低单副本并发、量化、GQA/MLA(减少每步要读的字节)、避免同批混入极端长请求 |
| 端到端延迟 | End-to-end latency | TTFT + TPOT × (输出 token 数 − 1) + 网络与网关开销 | 整体等待时长 | 减少输出长度、流式输出(不改变 E2E 但改善体感)、全链路优化 |
必须说的三件事:
- 两个指标要分开优化,因为它们对应不同的阶段(prefill vs decode)和不同的用户感受。一个 TTFT 很好但 TPOT 很差的系统,用户会觉得"开始得很快但吐字像卡带"。
- 端的延迟随输出长度线性增长(TPOT 那一项),所以**"限制最大输出长度"同时是最直接的延迟手段和最直接的成本手段**。
- 看 P50 / P90 / P99,不要只看平均。平均值会被大量短请求美化,而用户的糟糕体验来自尾部;长请求的尾延迟是交互式服务最常见的痛点。
吞吐与延迟的取舍#
先把两个词定义清楚:
- 吞吐:单位时间处理的 token 数或请求数(token/s、req/s)。是系统级指标。
- 延迟:单个请求的等待时间。是用户级指标。
- goodput:在满足延迟 SLO(如 P99 < 2s)的前提下能提供的吞吐 —— 这才是能卖的性能。脱离 SLO 谈吞吐没有意义。
批处理是两者张力的来源:
| 批更大 | 解释 |
|---|---|
| 吞吐 ↑ | decode 每步要读全部权重,读一次可以服务更多 token,权重读取被摊薄 —— GPU 利用率上升 |
| 排队延迟 ↑ | 请求要等到凑批/轮到才会被处理,TTFT 上升 |
| TPOT 可能 ↑ | 一步要算更多 token,单步耗时变长 |
所以"把 batch 调大"不是免费的:它用延迟换吞吐。正确的目标不是最大化吞吐,而是在 SLO 内最大化 goodput。
缓解张力的手段:
- 连续批处理:把"等最长序列"的浪费去掉,在同样延迟下拿到更高吞吐(→ 见
batching-parallelism.md)。 - chunked prefill:把长 prompt 的 prefill 切片执行,与 decode 交错,避免一个长 prompt 占住一步、把同批其他请求的 TPOT 顶爆。
- prefill / decode 分离(PD 分离):把两个特性完全不同的阶段(prefill 计算受限 vs decode 带宽受限)放到不同实例、用独立队列与独立 batch 处理。⚠️ 但别把它当吞吐手段说 —— 这是最容易被反问穿的一处:在同构硬件上,分离不但不提吞吐,还可能更低(八卡 A100:共置 3.05 req/s,整数分池后只能做到 2.83 req/s,低约 7%,因为整数划分让两池的能力无法恰好匹配)。分离真正的价值是两条:① 延迟更稳定(decode 侧每步耗时稳定,不被长 prefill 插队);② 两阶段可以各选最优的并行方式与 batch。吞吐的大幅提升只出现在异构硬件上(4×A100 做 prefill + 4×H20 做 decode → 4.55 req/s vs 共置 3.02,1.51×)。说「PD 分离提吞吐」之前,先问清是不是同构。
- 优先级 / 分类调度:交互式短请求和批量长请求分开处理,别让它们互相伤害。
- 模型路由:简单请求走小模型,本就避开了大模型的排队(→ 见
gateway-routing.md)。
每 token 成本怎么算(必须能当场推导)#
情形一:用 API(按量付费)
成本由单价 × 用量决定,且输入与输出通常是分开计价的,输出单价一般显著高于输入(因为 decode 更贵)。所以:
(各家计价单位与口径不同,具体价格与时变政策以官方价目为准,不要在面试里背数字。)
推论:一个"输入很长、输出很短"的摘要类请求,和一个"输入很短、输出很长"的生成类请求,成本结构完全不同 —— 前者该优化输入(检索压缩、prompt caching),后者该优化输出(限制长度)。
情形二:自建推理
成本由资源占用决定,与 token 单价无关:
⚠️ 这里此前写成「(GPU 每小时成本 ÷ 每小时有效产出)÷ 利用率」,是重复计数 —— 「有效产出」如果已经反映实际利用率,再除一次利用率会把成本算得偏高。两种写法二选一:分子用峰值产出、分母乘利用率;或者分子直接用实测有效产出、不要再除利用率。口径要明确说出用的是哪一种。
三条推论:
- 吞吐越高,单位成本越低 —— 这就是为什么 PagedAttention / 连续批处理 / 量化是直接的降本手段(同样硬件产出更多 token)。
- 利用率是分母 —— 空转的 GPU 照样按小时计费。低峰期用小实例、或用 serverless/按量方案,比一直开满大实例便宜。
- 模型越大不一定越贵 —— 要看"单位 token 成本 × 完成任务所需 token 数"。一个更大的模型用一半的输出 token 就把任务做对了,可能比小模型更便宜。所以成本决策必须以"每任务成本"而不是"每 token 单价"为口径。
还得把隐性成本算进去:重试、失败重跑、agent 的多步调用(一次任务可能十几次模型调用)、评测与标注的成本、以及运维人力。
prompt caching(同时降本和降延迟)#
机制:请求前缀(长 system prompt、few-shot 示例、工具描述、多轮对话历史)的 KV 是一次性算好的,如果多个请求共享同一前缀,可以复用已计算的 KV,跳过这段 prefill。
收益:
- TTFT 下降 —— 省掉前缀的 prefill 时间(前缀越长,省得越明显)
- 成本下降 —— 各家的具体口径不同(有的对缓存命中的输入 token 打折),但方向总是"缓存命中的部分更便宜"
- 吞吐上升 —— 省的 prefill 算力可以给别的请求
工程要点:
- 把稳定不变的内容放在 prompt 最前面(system prompt、工具定义、固定示例),把变化的内容放后面 —— 前缀一旦变化,后面的缓存全部失效。
- 每次改动前缀都会导致缓存失效(比如动态插入时间戳、随机排序的工具列表)。这是最容易踩的坑。
- 有最小可缓存长度与过期时间(各实现不同,以文档为准)。
- 自建场景下这与 vLLM 的 block 级前缀缓存是同一件事(→ 见
vllm-pagedattention.md)。
一份降本 + 降延迟清单#
| 手段 | 降本 | 降延迟 | 说明 |
|---|---|---|---|
| 模型路由 | ✔✔ | ✔(TTFT) | 简单请求走小模型,代价是误判风险(不对称,阈值偏保守) |
| prompt caching | ✔ | ✔✔(TTFT) | 需要稳定前缀 |
| 限制 max_tokens / 缩短输出 | ✔✔ | ✔✔ | 输出单价高且主导 E2E 延迟,通常回报最高 |
| 量化 | ✔(同硬件更多吞吐,或更小实例) | 视情况 | 收益依赖硬件内核(→ 见 quantization.md) |
| 连续批处理 / 分页 KV | ✔✔ | 视情况 | 提高吞吐即降低成本 |
| 流式输出 | — | 体感 ✔ | 不改变 E2E,但把"等 8 秒"变成"立刻有字" |
| 结果缓存 / 语义缓存 | ✔✔ | ✔✔ | 有误命中风险,需保守策略 |
| 上下文压缩 / 减少检索注入 | ✔ | ✔(TTFT) | 输入变短,prefill 变快 |
| 更小的模型 + 更好的 prompt/fine-tune | ✔✔ | ✔✔ | 最根本的一档 |
常见面试问法#
Q:TTFT 和 TPOT 分别是什么?为什么要分开看? A:TTFT 是从请求发出到第一个 token 返回的时间,由排队 + prefill + 网络构成,决定"多久开始有反应";TPOT(也叫 ITL)是生成阶段每个 token 之间的间隔,由 decode 每步耗时决定,决定"输出读起来顺不顺"。分开看是因为它们对应不同阶段(prefill 计算受限 vs decode 带宽受限)、不同优化手段、不同用户感受 —— 一个 TTFT 好但 TPOT 差的系统会被感知为"开始很快但吐字卡顿"。
Q:增大 batch 会怎样影响延迟和吞吐? A:吞吐上升 —— decode 每步要把全部权重读一遍,batch 越大这一读被越多的 token 摊薄,GPU 利用率提升。代价是排队延迟上升(要凑批/排队),TTFT 变差,单步计算的 token 变多所以 TPOT 也可能变差。所以 batch 大小是延迟与吞吐之间的旋钮,目标是在延迟 SLO 内最大化 goodput,而不是无脑拉高吞吐。缓解手段有连续批处理、chunked prefill、prefill/decode 分离和优先级调度。
Q:怎么估算每 token 的成本?
A:取决于用 API 还是自建。API 是 输入 token × 输入单价 + 输出 token × 输出单价,且输入输出通常分开计价、输出更贵(具体价格以官方价目为准)。自建是 GPU 每小时成本 ÷ 每小时产出的 token 数 ÷ 利用率 —— 所以吞吐越高、利用率越高,单位成本越低。而且真正该用的口径是每任务成本而不只是每 token 单价:一个更大的模型如果用了更少的 token 把任务做对,可能反而更便宜。
Q:prompt caching 为什么能同时降成本又降延迟? A:它复用共享前缀已经算好的 KV,跳过了这段前缀的 prefill。省掉 prefill 计算 → TTFT 下降(前缀越长越明显)、算力省下来给别的请求 → 吞吐上升;同时缓存命中的输入 token 在各家的计价里也更便宜(具体折扣口径以官方为准)→ 成本下降。前提是前缀稳定:把固定内容(system prompt、工具定义、示例)放在前面,动态内容放后面;前缀一旦变化,其后的缓存全部失效。
Q:用户抱怨"有时候很快,有时候很慢",你怎么查?
A:先看是不是尾部延迟问题 —— 看 P90/P99 而不是平均,确认波动来自排队还是计算。常见原因:① 请求长度长尾(少数长 prompt/长输出拖慢自身并影响同批请求);② 突发流量导致排队(TTFT 恶化);③ 缓存命中率波动(前缀缓存/结果缓存命中时快、不命中时慢);④ 上游供应商限流或降级触发了重试;⑤ 网关/路由/检索这些非模型环节的耗时波动。做法是把一次请求拆成若干 span 分段计时(网关、路由、检索、prefill、decode),定位是哪一段在抖(→ 见 observability.md)。
易错点#
- ❌ 只报"平均延迟",不看 P90/P99 —— 用户体验由尾部决定。
- ❌ 认为"优化吞吐"和"优化延迟"是同向的 —— 批处理本质上是拿延迟换吞吐。
- ❌ 脱离 SLO 谈吞吐 —— 没有延迟约束的吞吐数字没有业务意义,应该说 goodput。
- ❌ 用"每 token 单价"比较模型贵不贵 —— 应该用每任务成本(单价 × 完成任务所需 token 数)。
- ❌ 自建成本忘记乘利用率(分母),或忘记算重试、失败、agent 多步调用、评测与标注的隐性成本。
- ❌ 在面试里背时变的具体价格与硬件单价 —— 这些会过期,说清算法比说数字可靠。
⚠️ 但这条规则此前被扩大化了,代价很大:它被读成「一律不要说数字」,于是连有出处、有实验条件、不随时间变的数字也一起禁掉了 —— 而面试里最容易拉开差距的恰恰是「当场算一笔账」。 正确的口径:不给无出处的印象数字和会过期的价格;要给量级、公式与翻转条件。例如不说「B200 每卡时 6.79 美元」,而说「自建与 API 的采购交点由 决定,即 ,交点对应的利用率恰好是两种单价之比」。前者会过期,后者永远成立。
- ❌ 说流式输出"降低了延迟" —— 它不改变端到端总时长,改变的是体感(更早看到第一个字)。
- ❌ 用了 prompt caching 但前缀里塞了时间戳/随机排序,导致缓存永远不命中。
自测#
1. 一个请求 TTFT = 0.8s,输出 500 个 token,TPOT = 30ms。端到端大约多少?如果要把 E2E 减半,最有效的两个动作是什么?
答案
端到端 ≈ TTFT + TPOT × (500 − 1) + 网络开销 ≈ 0.8 + 0.03 × 499 ≈ 0.8 + 15.0 ≈ 15.8 秒。可以看到输出阶段主导了总时长(15 秒 vs 0.8 秒),所以最有效的两个动作是:① 缩短输出(限制 max_tokens、要求更简洁、把长任务拆分),它直接按比例削减 TPOT 那一项,同时也降低成本;② 降低 TPOT(提高 batch 利用率、量化权重以减少每步读取、用 GQA/MLA 类结构、避免长请求拖累同批)。相比之下优化 TTFT(前缀缓存、chunked prefill)在这个例子里收益有限 —— 它只占 0.8 秒。若还必须改善体感,用流式输出让用户更早看到内容(不改变 E2E)。
2. 为什么说"增大 batch 提升吞吐"和"降低延迟"是一对矛盾?怎么处理?
答案
因为 decode 阶段每步要把全部权重读一遍,batch 越大这次读取被越多 token 摊薄 → 吞吐上升;但请求需要排队等待组批、单步要算更多 token → TTFT 和 TPOT 都变差。二者本质是同一资源(GPU)在"服务更多人"和"服务更快"之间的分配。处理方式:① 明确 SLO,以 goodput(SLO 内吞吐)为目标而不是裸吞吐;② 用连续批处理去掉"等最长序列"的浪费,在同样延迟下拿更高吞吐;③ chunked prefill 让长 prompt 不阻塞 decode;④ prefill/decode 分离部署;⑤ 按优先级分类调度,交互式与批量请求分开。
3. 自建推理的每 token 成本怎么算?为什么说"模型更大不一定更贵"?
答案
自建成本 ≈(GPU 每小时成本 ÷ 每小时有效产出 token 数)÷ 利用率。吞吐越高、利用率越高,单位 token 成本越低 —— 这也是分页 KV、连续批处理、量化这些吞吐优化的直接商业价值。说"更大模型不一定更贵",是因为真正该看的是每任务成本 = 每 token 成本 × 完成任务所需 token 数(含重试与 agent 多步调用):一个更强的模型可能用更少的输出 token、更少重试就把任务做对,总成本反而更低;反之一个弱模型在复杂任务上反复失败重跑,累计成本可能超过强者。所以成本决策要以"每任务成本"为口径,而不是每 token 单价。
4. 你在网关里加了 prompt caching,但订单量没降。最可能是什么原因?
答案
最可能是前缀不稳定导致缓存从不命中。常见罪魁:① 在 system prompt 里插入了时间戳、请求 id、随机 id;② 工具列表或 few-shot 示例每次随机排序;③ 把用户具体问题放在了固定指令前面(变化的内容在稳定内容之前,前缀在第一个变化点就分叉);④ 前缀长度低于实现要求的最小可缓存长度;⑤ 缓存过期时间太短、请求间隔过长。排查方法:直接看缓存命中率这个指标(而不是只看账单),确认命中率再判断;修法是重排 prompt,把稳定内容前置、动态内容后置,并去掉前缀里的随机成分。
5. 用户说"有时候快有时候慢",你怀疑是尾部延迟,怎么定位?
答案
先确认是尾部还是普遍慢:看 P50/P90/P99 的分布,如果 P50 正常而 P99 很差 → 尾部问题;如果整体抬升 → 容量或上游问题。然后分段计时定位:把一次请求拆成网关、路由判断、检索、prefill(TTFT)、decode(TPOT)、网络等 span,逐段看哪个的尾部在恶化。常见成因:请求长度长尾(少数长 prompt/长输出拖累自身与同批)、突发流量导致排队(TTFT 恶化)、缓存命中率波动、上游限流触发重试、检索或工具调用变慢。定位方法就是"分桶 + 分段":既按请求类型分桶看,也按请求阶段分段看。