跳到主要内容

LLM 尾部延迟:为什么在 P50 表现良好时你的 P99 却是一场灾难

· 阅读需 12 分钟
Tian Pan
Software Engineer

你的 LLM API 返回的 P50(中位数)延迟为 800 毫秒。你的仪表板显示为绿色。你的 SLA 规定“两秒以内”。接着,一个用户提交了工单:“它转了 30 秒然后就放弃了。”你检查日志,发现 P99 延迟高达 28 秒。

这种差距——中位数与尾部延迟之间 35 倍的比率——并非偶然。这是 LLM 工作原理的结构性属性,仅仅通过调整超时时间是无法消除的。

核心问题在于,LLM 违反了你从传统软件继承的几乎所有 API 监控实践背后的基本假设:即响应时间大致是输入规模的函数。对于数据库查询、REST 端点或搜索索引调用,负载相似的输入会产生在可预测范围内的输出。而对于 LLM,输出长度是一个具有重尾分布(heavy-tailed distribution)的随机变量,且延迟与该随机变量成线性缩放。每一个用于推理“典型”API 延迟的工具都建立在一个根本站不住脚的假设之上。

为什么 LLM 延迟在结构上呈现重尾分布

在传统的解码批处理(decode batch)中,GPU 会并行处理所有请求,直到达到批处理成员中的最大输出长度。批处理中的其他所有请求都要等待最长的那个请求结束。如果一个请求生成了 4,000 个 token,而另外九个各生成 200 个,那么所有十个请求都要承担接近 4,000 个 token 的解码成本。

在衡量代码编写、摘要和问答任务的输出长度分布的研究中发现,最大值与中位数的比率在 2 到 4 倍之间:一个在代码任务上的 Llama 模型产生的输出长度可达其中位数长度的 3.27 倍;在长文本任务中,这一比率达到 4 倍。在非流式设置中,这一比率直接转化为 P99/P50 端到端延迟比率——而且批处理放大效应使其比单请求比率所暗示的情况还要糟糕。

排队论形式化地解释了为什么这如此具有破坏性。在服务时间具有高方差的系统中,平均排队延迟与服务时间的二阶矩(second moment)成正比,而不是均值。少数失控的长输出请求会急剧推高 E[S²],导致系统中其他所有请求的排队延迟增加。批处理无法通过重新排序来解决这个问题,因为生成过程中的抢占(preemption)成本非常高。

还有三个额外因素加剧了这种基础分布效应:

上下文长度的异质性。 首字延迟(TTFT)随未缓存的 prompt token 数量大致呈线性增长。一个具有 32,000 token 上下文的请求,其 TTFT 可能比一个 320 token 上下文的请求长 100 倍。在实际的生产负载中,即便应用程序本身看起来是统一的,对话历史也会导致上下文长度呈现重尾分布。

多步思维链(CoT)和智能体(agentic)调用。 扩展推理增加了每一步的输出 token 数。一个包含五个连续 LLM 调用的智能体工作流,会通过卷积效应复合每一步的重尾效应:端到端的尾部增长速度比任何单步的尾部都要快。单次调用中看起来 3 倍的输出长度比率,在五步链条中会演变成 15 倍以上的端到端延迟比率。

Prefill(预填充)与 Decode(解码)阶段的干扰。 Prefill(处理输入 token)属于计算密集型,可能使 GPU 饱和。Decode(每步生成一个 token)属于显存带宽密集型。当 decode 正在进行时,若有一个长 prefill 到达,它会抢占正在进行的批处理。对生产系统的测量显示,幼稚的混合批处理会导致 token 间延迟(inter-token latency)比纯 decode 批处理增加高达 28 倍。vLLM 的 P99 token 间延迟在内部负载中达到了 1.76 秒——而典型的中值 token 间延迟仅为几十毫秒。

那些修复了 P50 却破坏了 P99 的技术

面对延迟问题,自然的反应是求助于标准的基础设施策略。但对于 LLM,几个标准的做法反而会让尾部延迟变得更糟,而不是更好。

