1.1 延迟与吞吐的工程权衡 本节摘要:实时 AI 性能优化的起点,是把一次请求的端到端延迟拆开看清楚——网络往返、队列等待、预填充计算、逐 token 解码,这四块的占比直接决定优化方向。首字延迟(TTFT)衡量用户等多久才看到第一个字,token 间延迟(ITL)衡量后续字蹦出来的快慢,两者受 batch 大小、上下文长度、调度策略影响的方式完全不同。而低延迟和高吞吐天然冲突:小 batch 响应快但 GPU 吃不饱,大 batch 吞吐高但排队久。本节讲清这套矛盾的机理和不同业务场景的取舍策略。
本节摘要:实时 AI 性能优化的起点,是把一次请求的端到端延迟拆开看清楚——网络往返、队列等待、预填充计算、逐 token 解码,这四块的占比直接决定优化方向。首字延迟(TTFT)衡量用户等多久才看到第一个字,token 间延迟(ITL)衡量后续字蹦出来的快慢,两者受 batch 大小、上下文长度、调度策略影响的方式完全不同。而低延迟和高吞吐天然冲突:小 batch 响应快但 GPU 吃不饱,大 batch 吞吐高但排队久。本节讲清这套矛盾的机理和不同业务场景的取舍策略。
阅读完本节,你应当能够:
如果你跑过一个最简单的 LLM 服务,大概是这样的:用户发一个 prompt,服务端调模型,模型生成完所有 token,一次性返回 JSON。这种"批量返回"的做法在测试时挺好——你关心的是总耗时,几百毫秒到几秒都能接受。可一旦面向真实用户,问题就来了:用户敲下回车后盯着一个转圈圈看了 8 秒,然后整段答案"啪"地一下全出来了。8 秒的空白让用户以为服务挂了。
这就是为什么流式输出(streaming)几乎成了 LLM 服务的标配。它不是让总耗时变短,而是改变用户感知——第一个字在 300 毫秒内出现,之后每隔几十毫秒蹦一个字,用户就觉得"它在干活"。同样的总耗时,流式和非流式的体验天差地别。
但流式暴露了一个更深的问题:你不再只关心总耗时,而是关心两个新指标。第一个字多久出现(TTFT),以及后续每个字之间的间隔(ITL)。这两个指标受完全不同的因素影响,优化方向也不同。更麻烦的是,当你想同时服务很多用户时,会发现降低单请求延迟和提高整体吞吐量是冲突的。这就是本节要拆解的核心矛盾。
把一次流式推理请求从用户按下回车到收到最后一个字,拆成几个阶段:
具体看每个阶段:
| 阶段 | 做什么 | 典型耗时 | 主要受什么影响 |
|---|---|---|---|
| 网络上行 | 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",但用户真正在乎的是"第一个字多久出现"和"字蹦得流不流畅"。这两个指标要分开优化,混在一起调会事倍功半。
吞吐量(throughput)指单位时间处理的请求数或 token 数,延迟(latency)指单个请求的响应时间。理想情况下我们想要低延迟加高吞吐,但二者在 batch 这个变量上正面冲突。
假设你有一块 GPU,每秒能做固定量的计算。来一个请求就立刻跑(batch=1),延迟最低,但 GPU 算力只用了一小部分——大部分计算单元在等数据,吞吐很低。攒 32 个请求一起跑(batch=32),GPU 算力打满,吞吐最高,但第 32 个进来的请求白白等了前 31 个排队的时间,延迟飙升。
这就是经典的延迟-吞吐权衡曲线。随着 batch 增大,吞吐先快速上升(算力被填满),然后趋平(算力到顶),而延迟几乎线性上升(排队时间变长)。最优 batch 点在曲线的"膝盖"位置——吞吐已经接近峰值,但延迟还没涨太离谱。
真实系统里请求到达是随机的,不可能精确攒满 batch。这就要用一点排队论。当系统利用率(load)接近 100% 时,排队等待时间会趋于无穷——这不是线性增长,而是爆炸式增长。经验法则:想让延迟稳定,系统长期利用率别超过 70%;超过 80% 就要开始排队,超过 90% 延迟会剧烈波动。
| 长期利用率 | 排队情况 | 延迟表现 | 建议 |
|---|---|---|---|
| < 50% | 几乎不排队 | 延迟稳定,最低 | 浪费资源,可缩容 |
| 50–70% | 偶尔短队 | 延迟稳定 | 理想区间 |
| 70–85% | 经常排队 | 延迟开始上升 | 接近上限,监控告警 |
| > 85% | 长队堆积 | 延迟剧烈波动 | 必须扩容或限流 |
延迟和吞吐没有绝对的对错,全看业务要什么。下面是三类典型场景的取舍:
实时对话(客服、聊天机器人):用户在等回复,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 跑对话,用户投诉"太卡"。配置前先想清楚你的流量画像到底是哪种。
在动手优化前,一定要先埋点测量延迟构成。一个最基本的做法是在服务端记录每个阶段的时间戳:
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 算法毫无用处,该做的是改调度策略或扩容。
面对延迟超标,第一个反应往往是"加 GPU"。但加 GPU 是最贵的解法,要先确认瓶颈确实在算力。判断逻辑:
💡 关键直觉:延迟问题的解法顺序应该是——先测量定位、再调调度和 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,负载均衡和限流怎么做,这些"模型之外"的组件往往是性能黑洞。