vLLM/V1 推理调度器

在 token 预算和 KV 容量内连续安排请求

vLLM V1 每轮分配 token 预算,先继续运行中的请求,再尝试接纳等待者;KV 块不足时抢占请求,把它放回等待队列。

v0.8.5调度与资源分配核验 2026-10-11

工程问题#

生成请求的输入和输出长度不同,KV 缓存又持续增长。固定一批请求等到全部结束会留下空闲位置;只看 token 数接纳新请求,则可能超过可用 KV 空间。长提示词还可能吃完一轮计算预算。

实际实现#

Scheduler.schedule 统一计算每个请求尚需计算的 token 数,以本轮剩余预算、长预填充阈值和模型长度截断。KVCacheManager.allocate_slots 将 token 数换算成物理块需求,并扣除不能同时算作空闲的缓存命中块。若运行请求分配失败,调度器从 running 尾部取出请求、释放块并清零已计算计数,放回 waiting 头部;没有发生抢占时,再以剩余预算接纳等待请求。

处理流程#

  1. 每轮以 max_num_scheduled_tokens 初始化预算,先遍历 running 请求
  2. 计算尚需处理的 token,并按剩余预算、长预填充阈值和模型长度截断
  3. 为本轮 token 与必要 lookahead 申请 KV 槽,确认块容量足够后才记录调度
  4. 空间不足时抢占 running 尾部请求,释放 KV 引用,把请求放回 waiting 头部
  5. 本轮未抢占时,再尝试从 waiting 接纳请求;登记调度 token 数并扣减预算

源码片段#

固定版本 · 原文摘录
vllm/v1/core/sched/scheduler.pyv0.8.5 · L174–L191
查看原文件
174        # First, schedule the RUNNING requests.175        req_index = 0176        while req_index < len(self.running) and token_budget > 0:177            request = self.running[req_index]178179            num_new_tokens = (request.num_tokens_with_spec -180                              request.num_computed_tokens)181            if (0 < self.scheduler_config.long_prefill_token_threshold <182                    num_new_tokens):183                num_new_tokens = (184                    self.scheduler_config.long_prefill_token_threshold)185            num_new_tokens = min(num_new_tokens, token_budget)186187            # Make sure the input position does not exceed the max model len.188            # This is necessary when using spec decoding.189            num_new_tokens = min(190                num_new_tokens,191                self.max_model_len - request.num_computed_tokens)
Scheduler.schedule — token 预算

优先继续 running 请求,按未计算 token 数确定工作,再用长预填充阈值、剩余 token 预算和模型长度限制本轮规模。

vllm/v1/core/kv_cache_manager.pyv0.8.5 · L220–L234
查看原文件
220        num_required_blocks = cdiv(221            num_computed_tokens + num_tokens + num_lookahead_tokens,222            self.block_size)223        num_new_blocks = (num_required_blocks - len(req_blocks) -224                          len(new_computed_blocks))225226        # If a computed block of a request is an eviction candidate (in the227        # free queue and ref_cnt == 0), it cannot be counted as a free block228        # when allocating this request.229        num_evictable_computed_blocks = sum(1 for blk in new_computed_blocks230                                            if blk.ref_cnt == 0)231        if (num_new_blocks > self.block_pool.get_num_free_blocks() -232                num_evictable_computed_blocks):233            # Cannot allocate new blocks234            return None
KVCacheManager.allocate_slots — 块容量

先把 token 需求向上取整为块数。已命中但引用数为 0 的块不能一边复用、一边又计入可分配空闲块,因此容量判断要扣除它们。

vllm/v1/core/sched/scheduler.pyv0.8.5 · L216–L237
查看原文件
216            while True:217                new_blocks = self.kv_cache_manager.allocate_slots(218                    request,219                    num_new_tokens,220                    num_lookahead_tokens=self.num_lookahead_tokens)221                if new_blocks is None:222                    # The request cannot be scheduled.223                    # Preempt the lowest-priority request.224                    preempted_req = self.running.pop()225                    self.kv_cache_manager.free(preempted_req)226                    preempted_req.status = RequestStatus.PREEMPTED227                    preempted_req.num_computed_tokens = 0228                    if self.log_stats:229                        preempted_req.record_event(230                            EngineCoreEventType.PREEMPTED, scheduled_timestamp)231232                    self.waiting.appendleft(preempted_req)233                    preempted_reqs.append(preempted_req)234                    if preempted_req == request:235                        # No more request to preempt.236                        can_schedule = False237                        break
Scheduler.schedule — 抢占

