1.1 延迟与吞吐的工程权衡


文档摘要

1.1 延迟与吞吐的工程权衡 本节摘要:实时 AI 性能优化的起点,是把一次请求的端到端延迟拆开看清楚——网络往返、队列等待、预填充计算、逐 token 解码,这四块的占比直接决定优化方向。首字延迟(TTFT)衡量用户等多久才看到第一个字,token 间延迟(ITL)衡量后续字蹦出来的快慢,两者受 batch 大小、上下文长度、调度策略影响的方式完全不同。而低延迟和高吞吐天然冲突:小 batch 响应快但 GPU 吃不饱,大 batch 吞吐高但排队久。本节讲清这套矛盾的机理和不同业务场景的取舍策略。

1.1 延迟与吞吐的工程权衡

本节摘要:实时 AI 性能优化的起点,是把一次请求的端到端延迟拆开看清楚——网络往返、队列等待、预填充计算、逐 token 解码,这四块的占比直接决定优化方向。首字延迟(TTFT)衡量用户等多久才看到第一个字,token 间延迟(ITL)衡量后续字蹦出来的快慢,两者受 batch 大小、上下文长度、调度策略影响的方式完全不同。而低延迟和高吞吐天然冲突:小 batch 响应快但 GPU 吃不饱,大 batch 吞吐高但排队久。本节讲清这套矛盾的机理和不同业务场景的取舍策略。

学习目标

阅读完本节,你应当能够:

  1. 把一次流式推理请求的延迟拆成四个阶段,并说出每阶段受什么影响
  2. 区分首字延迟(TTFT)和 token 间延迟(ITL),解释它们的度量意义
  3. 说明 batch 大小如何同时影响延迟和吞吐,画出二者的此消彼长关系
  4. 针对话闲、翻译、批量处理三类场景,给出延迟-吞吐的倾斜建议
  5. 用基本的排队论直觉判断"加机器"和"调 batch"哪个更划算

一、问题与直觉

如果你跑过一个最简单的 LLM 服务,大概是这样的:用户发一个 prompt,服务端调模型,模型生成完所有 token,一次性返回 JSON。这种"批量返回"的做法在测试时挺好——你关心的是总耗时,几百毫秒到几秒都能接受。可一旦面向真实用户,问题就来了:用户敲下回车后盯着一个转圈圈看了 8 秒,然后整段答案"啪"地一下全出来了。8 秒的空白让用户以为服务挂了。

这就是为什么流式输出(streaming)几乎成了 LLM 服务的标配。它不是让总耗时变短,而是改变用户感知——第一个字在 300 毫秒内出现,之后每隔几十毫秒蹦一个字,用户就觉得"它在干活"。同样的总耗时,流式和非流式的体验天差地别。

但流式暴露了一个更深的问题:你不再只关心总耗时,而是关心两个新指标。第一个字多久出现(TTFT),以及后续每个字之间的间隔(ITL)。这两个指标受完全不同的因素影响,优化方向也不同。更麻烦的是,当你想同时服务很多用户时,会发现降低单请求延迟和提高整体吞吐量是冲突的。这就是本节要拆解的核心矛盾。

二、核心原理

2.1 一次请求的延迟到底花在哪

把一次流式推理请求从用户按下回车到收到最后一个字,拆成几个阶段:

具体看每个阶段:

阶段 做什么 典型耗时 主要受什么影响
网络上行 prompt 从客户端到服务端 10–100 ms 地理位置、协议、payload 大小
网关与队列 鉴权、限流、等 GPU 空位 0–数秒 并发量、batch 调度策略、是否有排队
Prefill 预填充 处理整个 prompt,算 KV Cache 100–2000 ms prompt 长度、模型大小、是否并行
Decode 循环 逐个生成 token 每 token 20–100 ms batch 大小、模型大小、KV Cache 命中
网络下行 每个 token 下发给客户端 每 token 5–30 ms 协议、是否压缩、客户端处理速度

这里有两个关键观察。第一,首字延迟(TTFT)几乎完全由前三段决定——网络上行、队列等待、prefill。decode 还没开始,第一个字就出来了。所以想让用户"快点看到反应",要优化的是网络、队列调度和 prefill 速度,而不是 decode。

第二,token 间延迟(ITL)主要由 decode 阶段决定。每生成一个 token,模型要做一次前向传播,读 KV Cache、算 attention、输出 logits。这个速度取决于 batch 里同时有几个请求在跑、模型多大、显存带宽够不够。

💡 关键直觉:很多人优化 LLM 服务时盯着"每秒生成多少 token",但用户真正在乎的是"第一个字多久出现"和"字蹦得流不流畅"。这两个指标要分开优化,混在一起调会事倍功半。

