vLLM-12-Scheduler模块-概览
模块职责
Scheduler(调度器)是 vLLM V1 架构的核心组件,负责:
- 请求调度:决定每个 step 处理哪些请求
- 资源分配:为请求分配 KV 缓存块
- 抢占策略:在资源不足时选择要抢占的请求
- 状态管理:维护请求状态(waiting、running、finished)
- 指标收集:记录调度统计信息
核心调度策略
FCFS(先来先服务)
- 按到达时间顺序处理请求
- 公平性好,但可能阻塞高优先级请求
- 适用于无优先级区分的场景
Priority(优先级调度)
- 按 (priority, arrival_time) 排序
- 低优先级请求可被抢占
- 适用于需要 SLA 保证的场景
KV 缓存管理
块分配机制
- 块大小:16 tokens(默认)
- 预分配策略:延迟分配,按需增长
- Copy-on-Write:支持前缀共享
抢占策略
# 优先级抢占:选择优先级最低、到达时间最晚的请求
preempted_req = max(
self.running,
key=lambda r: (r.priority, r.arrival_time),
)
Prefix Caching
- 基于哈希的缓存查找
- LRU 驱逐策略
- 跨请求共享,显著降低 TTFT
架构图
flowchart TB
subgraph Scheduler["Scheduler"]
WaitQueue[Waiting Queue<br/>等待队列]
RunQueue[Running List<br/>运行列表]
SchedLogic[Schedule Logic<br/>调度逻辑]
KVMgr[KV Cache Manager<br/>缓存管理器]
EncMgr[Encoder Cache Manager<br/>编码器缓存]
end
subgraph KVMgr_Detail["KV Cache Manager"]
BlockPool[Block Pool<br/>块池]
CacheMap[Block Hash Map<br/>缓存映射]
Allocator[Block Allocator<br/>分配器]
end
Request[New Request] -->|add_request| WaitQueue
WaitQueue -->|select| SchedLogic
RunQueue -->|select| SchedLogic
SchedLogic -->|allocate blocks| KVMgr
KVMgr -->|get free block| BlockPool
KVMgr -->|find cached| CacheMap
SchedLogic -->|schedule| Scheduled[Scheduled Requests]
SchedLogic -->|preempt| Preempted[Preempted Requests]
Preempted -->|free blocks| KVMgr
Preempted -->|prepend| WaitQueue
架构说明
1. 调度算法
调度器采用统一的 token-centric 调度策略:
核心思想:
- 每个请求有
num_computed_tokens(已计算的 token 数)和num_tokens_with_spec(包含投机 token 的总数) - 调度器目标:让
num_computed_tokens追赶num_tokens_with_spec
覆盖场景:
- Chunked Prefill:长 prompt 分块处理
- Prefix Caching:跳过已缓存的 token
- Speculative Decoding:处理投机生成的 token
- 常规 Decode:每次生成一个 token
2. 调度流程
sequenceDiagram
autonumber
participant Core as EngineCore
participant Sched as Scheduler
participant KV as KVCacheManager
participant Pool as BlockPool
Core->>Sched: schedule()
loop 遍历 waiting 队列
Sched->>Sched: 检查 budget
alt budget 充足
Sched->>KV: allocate_slots(req, num_tokens)
KV->>Pool: get_free_blocks()
alt 有空闲块
Pool-->>KV: blocks
KV-->>Sched: success
Sched->>Sched: 加入 scheduled
else 无空闲块
Sched->>Sched: 选择抢占对象
Sched->>KV: free(preempted_req)
KV->>Pool: return_blocks()
Sched->>Sched: 重试分配
end
else budget 不足
Sched->>Sched: break(停止调度)
end
end
Sched-->>Core: SchedulerOutput
3. 关键数据结构
Request:
class Request:
request_id: str # 唯一标识
arrival_time: float # 到达时间
priority: int # 优先级(越小越高)
num_computed_tokens: int # 已计算 token 数
num_tokens_with_spec: int # 总 token 数(含投机)
status: RequestStatus # waiting/running/finished/preempted
kv_blocks: list[KVCacheBlock] # 分配的 KV 块
SchedulerOutput:
class SchedulerOutput:
scheduled_new_reqs: list[Request] # 新调度的请求
scheduled_resumed_reqs: list[Request] # 恢复的请求
scheduled_running_reqs: list[Request] # 继续运行的请求
preempted_reqs: list[Request] # 被抢占的请求
num_scheduled_tokens: dict[str, int] # 每个请求的 token 数
req_to_new_blocks: dict[str, KVCacheBlocks] # 新分配的块
4. 性能优化
批次构建优化:
- Token budget 限制:避免单批次过大导致 OOM
- 公平性保证:FCFS 防止饥饿
- 优先级保证:Priority 模式支持 SLA
内存优化:
- Lazy allocation:按需分配块
- Prefix caching:跨请求共享
- Block pooling:避免频繁分配/释放
调度开销优化:
- O(n) 扫描 waiting 队列(n = 等待请求数)
- 堆排序优先队列(Priority 模式)
- 增量更新,避免全量重建
核心代码
调度主循环
def schedule(self) -> SchedulerOutput:
scheduled_new_reqs = []
scheduled_running_reqs = []
preempted_reqs = []
token_budget = self.max_num_scheduled_tokens
# 首先调度 running 请求
for request in self.running:
num_new_tokens = request.num_tokens_with_spec - request.num_computed_tokens
if token_budget < num_new_tokens:
break # budget 不足
# 尝试分配 KV 块
new_blocks = self.kv_cache_manager.allocate_slots(
request, num_new_tokens, ...)
if new_blocks is None:
# 分配失败,需要抢占
self._preempt_lowest_priority(preempted_reqs)
continue
# 调度成功
scheduled_running_reqs.append(request)
token_budget -= num_new_tokens
# 然后调度 waiting 请求
while not self.waiting.is_empty() and token_budget > 0:
request = self.waiting.peek_request()
num_new_tokens = min(request.num_prompt_tokens, self.max_chunk_size)
if token_budget < num_new_tokens:
break
# 分配 KV 块
new_blocks = self.kv_cache_manager.allocate_slots(request, num_new_tokens)
if new_blocks is None:
break # 无法分配,停止调度新请求
self.waiting.pop_request()
scheduled_new_reqs.append(request)
token_budget -= num_new_tokens
return SchedulerOutput(
scheduled_new_reqs=scheduled_new_reqs,
scheduled_running_reqs=scheduled_running_reqs,
preempted_reqs=preempted_reqs,
...
)
抢占逻辑
def _preempt_lowest_priority(self, preempted_reqs):
if self.policy == SchedulingPolicy.PRIORITY:
# 选择优先级最低、到达时间最晚的请求
victim = max(self.running, key=lambda r: (r.priority, r.arrival_time))
else:
# FCFS 模式:选择最后加入的请求(栈式抢占)
victim = self.running.pop()
# 释放 KV 块
self.kv_cache_manager.free(victim)
self.encoder_cache_manager.free(victim)
# 更新状态
victim.status = RequestStatus.PREEMPTED
victim.num_computed_tokens = 0
victim.num_preemptions += 1
# 重新加入等待队列(高优先级)
self.waiting.prepend_request(victim)
preempted_reqs.append(victim)
边界与异常
并发限制
- 最大运行请求数:
max_num_seqs(默认 256) - 最大批次 token 数:
max_num_batched_tokens(默认 8192)
超时处理
- 无显式超时机制(由上层控制)
- 长时间等待的请求会被优先调度(FCFS)
错误处理
- 块分配失败:触发抢占或拒绝新请求
- 抢占失败(所有请求优先级相同):停止调度新请求
- 状态不一致:抛出 AssertionError,需重启
总结
Scheduler 是 vLLM V1 的调度核心,通过统一的 token-centric 策略支持 chunked prefill、prefix caching、speculative decoding 等高级特性。KV 缓存管理采用 PagedAttention 机制,实现了高效的内存利用。抢占策略保证了多租户场景下的公平性和 SLA。