4、In-flight Batching 与显存复用:动态批处理原理、Continuous Batching 机制、显存池化与复用策略、TensorRT-LLM 的 In-flight Batching 配置
聊到 LLM 推理,大家最头疼的是什么?
我个人觉得,排第一的绝对是显存。你想想看,一张 A100 80G,看着挺大,但跑个大模型,稍微来点并发,显存就爆了。很多团队初期都遇到过这个问题:明明算力够,但显存撑不住,导致 GPU 利用率低得可怜。
这背后的核心矛盾,其实就是传统的静态批处理方式太死板了。它必须等一个 batch 里所有请求都生成完,才能释放显存,接下一批。这就像去食堂打饭,必须等所有人都吃完,才能让下一波人进去。中间空着的座位(显存),全浪费了。
In-flight Batching(也叫 Continuous Batching)就是来解决这个问题的。说白了,它允许请求「插队」和「随时下车」。一个请求生成了第一个 token,就可以立刻把它的显存让给新来的请求。GPU 不用再傻等,利用率自然就上去了。
核心思想: 不再以「请求」为单位管理显存,而是以「token」为单位。谁需要计算,谁就上;谁算完了,谁就退。
Continuous Batching 机制:到底是怎么「连续」的?
我刚开始接触这个概念时,也觉得有点抽象。咱们拆开来看。
传统批处理,一个 batch 里的请求,生命周期是同步的。一起进来,一起出去。但 Continuous Batching 不一样,它维护一个动态的「待计算队列」。GPU 每步只计算当前 batch 里所有请求的下一个 token。算完之后,那些已经生成完的请求(比如生成了结束符),立刻从 batch 里移除,腾出位置。然后,从队列里拉新的请求进来,补上这个空位。
嗯,这里要注意:新请求进来时,它需要从零开始计算第一个 token。而老请求可能已经在算第 50 个 token 了。它们混在一个 batch 里,怎么对齐?
答案是:用 padding 和 mask 来对齐。每个请求的序列长度不同,但计算时,我们通过 attention mask 让每个 token 只看到它自己该看的内容。新请求的 token 不会干扰老请求的上下文。
我的经验: 我曾经在优化一个对话系统时,发现 Continuous Batching 对短请求特别友好。因为短请求生成快,频繁退出,给长请求腾出了大量显存。但如果你所有请求都是长文本(比如摘要生成),那效果会打折扣。因为大家退出频率差不多,显存复用率不高。
显存池化与复用策略:别让显存「碎片化」
Continuous Batching 虽然好,但它带来了一个新问题:显存碎片化。
你想想看,请求随时进来,随时出去。每个请求占用的显存大小(KV Cache)是动态变化的。今天这个请求生成了 10 个 token,明天那个生成了 100 个。如果频繁分配和释放,显存会变得像一盘散沙,到处都是小碎片。明明总显存还有 10GB,但就是分配不出一个连续的 1GB 块。
怎么解决?显存池化。
说白了,就是提前从 GPU 申请一大块连续显存,切成固定大小的「块」(block)。每个请求的 KV Cache,不再按需动态分配,而是从池子里拿 block。用完了,把 block 还回池子,而不是还给 GPU。
这样做有两个好处:
- 消除碎片: 所有 block 大小一致,不存在碎片问题。
- 分配快: 从池子里拿 block 是 O(1) 的操作,比 cudaMalloc 快几个数量级。
我见过一些团队自己手写显存池,但说实话,坑很多。比如 block 大小怎么定?太小了,管理开销大;太大了,内部碎片浪费。TensorRT-LLM 在这方面做得比较成熟,它内置了 PagedAttention 机制,本质上就是一种显存池化。
避坑指南: 我曾经在一个项目中,把 block 大小设成了 64 个 token。结果发现,很多短请求只用了 1-2 个 block,但每个 block 里大部分空间是浪费的。后来我改成 16 个 token 一个 block,显存利用率提升了 30%。所以,block 大小要根据你的业务场景来调,不是越大越好。
TensorRT-LLM 的 In-flight Batching 配置
好了,理论说完了,咱们看看 TensorRT-LLM 里怎么配。
其实配置起来很简单,核心就几个参数。我直接给你看一个我常用的配置示例:
# 在构建 engine 时启用
trtllm-build \
--model_dir ./llama-model \
--max_batch_size 64 \
--max_input_len 2048 \
--max_output_len 512 \
--max_beam_width 1 \
--use_inflight_batching \
--enable_paged_kv_cache \
--paged_kv_cache_block_size 16 \
--tokens_per_block 16 \
--output_dir ./engine
这里几个关键参数,我解释一下:
- use_inflight_batching: 这个就是开关。打开后,TensorRT-LLM 内部会启用 Continuous Batching 调度器。
- enable_paged_kv_cache: 启用分页 KV Cache,也就是显存池化。建议一直开着。
- paged_kv_cache_block_size / tokens_per_block: 这两个参数控制 block 大小。我习惯设成 16,对于大多数场景是个不错的平衡点。
- max_batch_size: 这个不是传统意义上的 batch 大小了。在 In-flight Batching 下,它表示 GPU 同时能处理的最大请求数(包括正在生成的和等待的)。设得太大,显存会爆;设得太小,利用率上不去。我一般先设成 64,然后观察显存占用,再慢慢往上调。
我的调试习惯: 启动服务后,用 nvidia-smi 看显存占用。如果显存一直稳定在 90% 以上,说明配置合理。如果忽高忽低,说明 block 大小可能不合适,或者 max_batch_size 设得太激进。
一张图看懂 In-flight Batching 流程
说了这么多,不如一张图来得直观。下面是我画的流程图,展示了请求从进入队列到生成完成的完整过程:
这张图展示了完整的闭环:请求从队列进入调度器,调度器从显存池拿 block 分配给请求,然后请求进入 GPU 计算。计算完成后,block 被释放回显存池,等待下一个请求使用。整个过程是动态的、连续的。
总结一下
In-flight Batching 和显存池化,是当前 LLM 推理优化的两大基石。它们解决的不是「算得快不快」的问题,而是「显存用得好不好」的问题。显存用好了,GPU 才能同时处理更多请求,吞吐量才能上去。
配置上,TensorRT-LLM 已经封装得很好。你只需要关注 max_batch_size 和 paged_kv_cache_block_size 这两个参数,结合你的业务场景调优即可。记住,没有万能参数,只有最适合你场景的参数。
一句话总结: 别让 GPU 闲着,也别让显存闲着。In-flight Batching 让 GPU 忙起来,显存池化让显存不浪费。