增加批次大小(Batch Size) 是提高吞吐量的第一直觉,而且它确实能在一定程度上提高吞吐量。一旦超过计算饱和的临界点(根据 GPU 和模型大小,大约为 20–30 个请求),在批处理中添加更多请求会导致延迟呈非线性飙升。吞吐量-延迟曲线呈“曲棍球棒”形状:左侧平坦,右侧垂直。在超过该临界点后为了吞吐量进行批处理,本质上是在用 P99 换取总效率。

幼稚的重试策略是为瞬时故障设计的。LLM 响应慢并不是故障——模型仍在生成中。如果你对一个在三秒内没有响应的请求应用标准的指数退避,你就在一个已经饱和的系统上创建了一个重复的在途请求,使批次大小进一步越过曲棍球棒曲线的临界点,并恶化其他所有人的 P99。

标准 LRU 缓存对于统一的工作负载是最佳的,但在对话历史产生异质上下文长度时会失效。LRU 可能会在一个长期运行的对话正需要它之前,将其 KV 缓存驱逐,从而迫使进行完整的重新预填充(re-prefill),并在受影响用户参与度最高的时候导致延迟激增。关于对话感知缓存的研究表明,与考虑新近度和预期重用价值的策略相比,LRU 驱逐模式会恶化 SLO 违规率。

优先考虑 TTFT 而非吞吐量——这在交互式工作负载中看起来是正确的——会导致解码停顿(decode stalls)。急于对新请求进行 prefill 的调度器会中断正在进行的 decode 批处理,产生生成停顿气泡(stall bubbles),从而导致 P99 token 间延迟飙升。通过抢占 decode 实现的每一次 TTFT 改进,本质上都是将延迟转移给已经进行的请求的 token 间分布的尾部。

真正降低 P99 的方法

有效的方法都有一个共同的结构:它们降低的是服务时间的方差,而不仅仅是均值。

分块预填充 (Chunked prefill) 将长提示词分为 token 预算大小的分块,并将它们与解码(decode)步骤交替执行。由于解码受内存带宽限制且计算资源利用不足,因此可以协同调度预填充块而不降低解码吞吐量。这消除了长预填充抢占正在进行的解码批处理所造成的峰值。生产环境的测量显示,在大模型上使用分块预填充后,P99 token 间延迟从 1.08 秒下降到 0.29 秒——在不改变模型质量的情况下实现了约 3.7 倍的提升。分块预填充现在是主要推理框架中的默认配置。

长度感知请求路由 (Length-aware request routing) 将短请求和长请求分发到不同的 GPU 实例或批次中。在单个批次中混合长短序列会因为填充(padding)和 SM 间不平衡导致 GPU 内核效率低下,相对于长度同质的批次,会增加 10–110% 的延迟开销。根据预测的序列长度范围对请求进行分组路由,可以将 P99 尾部延迟降低 25–69%(取决于工作负载),中位数延迟也有类似的改善,且总吞吐量大致翻倍。

长度预测路由可以通过对输入特征运行轻量级 XGBoost 分类器来实现,其输出长度预测的平均绝对误差约为 5%——这足以将请求路由到正确的长度桶,而无需昂贵的逐请求分析。

投机解码 (Speculative decoding) 使用一个小型、快速的草稿模型投机性地提出 5–8 个 token,然后由大型目标模型在单次前向传播中验证所有候选 token。当接受率达到 60–80% 时,这可以将 token 间延迟降低 2–3 倍。这种收益在 GPU 利用率较低时最为显著;在饱和状态下,草稿模型会争夺计算资源,收益随之下降。生产部署报告显示,在采用 FP8 量化的 H100 上,Llama 3.1-70B 的速度提升了 2.31 倍,吞吐量提升了 3.6 倍。

流式传输作为感官延迟的修复方案 从数学上讲并不能降低尾部延迟,但它改变了用户体验。在生成 token 时立即发送,将问题从“等待 28 秒获取响应”转变为“等待 1–2 秒获取首个 token,然后以生成速度阅读”。对于交互式工作负载,首个 token 时间 (TTFT) 低于三秒,且 token 间延迟低于 100ms(每秒 3 到 10 个 token,匹配阅读速度)是关键目标。如果首个 token 能够快速送达,总生成时间的尾部延迟就变得不那么明显了。

