连续批处理(Continuous Batching)在推理服务里的地位已经没有争议了。Orca(OSDI 2022)提出迭代级调度之后,vLLM、LightLLM、TGI 基本都沿着这条路走。博客和论文里常见的说法是 GPU 利用率从 40% 提到 85%,吞吐量 2-4 倍。这些数字大体成立,但如果你是把 vLLM 直接部署上去看面板的工程师,会撞上另一半事实:吞吐确实涨了,尾延迟也涨了,而且抖得比静态批处理更没规律。
这篇文章只谈调度层面的那个矛盾:批得越满,单个请求的 TBT(token 间隔)越不可控。
先复习一遍它在换什么
LLM 推理分两段。Prefill 把整条 prompt 并行算一遍,计算密集;Decode 每次只出一个 token,内存带宽瓶颈,OP 密度很低。调用方看的两个指标:TTFT(首 token 延迟)主要由 prefill 长度和排队决定,TBT 决定「流畅感」,基本由 decode 阶段每个 iteration 的耗时决定。
静态批处理的问题是队头阻塞。一批请求要等最长的那个跑完才释放,短请求早就结束,但在显存和计算里陪着长请求继续占位。vLLM 那篇 PagedAttention 论文里说得更直接:传统做法为每个请求预分配 max_length 的 KV Cache,浪费能到 60%-80%。
连续批处理做两件事:把调度粒度从「请求级」降到「迭代级」,每个 decode step 结束都检查哪些请求出了 EOS,释放显存,空出的槽位给新到达的请求;同时用分页的 KV Cache 管理按需分配。合起来的效果是 batch 里的槽位几乎不留空。
iteration 0: [A, B, C, D] iteration 2: [A, B, C] # D 完成,释放 iteration 3: [A, B, C, E] # E 加入 iteration 4: [A, B, C, E, F] # F 加入
这一步本身没有引入额外计算,只是改变了「哪个请求在这一轮里」。但槽位填满了,单个 iteration 的工作量就变大了。
吞吐涨的机制,和延迟抖的机制是同一个
Decode 阶段的内存带宽瓶颈意味着,同一 iteration 里多塞一个请求,摊薄了模型权重的加载成本,总吞吐上升。但这一轮整体耗时也变长了,代价由这一轮里所有请求共同承担,而调度器不知道谁比较急。
更麻烦的是长 prefill 插进来的时候。你正在服务十个活跃的 decode 请求,这时来一个 1500 token 的 prompt。如果调度器让它整段做完,这十个请求会在几个毫秒到几十毫秒内完全停住,TBT 直接尖峰,用户看到的就是回答卡了一下。文献里把这个现象叫 Prefill Stall 或者 prefill 饥饿。
两种缓解手段,思路相反:
- Chunked Prefill:把长 prefill 切成固定大小(比如 512 token)的块,块之间穿插 decode 请求。Sarathi-Serve 走的是这条路,它把 chunked prefill 和 decode 混在同一批里调度,通过限制每批的 token 预算来平衡两边的延迟。
- Prefill-Decode 分离:DistServe、Mooncake 这类工作把两个阶段拆到不同实例上,decode 实例永远不会被长 prefill 打断。代价是多一跳 KV Cache 传输,以及两套实例的容量要分别规划。
LightLLM 也是把长 prompt 切块、让短请求精确填充计算空隙的做法。这些实现看起来是细节,实际决定了你的 TBT 分位数曲线长得像不像一把梳子。
线上参数该怎么看
几个经常被当成「开就完了」的开关,实际都有价格:
- 每批 token 预算:这是最直接的旋钮。调大,吞吐上去了,TBT 的 p99 也上去。不是单调的好。
- chunk size:切太小,prefill 被过度切碎,首 token 延迟拉长;切太大,decode 的穿插粒度又不够细。通常几百 token 级,需要一个能压真实流量回放的压测。
- 最大并发序列数:受 KV Cache 分页的实际占用限制,不是纯参数。PagedAttention 让内存利用率从约 40% 提到 90% 以上,但 page 内部的碎片还在,长请求和短请求混在一起时,可用页的碎片情况会漂。
- 等待队列的空闲槽是否立刻填:填得越积极,队列时间越短,但已经跑着的请求被稀释得越厉害。vLLM 的调度器会在「有等待序列且有可用显存」时持续加入,这在请求率突增时会让在跑的请求集体降速。
一个经常被忽略的现象:压测用固定长度输入输出,所有指标都好看;切到线上真实流量,长短不一、到达时间有突发,吞吐率往往只有压测的六到七成,p99 TBT 却能翻倍。根因不出在引擎的代码质量,出在你的 SLO 是按平均延迟定的,而调度器是在优化批填充率。
几个实际的判断
第一,如果你对 TBT 的 p99 有硬约束,就不该只看每秒 request 数来衡量调参是否成功。Goodput 这个提法在这类服务里比 Throughput 更有意义,也就是满足 SLO 的有效吞吐。很多情况下,把每批 token 预算调低一档,request 数掉 10%,p99 掉一半,是划算的。
第二,Chunked Prefill 不是无成本的开关。它把一次 prefill 在前向里拆成多次,同一批 token 数量下,kernel 启动和调度的开销会上去,在短 prompt 占多数的工作负载里未必划算。这个要按自己流量的 prompt 长度分布算,不能照搬别人的配置。
第三,Prefill-Decode 分离适合的场景和你的实际往往不重合。它适合 prefill 与 decode 各自配比极不均衡的 workload,也就是有人愿意为两套集群、多一跳 KV Cache 传输买单,换更稳的 decode 尾延迟。中小规模的部署,先把 chunked prefill 调对,收益已经拿到大半。
第四,监控上至少要看三层:每批的实际 token 数、迭代耗时的分布、以及被调度器推迟加入队列的次数。只看 GPU 利用率和总的 request 量,会把噪声当正常。
连续批处理带来的红利是真的重。Orca 在 Azure 线上负载上测得的 2.3-4.1 倍吞吐提升,那是把成批的存量浪费收回来。在这个基础上继续榨,空间来自更精细的预算和更清楚的优先级,而这需要你先承认一件事:调度器并不知道哪个请求的用户正在等,它只会按照你写进去的规则填槽。
转载请注明出处: 皮哈皮阿
本文的链接地址: https://blog.phpat.com/ai-engineering/650.html
微信或手机浏览器扫一扫,随时随地随心阅读与分享
-
推理成本下降之后:模型路由、KV Cache 与端侧推理
上个月一个团队拿着账单来找我:模型单价降了,费用却涨了三倍。看完调用链就明白了,问题不在单价,在用法。单位 token 便宜,不等于每次任务便宜。
2026/10/10
-
2026 年 AI 应用评测:从单轮打分到多轮轨迹评估
一个 Agent 的评测分数从 72% 涨到 81%,团队很高兴,直到发现它学会了在失败时直接结束任务,把“没做完”算成了“无需回答”。评测设计错了,指标越好看越危险。
2026/10/19
-
动态拼批不是开个开关:线上拼批的取舍与门禁
在线服务里的动态拼批和离线批量推理是两码事。窗口时长、KV Cache 水位、批内参数对齐共同决定拼接能否成立。上线前该盯的是 p99 和显存峰值,而不只是吞吐。
2026/10/25
-
长度惩罚参数调优:常见参数名字容易混淆
长度惩罚在主流 API 里并不存在。真正管长度的是 max_tokens 和 stop sequences,frequency、presence、repetition 惩罚管的是用词重复和新词探索。把这些旋钮分开,能避免输出被截断、术语被替换成同义词两类故障。
2026/10/23
-
推理模型的流式输出: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订阅器发布