把 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
微信或手机浏览器扫一扫,随时随地随心阅读与分享
-
推理成本下降之后:模型路由、KV Cache 与端侧推理
上个月一个团队拿着账单来找我:模型单价降了,费用却涨了三倍。看完调用链就明白了,问题不在单价,在用法。单位 token 便宜,不等于每次任务便宜。
2026/10/10
-
2026 年 AI 应用评测:从单轮打分到多轮轨迹评估
一个 Agent 的评测分数从 72% 涨到 81%,团队很高兴,直到发现它学会了在失败时直接结束任务,把“没做完”算成了“无需回答”。评测设计错了,指标越好看越危险。
2026/10/19
-
长度惩罚参数调优:常见参数名字容易混淆
长度惩罚在主流 API 里并不存在。真正管长度的是 max_tokens 和 stop sequences,frequency、presence、repetition 惩罚管的是用词重复和新词探索。把这些旋钮分开,能避免输出被截断、术语被替换成同义词两类故障。
2026/10/23
-
动态拼批不是开个开关:线上拼批的取舍与门禁
在线服务里的动态拼批和离线批量推理是两码事。窗口时长、KV Cache 水位、批内参数对齐共同决定拼接能否成立。上线前该盯的是 p99 和显存峰值,而不只是吞吐。
2026/10/25
-
推理模型的流式输出:thinking 块怎么流
推理模型的流式输出不只是逐字渲染:思考块、正文、工具调用参数混杂在 SSE 流里,客户端要按字段分流、独立缓冲、支持中途中断。记录接通义千问与智谱过程中的判断和踩坑。
2026/10/24
-
推理服务排队延迟的分层观测:从请求数到Token账本
盯 QPS 和 P99 看不到排队延迟长在哪。把这件事拆成入口→上游准入队列→后端等待→batch 解码四层,用 vLLM 暴露的排队与 KV 缓存指标做层级归因,讨论分层之后仍然看不见的部分与取舍。
2026/10/27
京公网安备11011402053654 
@muffin:链接已更新,分类图像功能...
AI网址导航模版图标库更换指南
作者大大,图标库邀请链接失效了,麻烦更新...
AI网址导航模版图标库更换指南
太好看了,快点更新!
友情链接
@戴墨镜的刘备:哈喽,路过点赞。
vue.js组件之手写签名板vue-signature-simple
太好看了,快点更新!太棒了!👍真的很赞!
vue.js组件之手写签名板vue-signature-simple
你写得非常清晰明了,让我很容易理解你的观...
Emlog插件RSS订阅器发布