2.2 延迟和吞吐为什么打架

吞吐量(throughput)指单位时间处理的请求数或 token 数,延迟(latency)指单个请求的响应时间。理想情况下我们想要低延迟加高吞吐,但二者在 batch 这个变量上正面冲突。

假设你有一块 GPU,每秒能做固定量的计算。来一个请求就立刻跑(batch=1),延迟最低,但 GPU 算力只用了一小部分——大部分计算单元在等数据,吞吐很低。攒 32 个请求一起跑(batch=32),GPU 算力打满,吞吐最高,但第 32 个进来的请求白白等了前 31 个排队的时间,延迟飙升。

这就是经典的延迟-吞吐权衡曲线。随着 batch 增大,吞吐先快速上升(算力被填满),然后趋平(算力到顶),而延迟几乎线性上升(排队时间变长)。最优 batch 点在曲线的"膝盖"位置——吞吐已经接近峰值,但延迟还没涨太离谱。

2.3 排队论的简单直觉

真实系统里请求到达是随机的,不可能精确攒满 batch。这就要用一点排队论。当系统利用率(load)接近 100% 时,排队等待时间会趋于无穷——这不是线性增长,而是爆炸式增长。经验法则:想让延迟稳定,系统长期利用率别超过 70%;超过 80% 就要开始排队,超过 90% 延迟会剧烈波动。

长期利用率 排队情况 延迟表现 建议
< 50% 几乎不排队 延迟稳定,最低 浪费资源,可缩容
50–70% 偶尔短队 延迟稳定 理想区间
70–85% 经常排队 延迟开始上升 接近上限,监控告警
> 85% 长队堆积 延迟剧烈波动 必须扩容或限流

三、工程实践要点

3.1 不同业务的倾斜策略

延迟和吞吐没有绝对的对错,全看业务要什么。下面是三类典型场景的取舍:

实时对话(客服、聊天机器人):用户在等回复,TTFT 是核心体验指标。我倾向于用较小 batch(4–8),宁可牺牲吞吐也要保证首字在 500 ms 内出现。这类场景的付费意愿高(用户直接交互),多开几块 GPU 是值得的。

实时翻译:延迟要求更严,但请求数相对少。用 batch=1 或 2,追求极致低延迟。翻译错了后果可控,但慢了 200 ms 用户就觉得卡顿。

批量内容生成(写文章、生成报告):用户提交后去做别的事,几分钟后回来看结果。这种场景吞吐优先,用大 batch(32–64)把 GPU 喂饱,延迟容忍到几十秒。成本敏感,要榨干每块 GPU。

场景 核心指标 推荐 batch 延迟容忍 吞吐优先级
实时对话 TTFT < 500ms 4–8
实时翻译 ITL < 50ms 1–2 极低
批量生成 总吞吐 32–64 高(分钟级)
混合负载 SLA 分级 动态调整 分级保障

⚠️ 常见坑:很多团队上线时用"对话场景"的 batch 配置跑"批量生成"的流量,结果 GPU 利用率只有 20%,成本翻倍。反之用大 batch 跑对话,用户投诉"太卡"。配置前先想清楚你的流量画像到底是哪种。

3.2 测量优于猜测

在动手优化前,一定要先埋点测量延迟构成。一个最基本的做法是在服务端记录每个阶段的时间戳:

import time class LatencyTracer: def __init__(self): self.marks = {} def mark(self, name): self.marks[name] = time.perf_counter() def report(self): keys = list(self.marks.keys()) parts = [] for i in range(1, len(keys)): dt = (self.marks[keys[i]] - self.marks[keys[i-1]]) * 1000 parts.append(f"{keys[i-1]}->{keys[i]}: {dt:.1f}ms") return " | ".join(parts) # 用法 tracer = LatencyTracer() tracer.mark("recv") # 收到请求 tracer.mark("auth_done") # 鉴权完 tracer.mark("scheduled") # 进入batch tracer.mark("prefill") # prefill完 tracer.mark("first_token") # 第一个token tracer.mark("last_token") # 最后一个token print(tracer.report()) # recv->auth_done: 5.2ms | auth_done->scheduled: 180.3ms | scheduled->prefill: 320.1ms ...

跑几百个真实请求,统计每个阶段的 P50、P95、P99。你会经常发现"瓶颈不在你想的地方"——比如 prefill 只占 200 ms,但队列等待占了 1.5 秒。这种情况下优化 prefill 算法毫无用处,该做的是改调度策略或扩容。

3.3 何时加机器,何时调参

