上一篇文章中我们介绍了 vLLM 调度器的预处理工作,我们已经知道了在进行调度之前,数据会被预处理然后用 SequenceGroup 对象进行封装。在这篇文章中,我们将会介绍 vLLM 调度器的具体调度策略。

本系列的代码基于 vLLM 的 0.6.3 版本介绍

vLLM 调度器是一个专为优化生成式模型而设计的调度器。与普通的任务调度器相比,它不仅要管理多个请求的排队和处理,还要动态分配 GPU 和 CPU 内存、控制数据的加载与交换,确保系统在不同负载下均能高效运行

本文将通过详细的源码解析,逐步带领读者深入理解 vLLM 调度器的设计与实现。我们将从基础概念、核心组件、调度策略以及资源管理等多方面展开,通过实例和源码逐步揭示调度器如何在实际应用中高效管理请求

1. 基础概念与背景

1.1 调度的定义与作用

在生成式模型的运行过程中,调度器的核心任务是高效地管理资源(主要是 GPU 和 CPU 内存)和控制请求的执行顺序。生成式模型通常需要处理多个并发请求,特别是在大规模部署环境中,这些请求可能来自不同的用户或任务,并且拥有不同的处理需求和优先级。

vLLM 调度器的设计旨在优化多任务环境中的 GPU 和 CPU 资源利用率。调度器通过以下几种机制来实现该目标:

  1. 优先级调度:为请求分配不同的优先级,确保关键任务或延迟敏感任务优先处理。
  2. 资源预算控制:定义了每一轮调度中允许的最大内存消耗和任务数量,以防止资源过载。
  3. 抢占策略:当高优先级请求进入系统而资源不足时,调度器可以选择暂停或中止低优先级任务来腾出资源。

1.2 请求类型:预填充和解码

vLLM 调度器主要处理两种类型的请求:

  1. 预填充(Prefill):指生成请求的初始阶段,其中模型需要处理用户输入并生成初始的上下文嵌入。预填充任务通常包含较大的计算量,因为它涉及模型权重和输入数据的第一次计算。
  2. 解码(Decode):在初始的预填充阶段完成后,解码任务会根据预填充的上下文生成实际的输出。这一过程包含模型的逐步推理,生成新单词或 token。解码任务通常是增量计算,因为每个步骤的生成都基于前一步的结果。

调度器会根据请求类型的不同,将它们放置在相应的队列中并进行优先级控制。通过对这两种请求进行分离处理,调度器可以更好地管理模型的生成过程。

1.3 优先级调度

在多任务环境中,调度器为每个请求分配优先级。在 vLLM 中,优先级主要由任务的重要性和到达时间决定。优先级调度确保高优先级的任务不被低优先级的任务阻塞,以便资源分配更加合理。

def _get_priority(self, seq_group: SequenceGroup) -> Tuple[Optional[int], float]:
    """ 获取 SequenceGroup 的优先级:首先根据用户定义的优先级值,其次按到达时间排序。 """    return seq_group.priority, seq_group.arrival_time

上面的代码段展示了调度器如何获取 SequenceGroup 的优先级:首先考虑用户指定的优先级值,其次按照到达时间排序。这一逻辑帮助调度器将高优先级任务快速安排到合适的计算队列中。