Continuous Batching如今已是大型模型推理框架的核心技术,也是各家性能优化竞赛的主战场。简单说,就是通过把多个在线请求拼到一起做批处理(Batching),让GPU的利用率往上走。在Transformer还没称霸的年代,Batching功能通常由独立的服务框架承担,比如tfserving对接TensorFlow XLA、NVIDIA Triton对接TensorRT。那时候的Batching有个基本前提:输入请求的形状必须相同,比如一批同样尺寸的图像。但Transformer一出来,输入序列长度和批次大小都变得灵活可变了,Batching这件事的难度和玩法也跟着彻底变了。
最近集中梳理了一下Continuous Batching的相关工作,不禁让人想起在腾讯微信(WXG)时的一段经历。2019年下半年,刚校招加入微信WeChat AI,负责开发一个Transformer模型的推理服务框架TurboTransformers,目标是跟FasterTransformers对标,满足团队NLP服务的线上需求。后来把TurboTransformers里几个亮点整理成论文发表在PPoPP 21上,核心贡献是针对encoder-only架构的变长输入问题提出了两个创新点:一是用chunk来管理动态内存,平衡GPU内存占用和临时分配的开销——思路跟后来PagedAttention用page管理KVCache类似,只是当时chunk管理的是中间激活结果;二是用动态规划寻找最优padding策略,最大化吞吐、减少无效计算。不过第二个方法实用性有限,更适合只能处理静态shape的推理runtime。同期字节的EffectiveTransformer用了另一种思路:仅对Attention部分做padding,其他部分则把batch size和sequence length维度融合,完全不用padding。所以TurboTransformers开源Repo里实际实现了两种Batch padding方法——模型黑盒就用动态规划padding,白盒可改就用类EffectiveTransformer的方法。
当时团队线上业务主要跑encoder-only和encoder-decoder类Transformer架构。WeChat AI只有Decoder-only的GPT做文本生成,他们后来在ChatGPT爆火前还搞出了WeLM。
TurboTransformers算是较早指出输入变长需要新Batching方法的论文。2020年上半年,开始琢磨如何把变长输入Batching方法扩展到Decoder架构中。当时深受RNN Batching方法BatchMaker的启发,觉得这套思路完全可以套到Transformer-Decoder模型里。BatchMaker也是ORCA论文中最主要的相关工作之一,论文里做了详细引用。说来也巧,BatchMaker的第一作者Pin Gao是清华高性能所隔壁实验室的学长,论文是他2018年访问纽约大学期间发表的EuroSys文章,刚出来就关注过。更巧的是,当时他也在WXG的图神经网络团队,还当面跟他聊过可以把他的想法用到Transformer推理中。
正准备动手时,临时被拉去做微信输入法的封闭开发——显然微信键盘更能让NLP技术普惠大众,那个想法就搁置了。2021年一整年注意力转移到大模型训练上,做了PatrickStar的工作。BatchMaker加Transformer Decoder这个未了的心结一直留着。
2022年的某一天,在Google Scholar推荐论文里看到ORCA时,眼前一亮——这不就是当年想实现的那个点子吗?系统研究知易行难,好点子常有,真正落地太难。ORCA的完成度非常高,换做自己去干,也做不出OSDI水平的工作。不过ORCA也赶上了好时代,LLM爆火让推理性能备受关注;要是Encoder时代没结束,ORCA很可能跟BatchMaker一样长期被埋没。
以上就是Pre-LLM时代做推理框架的一些往事,下面进入正题:Continuous Batching。
很多人是从2023年6月AnyScale的博客那张图开始了解Continuous Batching的,以至于后来各种PPT和公众号都默认引用那幅图。再次证明一图胜千言——ORCA论文里那些灰头土脸的设计图,都不如这张图让人一目了然。正是因为vLLM和AnyScale这些伯克利大佬们管它叫Continuous Batching,中文世界也就默认了这个叫法。其实首尔大学的ORCA团队自己称之为Iteration Batching。韩国人的工作命名权也只能掌握在美国人手里,背后也折射出MLSys领域的美国中心主义。顺带一提,ORCA的团队后来创立了PaaS创业公司FriendliAI,做大模型推理的PaaS服务。

还是先从RNN时代的Batching方法BatchMaker讲起。
BatchMaker:Low Latency RNN Inference with Cellular Batching
BatchMaker是一个专门为RNNs设计的serving系统,核心思想是以RNN Cell为粒度进行调度和批处理。RNN用相同的权重对不同输入进行计算。收到请求时,BatchMaker将处理请求的数据流图分解为RNN Cell(即一个iteration step),然后以Cell为粒度执行调度,并批处理相同的单元。由于每个RNN Cell始终执行完全相同的计算,BatchMaker可以不管单元的位置(即token索引)如何,都能以Batching方式执行多个RNN Cell。这样一来,新到达的RNN请求可以随时加入当前执行的批次,已完成的请求也可以随时离开,完全不用等到整个批次结束。

看下图就能发现,Cellular Batching的方法已经和Continuous Batching非常相似了。

