线上推理服务里最难受的一类请求,不是大 batch,也不是高 QPS,而是一个两万 token 的 prefill 排在队列前面不动。后面的短请求 TTFT 从 300ms 涨到两秒,GPU 利用率看着很漂亮,SLA 却在报警。chunked prefill 就是为这种局面设计的:把长 prefill 切成固定大小的块,一个 decode step 往前推一块,短请求的 prefill 能插进来。
但真正上线之后,问题会变成三个:块切多细、抢占谁、以及整条被切碎的长请求怎么收尾。
chunked prefill 解决的不是排队,是干扰
看 vLLM 优化文档里关于 chunked prefill 的描述,它最大的对手是连续的 decode。一个满长度 prompt 的 prefill 会把 GPU 长时间占在算子绑定状态下,正在 decode 的请求两个 token 之间的间隔(ITL)立刻被拉开。
网上常见的表述是:continuous batching 让新请求能在每个 decode step 动态加入、完成的移除;chunked prefill 给 prefill 也加上分时。
这两件事是互补的。continuous batching 管"什么时候能进 batch",chunked prefill 管"一次能进来多少"。很多团队只上连续批处理,最后发现吞吐很稳、长 prompt 一进来还是全员卡顿。原因不在调度器要不要收人,而在这一轮的时间片分配。
块大小不是性能旋钮,是 SLA 旋钮
切得粗,单次 prefill 块大,prefill 本身算得迅速,但每块时长变长,decode 请求的 ITL 抖动随之变大。切得细,插队机会多,反过来又增加单步调度开销和解码过程被打断的次数。项目里常见做法是先把块大小设为和 max_num_batched_tokens 相关的一个参数,再对着真实流量调。
这里有一条可以先算出来的线。假设一个请求的 inter-token 延迟目标是 50ms,正常 decode 每步花 10ms,那么留给新进来的 prefill 每一步的预算只有 40ms 上下,块大小就应该压在"单卡这 40ms 能算多少 token"以内。用固定块大小直接按指令周期估算,再到线上做压测,比只看吞吐曲线有意义。
TetriInfer 那套实验也在说同一件事:它在 prefill 调度里加了 PrefillSchedBatch,用一个预排序批次变量来控制一次调度多少。整队列二十个请求,如果批次设为 10,就分两次调度,每次内部排序。目的是防止过度排序与长期饥饿。切成块之后的 prefill 本质是分时复用,需要配一个能控制每次调度宽度的闸门。
抢占的代价主要是 KV,不是算力
长请求被反复打断,最贵的成本不是重算的那部分 prefill token(现在有前缀缓存重用),而是一串带着状态的请求在等重排队:它们的 KV 块一直占着显存,又不产生输出。抢占策略要看的指标应该是"这段 KV 已经停了多久还没有新 token",而不是请求本身有多长。
FastServe 的 MLFQ Skip-Join 给出了一个思路:多级反馈队列,长时运行的请求降优先级,并支持抢占。但这种抢占用在 decode 侧时就要小心,前面文章的结论仍然成立:光靠优先级让长请求在等待中饿死,并不是。工程上就要有"逃逸阀":抢占超过 N 次或被降级到最后一档之后,长请求要么升回优先级,要么按预测剩余长度临时标记为不可抢占。
另一类实现更锋利。SCLS(slice-level scheduling)把每段最大生成长度分解成固定长度的片段,顺序处理后组合。它的好处是片段边界清晰,服务时间和内存都以片段为单位预估;代价是长回答被强制按片段结束,分段之间无法共享完整的 KV。拿它适用于哪些交互,心中要有数:一轮对话一次输出的场景更容易出问题。
收尾比切片更容易翻车
前面那条前排长请求整切片跑完 prefill 之后进入 decode,然后往往撞上另一个问题:没有合理停止条件。vLLM 论坛记录遇到过一类真实故障:请求持续一小时以上,模型一直输出重复图案。讨论的结论是 stop token、max_tokens 拦截缺失或无终止条件时模型进入重复循环,vLLM 的服务端本身不会自动识别"这内容明显异常,我要切断",主要依赖调用方设置参数,或在 API 层监控流。
切片的引入使这个问题变得更坏。块切得越细,单个切片里观测到的"已生成 token 数"和"已消耗时间"越难和最大长度的逻辑上限对齐。再加上一些调度参数以 token 或时间片为单位,可能让"完成但没退出"状态持续时间拉长。调度侧不能把无限排队当成稳态。
工程上,我偏向给每个被切的请求挂三条规定外挂:一类是单请求 token 上限、输出侧循环检测阈值、从进入候选到排空的墙钟超时。第二,抢占不超过限定次数,超过之后要进显式的慢池。第三,流式流回来的时候靠网关做模式断流,真的不行还要有流式断连接压底。
切与抢的顺序
最后给出一个粒度上的操作顺序,按这个顺序排过的方案线上不太会翻车。
- 先量化目标 ITL 每步预算,按预算定块大小,不把块大小当吞吐旋钮。
- 切片只在块粒度,避免同一请求的已缓存前缀和新的块前缀重复计算,要确保 cache 复用真实命中。
- 为防饥饿设重排队宽限:等待次数、被迫切换次数单独记账。
- 超时和循环检测做在流外侧,越接近模型越不灵活,越容易写出一个让人不敢看的参数量。
做完这一轮,通常会看到一个不太好看的结果:平均延迟改善明显,p99 基本没动。别急着回滚——长请求本来就该用尾巴来表示,翻车前必须先能看见才谈得上优化。
转载请注明出处: 皮哈皮阿
本文的链接地址: https://blog.phpat.com/ai-engineering/682.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订阅器发布