深入解析 vLLM:高性能 LLM 服务框架的架构之美(一)原理与解析
深入解析 vLLM:高性能 LLM 服务框架的架构之美(二)调度管理
1. vLLM 调度器结构与主要组件
在 vLLM 中,调度器的结构设计围绕任务的状态管理和内存资源的优化利用展开。为此,它将任务划分为不同的状态队列,并通过缓存管理和内存调度来保证任务处理的高效性。下面详细介绍调度器的核心组成部分:waiting、running 和 swapped 三个状态队列的作用及设计。
图 1: vLLM 调度器结构图
vLLM 调度器依赖三个状态队列来管理任务的不同阶段:
-
waiting 队列:waiting 队列用于存放新加入的任务或已完成预填充阶段但等待解码的任务。调度器会从 waiting 队列中取出任务,判断其是否可以分配资源,如果资源允许,则将其移至 running 队列。
-
running 队列:running 队列包含当前正在处理的任务,通常是在解码阶段。调度器会根据当前资源情况和任务优先级,决定是否抢占任务,将其暂时移出 GPU 以腾出资源。抢占的任务会被转移到 swapped 队列中。
-
swapped 队列:swapped 队列用于存放因资源不足而被暂时从 GPU 移至 CPU 的任务。调度器会定期检查 swapped 队列,如果资源允许,则将任务重新加载到 GPU,继续运行。
相关实现在 vllm/engine/scheduler.py
中。
2. 初始化与请求队列的管理
调度器的初始化和请求队列管理是 vLLM 高效运行的关键。这部分主要实现在 vllm/engine/scheduler.py
中的 Scheduler
类。
Scheduler 的初始化过程集中在解析配置项、创建 BlockSpaceManager 和初始化状态队列,以确保调度器能够动态、高效地管理多任务请求。初始化过程中涉及三个主要配置:
- scheduler_config:包含调度器行为相关的设置,如任务优先级、调度策略等。
- cache_config:定义内存分配策略、GPU 与 CPU 缓存的使用等。
- lora_config(可选):用于配置 LoRA 模型的特定要求,如并行度和内存需求。
# File: vllm/core/scheduler.py
class Scheduler:def __init__(self,scheduler_config: SchedulerConfig,cache_config: CacheConfig,lora_config: Optional[LoRAConfig] = None,pipeline_parallel_size: int = 1,output_proc_callback: Optional[Callable] = None,) -> None:self.scheduler_config = scheduler_configself.cache_config = cache_configself.lora_config = lora_config# 根据版本及配置选用不同的 BlockSpaceManagerversion = "selfattn" if not (scheduler_config.task == "embedding" or cache_config.is_attention_free) else "placeholder"BlockSpaceManagerImpl = BlockSpaceManager.get_block_space_manager_class(version)# 配置 GPU 和 CPU 块的数量num_gpu_blocks = cache_config.num_gpu_blocks // pipeline_parallel_sizenum_cpu_blocks = cache_config.num_cpu_blocks // pipeline_parallel_size# 初始化 BlockSpaceManager 以管理内存块self.block_manager = BlockSpaceManagerImpl(block_size=cache_config.block_size,num_gpu_blocks=num_gpu_blocks,num_cpu_blocks=num_cpu_blocks,sliding_window=cache_config.sliding_window,enable_caching=cache_config.enable_prefix_caching)# 初始化请求的状态队列self.waiting: Deque[SequenceGroup] = deque()self.running: Deque[SequenceGroup] = deque()self.swapped: Deque[SequenceGroup] = deque()# 用于记录完成请求的 IDself._finished_requests_ids: List[str] = []self.output_proc_callback = output_proc_callbackself.cache_id = 0self._async_stopped: List[SequenceGroup] = []
每个新请求在进入调度队列前需要被解析并转换成 SequenceGroup
实例。这一步骤的关键在于为请求分配适当的 token 数、调度优先级、资源需求等信息,然后将其分配至 waiting
、running
或 swapped
队列。
SequenceGroup
:一个请求可能包含多个序列(如生成任务的多个解码路径)。SequenceGroup
用于管理请求的多个序列,并跟踪该组序列的状态。add_seq_group
方法:将一个新请求包装为SequenceGroup
,并添加到 waiting 队列。
一个新请求在进入调度器后,会首先经过add_seq_group
的包装流程,生成SequenceGroup
实例,并加入 waiting 队列。假设一个请求需要生成文本的多种可能性,调度器会解析该请求并通过add_seq_group
将其加入等待队列,等待资源分配。
3. 调度流程与任务分配策略
vLLM 的调度系统设计堪称一件精妙的艺术品。它通过精心设计的状态转换机制,优雅地解决了大规模并发请求下的资源调度难题。在 vllm/engine/scheduler.py
的 Scheduler
类中,调度器通过 _schedule
方法作为总指挥,协调着整个系统的运转。
调度器会根据当前系统负载和请求特征,自适应地选择最优的调度策略。在标准场景下使用 _schedule_default
,而在需要处理大批量请求时,则会切换到专门优化的 _schedule_chunked_prefill
模式。这种灵活的策略选择确保了系统在不同负载下都能保持最佳性能。
整个调度过程可以优雅地分为三个阶段:
预填充阶段(Prefill Phase)
这是请求处理的第一个关键阶段。当新请求到达时,调度器首先评估其资源需求,包括所需的 KV Cache 空间和计算资源。系统会为请求生成初始的注意力缓存,为后续的解码做好准备。这个阶段就像是为任务搭建舞台,布置好所有必要的道具。
解码阶段(Decode Phase)
这是生成过程的核心阶段。调度器在这个阶段需要精确地分配计算资源,确保每个请求都能得到持续的服务。通过细粒度的资源管理,系统能够在保证响应速度的同时,最大化 GPU 利用率。解码阶段的任务会被放入 running 队列,接受调度器的持续监控和管理。
交换阶段(Swap Phase)
为了处理资源竞争,调度器实现了优雅的任务交换机制。当系统资源紧张时,调度器会识别优先级较低的任务,将它们暂时转移到 CPU 内存(swapped 队列),为急需资源的高优先级任务让路。这种动态调度机制确保了系统资源的最优利用,同时保证了服务质量。
这三个阶段通过精密的协作,构成了一个流畅的任务处理管道。调度器通过维护 waiting、running 和 swapped 三个队列的状态平衡,实现了类似操作系统页面调度的高效内存管理。每个阶段都配备了完善的监控和容错机制,确保了整个系统的稳定性和可靠性。
值得一提的是,vLLM 的调度系统还实现了优先级抢占、动态批处理等高级特性,这些机制共同保证了系统在高负载下依然能够提供稳定且高效的服务。整个调度过程就像一场精心编排的交响乐,每个组件都恰到好处地配合,共同奏响高性能服务的乐章。
3.1 scheduler 调度生命周期
图 2: vLLM 调度器状态队列示意图 5
3.2 _schedule_default调度逻辑
_schedule_default
方法是 vLLM 调度系统的指挥中心,它通过精心设计的流程来协调各类请求的处理。让我们逐步解析这个复杂而优雅的调度过程:
(1) 预算初始化与资源盘点
# File: vllm/core/scheduler.py
# 初始化调度预算
budget = SchedulingBudget(token_budget=self.scheduler_config.max_num_batched_tokens,max_num_seqs=self.scheduler_config.max_num_seqs,
)
# 统计当前运行任务的资源占用
for seq_group in self.running:budget.add_num_seqs(seq_group.request_id,seq_group.get_max_num_running_seqs())
这就像交通管理员在早高峰前先统计可用车道和当前道路占用情况。系统会创建一个预算对象,记录最大可处理的 token 数和序列数,并统计当前正在运行的任务占用的资源。
(2) 优先级调度策略
# File: vllm/core/scheduler.py
# 如果没有交换出的请求,则处理新的预填充请求
if not self.swapped:prefills = self._schedule_prefills(budget,curr_loras,enable_chunking=False)# 当启用优先级策略且无预填充任务时,执行优先级抢占
if len(prefills.seq_groups) == 0 and self.scheduler_config.policy == "priority":self._schedule_priority_preemption(budget)
这类似于交通管理系统的优先车道管理。系统优先处理已被交换出去的紧急任务,就像先让救护车通行。如果没有紧急任务,则开始处理新到达的请求。当采用优先级策略时,还会考虑是否需要让高优先级任务抢占资源。
(3) 解码任务调度
# File: vllm/core/scheduler.py
# 仅在无预填充任务时调度解码任务
if len(prefills.seq_groups) == 0:running_scheduled = self._schedule_running(budget,curr_loras,enable_chunking=False)# 仅在无抢占和交换出时尝试恢复交换任务if len(running_scheduled.preempted) + len(running_scheduled.swapped_out) == 0:swapped_in = self._schedule_swapped(budget, curr_loras)
这就像管理正常行驶的车辆。只有在处理完紧急情况后,系统才会调度常规的解码任务。如果道路通畅(没有任务被抢占或交换出),还会尝试让一些临时停靠的车辆(被交换出的任务)重新上路。
(4) 状态更新与队列管理
# File: vllm/core/scheduler.py
# 更新等待队列
self.waiting.extendleft(running_scheduled.preempted)
# 更新运行队列
if len(prefills.seq_groups) > 0:self.running.extend([s.seq_group for s in prefills.seq_groups])
self.running.extend(running_scheduled.decode_seq_groups_list)
# 更新交换队列
self.swapped.extend(running_scheduled.swapped_out)
这类似于实时更新交通状态。系统会更新各个队列的状态:被抢占的任务回到等待队列,新的任务进入运行队列,需要暂时让路的任务进入交换队列。
(5) 结果整合与输出
# File: vllm/core/scheduler.py
return SchedulerOutputs(scheduled_seq_groups=scheduled_seq_groups,num_prefill_groups=num_prefill_groups,num_batched_tokens=budget.num_batched_tokens + budget.num_cached_tokens,blocks_to_swap_in=swapped_in.blocks_to_swap_in,blocks_to_swap_out=running_scheduled.blocks_to_swap_out,...
)
最后,系统会生成一份完整的调度报告,包含了所有需要执行的操作:哪些任务要执行,哪些内存块需要交换,以及各类统计信息。这就像交通指挥中心生成的实时路况报告,为下一个调度周期提供决策依据。
通过这种精密的调度机制,vLLM 能够在保证公平性的同时,最大化系统的吞吐量和响应速度。整个过程就像一个训练有素的交通指挥系统,让每个请求都能得到恰当的处理,保证了整个服务的高效运转。
3.3 预填充阶段的精密编排
预填充阶段就像一个精心设计的入场管理系统,需要确保每个请求都能得到合适的处理。让我们通过几个关键步骤来理解这个过程:
(1) 初始检查与资源评估
# File: vllm/core/scheduler.py
waiting_seqs = seq_group.get_seqs(status=SequenceStatus.WAITING)
assert len(waiting_seqs) == 1, ("Waiting sequence group should have only one prompt sequence.")
num_new_tokens_uncached, num_new_tokens_cached = (self._get_num_new_uncached_and_cached_tokens(seq_group, SequenceStatus.WAITING, enable_chunking, budget))
这就像检票员在入场时检查门票。系统首先确认每个等待组只包含一个提示序列,然后计算需要处理的新token数量,区分哪些需要重新计算(uncached),哪些可以直接使用缓存(cached)。
(2) 容量限制检查
# File: vllm/core/scheduler.py
prompt_limit = self._get_prompt_limit(seq_group)
if num_new_tokens > prompt_limit:logger.warning("Input prompt (%d tokens) is too long"" and exceeds limit of %d", num_new_tokens, prompt_limit)for seq in waiting_seqs:seq.status = SequenceStatus.FINISHED_IGNOREDignored_seq_groups.append(seq_group)waiting_queue.popleft()continue
这类似于场馆的容量管理。如果一个请求太大(token数超过限制),就像一个太大的团队无法进入会场一样,系统会将其标记为"已忽略",并移出等待队列。
(3) 内存分配评估
# File: vllm/core/scheduler.py
can_allocate = self.block_manager.can_allocate(seq_group, num_lookahead_slots=num_lookahead_slots)
if can_allocate == AllocStatus.LATER:break
elif can_allocate == AllocStatus.NEVER:logger.warning("Input prompt (%d tokens) + lookahead slots (%d) is too long",num_new_tokens, num_lookahead_slots)
这就像检查场地是否有足够的座位。系统会评估是否有足够的内存块来容纳这个请求。如果暂时没有空间(LATER),就等待下次尝试;如果永远无法容纳(NEVER),则将请求标记为无法处理。
(4) LoRA 资源管理
# File: vllm/core/scheduler.py
if self.lora_enabled:if (lora_int_id > 0 and lora_int_id not in curr_lorasand len(curr_loras) >= self.lora_config.max_loras):leftover_waiting_sequences.appendleft(seq_group)waiting_queue.popleft()continue
这类似于特殊设备的分配管理。如果请求需要使用LoRA(一种特殊的模型适配技术),系统会检查是否还有可用的LoRA槽位。如果没有,该请求会被暂时搁置。
(5) 预算控制
# File: vllm/core/scheduler.py
if (budget.num_batched_tokens >= self.scheduler_config.max_num_batched_tokens):breaknum_new_seqs = seq_group.get_max_num_running_seqs()
if num_new_tokens_uncached == 0 or not budget.can_schedule(num_new_tokens=num_new_tokens_uncached,num_new_seqs=num_new_seqs,
):break
这就像控制场地的实时容量。系统会严格监控已分配的资源是否达到上限,包括总token数和序列数,确保不会超出系统的处理能力。
(6) 资源分配与状态转换
# File: vllm/core/scheduler.py
waiting_queue.popleft()
self._allocate_and_set_running(seq_group)seq_groups.append(ScheduledSequenceGroup(seq_group=seq_group,token_chunk_size=num_new_tokens))
budget.add_num_batched_tokens(seq_group.request_id,num_batched_tokens=num_new_tokens_uncached,num_cached_tokens=num_new_tokens_cached,
)
这是最后的入场阶段。当所有检查都通过后,系统会正式为请求分配资源,将其状态更新为"运行中",并记录相应的资源使用情况。
(7) passed_delay均衡
模型在做推理时,waiting 队列中是源源不断有 seq_group 进来的,一旦 vLLM 选择调度 waiting 队列,它就会停下对 running/swapped
中 seq_group
的 decode 处理,转而去做 waiting 中 seq_group
的 prefill,也即vLLM 必须在新来的 seq_group
和已经在做推理的 seq_group
间取得一种均衡:既不能完全不管新来的请求,也不能耽误正在做推理的请求。_passed_delay
就是用来做这个判断的。
# File: vllm/core/scheduler.py
def _passed_delay(self, now: float) -> bool:if self.prev_prompt:self.last_prompt_latency = now - self.prev_timeself.prev_time, self.prev_prompt = now, Falseif self.scheduler_config.delay_factor > 0 and self.waiting:earliest_arrival_time = min([e.metrics.arrival_time for e in self.waiting])passed_delay = ((now - earliest_arrival_time) > (self.scheduler_config.delay_factor * self.last_prompt_latency)or not self.running)else:passed_delay = Truereturn passed_delay
prev_prompt 和 prev_time 用于记录上一个请求的执行时间,从而动态计算 last_prompt_latency 以调整调度速率。这种延迟控制机制可以防止过度调度,减少 GPU 负载。
当延迟条件满足时,_schedule_prefills 会获取 waiting 队列中第一个请求的 token 数量,并通过 _get_num_new_tokens 方法来确定这个请求所需的 token 数量。随后,该 token 数量与 _get_prompt_limit 限制值进行比较,以确保请求不会超出模型的最大 token 负载。
# File: vllm/core/scheduler.py
num_new_tokens = self._get_num_new_tokens(seq_group, SequenceStatus.WAITING, enable_chunking, budget)
if num_new_tokens > self._get_prompt_limit(seq_group):ignored_seq_groups.append(seq_group)self.waiting.popleft()continue
若 can_allocate 返回 AllocStatus.LATER,表明当前 GPU 资源不足,调度器将等待下次机会;若返回 AllocStatus.NEVER,则说明该请求太大无法分配,调度器将忽略该请求并将其移出等待队列。
(8) 资源分配与状态转换
# File: vllm/core/scheduler.py
def _allocate_and_set_running(self, seq_group: SequenceGroup) -> None:self.block_manager.allocate(seq_group)for seq in seq_group.get_seqs(status=SequenceStatus.WAITING):seq.status = SequenceStatus.RUNNING
allocate 函数通过 block_manager 的内存管理功能,为请求在 GPU 上分配所需的内存空间,从而确保其可以顺利进入解码阶段。
通过这种细致的分层管理,vLLM能够高效地处理大量并发请求,既确保了资源的最优利用,又保证了每个请求都能得到公平的处理机会。整个预填充阶段就像一个精密的入场管理系统,通过多重检查和平衡机制,确保系统的稳定运行和最佳性能。
3.4 解码阶段的精密调度
解码阶段是 vLLM 处理请求的核心环节,就像一条高效的生产线,需要精确地控制每个环节的运转。让我们深入理解这个复杂而优雅的过程:
(1) 初始化与缓存清理
# File: vllm/core/scheduler.py
ret: SchedulerRunningOutputs = \self._scheduler_running_outputs_cache[self.cache_id].get_object()
ret.blocks_to_swap_out.clear()
ret.blocks_to_copy.clear()
ret.decode_seq_groups.clear()
ret.prefill_seq_groups.clear()
这就像生产线开始新的班次前的准备工作。系统会清理所有临时状态,为新一轮调度做好准备。通过使用对象池(cache)来复用对象,避免频繁创建新对象带来的性能开销。
(2) 资源评估与任务获取
# File: vllm/core/scheduler.py
running_queue = self.running
while running_queue:seq_group = running_queue[0]num_uncached_new_tokens, _ = (self._get_num_new_uncached_and_cached_tokens(seq_group, SequenceStatus.RUNNING, enable_chunking, budget))
这类似于生产线上的任务评估。系统会检查每个正在运行的任务组,评估其资源需求。特别的是,在解码阶段我们不需要考虑缓存的 tokens,因为它们在预填充阶段就已经处理完毕。
(3) 异步处理保护
# File: vllm/core/scheduler.py
if self.use_async_output_proc and seq_group.seqs[0].get_len() > self.scheduler_config.max_model_len:self._async_stopped.append(seq_group)continue
这就像生产线上的安全检查点。当使用异步处理器时,系统会检查序列长度是否超出限制,以防止内存溢出。这种保护机制确保了系统的稳定性。
(4) 资源竞争处理
# File: vllm/core/scheduler.py
while not self._can_append_slots(seq_group, enable_chunking):# 确定牺牲者序列if running_queue:victim_seq_group = running_queue.pop() # 选择优先级最低的序列组else:victim_seq_group = seq_group # 没有其他可抢占的序列,抢占当前序列cont_loop = False
这类似于生产线上的资源调度。当资源不足时,系统需要决定哪些任务需要暂时让出资源。它会优先选择优先级较低的任务组作为"牺牲者",就像在繁忙时段将部分生产线暂时切换到更紧急的订单。
(5) 抢占执行与状态转换
# File: vllm/core/scheduler.py
if do_preempt:preempted_mode = self._preempt(victim_seq_group, blocks_to_swap_out)if preempted_mode == PreemptionMode.RECOMPUTE:preempted.append(victim_seq_group)else:swapped_out.append(victim_seq_group)
这就像生产线上的任务切换。系统会根据具体情况决定是将任务暂存(swap out)还是标记为需要重新计算(recompute)。这种灵活的处理机制确保了资源的最优利用。
(6) 任务调度与资源分配
# File: vllm/core/scheduler.py
self._append_slots(seq_group, blocks_to_copy, enable_chunking)
scheduled_seq_group.seq_group = seq_group
if is_prefill:scheduled_seq_group.token_chunk_size = num_running_tokensprefill_seq_groups.append(scheduled_seq_group)
else:scheduled_seq_group.token_chunk_size = 1decode_seq_groups.append(scheduled_seq_group)
这类似于生产线上的实际加工过程。系统会为任务分配所需的资源槽位,并根据任务类型(预填充或解码)设置不同的处理参数。解码任务通常每次只处理一个 token,而预填充任务可能会批量处理多个 tokens。
(7) 资源统计更新
# File: vllm/core/scheduler.py
budget.add_num_batched_tokens(seq_group.request_id, num_running_tokens)
if enable_chunking:num_running_seqs = seq_group.get_max_num_running_seqs()budget.add_num_seqs(seq_group.request_id, num_running_seqs)
这就像生产线上的资源使用统计。系统会实时更新资源使用情况,包括已批处理的 tokens 数量和运行中的序列数。当启用分块处理时,还需要额外更新序列数统计。
通过这种精密的调度机制,vLLM 能够在保证公平性的同时,最大化系统的吞吐量和响应速度。整个解码阶段就像一条精心设计的智能生产线,通过灵活的资源调度和任务管理,确保了系统的高效运转。
3.5 交换阶段的精密调度
交换阶段是 vLLM 内存管理的关键环节,就像一个智能仓储系统,需要在 CPU 和 GPU 内存之间灵活调度资源。让我们深入理解这个复杂而精密的过程:
(1) 初始化与资源准备
# File: vllm/core/scheduler.py
blocks_to_swap_in: List[Tuple[int, int]] = []
blocks_to_copy: List[Tuple[int, int]] = []
decode_seq_groups: List[ScheduledSequenceGroup] = []
prefill_seq_groups: List[ScheduledSequenceGroup] = []
infeasible_seq_groups: List[SequenceGroup] = []
这就像仓库管理员准备工作清单。系统会初始化各种任务列表,包括需要从 CPU 加载到 GPU 的内存块、需要复制的数据块,以及不同类型的任务组。
(2) 可行性评估
# File: vllm/core/scheduler.py
is_prefill = seq_group.is_prefill()
alloc_status = self.block_manager.can_swap_in(seq_group,self._get_num_lookahead_slots(is_prefill, enable_chunking))
if alloc_status == AllocStatus.LATER:break
elif alloc_status == AllocStatus.NEVER:logger.warning("Failing the request %s because there's not enough kv cache blocks",seq_group.request_id)
这类似于仓库管理员评估货物存放可能性。系统会检查是否有足够的 GPU 内存来容纳被交换出的任务。如果暂时没有空间(LATER)就等待下次机会,如果永远无法容纳(NEVER)则将任务标记为无法执行。
(3) LoRA 资源检查
# File: vllm/core/scheduler.py
if self.lora_enabled:if (lora_int_id > 0 and (lora_int_id not in curr_loras)and len(curr_loras) >= self.lora_config.max_loras):leftover_swapped.appendleft(seq_group)swapped_queue.popleft()continue
这就像检查特殊设备的可用性。如果任务需要使用 LoRA 适配器,系统会检查是否有可用的 LoRA 槽位。如果没有,该任务会被暂时保留在交换队列中。
(4) 资源预算评估
# File: vllm/core/scheduler.py
num_new_seqs = seq_group.get_max_num_running_seqs()
num_new_tokens_uncached, num_new_tokens_cached = (self._get_num_new_uncached_and_cached_tokens(seq_group, SequenceStatus.SWAPPED, enable_chunking, budget))if num_new_tokens_uncached == 0 or not budget.can_schedule(num_new_tokens=num_new_tokens_uncached,num_new_seqs=num_new_seqs):break
这类似于评估仓库的剩余容量。系统会计算任务所需的资源,包括新序列数量和token数量,确保在预算范围内才进行调度。
(5) 执行交换操作
# File: vllm/core/scheduler.py
swapped_queue.popleft()
self._swap_in(seq_group, blocks_to_swap_in)
self._append_slots(seq_group, blocks_to_copy, enable_chunking)
这就像实际执行货物搬运。系统会将任务从 CPU 内存移回 GPU,并为其分配必要的计算资源。这个过程包括数据传输和内存分配两个关键步骤。
(6) 任务分类与资源记录
# File: vllm/core/scheduler.py
if is_prefill:prefill_seq_groups.append(ScheduledSequenceGroup(seq_group,token_chunk_size=num_new_tokens_uncached + num_new_tokens_cached,))
else:decode_seq_groups.append(ScheduledSequenceGroup(seq_group, token_chunk_size=1))
这类似于货物分类入库。系统会根据任务类型(预填充或解码)将其放入相应的处理队列,并设置适当的处理参数。
(7) 资源统计更新
# File: vllm/core/scheduler.py
budget.add_num_batched_tokens(seq_group.request_id,num_batched_tokens=num_new_tokens_uncached,num_cached_tokens=num_new_tokens_cached,
)
budget.add_num_seqs(seq_group.request_id, num_new_seqs)
这就像更新仓库库存记录。系统会更新资源使用统计,包括已使用的token数量和序列数,确保资源使用始终在可控范围内。
通过这种精密的交换机制,vLLM 实现了 CPU 和 GPU 内存之间的高效协同,既保证了资源的充分利用,又确保了任务的连续性。整个交换阶段就像一个智能仓储系统,通过灵活的调度策略,实现了计算资源的最优配置。
4. 资源管理与调度策略
4.1 预算管理系统设计
SchedulingBudget 类是 vLLM 资源管理的核心组件,它就像一个精密的预算管理系统,负责追踪和控制系统的资源使用。让我们深入理解它的设计:
(1) 核心资源指标
# File: vllm/core/scheduler.py
@dataclass
class SchedulingBudget:token_budget: int # 总的token预算max_num_seqs: int # 最大序列数限制_num_cached_tokens: int = 0 # 已缓存的token数_num_batched_tokens: int = 0 # 实际批处理的token数_num_curr_seqs: int = 0 # 当前序列数
这就像企业的预算系统,定义了关键的资源限制:总预算(token_budget)、人员限制(max_num_seqs),以及当前的资源使用状况。系统通过这些指标来确保资源使用不会超出限制。
(2) 请求追踪机制
_request_ids_num_batched_tokens: Set[str] = field(default_factory=set)
_request_ids_num_curr_seqs: Set[str] = field(default_factory=set)
这类似于预算系统中的交易记录追踪。系统使用集合来记录已经处理过的请求ID,避免重复计算同一个请求的资源消耗。这种设计特别适合处理分布式环境下的资源统计。
(3) 调度可行性检查
# File: vllm/core/scheduler.py
def can_schedule(self, *, num_new_tokens: int, num_new_seqs: int):assert num_new_tokens >= 0assert num_new_seqs != 0return (self.num_batched_tokens + num_new_tokens <= self.token_budgetand self.num_curr_seqs + num_new_seqs <= self.max_num_seqs)
这就像预算审批过程。在添加新的任务前,系统会检查是否有足够的资源来处理它。检查包括两个方面:
- token数量是否在预算范围内
- 序列数是否超过系统限制
(4) 资源分配与回收
# File: vllm/core/scheduler.py
def add_num_batched_tokens(self, req_id: str,num_batched_tokens: int,num_cached_tokens: int = 0):if req_id in self._request_ids_num_batched_tokens:returnself._request_ids_num_batched_tokens.add(req_id)self._num_batched_tokens += num_batched_tokensself._num_cached_tokens += num_cached_tokensdef subtract_num_batched_tokens(self, req_id: str, num_batched_tokens: int):if req_id in self._request_ids_num_batched_tokens:self._request_ids_num_batched_tokens.remove(req_id)self._num_batched_tokens -= num_batched_tokens
这类似于预算的分配和回收流程:
- add_num_batched_tokens:为新请求分配资源,同时记录请求ID避免重复计算
- subtract_num_batched_tokens:在请求完成或被中断时回收资源
(5) 序列数管理
# File: vllm/core/scheduler.py
def add_num_seqs(self, req_id: str, num_curr_seqs: int):if req_id in self._request_ids_num_curr_seqs:returnself._request_ids_num_curr_seqs.add(req_id)self._num_curr_seqs += num_curr_seqsdef subtract_num_seqs(self, req_id: str, num_curr_seqs: int):if req_id in self._request_ids_num_curr_seqs:self._request_ids_num_curr_seqs.remove(req_id)self._num_curr_seqs -= num_curr_seqs
这就像人力资源管理系统,负责跟踪当前正在处理的序列数量:
- 添加新序列时会检查是否已经计入统计
- 移除序列时会相应减少计数
(6) 资源状态查询
# File: vllm/core/scheduler.py
@property
def num_batched_tokens(self):return self._num_batched_tokens@property
def remaining_token_budget(self):return self.token_budget - self.num_batched_tokens
这类似于预算系统的报表功能,提供了查询当前资源使用状况的接口:
- 已使用的token数量
- 剩余可用的token预算
- 当前序列数等关键指标
通过这种精密的预算管理机制,vLLM 能够准确地追踪和控制系统资源的使用情况,确保系统在高负载下依然能够稳定运行。整个预算管理系统就像一个高效的企业财务系统,通过严格的资源控制和灵活的分配策略,实现了计算资源的最优利用。
4.2 优先级调度和抢占策略的精密实现
优先级调度和抢占机制是 vLLM 处理资源竞争的核心策略,让我们深入理解这个精密的调度过程:
(1) 队列初始化与优先级排序
waiting_queue = self.waiting
running_queue = deque(sorted(self.running, key=self._get_priority))
blocks_to_swap_out: List[Tuple[int, int]] = []
force_preemption_count = 0
这就像应急指挥中心的初始准备工作:
- 获取当前等待的请求队列
- 将运行中的任务按优先级排序
- 准备记录需要暂时撤离的资源
- 初始化强制抢占计数器
(2) 优先级反转检测
if waiting_queue:seq_group = waiting_queue.popleft()num_new_seqs = seq_group.get_max_num_running_seqs()num_new_tokens_uncached, _ = (self._get_num_new_uncached_and_cached_tokens(seq_group, SequenceStatus.WAITING, False, budget))
这类似于应急响应中的情况评估:
- 检查是否有等待中的请求
- 评估新请求的资源需求(序列数和token数)
- 准备进行优先级比较
(3) 抢占条件评估
# File: vllm/core/scheduler.py
while running_queue and self._get_priority(running_queue[-1]) > self._get_priority(seq_group):can_allocate = self.block_manager.can_allocate(seq_group)if (num_new_tokens_uncached > 0and can_allocate == AllocStatus.OKand budget.can_schedule(num_new_tokens=num_new_tokens_uncached,num_new_seqs=num_new_seqs,)):break
这就像应急处理中的资源调配决策:
- 检查是否存在优先级反转(低优先级任务占用资源而高优先级任务等待)
- 评估是否有足够的资源来处理新请求
- 如果资源充足,则无需进行抢占
(4) 受害者选择与资源回收
# File: vllm/core/scheduler.py
vseq_group = running_queue.pop()
num_running_tokens_uncached, _ = (self._get_num_new_uncached_and_cached_tokens(vseq_group, SequenceStatus.RUNNING, False, budget))
budget.subtract_num_batched_tokens(vseq_group.request_id, num_running_tokens_uncached)
num_running_seqs = vseq_group.get_max_num_running_seqs()
budget.subtract_num_seqs(vseq_group.request_id, num_running_seqs)
这类似于应急情况下的资源重新分配:
- 选择优先级最低的运行中任务作为"受害者"
- 计算需要回收的资源数量
- 从预算中扣除这些资源
- 更新序列计数
(5) 抢占执行与状态转换
self._preempt(vseq_group, blocks_to_swap_out)
waiting_queue.appendleft(vseq_group)
force_preemption_count += 1
这就像执行应急预案:
- 执行实际的抢占操作,将任务从 GPU 移出
- 将被抢占的任务放回等待队列前端
- 记录抢占次数以进行监控
(6) 队列重排序与状态更新
waiting_queue.appendleft(seq_group)
waiting_queue = deque(sorted(waiting_queue, key=self._get_priority))
self.waiting = waiting_queue
self.running = running_queue
这类似于应急处理后的现场恢复:
- 将触发抢占的请求放回等待队列
- 重新按优先级排序所有等待的请求
- 更新系统的队列状态
通过这种精密的优先级调度和抢占机制,vLLM 能够在资源竞争激烈的情况下,保证高优先级任务的及时处理,同时尽可能减少对低优先级任务的影响。整个机制就像一个高效的应急响应系统,通过合理的资源调配和任务管理,确保了系统的公平性和效率。
4.3 抢占机制的精密实现
vLLM 的抢占机制提供了两种处理模式:重新计算(Recompute)和交换(Swap)。这种灵活的设计让系统能够根据不同场景选择最优的处理策略。让我们深入理解这个精密的抢占过程:
(1) 抢占模式选择
if self.user_specified_preemption_mode is None:if seq_group.get_max_num_running_seqs() == 1:preemption_mode = PreemptionMode.RECOMPUTEelse:preemption_mode = PreemptionMode.SWAP
这就像应急预案的选择过程:
- 默认情况下,系统会根据序列组的特征自动选择最优策略
- 对于单序列任务,优先选择重新计算模式,因为它的开销较小
- 对于多序列任务(如束搜索),由于当前不支持重新计算,会选择交换模式
(2) 用户自定义模式处理
elif self.user_specified_preemption_mode == "swap":preemption_mode = PreemptionMode.SWAP
else:preemption_mode = PreemptionMode.RECOMPUTE
这类似于应急响应中的人工干预:
- 系统允许用户指定特定的抢占模式
- 如果用户明确要求使用交换模式,系统会遵循这个选择
- 其他情况下默认使用重新计算模式
(3) 性能监控与警告机制
if self.num_cumulative_preemption % 50 == 0:logger.warning("Sequence group %s is preempted by %s mode because there is ""not enough KV cache space. This can affect the end-to-end ""performance. Increase gpu_memory_utilization or ""tensor_parallel_size to provide more KV cache memory. ""total_num_cumulative_preemption=%d", seq_group.request_id,preemption_mode, self.num_cumulative_preemption + 1)
这就像系统的健康监控:
- 每50次抢占操作会触发一次警告
- 提醒管理员系统可能需要调整资源配置
- 建议增加GPU内存利用率或张量并行度来缓解抢占压力
- 记录累计抢占次数以便跟踪系统状态
(4) 抢占执行
if preemption_mode == PreemptionMode.RECOMPUTE:self._preempt_by_recompute(seq_group)
elif preemption_mode == PreemptionMode.SWAP:self._preempt_by_swap(seq_group, blocks_to_swap_out)
else:raise AssertionError("Invalid preemption mode.")
这类似于应急预案的实际执行:
- 重新计算模式:放弃当前进度,后续需要从头计算
- 交换模式:将数据暂存到CPU内存,保留计算进度
- 严格的错误检查确保只执行有效的抢占模式
通过这种精密的抢占机制,vLLM 实现了资源的灵活调度:
- 通过自动选择最优抢占模式,最小化性能开销
- 提供用户自定义选项,满足特定场景需求
- 内置监控机制,及时发现系统瓶颈
- 支持多种抢占策略,适应不同的任务特征
这种设计就像一个智能的应急处理系统,既能自动选择最优方案,又保留了人工干预的可能,同时通过持续监控确保系统的健康运行。
5. vLLM 调度系统总结
vLLM 的调度系统是一个精心设计的多层次架构,通过多个协同工作的组件实现了高效的资源管理和任务调度。让我们从整体到细节来回顾这个系统。
5.1 核心架构设计
调度系统的基础建立在三个关键队列之上。waiting 队列负责存放新到达的请求,running 队列管理正在执行的任务,而 swapped 队列则暂存被交换出的任务。这种三队列设计为系统提供了灵活的任务状态管理能力,使其能够有效应对各种负载情况。
5.2 调度流程的精密编排
整个调度过程分为三个主要阶段,每个阶段都有其独特的职责。在预填充阶段,系统会对新请求进行资源评估,检查容量限制和内存可用性,同时进行 LoRA 资源管理,最后执行预算控制和资源分配。
进入解码阶段后,系统的重点转向管理运行中任务的资源使用。这包括处理异步任务和长度限制,执行资源竞争处理,以及维护任务状态和资源统计。
在交换阶段,系统负责在 CPU 和 GPU 内存间进行任务迁移。它会评估交换操作的可行性,执行数据传输和资源重分配,并及时更新系统状态和资源记录。
5.3 资源管理的创新机制
系统通过 SchedulingBudget 类实现了精确的资源控制。这个类不仅维护着 token 和序列数预算,还负责追踪请求级别的资源使用。它提供了完整的资源分配和回收机制,并通过实时监控确保系统资源的高效利用。
5.4 优先级调度与抢占策略
为了处理资源竞争,系统实现了两种抢占模式。Recompute 模式适用于单序列任务,因其开销较小而被优先选用。而 Swap 模式则适用于多序列任务,它能够保留计算进度,虽然开销较大但更适合某些特定场景。
这种灵活的抢占机制确保了高优先级任务能够及时获得资源,同时也保证了系统资源的最优利用和任务处理的连续性。通过精心的设计,系统在处理资源竞争时既保证了效率,又维护了公平性。
5.5 性能优化与监控
在性能优化方面,系统采用了多项创新机制。对象池的复用有效减少了内存分配开销,延迟控制机制避免了过度调度,而批处理优化则显著提高了系统吞吐量。同时,完善的实时监控和警告机制确保了系统运行的稳定性。
通过这些精密的设计,vLLM 的调度系统实现了资源的高效利用、任务的灵活管理、服务的可靠保障和性能的持续优化。这种多层次、多维度的调度架构,使 vLLM 能够在处理大规模并发请求时保持稳定高效的性能,为 LLM 服务提供了可靠的基础设施支持。