loading

Loading

探索与发现

首页 AI 工程化

推理服务P99拆账:TTFT与ITL分开看才有的救

分类:AI 工程化
字数: (3285)
阅读: (3)
0
摘要:LLM 的 P99 不是监控伪影,是输出长度重尾分布加批处理放大的结构性问题。端到端 P99 至少要拆成 TTFT 和 ITL 两段,前者看上下文异质性与 KV 缓存,后者看预填充对解码的打扰。分块预填充、长度感知路由、准入控制和输出限额各自管一段方差,写下来的是取舍和数值。

P50发绿,P99是灾难

你的仪表盘显示 P50 延迟 800 毫秒,SLA 写的是两秒以内。一切正常。然后有人提交工单:它转了 30 秒才放弃。查日志一看,P99 是 28 秒。这个 35 倍的差距不是监控 bug,也不是可以配置超时字段消掉的异常值,它来自输出长度的重尾分布本身,以及推理系统的批处理把这种分布放大了一层。

我在处理这类问题上踩过的第一个坑,是把端到端延迟当单一指标看。只报一个 P99 数字,你会同时看到两种完全不同的故障:预填充阶段被长 context 撑爆,和解码阶段被预填充插队打断。两者混在一起的时候,改什么都像在试探。

TTFT 和 ITL 是两笔债

把时间拆成两段:

  • TTFT(首个 token 时间),对应预填充成本,大致随未缓存的 prompt token 数量线性增长。32000 token 的上下文,TTFT 可能比 320 token 的请求长 100 倍。
  • ITL 或 TPOT(token 间延迟),对应解码过程的稳定性。解码受显存带宽限制,单步算一个 token,正常时中位几十毫秒。

拆开之后诊断路径就很清楚了。如果 TTFT P99 高而且 ITL P99 正常,问题在上下文长度的异质性或者 KV 缓存被逐出;优先去想长度感知路由和对话感知的缓存策略。反过来,如果 TTFT 正常但 ITL P99 高,那基本是预填充打扰了正在解码的批处理,得去看调度器。

生产环境的测量里,vLLM 的一个内部负载把 P99 token 间延迟做到过 1.76 秒,而中位 ITL 只有几十毫秒。这里说的就是纯 ITL 的尾部,不是端到端。

重尾来自哪里:解码级的等待放大

解码批处理的行为是 GPU 并行推进所有请求,直到批内最长的那个结束。一个请求生了 4000 token,另外九个各 200 token,十个请求一起承担接近 4000 token 的解码成本。有研究统计过代码、摘要和问答任务上的输出长度分布,最大值与中位数的比值在 2 到 4 倍之间:代码任务上的 Llama 模型达到 3.27 倍,长文本任务到 4 倍。非流式场景下,这个比值直接落到 P99 和 P50 的端到端比值上,批处理再把它放大一次。

排队论能解释后半截:平均排队延迟和服务时间的二阶矩成正比,不是均值。几个失控的长输出请求就能把 E[S²] 推高,其他请求一起受罪。批处理也不方便用重排解决,因为生成过程中的抢占成本很高。

再叠加 agent 层面的卷积。一步调用里 3 倍的输出长度比值,五步链路走下来就变成 15 倍以上的端到端比值。这也是为什么 agent 工作流在报表上会突然把端到端 P99 拖得不像单步统计能解释的样子。

几种让 P50 变好、P99 更烂的常见操作

无脑加大 batch size

吞吐优先的直觉是扩大批。(怎么算都行,但先说清一件事:大约 20 到 30 个请求之后,> 具体阈值随 GPU 和模型大小变化),超过计算饱和拐点后再往批次里塞请求,延迟会非线性飙升。那条吞吐-延迟曲线左边是平的,右边是陡的。越过拐点用吞吐换 P99,很难是笔划算的交易。

标准重试打在慢请求上

传统索引后退避假设的是瞬时故障。LLM 响应慢不等于失败了,模型还在跑。对一个三秒没返回的请求做退避重试,等于在已经饱和的系统上再造一个在途重复请求,批次进一步滑过拐点,别人的 P99 就一起崩。

优先保 TTFT

交互式工作负载里这看起来很对,但优先给新请求预填充就会打断正在进行的解码批,制造出生成停顿的气泡,ITL 的尾部立刻抬头。每一次以抢占 decode 为代价换来的 TTFT 改善,本质上是在把延迟转移到已经在生成的那些请求身上。

纯 LRU 清 KV 缓存