ORCA:更适合Transformer宝宝体质的Batching方法
ORCA借鉴了BatchMaker的思想,将其适配到Transformer Decoder的生成过程。虽然Transformer Decoder和RNN在生成时都是逐个token迭代,但两者有本质区别。第一,Transformer Decoding阶段每个迭代要把当前token和之前生成的token序列拼接起来传入模型。尽管每次只生成一个token,计算量近似,但每个迭代的KVCache长度会逐渐增长。第二,Decoder解码时需要进行Prefill过程,这是RNN没有的。Prefill计算是一堆token一起算,与Decoding阶段的计算模式截然不同——前者计算密集,后者访存密集。
为了解决这些问题,ORCA提出了两个设计思路:Iteration-level Batching和Selective Batching。Iteration-level Batching可以看作是对BatchMaker Cell粒度处理思想的致敬,而Selective Batching则是针对Transformer的独特处理,用来支持batch size和input sequence两个维度动态变化对Batching执行的影响。
由于Attention机制和FNN的Batching方式不同,Linear层可以把batch size和seq_len两个维度融合为一个维度(类似前文提到的Efficient Transformer思想),但Attention不行。因此,一个Transformer Layer可以划分为PreAttn、Attn和PostAttn三个部分,从而把Prefill阶段和Decoding的一个step打成同一个batch处理。如下图所示,QKV Linear和Attn Out Linear可以打成一个batch(size=7),而Attn的计算不打包,每个request单独处理,所以在Attn前后有Split和Merge操作。

需要注意的是,ORCA当时还没有考虑KVCache内存管理优化,每个序列都预先分配max token数的显存空间给KVCache。实验也都按照max token来生成,不会考虑遇到eos就提前结束的情况。
2023年更多Continuous Batching的变种
2023年Continuous Batching迎来了大发展,在vLLM的推动下已成为推理框架的事实标准。不同框架的实现有差异,主要体现在对Prefill的处理方式上——是单独处理Prefill,还是与Decoding融合,以及以什么样的粒度融合,这里面有不少讲究。
1. vLLM(UC Berkeley)
vLLM是SOSP 2023的论文,也是热门开源项目。其创新点Paged Attention(PA)减少了内存碎片,提高了内存效率,从而能增大batch size、提升吞吐。Batching策略是为PA设计服务的,所以没有照搬ORCA的实现。与ORCA不同,vLLM在Batching时将Prefill和Decoding分开处理,一个Batching step要么处理Decoding,要么处理Prefill,不会混在一起。这样实现比ORCA更简单:Prefill直接调用xformers处理计算密集的Prefill attention,Decoding则手写CUDA PA处理访存密集的attention计算。
从设计逻辑上看,vLLM之所以没有采用ORCA的方案,是因为PA是手写CUDA Kernel实现的,可以处理不同序列长度的输入,Attention的Batching方式可以与Non-Attn部分统一。所以用了一个糙快猛的方法:不搞Selective Batching,Decoding整体一起处理一个batch的step计算,Prefill不与Decoding step融合。如果要把Prefill和一个Decoding step融合,还需要拆分Attn和Non-Attn,Attention实现也更复杂,不利于展示PA的思想。
不过,因为Prefill过程会抢占Decoding的step前进,如果输入prompt sequence length过长,所有Decoding请求都得等待,造成更长的延迟。这个优化空间后来也引发了vLLM和DeepSpeed之间的一段“孽缘”。
2. FastGen(DeepSpeed)
微软DeepSpeed团队在2023年11月的MII项目中提出了Continuous Batching的一个变种——SplitFuse。发布时直接把vLLM当作靶子打,vLLM也很快还击,逐渐演变成两个门派之间的口水战。SplitFuse的核心想法是:把长prompt request分解成更小的块,分散到多个forward step中调度,只有最后一块的forward完成后才开始这个prompt request的生成。短prompt request则组合起来精确填充每个step的空隙。这样每个step的计算量基本相等,所有请求的平均延迟能变得更稳定。

3. LightLLM
商汤发布的Pythonic LLM serving框架,简单高效,易于二次开发,与其他框架的集成也很方便。与vLLM不同,LightLLM的Prefill和Decoding可以在一个step中打包成一个Batch处理,算是ORCA的原教旨主义者。同时它改进了PagedAttention,弄成了tokenAttn(即PagedAttention的page size=1),也支持了FastGen的SplitFuse方法。
4. TensorRT-LLM
TensorRT也使用了Continuous Batching,不过他们管它叫Inflight Batching。这个模块是闭源的,但从公开信息看,他们把Prefill和Decoding step融合,更像ORCA而不是vLLM。
总结
Continuous Batching这一大模型推理的关键技术并非凭空而来,其思想最早源自Pin Gao对RNN Batching的研究——BatchMaker。目前不同大模型框架对Continuous Batching的实现略有差异,核心分歧主要在于如何处理Prefill负载。无论是分开处理还是融合处理,各有各的考量,也各有各的优化空间。未来随着模型规模和推理场景的进一步变化,这个领域还会继续演化。
