Scheduler(scheduler.py,113KB)是 vLLM V1 的中枢。一句话职责:
它持有的核心状态:
| 状态 | 类型 | 含义 |
|---|---|---|
self.running | list[Request] | 正在执行的请求列表 |
self.waiting | RequestQueue | 等待调度的请求队列(FCFS 或 PRIORITY) |
self.skipped_waiting | RequestQueue | 因异步依赖/约束被跳过的请求 |
self.kv_cache_manager | KVCacheManager | KV Cache 门面(博客 09 讲过的) |
self.max_num_scheduled_tokens | int | 每轮 token_budget 上限(由 scheduler_config 配置) |
schedule()(行 351 ~ 970,约 620 行)每轮被 Engine 调用一次。六步流程:
schedule() 开头调 kv_cache_manager.new_step_starts() 重置计数器,但真正"收尾"发生在上轮 Worker 返回后、Engine 调 update_from_output() 时(见 §8)。Scheduler 上轮结束后,已完成请求的 block 已被释放。
token_budget = self.max_num_scheduled_tokens
# 如果调度器 paused,token_budget = 0(任何请求都不调度)
token_budget 约束本轮能处理的总 token 数。RUNNING 和 WAITING 共享这个预算,消耗完即止。
另外,block 资源约束是隐式的:当 kv_cache_manager.allocate_slots() 返回 None 时,表示 block 不够。
遍历 self.running 列表,逐个尝试加入本轮 batch:
while req_index < len(self.running) and token_budget > 0:
request = self.running[req_index]
# ---- 跳过条件 ----
if 已达 max_tokens 且没 spec tokens 可验证: continue
if 未到 next_decode_eligible_step(V2+PP+async): continue
if num_new_tokens == 0(PP 未完成/达上限/encoder budget 耗尽): continue
# ---- 确定本步 token 数 ----
num_new_tokens = 请求剩余待处理 token 数
if long_prefill_token_threshold 生效: 截断到这个阈值
num_new_tokens = min(num_new_tokens, token_budget)
num_new_tokens = min(num_new_tokens, max_model_len - 1 - 已计算 token 数)
# ---- 分配 block ----
while True:
new_blocks = kv_cache_manager.allocate_slots(request, num_new_tokens)
if new_blocks is not None:
break # 分配成功
# 分配失败 → 抢占
preempted_req = self.running.pop() # 默认抢最后一个
或 max(running, key=priority) # PRIORITY 策略
self._preempt_request(preempted_req)
preempted_reqs.append(preempted_req)
# ---- 加入本轮 ----
scheduled_running_reqs.append(request)
token_budget -= num_new_tokens
req_index += 1
num_new_tokens == 0 时不 break 而是 continue——代码注释说这是有意放宽 FCFS,允许后面的请求先调度。
RUNNING 调度完成后,用剩余 token_budget 从等待队列拉新请求:
while (self.waiting or self.skipped_waiting) and token_budget > 0:
request_queue = self._select_waiting_queue_for_scheduling()
request = request_queue.peek()
# ---- 跳过条件 ----
if 已停等(lora 约束 / kv connector 传输中等): 跳过 / 放回
if 超过 max_loras 限制: break # 整个队列停止
# ---- 分配 block(同 RUNNING 流程) ----
new_blocks = kv_cache_manager.allocate_slots(request, num_new_tokens)
if new_blocks is None:
if 已经有过抢占: break # 不能再抢了
抢占最后一个 running 请求
continue # 重试
# ---- 加入本轮 ----
scheduled_new_reqs.append(request)
token_budget -= num_new_tokens
仅 23 行(行 995-1017):
def _preempt_request(self, request, timestamp):
# 1. 释放该请求的所有 KV block
self.kv_cache_manager.free(request.request_id)
# 键:释放后 block 回到 free_block_queue,hash 保留(Lazy Eviction)
# 2. 重置请求状态
request.status = waiting 状态
# 重新 put 到 waiting 队列头部 → 等有资源后优先恢复
V1 代码里不区分 Swap 和 Recomputation——抢占 = 释放 block + 重新入队。论文描述的两种策略在 V1 被统一为"释放重排"——block 回到 free_queue 时 hash 还留着,如果短时间内恢复且命中 Prefix Cache 就是"免费召回";如果 block 被新请求占用则等价于 recomputation。
schedule() 末尾把本轮决策组装成 SchedulerOutput(定义在 interface.py):
| 字段 | 类型 | Worker 用来做什么 |
|---|---|---|
scheduled_new_reqs | list[Request] | 本轮新加入的 Prefill 请求 |
scheduled_resumed_reqs | list[Request] | 之前被抢占、本轮恢复的请求 |
scheduled_running_reqs | list[Request] | 本轮继续执行的 Decode 请求 |
preempted_reqs | list[Request] | 被抢占的请求 |
num_scheduled_tokens | dict[str, int] | 每个请求本轮要处理的 token 数 |
req_to_new_blocks | dict[str, KVCacheBlocks] | 每个请求新分配的 KV block |
finished_req_ids | set[str] | 已确认完成(供 Worker 跳过) |
scheduled_spec_decode_tokens | dict[str, list[int]] | 投机解码的 draft token |
update_from_output()(行 1350 ~ 1673,约 320 行)在 Worker 完成后被 Engine 调用。核心逻辑:
def update_from_output(self, scheduler_output, model_runner_output):
for req_id, output in model_runner_output.items():
request = self.requests[req_id]
# 1. 更新请求状态
request.num_computed_tokens += output.num_tokens
追加 output.new_token_ids
# 2. 处理完成
if 生成了 EOS 或 stop_string 或达到 max_tokens:
self._free_request(request) # 释放所有 block
running 列表移除
# 3. 处理抢占后的恢复
如果之前被抢占的请求恢复 → scheduled_resumed_reqs
# 4. 更新 running 列表
self._update_after_schedule(scheduler_output)
核心操作:_free_request → kv_cache_manager.free(request_id) → block_pool.free_blocks → ref_cnt - 1 → 到 0 归还。
| 论文概念 | 代码对应文件 | 对应行 |
|---|---|---|
| Iteration-level scheduling | scheduler.py | schedule() ~351 |
| Continuous Batching | scheduler.py | RUNNING + WAITING 混排 ~387-780 |
| FCFS scheduling | scheduler.py | FCFS 为默认策略 + _select_waiting_queue_for_scheduling |
| Preemption(抢占) | scheduler.py | _preempt_request ~995 |
| SchedulerOutput | interface.py + output.py | 共 19KB |
| 请求队列 | request_queue.py | RequestQueue / FCFSQueue |
| 论文没提的 | token_budget / long_prefill_threshold / PRIORITY 策略 / async PP 约束 | |
running.pop()——抢最后进入 running 列表的;PRIORITY:max(running, key=priority, arrival_time)——抢最低优先级且最晚到的。
kv_cache_manager.free() 释放 block;② 请求放回 waiting 队列。V1 不区分 Swap/Recompute——统一为释放重入队。block 的 hash 保留实现免费召回(命中 Prefix Cache 时无需重算)。
max_num_scheduled_tokens 配置。
scheduled_new_reqs / scheduled_running_reqs / scheduled_resumed_reqs(三类请求)、preempted_reqs(被抢的)、num_scheduled_tokens(每请求 token 数)、req_to_new_blocks(新 KV block)、finished_req_ids(已完成的)。
这是 Stage 1 / Week 3 的内容,对应 AI Infra 转型学习计划中"vLLM 调度器"任务。
上一篇:09 · KV Cache 管理层 | 下一篇预告:worker/ 和 attention/ 模块。