上篇文章介绍了vLLM核心之一的 paged attention(点积查看),今天继续复习 continuous batching;
因为我目前就是在做调度系统,而这种动态batching也算一种调度,并且本周也还被人问到了, 所以即便周末北平天气这么好,非常想去爬山,但也还是先坚持沉淀一下;
以下内容均来自 对vLLM 官方文档 和 源码(截至2025-11月最新tag)的个人理解,如果错误请指正;

batching 的重要性
首先回顾下,为什么说batching是提升推理性能的关键,以及传统方案和问题:
- 目标:最大化GPU利用率,跑满GPU,在单位时间内尽可能多的处理请求,提高吞吐
- 挑战:经典的 ①显存资源受限 和 ②计算资源受限
传统方案:把多个请求收集起来(一般可以限定在 max_delay_ms 时间内),拼凑成一个固定的batch,一次性的喂给LLM
缺陷:
- 长尾延迟:整个批次必须等待所有请求完成后才能释放,所以其中的长尾请求,会严重拖慢整体响应
- 资源浪费:一个batch内原始输入 prompt 长短不一,必须 padding 到该批次的最长序列的长度,或直接干脆全都padding到
max_prompt_len,但这样大量资源就会被浪费到了无效的padding token上 - 灵活性差:因为大多提供的都是实时在线服务,不是离线任务,无法动态响应不断到达的新请求
token-level 动态 batching 调度
目前主流框架的解法是 token-level 的动态batching调度,像 vLLM 的 continuous batching 和 TensorRT-LLM 的 In-Flight batching 都属于这类;
核心思想就是:打破传统 request 级别的调度,采用 token 级别的调度(更细粒度的调度策略)
在 request 级别的调度策略中,一个batch内的所有request,类比大家中午去食堂吃饭,一起坐下,但要等最后那个人吃完再撤(一般是我… 此时旁边已经站了一圈压力怪盯着你…)

上图中,batch内4个request,黄色是prefill阶段,蓝色是decode阶段,每次生成1个token,直到全部完成输出
而 token 级别的调度策略中,则大家以 token 作为调度粒度,还拿中午去食堂吃饭类比,一桌人每吃完一口,就看看他是不是吃完了,如果是的话,吃完不等同行的人直接撤,旁边等座的就可以坐下吃饭了(提高了食堂吞吐、又降低了食堂座位资源的浪费,座位满负荷运转)

上图中,当一个requst完成后,立即有新的request被压进来,执行prefill-then-decode(这图让我想起了PP流水线并行中,解bubble气泡的1F1B,所以很多东西底层都是类似、相通的,就那些原理,只不过很多东西没时间了解罢了)
想法很简单,细节很魔鬼
话虽如此,听起来很简单,但实现起来还是有很多细节的
首先第一个问题就是,不对啊,新加入的reqeust和已经正在处理的request所处的阶段stage都不同,GPU也不能在同一个时间步里执行啊;
就像食堂一张桌子(1个Warp)4个椅子(32个线程),上一个人吃完离席后,小A刚坐下,需要先掏出手机刷两下、发个微信、拍张照报备啥的(prefill - attn kv cache - 计算密集型),而其他三个人小BCD都在一口一口吃(decode - NTP中 - io密集型),没法在同一个时间步里同步啊,这不就出现线程束分化了么;
设计细节

虽然整个NTP流程分成PD两个阶段,但除了attention外,其余层都是per-token处理的;
也就是说不管这个token是prefill的token还是decode的token,在像FFN这些层中,单独处理,还是放到一块儿处理的逻辑是一样的,所以可以merge到一起跑;
但是attn层不行,prefill和decode处理的逻辑不同,需要进行区分;
源码细节
都在 【scheduler.py】里

首先,vLLM 底层设计了 3个queue 来实现这种调度

其实准确来说有其他queue,通过搜create_request_queue,还能看到一个 skip_waiting_queue(这玩意不重要先不管它,其实是我不想看了,狗头…)

所以:
- 1个是
waiting_queue:用来存储等待执行的request的 - 1个是
running_queue:用来存储当下正在执行的request - 1个是
finished_req_ids:存储已经处理完的request,然后可以释放掉它的资源了
其中,waiting_queue 可以看到有个参数 policy,指的是等待策略,默认是 fcfs,也就是 first - come - first -serve,但也允许根据优先级 priority;
配置代码在 SchedulerConfig 中:

在处理reqeust的时候,针对request本身,vLLM并不会显式的区分它是在prefill还是decode,而都当它们在追token;

但是在attention计算的时候,确实会进行拆分,batch中哪些是prefill的request,哪些是decode的request

同时,vLLM内部还使用了 paged attention,查看其源码也可以看到,区分了prefill和decode分别执行:
在 chunked_prefill_paged_decode.py 中,根据参数 query_len 来区分 prefill 还是 decode:
query_len == 1:则是 decode 阶段,只需要生成1个tokenquery_len > 1:则代表了 prefill 阶段,同时配置参数skip_decode = true

所以:
- 先处理 prefill,执行
context_attention_fwd - 然后再处理 decode,执行
kernel_paged_attention_2d

以上简单介绍了vLLM如何优化了batching策略,从而提高serving性能,下一篇继续盘一盘vLLM中还有哪些推理优化手段。
1301

被折叠的 条评论
为什么被折叠?



