
批处理为什么重要:GPU 是个饿汉
推理服务对外怎么做到又快又稳(SLA 的口径与工程)「推理服务 SLA 工程」一篇讲过;本篇钻进引擎内部,讲吞吐的分水岭技术——批处理调度。要理解批处理,先要看清 LLM 解码的一个反直觉事实:生成单个 token 是一次极小的计算搬动一大份权重。解码每一步都要把模型的全部权重从显存过一遍,而这一步的算术量相对很小——算力大量闲置,显存带宽才是瓶颈。单条请求慢慢吐字时,GPU 的绝大部分算力在围观。
给个直觉的数量感(演示口径):单请求解码时,算力单元的有效占用常在个位数百分比;批到几十路并发时,同样的权重搬运服务几十条请求,吞吐轻松翻上数倍——具体倍数取决于模型大小与批的构成,但方向从不例外。这也是推理引擎几乎所有调度创新的出发点。
把生成过程拆成两个阶段看,批处理的意义会更清楚:prefill(读入整段提示、一次性填好 KV Cache)是大矩阵乘的算力密集阶段,本来就能把算力吃满;decode(逐 token 生成)才是那个每步搬全部权重的带宽饥渴阶段。批处理主要救的是 decode——把几十条请求的解码步捆在一起,把权重搬运的固定成本摊薄。推理吞吐优化的火力因此几乎都集中在解码调度上。
批处理是把这个浪费变现的钥匙:把几十条请求捆在一起跑一步,权重只需搬运一次,算力同时服务几十条——每条请求摊到的权重搬运成本骤降,吞吐成倍涨。自回归生成的特点(每条请求要走几十到几百步、逐 token 吐出)决定了批处理不能像传统深度学习那样一次前向定输赢,而是一场跨越许多步的动态组队——怎么组队,就是静态与 continuous 两代方案的分野。
还要回答一个前置疑问:为什么不干脆让请求排队、一张卡一次只跑一条?串行看似简单,实则最差——每条请求独占整卡的权重搬运带宽,吞吐就是单请求速率本身;而且队尾的等待随队列长度线性叠加,第十条请求要等前九条全部生成完,长队之下的尾延迟没有下限。批处理的本质,是在「串行独占」与「并行混跑」之间找最优的并发形态。
静态 batching 的浪费:同批同命
第一代方案是静态 batching:攒够一批请求,整批同时开始、整批同时结束,批内请求的生命周期绑死。它的浪费来自「同批同命」假设与现实的冲突——请求长短天然不一。极端一点的例子:一批 32 条请求,30 条 50 步就生成完毕,2 条要跑 500 步;静态调度下那 30 条的槽位与算力陪着最后 2 条干等到第 500 步,批尾大量计算单元空转。再加上凑批本身的等待(攒够一批才开跑,先到的请求干等后到的)与长短不齐带来的填充浪费,静态方案在真实混合流量下的有效利用率很难看。
批越大,这个浪费反而越放大:批里「至少有一条长请求」的概率随批大小上升,几十条的批几乎必然被某条长尾绑架到终点。所以静态方案的常规调优思路——加大批来提吞吐——恰恰会加重它的病,这是内在的死循环,也注定了它只适合「请求长度可控可分桶」的场景:离线批量任务可以按长度分桶凑批,桶内长短接近,浪费就小;在线流量的长度分布不可控,只能换调度方案。
工程上还有一个过渡形态值得一提:按长度分组的动态凑批——把相近长度的请求分到同组,组内做静态批。它介于两代方案之间,实现简单、改善有限,适合从静态方案渐进迁移的过程态;也常被用作评测基线,衡量 continuous 方案在自家流量上真实多赚了多少。
这个缺陷在流量越「杂」时越致命:真实线上流量的请求长度分布天然是长尾的,均匀短请求的理想场景在现实里几乎不存在。所以静态 batching 只活在离线批处理这类可以自己排队排产的场景;在线服务要吃下真实流量,需要下一代方案。
continuous batching:iteration 级调度
第二代方案 continuous batching,核心思想一句话:调度粒度从「批」降到「解码的一步」。这个思路由 Orca 论文提出(业内公知的出处),此后成为主流推理引擎的标配。它不再要求同批请求同生同死,而是每个解码 step 结束时重新审视全场:本步有请求生成完毕?立刻退出,槽位腾出。等待队列里有新请求?下一步就插入当前批开跑。批的成员每个 step 都在变,批内永远保持「都在干活」的饱和状态,不存在陪跑。
收益量级业内有常识口径:真实混合流量下,continuous batching 的吞吐可达静态方案的数倍,流量长短越参差,差距越大。直观理解这数倍从哪来:静态方案的批尾空转与批头等待被全部消灭,槽位周转率接近理论上限——完成后下一个 step 就有新请求顶上,GPU 每一步都在满员干活。
调度器还要处理批内的优先级与公平:多业务共用一个实例时,谁的新请求先入批、显存吃紧时谁被抢占,都要有规则——高优先级业务插队、低优先级被换出的策略,与多租户混部的思路一脉相承。批内成员每步重审还有个顺带的红利:取消变得即时——客户端断开连接,请求在下一个调度点直接出批,不再生成没人要的 token;这一点静态方案几乎做不到。
实现的代价也要如实交代:每步重审调度,意味着调度器本身要足够快——决策逻辑必须远轻于一次解码步,否则调度开销反过来吃掉收益。成熟引擎把这块做成了轻量的常数时间操作;自己魔改调度逻辑的团队,要先量一量这笔开销在总耗时里的占比。
与 PagedAttention 的共生关系
continuous batching 能跑起来的前提,是批内 KV Cache 的显存管理跟得上——这正是它与 PagedAttention 共生的原因。批成员每个 step 都在进出,每条请求的 KV Cache 长短不一、随生成动态增长,显存分配如果按「整块预留」的老办法,碎片与浪费会让批大到一定程度就爆显存。PagedAttention 把 KV Cache 切成固定大小的块按需分页(机制在「vLLM 与 PagedAttention」一篇已拆),显存利用率大幅提高,同显存能塞下的并发请求量翻着倍地涨——批才滚得起来、滚得大。两件事合起来才是完整的现代推理调度:分页管显存空间,iteration 级调度管时间,缺一不可。vLLM 的出圈正是这两个机制的合体,此后各家引擎(SGLang 等)都在这条主线上继续演化。
显存水位与批上限是联动的:每插一条新请求,调度器都要评估「它的 KV Cache 长到最长会占多少」再决定放不放行——显存富余就插,临界就让它排队,水漫了才抢占驱逐。所以看到线上「批上不去」的现象,第一反应不是调大上限参数,而是查显存水位:上限参数只是闸门的刻度,显存才是闸门本身。前缀共享还能再省一层:批里多条请求共享同一段公共前缀(系统提示词、多轮对话历史)时,KV 按前缀复用(机制见「Prefix Caching」篇),同一份显存能容纳更多并发请求。
值得多提一句的演进方向是 prefill 与 decode 的关系:长请求的 prefill(处理输入、填 KV Cache)是计算密集的一步,混进解码批里会让同批解码请求的延迟抖一下,后来的引擎普遍引入 prefill 分块等手段把这笔账抹平(SGLang 相关篇目有更多展开)。理解到「批内每一步的工作量也要均衡」这一层,就算摸到 iteration 级调度思想的精髓了。
更激进的资源专业化是 PD 分离:把 prefill 与 decode 拆到不同的卡上、各自集群化——prefill 集群吃算力,decode 集群吃带宽,各自按自己的负载特性调批(机制与取舍见「PD 分离」篇)。它解决的是同批之内两类阶段互相干扰的问题,是 iteration 级调度思想往集群维度的自然延伸。
参数取舍与实操误区
两个核心参数
落地到引擎配置,continuous batching 的体验主要被两个参数塑造。最大批大小(max batch size 或 max num seqs 一类的名字):上限越大吞吐潜力越大,但单步计算量也越大——批里每条请求的每步延迟随之拉长,表现为 token 间隔变毛、首 token 延迟变差;上限太小则吞吐起不来。取值要在吞吐与延迟之间按 SLA 实测,没有万能数字。攒批等待窗口:新请求到达后等多久再入批。窗口长,批凑得满、吞吐好,但先到请求平白多等;窗口短,首 token 快,批却稀。低延迟业务把窗口压小,吞吐优先的业务放宽,同样是按 SLA 定的取舍。另有一个隐含机制值得知道:显存吃紧时引擎会抢占——把部分请求的 KV Cache 换出或丢弃待重算(各引擎策略细节不同,属常识口径),所以批上限不是唯一的安全阀,显存水位才是硬约束。
prefill 分块也带来一个可调参数:单步塞进多大的 prefill 分块。块大,prefill 步数少、吞吐好,但单步时长被拉长,同批解码请求的间隔毛刺变大;块小,毛刺平滑、整体吞吐略降——又是吞吐与延迟的老交易,按业务的延迟预算定。请求取消与超时也别漏配:客户端等多久放弃、放弃后服务端要不要立刻停止生成,直接影响算力的有效利用率,别让它停留在没人管的默认值上。
另有一组容易被忽略的观察指标:批内长度的方差与排队时间的方差。方差突然变大,多半说明请求结构在变——新业务上线、客户行为漂移,参数的最优值也会随之漂移。所以参数不是调完就完:定期用近期真实流量回放复测,把参数校准做成季度例行项,比一次性精调可靠得多。
三个常见误区
实操层面三个误区高频出现。其一,把客户端并发当批大小:压测工具开了 64 并发不代表服务端批就是 64——调度、排队、上限配置都会截断它,看服务端每步实际批大小指标才算数。其二,认为批越大越好:SLA 有延迟约束的服务,把批上限拉满换吞吐,结果是 P99 崩掉、体验劣化,吞吐数字好看业务遭殃。其三,用均匀流量评测批处理能力:全部等长请求的压测会严重高估静态方案、也测不出 continuous 方案在真实长尾下的优势——压测流量必须用长短混合的真实分布(压测方法的展开在「推理服务压测方法」篇)。调参的正确姿势是盯三组指标:每步实际批大小、排队时间分布、首 token 与 token 间隔的分位数。
补第四个误区:只看平均值不看分位。批处理让平均延迟与平均吞吐双双变好,但它的代价藏在尾部——批大时的间隔毛刺、抢占时的重算停顿,全体现在 P99 上;拿平均值汇报、被 P99 打脸,是调度调参最常见的剧本。压测还有两个细节:开流式(流式与非流式的调度行为不同),并用真实的采样参数(温度影响生成长度分布,进而影响批的动态构成)。
小结
continuous batching 是推理引擎从「能用」到「能扛」的分水岭:它把调度粒度从整批降到解码的单步,完成的请求即走、新请求随到随插,消灭了静态方案的批头等待与批尾陪跑,真实混合流量下吞吐数倍于静态方案。它跑起来的前提是 PagedAttention 式的细粒度显存管理——分页管显存空间,iteration 级调度管时间,两者共生才构成现代推理引擎的底盘。落地的艺术在参数:批上限与等待窗口都在吞吐与延迟之间做交换,按 SLA 实测定值,别迷信大批。
回头看,这项技术的思想内核其实只有一句:不要让任何一次昂贵的资源占用(权重搬运)被少数长尾请求绑架。这个思想还在向更远处延伸——批内工作量的均衡(prefill 分块)、抢占与优先级(多租户混部)、与投机解码的配合(每步多吐 token 改变批的算术),推理调度的演化仍在继续。理解了本篇的分母与分子,再去读各引擎的调度文档,看到的就是同一张地图上的不同路径。
读者留言
COMMENTS 暂无还没有留言,来说第一句?