分配失败时弹出 running 尾部请求,释放它的 KV 引用,标记 PREEMPTED、清零计算进度并放回 waiting 头部。如果连当前请求也被抢占,本轮停止尝试安排它。

vllm/v1/core/sched/scheduler.pyv0.8.5 · L337–L347
查看原文件
337                # Number of tokens to be scheduled.338                # We use `request.num_tokens` instead of339                # `request.num_prompt_tokens` to consider the resumed requests,340                # which have output tokens.341                num_new_tokens = request.num_tokens - num_computed_tokens342                if (0 < self.scheduler_config.long_prefill_token_threshold <343                        num_new_tokens):344                    num_new_tokens = (345                        self.scheduler_config.long_prefill_token_threshold)346                num_new_tokens = min(num_new_tokens, token_budget)347                assert num_new_tokens > 0
Scheduler.schedule — 等待请求分块

等待或恢复请求只安排扣除缓存命中后的剩余 token;同样受长预填充阈值和全局预算截断。后续 allocate_slots 成功才会把它加入 running。

vllm/v1/core/sched/scheduler.pyv0.8.5 · L398–L406
查看原文件
398                if self.lora_config and request.lora_request:399                    scheduled_loras.add(request.lora_request.lora_int_id)400                req_to_new_block_ids[request.request_id] = [401                    b.block_id for b in computed_blocks + new_blocks402                ]403                num_scheduled_tokens[request.request_id] = num_new_tokens404                token_budget -= num_new_tokens405                request.status = RequestStatus.RUNNING406                request.num_computed_tokens = num_computed_tokens
Scheduler.schedule — 记账与状态

记录缓存命中块与新块,登记本轮 token 数并扣减预算,然后设置 RUNNING 和已有计算进度;这不意味着 GPU 已经完成本轮计算。

片段分别对应计算预算、物理块检查、抢占恢复状态、等待请求分块和预算记账。它们展示 CPU 调度与缓存管理,不是 GPU Attention 内核或性能基准。

收益与取舍#

工程分析

带来的收益

  • 迭代之间可以加入新请求,批次成员不必等待同一批全部完成
  • 长预填充可以分多轮推进,计算量和 KV 容量都有明确的准入检查
  • 等待队列用 deque 支持头部接纳与抢占后前插,已有前缀缓存可参与恢复

付出的代价

  • 抢占清零 num_computed_tokens,恢复需要重新取得可复用块或重新计算;释放的是引用,不能理解成所有 KV 数据都永久丢失
  • 先处理 running 及等待队列中的特殊跳过规则,不构成严格公平或严格 FCFS 保证
  • token 预算限制本轮计算输入量,不能直接保证延迟或显存总量;模型执行、块管理、编码器和推测解码另有开销

可比较的选择

固定批处理固定成员后一起执行,组织简单;变长请求完成时间不同,若等待整批结束才能补位,会降低可用并行度。具体吞吐差异必须结合模型和负载测量。

一次完成整段 prefill减少长提示词被拆成多轮的调度,但较长输入可能占用更大单轮预算,影响其他请求的等待时间。该版本 V1 统一按 token 进度调度,不应套用 V0 的阶段划分。

版本与适用范围#

固定 vLLM v0.8.5 的 V1 调度器。prefill 与 decode 共用 token 进度模型;long_prefill_token_threshold 仅在正值且低于本轮需求时额外截断。不能把 V0 的调度开关或新版调度策略当作本版本保证。多模态编码器、LoRA、推测解码和外部 KV 连接器还会加入各自约束。

参考来源#

源码、官方文档与原始论文
01
vllm · Scheduler.schedule — 调度说明

vllm/v1/core/sched/scheduler.py · Scheduler.schedule — 调度说明

源码v0.8.5
02
vllm · Scheduler.schedule — running / preemption

vllm/v1/core/sched/scheduler.py · Scheduler.schedule — running / preemption

源码v0.8.5
03
vllm · Scheduler.schedule — waiting admission

vllm/v1/core/sched/scheduler.py · Scheduler.schedule — waiting admission

源码v0.8.5
04
vllm · KVCacheManager.allocate_slots

vllm/v1/core/kv_cache_manager.py · KVCacheManager.allocate_slots

源码v0.8.5

相关实现与比较#

4 个相关案例
11 个匹配案例