写点什么

解析 vLLM 架构及源码系列:V1 调度器分析

作者:Jason黄
  • 2025-08-21
    广东
  • 本文字数:2435 字

    阅读完需:约 8 分钟

解析 vLLM 架构及源码系列:V1 调度器分析

调度器是 V1 架构优化的重点,新的调度器逻辑得到了进一步的简化,例如:


  • 原 V0 架构的请求队列包括 waiting、running、swapped 三个队列,而 V1 只有 waiting 和 running 两个队列

  • 原来的 KV Cache 是由 BlockManager 管理的,V1 重构之后的代码在 KVCacheManager

  • V0 架构针对 prefill、chunk_prefill、decode 等不同类型的请求,都做不同的调度代码,V1 重构之后统一只有一份调度逻辑


所以阅读 V1 代码是比较简单的理解推理的调度过程,接下来让我们先看一下 V1 架构的入口:


# vllm/v1/engine/core.py
def run_busy_loop(self): """Core busy loop of the EngineCore."""
# Loop until process is sent a SIGINT or SIGTERM while True: # 1) Poll the input queue until there is work to do. self._process_input_queue() # 2) Step the engine core and return the outputs. self._process_engine_step()
复制代码


以上的 run_busy_loop 就是引擎驱动调度的起始位置了:



process_input_queue 负责接收请求,并将请求添加到 wating 队列中,调度器从 waiting 中提取请求,并将正在运行的请求追加到 running 中。请求的每一次执行完成,就会将输出部分追加到 output 队列中。



self.step_fn = (self.step if self.batch_queue is None else self.step_with_batch_queue) def _process_engine_step(self) -> bool: """Called only when there are unfinished local requests."""
# Step the engine core. outputs, model_executed = self.step_fn() # Put EngineCoreOutputs into the output queue. for output in (outputs.items() if outputs else ()): self.output_queue.put_nowait(output)
return model_executed
复制代码


step_fn 有两种实现:


  • step: 正常的调度是从这个函数走

  • step_with_batch_queue: 通过 PP 并行的调度,为了缓解流水线不同阶段之间的气泡时间,通过构建 batch 队列,提前准备好 batch


我们从 step 的代码进行分析,先搞清楚标准的调度逻辑是怎样运行的。

调度逻辑

调度逻辑就好比是发车员,每一辆车就是一个 batch,发车员从 runing 和 waiting 中带走乘客 (request),当列车装满之后,就发车。



V1 和 V0 的调度逻辑有一个很大的调整,就是从 prefill 优先改为了 decode 优先,这样是为了保障服务的连续性,让完成 prefill 阶段获得首字的请求优先处理,优化用户体验。当然高并发情况下,尾部那部分用户请求的首字延迟 TTFT 会变长。


一辆车的总位置是有限的,总共有 token_budget 个位置。所以发车员会优先发送持有 “一等票” (decode) 的旅客 (request),这些旅客占的位置比较少,每一个旅客只占用一个位置。只有持有一等票(decode)的旅客全部上车之后,才会发送持有二等票(prefill)的旅客。持有二等票的旅客占的位置多,一次要占多个位置(序列长度或者 chunk_size 的长度).


每个旅客都带有行李(KV Cache),行李空间是比较大的,能够放下很多行李,凡是持有一等票(running_queue)的旅客,行李都会被装到行李空间(running 队列中的请求会缓存 kv cache).

行李位(KV Cache block)也有用完的时候,因此每一个乘客上车之前,先要申请到行李位(token 占用的 block 空间), 只有行李位够的话,才能上车。


这里有一种情况,一旦行李车(block)空间不够了,那么就要进行抢占逻辑: 要让那些在拿着一等票的旅客(decode)去二等票(prefill)的队伍前面等着,他们的行李位(block 空间)也要释放出去.


抢占会一直进行,直到当前发车的乘客有足够的行李位了,才能正常发车。有一种极端情况,就是行李位太紧张了,即便所有的一等票的乘客全都被抢占了,也不够,那么这时这位乘客就不能上车,发车员直接发车。

调度逻辑分析

可以看到调度逻辑非常简单,但是他覆盖了很多的场景:


  1. Token 量化调度


token 作为了一种配额,prefill 请求和 decode 请求的区别仅在于所需的 token 数量不一样,而 chunk_prefill 的请求和正常的 prefill 请求也没什么区别,只是将一个过长的 request 截断,分多次发车。


  1. 优先级抢占


running_queue 有两种实现,一种是基于优先级的,一种是先入先出的。那么抢占逻辑发生的时候,就会根据这两种不同的策略,进行抢占。


从这一点看,vLLM 的调度在优先级和抢占层面是极度简单的,并且只在 KVCacheManager 无法分配 block 空间的时候才会出现,其他的正常工况下,不会发生优先级相关的业务逻辑。


  1. step 的调度和执行逻辑是同步的


这一点我在看代码的时候,本以为从调度的层面,提前调度好 batch,节省 CPU 的计算时间,那么 model.executor 这个应该是异步执行的,但是看代码发现并不是:


scheduler_output = self.scheduler.schedule()model_output = self.execute_model_with_error_logging(    self.model_executor.execute_model,  # type: ignore    scheduler_output)engine_core_outputs = self.scheduler.update_from_output(    scheduler_output, model_output)  # type: ignore
复制代码


如果是 step_with_batch_queue 的话,执行的环节就是异步执行的,但是要在启动流水线并行的情况下。主要作用似乎也不是为了单一的优化单次调度的 CPU 时间,而是在多个阶段之间减少气泡时间。


if not self.batch_queue.full():    scheduler_output = self.scheduler.schedule()    if scheduler_output.total_num_scheduled_tokens > 0:        future = self.model_executor.execute_model(scheduler_output)        self.batch_queue.put_nowait(            (future, scheduler_output))  # type: ignore
复制代码


流水线并行的调试相对麻烦一点,暂时还没有经过调试确认这部分的异步逻辑。


以上就是 V1 调度器的主要逻辑了,其中还有很多细节并没有去深究,例如调度器对于投机采样的处理、prefix_cache 的处理、chunk_prefill 的处理。这些后面再继续深入剖析了。


系列文章:

解析 vLLM 架构及源码系列 - 整体架构

解析 vLLM 架构及源码系列 - API Server

解析 vLLM 架构及源码系列:KVCache初始化之V1版本分析

发布于: 刚刚阅读数: 2
用户头像

Jason黄

关注

还未添加个人签名 2018-10-18 加入

云原生、AI Infra领域从业者

评论

发布
暂无评论
解析 vLLM 架构及源码系列:V1 调度器分析_vLLM源码_Jason黄_InfoQ写作社区