准入控制 (Admission control) 从源头上防止“曲棍球杆式”性能退化。将活动批处理大小保持在计算饱和阈值以下——保留 20–30% 的空余容量——可以维持近乎线性的延迟扩展。基于空余容量的主动扩缩容比反应式自动扩缩容能实现更高的有效吞吐量,正是因为它完全避开了非线性区域。输出长度限制(例如,对重尾分布的工作负载设置 1,600 token 的硬限制)可以将排队延迟减少 58%,同时在不截断的情况下完全服务 70% 的请求。

对冲请求 (Hedged requests) 解决提供商层面的尾部延迟。如果后端在延迟阈值内(例如,对于 P50 为 500ms 的端点,设置为 2 秒)没有响应,则向第二个提供商发送相同的请求,并返回最先响应的结果。在 P50 两倍的阈值下,重复请求的成本约为十分之一;当延迟源自提供商响应变慢时,P99 的改善非常显著。

衡量 LLM 工作负载的 P99

最重要的变化是将延迟分解为两个阶段,并分别跟踪每个阶段的百分位数。

TTFT(首个 token 时间)捕捉预填充成本和感官响应速度。Token 间延迟 (ITL) 或每个输出 token 的时间 (TPOT) 捕捉解码的稳定性。一个应用可能拥有极佳的 TTFT,但 P99 ITL 却非常糟糕——反之亦然。只报告单一的端到端延迟数值会掩盖这两种故障模式。

在生产环境中至关重要的指标:

  • P50、P95 和 P99 的 TTFT —— 对于聊天机器人工作负载,如果 P95 TTFT 超过 1 到 2 秒,则发出警报
  • P99 的 ITL —— 对于交互式工作负载,如果超过 500ms,则发出警报(这表示解码出现停顿)
  • 有效吞吐量 (Goodput) —— 同时满足 TTFT 和 ITL SLO 的请求比例;这比任何单一百分位数的端到端延迟都是更好的核心 SLA 指标
  • KV 缓存利用率 —— 领先指标;利用率激增至 90% 以上预示着随着前缀缓存未命中增加,TTFT 即将出现峰值
  • P95 的请求队列深度 —— 在达到“曲棍球杆”拐点之前,批次饱和的领先指标

不要将平均延迟作为主要信号。均值受长尾影响极大,在不代表典型用户体验的情况下也可能剧烈波动。应将 P50(典型用户)、P95(运气较差的用户)和 P99(接近最差情况)作为独立的序列进行跟踪。

直方图分箱 (Histogram buckets) 需要覆盖完整的预期范围:50ms、100ms、200ms、500ms、1s、2s、5s、10s、30s。如果标准的 API 监控直方图止于 2–5 秒,则会将每个长尾请求记录为溢出,导致尾部分布的形态变得不可见。

诊断序列

当 P99 远差于 P50 时,请按照以下序列进行排查:

  1. 将 TTFT 与 ITL 分开。 如果 TTFT P99 很高但 ITL P99 正常,问题在于上下文长度的差异或 KV 缓存逐出 —— 请优先考虑长度感知路由(length-aware routing)和对话感知缓存(conversation-aware caching)。如果 ITL P99 很高但 TTFT 正常,问题在于预填充干扰导致的解码停顿 —— 请优先考虑分块预填充(chunked prefill)。

  2. 检查批处理利用率。 如果 GPU 利用率处于或高于饱和拐点(saturation knee),解决方案是准入控制(admission control)。如果低于 50%,投机采样(speculative decoding)有空间在不竞争计算资源的情况下提高 ITL。

  3. 分析输出长度分布。 如果你的 P99 输出长度超过 P50 的 3 倍,说明你面临根本性的重尾分布(heavy-tail distribution)问题。设定长度上限或采用长度感知路由,比任何其他单一改动都更有效。

  4. 审计重试逻辑。 如果你的重试策略是在响应缓慢时触发,而不是在失败时触发,请禁用它或将其替换为对冲请求(hedging)。重试缓慢的 LLM 请求是将延迟问题转化为吞吐量崩溃最快的方法。

P50 和 P99 之间 35 倍的差距并不是监控伪影,也不是可以过滤掉的异常值。它是分布的形态,并且会在你下一次事故中再次出现,直到你的基础设施被明确设计为管理波动,而不仅仅是平均吞吐量。

References:Let's stay in touch and Follow me for more thoughts and updates