连续批处理:动态插入请求消除生成等待
连续批处理:让大模型推理告别“等待一代”
在大语言模型(LLM)的推理服务中,我们经常需要同时处理多个用户的请求。传统做法是将一批请求打包,一起交给模型计算。但你是否遇到过这样的尴尬:某位用户的问题只生成了两个字就结束了,却要陪着长篇大论的用户一起“发呆”——白白占用着GPU,既不能释放资源,也无法接纳新的请求?
连续批处理(Continuous Batching) 正是为解决这一痛点而生。它在不改变模型本身的前提下,通过一种“动态插队”的调度策略,让GPU几乎一刻不停地干活,推理吞吐量提升数倍成为可能。
为什么传统批处理会“卡住”
在理解连续批处理之前,我们先看看传统批处理(Static Batching)的工作方式。
- 请求打包成批次:当服务端凑齐 N 个请求(或等待超时)后,将它们组成一个 batch。
- 一起编码,一起生成:这个 batch 被送入模型进行 Prefill 阶段(预填充,即处理输入 prompt),然后进入 Decode 阶段(逐词生成)。模型对着这一批序列,一步一步地产生下一个 token,直到批次中所有序列都生成完毕。
- 木桶效应明显:批次中的某个请求可能只需要生成 5 个 token 就结束了(如回答“是”或“否”),而另一个请求需要生成 1024 个 token。但传统批处理必须等最长的那个序列跑完,整个批次才算完成。期间 GPU 算力虽然在跑,但大部分计算是在“伺候”那个长序列,短序列完成的那些位置已经被浪费掉了(填充为无效计算)。
更致命的是,在等待长序列生成时,新的请求只能干等,无法被插入到当前批次中——因为传统 API 是“一批一走”的,批次一旦开始,就无法动态增减成员。这直接导致两个问题:
- GPU 利用率跳水:长序列拖住整个批次,GPU 负载不饱和。
- 尾延迟(Tail Latency)严重:一个慢请求拖垮一批请求的响应时间。
连续批处理的核心思想:迭代级调度
连续批处理的本质是将调度粒度从“批次级”下降到“迭代级”。它允许在每一个 token 生成步骤(即每一次 decode 迭代)之后,动态地插入新请求和移除已完成请求。
这就像流水线作业:传统模式是一条固定长度的传送带,必须等所有包裹都抵达终点才能卸货、上新包裹。连续批处理则把传送带切成了无数小节,每前进一小步,就能把已经完成的包裹拿下来,同时换上新包裹——任何时候传送带都可能是满负荷的。
在LLM推理中表现为:
- 上一个解码步结束后,立即检测哪些序列已经生成
EOS(结束符)或达到最大长度,将它们移出 batch,释放显存和计算槽位。 - 与此同时,检查等待队列,将新请求的预填充阶段(如果需要)或将要解码的序列插入到刚才空出的槽位中。
- 构成一个新的 batch,立刻开始下一轮解码。
因此,batch size 不再是启动时固定的,而是随着时间动态变化,始终保持在硬件能承受的尽可能高的水平。
一步一步看连续批处理的工作流程
假设我们有一台模型服务器,同时处理请求 A、B、C,随后来了新请求 D。
-
初始批次
服务端将请求 A、B、C 组成初始 batch,执行 Prefill 后开始 step-by-step 解码。 -
第 t 步解码完成
此时 A 刚刚生成了EOS,序列结束。B 和 C 仍在继续。 -
动态重组
- 系统立即把 A 移出 batch。
- 等待队列中有请求 D(已完成 Prefill,或者将 Prefill 与解码混合处理),系统将 D 插入 batch。
- 新 batch 变为 [B, C, D],马上进入第 t+1 步解码。
-
持续插入与移除
再往下,B 可能在第 t+3 步结束,被移出;请求 E 刚好到达,插入……如此往复,直到没有更多请求。
这样,服务器几乎总能在每一步都维持一个“满额”的工作 batch,GPU 的空泡时间被极大压缩。
连续批处理的关键优势
- 吞吐量大幅提升:实验表明,在相同硬件和模型下,连续批处理可将推理吞吐量提升 2~4 倍。原因很简单:GPU 总是被喂饱。
- 单请求延迟显著降低:新请求不再需要等待前一批所有请求完成,而是“随到随插”,只要当前迭代结束、有空位就可以进入。这大幅缩短了排队时间,P50/P99 延迟曲线向低处平移。
- 更好的资源弹性:显存占用更加平滑,避免了固定大 batch 可能导致的 OOM(显存溢出),同时 batch size 动态调节,能够应对突发流量。
实现中的典型策略与变体
目前主流的高性能 LLM 推理框架几乎都内置了连续批处理,例如:
- vLLM:通过 PagedAttention 实现内存高效管理,结合连续批处理实现了极高的吞吐。
- Hugging Face TGI(Text Generation Inference):支持基于连续批处理的动态调度。
- NVIDIA TensorRT-LLM:提供 inflight batching(飞行中批处理),本质上就是连续批处理。
- LMDeploy、SGLang 等也实现了类似机制。
在实际实现中,会面临 Prefill 与 Decode 如何混合的挑战。通常有两种做法:
- 分离式(Disaggregated):将 Prefill 阶段和 Decode 阶段分给不同硬件处理,避免相互抢占算力。
- 混合式(Chunked Prefill):将较长的 Prefill 计算切分成小块,穿插在解码迭代之间,防止 Prefill 大计算量造成解码延迟抖动。
但无论哪种,动态插入和移除的能力是连续批处理的灵魂。
一个形象化的通俗理解
想象你在食堂排队打饭(传统批处理),必须凑齐 10 个人才开始叫号,大家要等你旁边那位犹豫半天选菜的人挑完,整个队伍才能解散。连续批处理则是:每个人打完一个菜就可以端着盘子离开(移除),厨师立刻让下一个人补上来接着打菜,档口永远满负荷运转,每个人都不需要等待最慢的人——是不是高效得多?
上手注意事项与误区
- 连续批处理对显存的要求更高吗? 通常不会。动态 batch size 反而避免了显存分配上的浪费,配合 PagedAttention 等技术,显存利用率更高。
- 会不会让单个请求变慢? 从端到端来看,总等待时间下降,但单步计算时间由于 batch 更大可能略有增加,但整体延迟是改善的。
- 需要修改模型吗? 完全不用。连续批处理是调度层面的优化,与模型结构无关,可直接用于各种 Transformer 模型。
总结
连续批处理用“动态插入请求,消除生成等待”的方式,让大语言模型推理服务从“批次级粗放调度”进化到“迭代级精耕细作”。它将 GPU 计算资源利用到极致,实现了吞吐与延迟的双赢,目前已经成为高性能推理服务的标配。如果你正在搭建或优化一个 LLM 应用,理解和启用连续批处理是走上“快车道”的关键一步。