面对延迟超标,第一个反应往往是"加 GPU"。但加 GPU 是最贵的解法,要先确认瓶颈确实在算力。判断逻辑:

  • 如果 GPU 利用率已经 > 90%,确实是算力瓶颈,加机器合理。
  • 如果 GPU 利用率只有 40% 但延迟很高,瓶颈在排队或调度,加机器没用(新机器也会排队),该调 batch 策略或调度算法。
  • 如果 TTFT 高但 decode 快,瓶颈在 prefill 或队列,优化这两个阶段。
  • 如果 TTFT 低但 ITL 高,瓶颈在 decode,考虑更小 batch 或更激进的 KV Cache 管理。

💡 关键直觉:延迟问题的解法顺序应该是——先测量定位、再调调度和 batch、最后才加硬件。跳过前两步直接加机器,往往是花钱买了个仍然慢的系统。

本节要点回顾

  • 一次推理延迟分四段:网络、队列、prefill、decode,每段受不同因素影响,优化前必须先测量各段占比。
  • TTFT 和 ITL 是两个独立指标:前者由网络、队列、prefill 决定,后者由 decode 决定,要分开优化。
  • 延迟和吞吐在 batch 上正面冲突:小 batch 低延迟低吞吐,大 batch 高延迟高吞吐,最优解在曲线膝盖处。
  • 系统利用率超 70% 延迟开始恶化:排队论告诉我们接近满载时等待时间爆炸式增长,留余量比榨干更稳。
  • 业务场景决定倾斜方向:对话重 TTFT、翻译重 ITL、批量重吞吐,配置前先认清流量画像。
  • 测量优于猜测:动手优化前先埋点统计各阶段延迟分布,别在错误的地方使劲。

图:延迟-吞吐随 batch 大小的演化曲线

图:延迟-吞吐随 batch 大小的演化曲线

延迟优化的历史脉络与进阶讨论

实时系统对延迟的敏感并不是 LLM 时代才有的。电信交换机时代的话音通话要求单向延迟低于 150 ms,否则人耳能察觉回声和断续;高频交易把往返延迟压到微秒级,为此把机房搬进交易所同一栋楼。流式 AI 服务处在两者之间:它处理的是"类人对话",用户对机器回复的耐心阈值大约在 1 秒量级——这解释了为什么 TTFT 的工程目标普遍定在 300 到 500 ms。理解这条历史线有助于判断哪些优化手段是老经验平移(排队、批处理、预取),哪些是 LLM 特有的新问题(KV Cache 显存压力、变长输出导致的延迟长尾)。

为什么延迟要用分位数而不是平均值管理。假设 100 个请求里有 95 个在 200 ms 完成、5 个拖到 8 秒,平均值只有 580 ms,看起来"还行",但每 20 个用户就有 1 个遭遇灾难体验,而且这 5% 的用户往往是发了长 prompt 或赶上排队高峰的高价值用户。生产系统的 SLA 应该写"P99 TTFT < 1.5 s"而不是"平均延迟 < 500 ms"。长尾通常来自三个源头:超长 prompt 的 prefill、调度器攒批时的冷启动、以及 GPU 显存碎片触发的 KV Cache 重分配。逐个排查才有意义,平均数会把这些信息全部抹平。

几个高频问题的回答。第一,"流式输出能不能降低总耗时"——不能,总计算量没变,改变的只是交付节奏和用户感知;但注意流式可以让客户端更早开始渲染,间接省掉用户侧的整段等待。第二,"chunked prefill 值不值得开"——值得,它把长 prompt 的 prefill 切成小块穿插进 decode 轮次,用少量 ITL 上涨换 TTFT 大幅平稳,对混合长短 prompt 的流量尤其有效。第三,"限流应该限并发数还是限 token 速率"——对话类服务限并发(保护队列不堆积),API 类服务限 token 速率(保护计费公平),两者通常叠加使用。

最后一个容易被忽略的细节:延迟数字必须在目标用户的网络环境下测。机房内网测出的 80 ms TTFT,到了移动网络加上 TLS 握手和跨运营商绕路,可能变成 600 ms。压测报告里如果没有标注客户端位置和网络条件,基本没有参考价值。真实做法是在多个地域部署拨测节点,持续打真实流量的回放,把网络段延迟从总延迟里剥出来单独立项优化——CDN、就近接入、HTTP/3 这些"和模型无关"的手段,往往比调一周 batch 参数收益更大。

下一节我们把视角从延迟机理转到工程架构,看流式服务该怎么搭——选 WebSocket 还是 SSE 还是 gRPC,负载均衡和限流怎么做,这些"模型之外"的组件往往是性能黑洞。


作者与出处
原作者: 灏天文库智能体
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库智能体 转发
评论区 (0)
U