大语言模型的GPU推理和普通神经网络推理有一个很大的区别:请求不是一次计算完就结束,而是通常要经历输入提示词的Prefill阶段,然后进入逐token生成的Decode阶段。不同用户的提示词长度不同,生成长度不同,请求到达时间也不同。如果GPU每次都必须等待一整批请求同时开始、同时结束,计算资源就很容易出现空档。
Continuous Batching,也称连续批处理或Iteration-Level Batching,就是针对这个问题发展出来的推理调度机制。它并不是简单地把固定批次变成“动态批次”,而是把调度粒度进一步缩小到模型迭代。每完成一次模型前向计算,调度器都会重新决定下一轮处理哪些请求、每个请求处理多少token。vLLM目前的调度机制就是按照iteration进行调度,每一个调度步骤对应一次模型forward pass;TensorRT-LLM则把类似机制称为In-Flight Batching。
传统Static Batching的工作方式相对容易理解。假设服务器设置batch size为8,系统收集8个请求后,把它们组成一个批次,一起送进GPU。对于普通图像分类或者固定长度的数据,这种方法非常有效,因为一批数据通常具有比较接近的计算量。
但是LLM生成任务完全不同。假设8个用户同时进入一个batch,其中几个用户只需要生成几十个token,另外几个用户可能需要生成几百甚至上千个token。如果采用严格的静态批处理,短请求完成以后,剩余计算就会受到长请求拖累。系统如果采用padding保持张量形状统一,又会让GPU执行一些没有实际意义的计算。
更麻烦的是,请求并不是同时到达的。一个请求可能已经开始生成,另一个请求此时才刚刚进入服务器。如果系统必须等待下一个完整batch才能启动,就会产生额外排队时间。GPU此时可能并不是没有计算能力,而是调度器没有把足够的工作及时交给它。
Continuous Batching改变的就是这一层调度逻辑。
在连续批处理中,一个batch不再被看成一个必须同时出生、同时死亡的固定对象。请求可以在不同时间进入计算,也可以在不同时间结束。正在Decode阶段的请求继续生成下一个token,新到达的请求可以进入Prefill阶段,已经完成生成的请求则从运行集合中退出。下一轮forward pass开始之前,调度器重新组织当前能够执行的请求。
可以把GPU想象成一条高速生产线。传统Static Batching更像是等8辆车全部到齐以后一起开工,而且必须等这一组车全部处理完才能换下一组。Continuous Batching则更像不断补充生产线上的工作:一辆车完成后立即退出,新的车辆进入,其他车辆继续处理。生产线本身不需要等待所有车辆同时结束。
这也是Continuous Batching提高GPU利用率的关键。
这里还需要澄清一个容易出现的误解:Continuous Batching并不是让不同用户共享同一份KV Cache。每个请求仍然拥有自己的上下文状态和KV Cache。真正发生变化的是调度器如何管理这些缓存,以及如何让不同请求在不同生命周期阶段共同参与GPU计算。
现代推理框架通常会把KV Cache切分成更容易管理的块。例如vLLM使用PagedAttention以及块式KV Cache管理机制,并根据调度情况为请求分配和释放缓存空间。vLLM当前的调度器还会考虑KV Cache容量,在显存紧张时避免过度接纳请求,并提供水位机制降低频繁驱逐和重新计算带来的开销。
KV Cache管理的重要性来自LLM的生成方式。模型在生成第一个token以后,并不需要每次都重新计算整个历史上下文,而是利用已经计算过的Key和Value继续生成。因此,一个请求运行时间越长、上下文越长,它占用的KV Cache通常也越多。连续批处理如果只追求“尽可能多塞请求”,很容易把GPU显存吃满,最终出现缓存驱逐、重新计算甚至请求被抢占的问题。
所以Continuous Batching并不是无限增加batch size。现代推理引擎通常会同时限制每次迭代能够处理的token数量、运行请求数量以及KV Cache容量。以vLLM为例,其调度器存在max-num-batched-tokens和max-num-scheduled-tokens等限制,用来控制单次iteration能够处理多少token。
这也解释了为什么Continuous Batching和传统Dynamic Batching不能简单画等号。
传统Dynamic Batching通常是在一个时间窗口内收集请求,例如等待几毫秒,然后把这一批请求一起送进模型。批次形成以后,模型执行完整的一轮计算,等这一批结束后再重新形成下一批。
Continuous Batching的调度粒度更细。模型每完成一次iteration,调度器就可以重新安排下一轮。对于Decode阶段,一个请求可能每次只贡献一个新token;与此同时,新进入的请求可能正在执行Prefill,需要处理大量输入token。TensorRT-LLM的In-Flight Batching正是允许Context阶段和Generation阶段的序列在同一个执行框架中交错处理,从而减少GPU空闲。
这种机制对于LLM尤其重要,因为Prefill和Decode本身就具有不同的计算特征。Prefill通常需要一次处理大量输入token,更偏向计算密集型;Decode则通常每个请求每轮只生成一个token,但需要不断读取模型权重和访问KV Cache,更容易受到内存系统和调度效率影响。
如果采用过于僵硬的批处理策略,Prefill和Decode之间很容易出现资源利用不均。连续调度可以让系统根据当前工作负载重新安排两类任务。例如,一个请求刚进入系统,拥有很长的prompt,需要进行Prefill;另外几个请求已经进入Decode阶段。调度器可以根据当前token预算和KV Cache容量决定本轮处理哪些工作,而不是等待所有请求处于同一个阶段。
因此,Continuous Batching提升GPU利用率,并不是因为它“动态增加GPU计算单元”。GPU的SM、Tensor Core等硬件资源数量并没有因为请求增加而发生变化。所谓提高利用率,是让已经存在的计算资源减少等待,让更多时间处于有效计算状态。
这一点也决定了Continuous Batching并不意味着任何情况下都能提高单请求延迟。
如果GPU本来只有一个请求,而且这个请求的模型计算已经无法进一步并行,那么Continuous Batching几乎没有额外请求可以填补计算空档。它的优势主要出现在多个请求存在、请求生命周期不同以及请求不断到达的服务环境中。
而当并发请求增加以后,连续批处理能够把这些请求的不同生命周期组合起来。例如请求A已经生成到第100个token,请求B刚刚完成Prefill,请求C刚刚进入系统,请求D已经接近完成。在下一次调度时,它们并不需要因为生命周期不同而被分成完全独立的执行批次,而可以由调度器按照当前资源条件共同参与计算。
这也是为什么Continuous Batching特别适合在线LLM服务。实际生产环境的请求流通常不会像实验室测试那样整齐。用户在不同时间发送请求,有的人输入几十个token,有的人输入几千个token,有的人很快停止生成,有的人则要求很长的回答。请求生命周期的不一致,本身就是静态批处理效率下降的重要原因。
不过,连续批处理也会增加系统复杂度。
第一个问题是调度。调度器必须在吞吐量和延迟之间不断寻找平衡。如果每一轮都尽可能塞入更多token,GPU吞吐可能提高,但新请求的等待时间也可能增加。反过来,如果调度器过度追求低延迟,每次只安排很少工作,GPU又可能无法保持高效率。
第二个问题是KV Cache。KV Cache不仅占用显存,而且决定了系统能够同时容纳多少正在运行的请求。vLLM目前甚至允许直接配置KV Cache容量和KV Cache的数据类型,包括BF16、FP16、FP8以及其他格式。KV Cache容量越充足,系统通常能够容纳更多并发上下文,但显存资源毕竟有限。
第三个问题是不同请求之间的长度差异。一个拥有超长上下文的请求可能占用大量KV Cache,同时又持续很长时间。如果调度器处理不当,它可能挤压其他请求的资源。因此现代推理系统需要考虑请求优先级、token预算、KV Cache空间以及请求等待时间,而不是简单按照“batch越大越好”的思路设计。
第四个问题是Prefill和Decode的协调。现代系统还可以采用Chunked Prefill,把很长的输入拆成多个部分进行调度,而不是让一个超长prompt一次性占据大量计算资源。vLLM目前就支持chunked prefill,并且调度器会根据剩余token预算决定能够安排多少Prefill工作。
第五个问题是GPU kernel效率。Continuous Batching并不会自动保证Tensor Core达到最高利用率。如果每一轮实际参与计算的token数量过少,或者输入张量组织方式不适合当前kernel,GPU依然可能处于低利用率状态。因此,真正的推理优化通常是调度器、PagedAttention、FlashAttention、CUDA Graph、量化kernel以及显存管理共同作用,而不是单独依靠Continuous Batching。
vLLM目前已经把Continuous Batching、PagedAttention、Chunked Prefill、Prefix Caching以及多种量化和优化kernel结合在同一套推理系统中。 TensorRT-LLM也采用Paged Attention和In-Flight Batching,并要求相关输入以packed tensor方式组织,以减少生成阶段padding带来的资源浪费。
Prefix Caching还可以进一步减少重复的Prefill计算。如果不同请求拥有相同的开头,例如大量用户都在调用同一个系统提示词,那么已经计算过的KV Cache块可以被后续请求复用。这里的“复用”与Continuous Batching的概念不同:前者解决的是重复计算,后者解决的是请求调度和GPU工作连续性。vLLM的Prefix Caching就是通过缓存已经计算过的KV Cache块,让拥有相同prefix的新请求减少重复计算。
从工程角度看,Static Batching并没有消失。对于离线推理、数据处理和请求特征高度一致的任务,静态批处理仍然可以非常高效。因为系统可以提前知道数据规模和任务边界,并通过固定batch让GPU保持稳定的矩阵计算。
Continuous Batching的优势主要出现在在线服务。请求不断进入、不断完成,而且每个请求的输入长度和输出长度都不同,这时固定batch很难充分利用GPU。连续调度能够让请求生命周期互相填补,从而提高单位GPU时间能够完成的有效token数量。
因此,两者最核心的区别并不是“一个固定、一个动态”这么简单,而是调度粒度不同。Static Batching通常围绕完整batch组织计算;Continuous Batching则把模型执行过程拆成连续的iteration,在每一轮重新决定哪些请求参加计算以及处理多少token。
对于GPU推理服务器来说,最终应该观察的指标也不只是GPU Utilization。一个系统即使监控软件显示GPU利用率很高,如果请求排队时间过长,用户体验依然可能很差。生产环境通常需要同时关注Time to First Token、Inter-Token Latency、吞吐量、并发请求数、KV Cache使用率、抢占次数以及端到端延迟。
Continuous Batching的意义就在这里。它不是让GPU凭空获得更多计算能力,而是改变GPU获得工作任务的方式。面对不断变化的请求流,调度器尽量让已经完成的请求及时退出,让等待中的请求及时进入,让Prefill和Decode在资源允许的情况下交错执行,同时控制KV Cache和每轮token预算。
当这些机制配合得当,GPU就不必频繁等待一个完整batch形成,也不必因为某几个短请求已经完成而让剩余计算资源长期闲置。对于现代大语言模型服务,这种从“批次级计算”转向“迭代级调度”的变化,已经成为提高单卡吞吐和在线服务效率的重要基础。
Continuous Batching 是怎样提高 GPU 利用率的,动态批处理与传统批处理有什么区别
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP