vLLM V1 Scheduler 源码学习

541 阅读3分钟

版本:vLLM v0.8.3

代码链接

引言

为什么我们要学习V1 Scheduler? 在回答这个问题之前,我们要知道vLLM V0和V1的不同,V0集成了大量社区贡献的Feature,随之而来是不断集成的复杂性,一定程度上影响了框架性能和后续开发(包括用户自行扩展)的便捷性。为此,vLLM团队在25年1月发布了vLLM V1,致力于提成性能和降低代码复杂度。其中,主要重构对象为 scheduler,KV cache manager,worker,sampler,和 API server。

相比V0的Scheduler,V1大大精简代码,从两千多行减少至六百多行,更易于学习和扩展。接下来由笔者带大家一起浅析V1 Scheduler的具体设计与实现。

从功能开始

V1 Scheduler用于支持Chunked prefills (aka Dynamic SplitFuse)以及前缀缓存 (prefix caching)。

v1 chuncked prefill

(图片来自vLLM V1 Blog)

Chunked Prefills

Chuncked prefills允许在一个步骤中灵活地处理不同的请求。考虑一个同时处理上图所示的三个请求,R1,R2和R3,其中R3很长而R1和R2很短,假设我们一步只能处理10个token的prefill或者decode,如果全部prefill完成再进行decode,无法很好利用资源。而Chunked prefills使得我们在Step 0(第一步)中完成R1,R2以及R3前两个token的prefill。随后在Step 1(第二步)中开始R1和R2的decode并继续R3的prefill。

Prefix Caching

Prefix Caching使我们能够跳过cache中已有的前缀的处理。

v1 prefix caching

(图片来自vLLM V1 Meetup Slides)

实现

在Scheduler中,请求有不同的状态,RUNNINGWAITING

  • RUNNING: 处于运行状态的请求,在self.running中,
        while req_index < len(self.running) and token_budget > 0:
        # 1. 如果已经schedule,跳过
        # 2. 根据num_tokens_with_spec (len(prompt_token_ids) + len(output_token_ids) + len(spec_token_ids)),schedule token数量为
        #    min(num_tokens_with_spec - num_computed_tokens, long_prefill_token_threshold, token_budget)
        # 3. 如果有encoder输入,schedule encoder输入
        # 4. 分配block,如果没有足够资源,抢占running中的最低优先级请求,也就是最后的请求,然后标记这个最后的请求为PREEMPTED,加到waiting队列的左侧,也就是waiting中给最高优先级,如果没有请求可以抢占了,标记can_schedule为false
  • WAITING: 处于等待状态的请求,在self.waiting的尾部,
        while self.waiting and token_budget > 0:
        # 1. 如果为结构化输出请求,且FSM编译未完成,跳过该请求
        # 2. 检查LoRA配置,如果超过max_loras,跳过该请求
        # 3. 检查已经缓存的token,num_compted_tokens即为sequence中已缓存的token数,
        #    根据num_tokens (len(prompt_token_ids) + len(output_token_ids)),schedule token数量为
        #    min(num_tokens - num_computed_tokens, long_prefill_token_threshold, token_budget)
        # 4. 如果有encoder输入,schedule encoder输入
        # 5. 分配block,如果没有足够资源,跳过
        # 6. 更新请求,转为RUNNING
  • PREEMPTED: 处于被抢占状态的请求,在self.waiting的头部。

补充说明

一个比较巧妙的地方是使用下面的式子来计算一个请求要schedule的token数量

num_tokensnum_computed_tokens=len(prompt_token_ids)+len(output_token_ids)num_computed_tokensnum\_tokens - num\_computed\_tokens = len(prompt\_token\_ids) + len(output\_token\_ids) - num\_computed\_tokens

由于vLLM V1不再显式区分prefill和decode,因此不是判断一个请求是不是在decode,然后直接设置schedule的token数量为1。而是利用一旦完成prefill,一定会有一个output_token_id不在context中来自动确保对于处在decode的请求,schedule的token数量一定为1。