loading

Loading

探索与发现

首页 AI 工程化

TTFT 方差来源:排队、预填充和缓存命中

分类:AI 工程化
字数: (2198)
阅读: (6)
0
摘要:TTFT 的 P99 不是单一原因造成的。把排队、预填充、缓存命中三段延迟分开统计方差份额,才知道该扩容量、该限流还是该冻结前缀结构。

把 TTFT 当成一个数字来看,几乎没什么用。P50 到了 300ms,P99 是 2.4s,这中间差的 2 秒不是模型变慢了,而是几件互不相关的事混在一起:请求在队列里躺了多久、预填充算了多少个 token、前缀缓存有没有命中、KV Cache 是从显存里读还是从远端拉。只盯一个均值,调优基本靠猜。

我的做法是把 TTFT 拆成加法式的账本,而非追求拟合:“排队 + 缓存加载 + 预填充计算 + 首个 token 解码 + 网络与网关”,然后逐项统计方差份额。拆开之后,改动哪个组件能回收多少毫秒,才有可算的地方。

先承认端到端公式是理想的

常见写法是 end-to-end ≈ TTFT + TPOT × (输出 token 数 − 1)。这个式子用作记账没问题,但它隐含了几个假设:TPOT 稳定、网络不抖、排队只发生在开头。线上并不成立。decode 阶段的每个 token 都要重读权重和 KV Cache([1]),显存带宽一旦被别的并发抢走,TPOT 自己就会漂。所以拆 TTFT 方差时,第一步要固定输出长度分桶,否则同一组数字里混着短问答和长文生成,比出来的东西没有意义。

预填充和排队,方差形态完全不同

预填充是计算密集型([1]),输入 token 可以并行算,耗时大致随输入长度增长,方差也呈现为稳定的、平的分布:一个 8k 输入的请求,它就是比 800 token 慢,这属于确定性成本,可预测,可预算。真正制造长尾的是排队。

排队时间在低并发下几乎为零,一到峰值会突然变成主导项。更糟的是它和预填充共享同一批 GPU:chunked prefill、PD 分离这些方案都是在解决 prefill 抢计算、decode 抢带宽的竞争问题([3])。一旦 admit 策略偏保守,长预填充档位被推迟,P99 就一路往上升,而 P50 看起来一切正常。所以我在监控里把“排队时长”和“预填充时长”分成两条曲线按 P95/P99 看,长尾第一眼就能定位到是调度还是计算。

缓存命中率是需求离散性的重要来源

前缀缓存能把预填充省掉一大截,但它天然带有业务特征。按 [1] 的说法,系统提示词频繁变化会降低命中率;如果把语气、时间戳、用户 ID 拼进系统提示词,等于每个请求都是冷缓存。这类 bug 在压测里看不出来:压测流量通常是同一个 prompt 打一万遍,命中率好看得不像真的,上线后被真实用户打散,TTFT 直接上台阶。

做法是记录每个请求的“缓存命中 token 数”,把它和 TTFT 放在一张散点图里。这条曲线越陡,说明预填充在 TTFT 中的占比越大,越值得保障前缀稳定;如果曲线很平,说明主要时间在别处,花力气做缓存只是白费。我的经验是 KEEP 住一小段稳定前缀就够,别为了多命中几百 token 去重排整个 prompt 结构,代价往往是生成质量下降。

给各项做方差分解,别做回归拟合

不用复杂的模型。每次请求记:入队时间戳、开始预填充时间戳、首 token 出队时间戳、输入 token 数、缓存命中 token 数、batch 大小、输出 token 数。然后用简单分组统计:按输入长度分桶看预填充的 P50 与 P99 差多大;按并发水位分桶看排队占 TTFT 的比例;比较命中与未命中两组的 TTFT 差值中位数。

bucket = 输入长度分桶
排队的方差 ≈ 各桶内 (admit_ts - arrive_ts) 的 P99 - P50 之和
预填充的方差 ≈ 各桶内 (first_token_ts - admit_ts) 的 P99 - P50 之和
缓存的贡献 ≈ 命中组与未命中组的首 token 时延中位差

这三项加起来能解释多少总方差,本身就是个质量指标。如果只能解释 40%,剩下的额度不去追查,因为线上加埋点本身也有开销,追到那一层往往不值得:比如一小部分来自网关重试、一部分来自代理冷连接,锁定的成本比收益高。

常用的几个优化动作和它们的上限

  • 入口限流按预填充预算来划:不只限 QPS。长 prompt 天然消耗更多前提算力,用 token/s 或者“排队预填充 token 总量”做门槛更贴近实际。
  • 分桶超时:长输入档位的排队等待超过阈值就直接拒绝、复用已有预热实例,把队列深度压住,保证短任务 P99。
  • 前缀结构冻结:把系统提示词与模板版本分开,改动走灰度;一改,重新压测并重新测算方差分解,不沿用旧结论。
  • 换模型或换平台重测:口径必须统一,注明计时是从客户端还是服务端开始、是否包含网络传输([1]),不统口径的对比数字没法做决策。

还有一个容易踩的坑:压测工具用错。通用压测如 Locust 或者 K6 主要测容量和整体一致性,衡量 token 级别的吞吐和延迟需要用 NVIDIA GenAI-Perf 或者各家推理引擎自带的脚本([2]),否则连最先一个 token 的时间都测不准。分布时刻也只是观测手段,告诉你是哪段在波动;具体的阈值得看你的业务场景,实时语音对首字极敏感,离线批量任务则在意吞吐([2])。这一点应先定下来,再去拆分。

把 TTFT 的方差写成账本,并不是为了多找几个数字,而是让“这次 P99 恶化是哪一段在起作用”变成一次调用就能回答的事。做不到这一步,扩容和调参都很难落到实处。

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

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

手机扫码阅读

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

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