LRU 假设访问模式统一,但对话历史让上下文长度高度不均。一段冷门但一直在进行的对话的 KV 缓存可能很快被逐出,等用户下一个请求进来只能做一次完整的重新预填充,TTFT 尖峰正好出现在他参与度最高的时候。有研究把对话感知缓存与按新近度加预期重用价值的策略做过对比,LRU 逐出模式抬高了 SLO 违规率。

降低方差的几个可直接上的手段

分块预填充

把长 prompt 按 token 预算切块,跟解码步交替执行。解码受显存带宽限制,计算资源本身有富余,所以插进去的预填充块不会明显降低解码吞吐,同时消除了长预填充一把抢走解码批的尖峰。测量数据显示,大模型上用分块预填充后 P99 token 间延迟从 1.08 秒降到 0.29 秒,约 3.7 倍。这项现在是主流推理框架的默认开关了,如果你还在手调,先确认它不是被关掉的。

长度感知路由

长短请求塞同一批会带来 padding 和 SM 间不对齐,相当于把长度同质批次的延迟开销额外抬高 10 到 110%。按预测的序列长度分桶路由,按不同的工作负载,P99 尾部延迟能降 25 到 69%,中位也接近的改善,总吞吐大致翻倍。长度预测不必做得太重,用输入特征跑个很轻的 XGBoost 分类器,输出长度的平均绝对误差大概 5% 就足够把请求送进正确的桶。

准入控制与配额的账

把活跃批次控制在饱和拐点以下、留出 20 到 30% 的富余容量,延迟能接近线性扩展。按富余容量做主动扩缩容,平均有效吞吐比反应式更高,因为根本不用进非线性区。对重尾的工作负载硬性限输出长度(比如 1600 token),可以把排队延迟减少 58%,同时有 70% 的请求完全不需要截断就能服务完。

投机解码看时机

小模型草稿加目标模型一轮验证,接受率 60 到 80% 时 ITL 能降 2 到 3 倍。但这里有个坑:收益在 GPU 利用率偏低的时候最明显,饱和状态下草稿模型会争抢计算资源,加速比就回去了。生产上有的部署报告过并合场景的性能数字(FP8 量化 H100 上的 Llama 3.1-70B 有 2.31 倍速度、3.6 倍吞吐的提升),但这建立在负载留有余量的前提下。所以要结合 GPU 利用率判断,50% 以下再谈,用过了就是白忙。

另外不要对比数据只看加速比。投机解码不改善 TTFT 的尾部,它明显的是解码这一段。TTFT P99 高的时候上这东西只是白忙。

对冲请求的边界

provider 层面的尾部可以用对冲来兜。设个延迟阈值(比如 P50 500 毫秒的端点放到 2 秒),过了阈值并行发第二家,谁先回用谁的。P50 两倍的阈值下重复请求的成本大致十分之一,如果慢的原因在那一侧(真的),>那个 provider 响应变慢,P99 的改善会非常明显。但要小心,对冲和原来的重试不要在代码地里互相叠成一层层的乘法放大。对于同样性质的慢,保留一个机制就行。

监控上该记什么

直方图分箱要把可能范围铺满:50ms、100ms、200ms、500ms、1s、2s、5s、10s、30s。如果你的标准监控直方图只读到 2 到 5 秒,那么所有长尾都会挤在溢出桶里,尾部的形状就完全看不到了。

几个值得单独记录的点:P50、P95、P99 的 TTFT(聊天场景里 P95 TTFT 超过 1 到 2 秒就应当鸣);P99 的 ITL(交互场景超过 500ms 就该查解码停顿);有效吞吐(同时满足 TTFT 和 ITL SLO 的请求占比,这是比任何单一百分位的端到端延迟都更好的 SLA 指标);KV 缓存利用率(领先信号,冲到 90% 以上往往预告 TTFT 会抬头);P95 队列深度(批批饱和的预警)。

平均值最好不要拿来做主要信号。均值受尾部牵动很大,还不容易对应到具体的用户。分位和分位之间本来就是独立序列,P50、P95、P99 分开看已经够了。

两条最短的排障起手

  1. 先拆 TTFT 和 ITL 的 P99。TTFT 坏就去查上下文长度分布与 KV 缓存命中,ITL 坏就查预填充对解码的打扰。
  2. 低头看输出长度分布。如果 P99 输出长度超过 P50 的 3 倍,说明尾巴的源头不在基础设施上,重尾本身才是。这种情况下限额或长度路由带来的收益比任何别的小打小闹都大。

转载请注明出处: 皮哈皮阿

本文的链接地址: https://blog.phpat.com/ai-engineering/666.html

手机扫码阅读

微信或手机浏览器扫一扫,随时随地随心阅读与分享

您可能对以下文章感兴趣
1 / 1
预览图