/ AI Infra
[Infra-17] Tail Latency in LLM Inference: From Request Scheduling to MoE Stragglers
A practical guide to request-level and execution-level tail latency, covering chunked prefill, PD disaggregation, KV cache pressure, expert load balancing, and a measurement-driven diagnosis workflow.
推理服务的平均吞吐量很好,不代表每个用户都能顺畅地得到回答。少数请求可能等了很久才收到第一个 token,也可能在生成途中突然停顿;在分布式 MoE 推理中,还可能只有一个 GPU 明显慢于其他 GPU,却让整个阶段都等着它完成。
这些现象都与长尾有关,但它们发生在不同层级。讨论优化之前,首先要回答两个问题:谁在变慢,以及谁在等待它?
我更愿意把推理长尾优化理解为:找到慢请求或慢计算任务的来源,减少它们对其他任务的影响,并在吞吐量、成本与服务等级目标(SLO)之间控制 P95/P99 延迟。单纯提高平均吞吐,未必能改善这件事。
1. 先定义:什么样的慢才值得优化#
请求级长尾与计算级长尾#
| 层级 | 典型现象 | 优先检查的方向 |
|---|---|---|
| 请求级(Request-level) | 少数请求的首 token、生成过程或端到端延迟很高 | 排队、调度、请求隔离、KV 容量 |
| 计算级(Execution-level) | 某个 GPU、Expert 或通信 rank 明显落后 | 负载均衡、通信、同步、关键路径 |
两者不是互斥的:计算级的慢节点可能通过同步等待,最终表现为一批请求的 ITL 尖峰。但如果只观察请求 P99,就无法确定瓶颈究竟来自调度还是执行。
假设一组相近输入、输出长度的请求具有如下端到端延迟分布:
| 分位数 | 延迟(假设性示例) |
|---|---|
| P50 | 200 ms |
| P95 | 800 ms |
| P99 | 1,600 ms |
这些数字仅用于说明现象,不是任何模型或硬件的实测结果。P99 为 1,600 ms,意味着约 99% 的观测值不超过这一阈值,仍有约 1% 位于更慢的一端。流量足够大时,这部分用户的绝对数量也可能很大。
不过,长请求不自动等于异常请求。生成 10,000 个 token,通常就比生成 100 个 token 更耗时。必须结合输入长度、输出长度、缓存命中情况与业务 SLO,判断某个请求是否真的异常。
TTFT、TPOT 和 ITL 不能混为一谈#
| 指标 | 关注的问题 | 容易忽略的地方 |
|---|---|---|
| TTFT:Time To First Token | 多久能看到第一个 token? | 可能包含排队、Prefill,以及 PD 分离后的交接开销 |
| TPOT:Time Per Output Token | 一个请求平均多久生成一个后续 token? | 请求内部的平均值会掩盖单次卡顿 |
| ITL:Inter-Token Latency | 相邻输出 token 的间隔有多长? | token 级分布与请求级 TPOT 分布不是同一统计口径 |
| E2E Latency | 完整回答多久结束? | 强烈依赖输出长度 |
对于普通逐 token 输出,若总输出数为 ,一个常见定义是:
一个请求即使平均 TPOT 不高,也可能出现某次很长的 ITL。投机解码、服务端缓冲或网络分块还会影响客户端实际看到的输出间隔。因此,报告 P99 时要说明统计对象、采集位置和时间窗口,避免将客户端体验与服务端执行指标混用。
2. 请求级长尾:减少请求之间的干扰#
Chunked Prefill:限制一次 Prefill 的工作量#
假设 Decode 原本每轮约需 10 ms,此时一个 32K 输入请求到达,完整 Prefill 需要约 200 ms。若调度器连续执行这段长 Prefill,已有请求的下一次 Decode 就可能被推迟。这里的时长同样只是示意。
Chunked Prefill 将长输入拆成较小的计算块,通过每轮 token budget,限制 Prefill 对 Decode 的干扰:
完整 Prefill:
[Decode] [-------------- Long Prefill --------------] [Decode]
分块 Prefill:
[Decode + Chunk 1] [Decode + Chunk 2] ... [Decode + Chunk N]图中表达的是调度关系,方框宽度不代表实测时间。真实实现可以在一个 forward step 内混合 Decode token 与 Prefill chunk,并不一定严格交替执行。
vLLM V1 的相关调度会优先安排 Decode,再用剩余 token budget 安排 Prefill;大 Prefill 超出预算时会被分块。具体行为和配置应以部署版本为准。vLLM 优化文档
这里的关键是控制每轮干扰,而不是把 chunk 无限缩小。更小的 chunk 可能改善 Decode 间隔,却增加 Prefill 的轮数和开销,影响新请求的 TTFT。需要在真实负载下寻找合适预算。Sarathi-Serve 也围绕分块 Prefill 与混合调度讨论了吞吐和延迟的权衡。
PD 分离:把干扰移出同一个执行实例#
当在线集群规模足够大,可以将 Prefill 和 Decode 放到不同的 GPU 实例上。这样,长 Prefill 不再直接占用 Decode 实例的执行时间。SGLang 的 PD 分离文档 将 Prefill 对 Decode 的干扰,以及 Attention DP worker 间的不均衡,列为采用这一架构的动机。
但隔离并不意味着长尾消失。分离之后,关键路径上会出现 KV Cache 传输与接收侧容量约束:
请求进入 → Prefill 排队与执行 → KV 交接 → Decode 执行 → 完成
↑
传输、接收侧容量与排队都可能成为瓶颈因此要同时观察 P/D 两侧队列、KV 传输耗时与 Decode 可用 KV 容量,并根据负载调整实例配比。Prefill 很快、Decode 却接不住请求时,扩容 Prefill 不会解决问题。
相关背景可以结合本站的 PD 分离 与 vLLM KV Connector 阅读。
隔离、负载均衡与准入控制#
128K 长上下文请求和 1K 短请求共享资源池时,除了执行时间,还会争抢显存、带宽与调度机会。不同手段对应不同代价:
| 策略 | 主要作用 | 代价与边界 |
|---|---|---|
| 长短请求分池 | 降低不同长度请求的相互干扰 | 资源池变小,利用率可能下降 |
| SLO-aware Scheduling | 根据延迟预算与优先级调度 | 需要公平性约束,防止饥饿 |
| 动态负载均衡 | 避免请求集中到忙实例 | 请求数不足以准确描述负载 |
| Live Migration | 将运行中的请求及状态迁移到其他实例 | 搬运 KV 占用带宽,可能引入新停顿 |
| Admission Control | 防止系统超出可承载容量 | 可能延长外部等待或拒绝请求 |
Llumnix 是这一方向的代表工作:通过跨实例运行时重调度与请求状态迁移,改善负载不均、资源碎片和隔离问题。
准入控制尤其需要明确测量边界。如果只是把等待移到网关之外,服务端 P99 可能变好,但用户端延迟没有改善。应同时报告成功率、拒绝率和端到端等待时间。
3. 计算级长尾:MoE 中的 Straggler#
假设一个 MoE 阶段使用 8 张 GPU 做 Expert Parallelism,其中 7 张 GPU 在约 3 ms 后完成,最后一张需要 12 ms。如果后续操作依赖所有参与方的结果,那么这个同步阶段的耗时将受最慢参与方约束:
这是对同步阶段的简化描述;实际耗时还包含通信、同步及其重叠关系,不能直接当成整个模型的耗时公式。这个例子中的数字也只是示意。
此时继续优化已经在 3 ms 完成的 GPU,可能几乎不改变阶段总延迟。真正需要解决的是 Straggler:它为什么晚完成,又挡住了哪些依赖?
EPLB:让热点专家的工作分散执行#
MoE routing 会将 token 分配给不同专家,专家负载未必均匀。某些 Hot Expert 收到更多 token,就可能让承载它们的 GPU 成为慢节点。
DeepSeek 开源的 EPLB 根据估计的专家负载,计算冗余专家复制与放置方案,并在适用条件下考虑节点内放置以减少跨节点流量。这与训练阶段让模型学习更均匀的 routing 不是同一层面的机制。
部署侧的直观过程是:
统计 Expert 负载 → 识别热点 → 分配冗余副本 → 调整物理放置
↓
运行时将工作分配给相应副本副本需要额外显存;负载估计也可能落后于实际热点。因此,除了专家 token 数,还要看负载变化速度、重配置成本和最终阶段耗时。即使长期 routing 分布均匀,小 Batch 下仍可能出现明显的瞬时波动。
All-to-All:均衡计算以后还要看通信#
Expert token 数接近,不代表执行时间就一定接近。Dispatch 和 Combine 还可能受到跨节点流量、网络拥塞、通信启动开销以及参与方到达时间差异的影响。
定位时,应把同一轮的各 rank 时间线对齐,区分“发送方迟到了”和“数据真的传得慢”。只看通信算子的总时长,容易把上游计算不均衡误判成网络问题。
可优先观察每个 rank 的 token 数、收发字节数、Dispatch/Combine 耗时,以及同一轮最慢 rank 的变化。这里的 rank-level P99 应来自多轮观测,不能把单轮 8 个 rank 的耗时误当成稳定的尾部统计。
Micro-batch Pipeline:重叠能隐藏部分等待#
将大 Batch 拆成多个 Micro-batch,可以尝试让一个 Micro-batch 的专家计算与另一个的通信重叠:
Micro-batch A: [Dispatch] [Expert Compute] [Combine]
Micro-batch B: [Dispatch] [Expert Compute] [Combine]这是理想化的依赖示意。收益取决于硬件、通信实现、计算粒度和资源竞争;切得过细还可能降低 kernel 效率。Overlap 可以隐藏一部分等待,但无法消除仍然处在关键路径上的慢任务。
4. KV Cache:显存压力如何变成延迟尖峰#
长尾不一定来自某个 kernel 变慢,也可能来自请求暂时失去继续执行的资源。
随着 Decode 并发与上下文长度增加,KV Cache 占用持续增长。可用空间不足时,引擎可能暂停部分请求,之后通过重计算恢复状态。vLLM 的 Preemption 文档 说明了这类机制及其延迟影响。
KV 压力增大 → 请求被抢占 → 等待资源 → 重计算恢复 → 请求延迟出现尖峰因此,Batch 越大并不总是越好。调参时应同时考虑运行中的序列数量、每轮 token budget、上下文长度分布和可用 KV 容量。在 vLLM 中,max_num_seqs 和 max_num_batched_tokens 控制不同维度,不能把它们当成同一个“Batch 大小”。
增加 KV 空间可能缓解抢占,但也要给模型权重、激活与其他运行时开销留下余量。判断优化是否有效,应看抢占、排队与请求延迟是否共同改善,而不只是 KV 使用率是否下降。
5. 定位方法:从请求时间线追到关键路径#
我建议先确认用户侧的症状,再逐步下钻到实例、GPU、Expert 和通信层。
| 观测现象 | 优先排查 | 可验证的优化方向 |
|---|---|---|
| P99 TTFT 高 | 排队、长 Prefill、KV 交接 | 调度预算、PD 配比、接收侧容量 |
| P99 ITL 高 | Prefill 干扰、单轮 Batch 过重、同步等待 | Decode 调度、限制每轮工作量、执行 Profile |
| 并发升高后 P99 突然恶化 | KV 不足、抢占、队列积压 | KV 容量管理、准入控制 |
| 某些 GPU 经常落后 | Expert 热点、通信、硬件差异 | EPLB、拓扑放置、通信诊断 |
| 相近长度请求延迟差异很大 | 调度公平性、缓存命中、资源竞争 | 请求隔离、负载均衡、迁移 |
表中的原因只是排查假设,不能从某一个指标直接推导出结论。例如,TTFT 变高可能只是队列变长,并不代表 Prefill kernel 退化。
先把指标口径固定下来#
对于 vLLM,可以从 Production Metrics 和实际部署实例的 /metrics 入手,检查排队、TTFT、生成间隔、KV 使用率、运行中请求数以及抢占计数。指标名称与可用性会随版本变化,应以运行实例导出的名称和说明为准。
原始样本或直方图应保留必要的模型、实例与负载分组。不要简单平均各实例的 P99:多个分位数的平均值不等于整体分位数。聚合直方图时,还要保证桶边界与观测口径一致。
用两组负载区分问题#
第一组固定输入、输出长度,逐步提高并发,观察 TTFT、ITL、抢占与队列的变化。这样更容易判断系统在哪个容量点开始退化。
第二组使用真实业务的长度分布,分别观察长短请求,检查短请求是否被少量长请求拖慢。仅用固定长度 benchmark,容易漏掉这种干扰。
每次实验还应记录到达率、完成率、错误率、超时与拒绝请求,避免只统计成功返回的请求而低估尾部问题。若负载发生器总是等待上一个请求完成后才发下一个,系统变慢时发压速率也会下降;评估排队长尾时,需要考虑这种闭环负载与真实到达过程的差异。
最后才决定改哪里#
将请求时间线与对应执行轮次关联起来,回答:延迟尖峰是在排队、Prefill、KV 交接,还是某一轮 Decode 中产生?如果出现在 Decode,再看是否有某个 rank、Expert 或通信阶段落后。
一次调整一个主要变量,在相同负载下比较。除了 P99,也要保留吞吐、成本与 SLO 达标率,防止“延迟优化”只是以大幅降低承载能力换来的结果。
6. 更值得研究的是跨层协同#
在大规模 MoE 推理中,请求调度、KV 分配、Attention DP、Expert Parallelism 和 All-to-All 并不是彼此独立的模块。
一个实例的请求数最少,不代表它的 KV 占用最低,也不代表相关 EP 组的专家计算最轻。一个专家放置方案让计算更均衡,也可能增加跨节点通信。局部指标变好,最终 P99 未必变好。
因此,一个值得探索的研究方向是:综合请求长度、KV 压力、专家负载与通信状态,在端到端 SLO 约束下进行调度和放置。这是本文提出的优化方向,而不是已经验证的通用最优方案。评价时应比较满足 SLO 的有效服务能力,并计入迁移与重配置成本。
我会把这些手段归纳成三类:隔离减少相互干扰,均衡减少最慢参与方的拖累,重叠隐藏可以并行的等待。但选择手段之前,必须先找到真正的关键路径。
如果所谓的“长尾”只是少量请求生成特别多 token,长期占据 KV Cache 和调度槽位,那么重点应回到长度感知调度、公平性、抢占和迁移。它与 MoE 专家执行不均衡可以同时存在,却需要分别测量和处理。