04 · Scheduler:连续批处理的决策中心
先建立直觉:短请求为什么还在等?
当一个短请求已经完成、却仍有新请求在等待时,GPU 的空位是怎样产生的?
先观看下面这段动画。它把 Scheduler 的核心决策拆成可观察的状态迁移:固定批处理的空等、每轮重新组装的连续批处理、token 预算的分配、Prefill 与 Decode 的不同节奏,以及 schedule() 附近应当阅读的代码线索。
观看时只抓住一个结论:Batch 不是一份固定名单。每个调度轮次结束后,完成的请求应立即离场;Scheduler 再依据阶段、队列与可用 token 预算重新选择工作。
Scheduler 每轮只回答一个问题
在资源有限的情况下,这一轮让哪些 Sequence 处理多少 token?
它同时受到三类约束:
max_num_seqs:一个批次最多容纳多少序列。max_num_batched_tokens:本轮最多处理多少 token。- KV Cache block 是否足够。
Prefill 阶段
Scheduler 先查看 waiting 队列。对队首请求,它会询问 BlockManager 能否分配并返回可命中的缓存 block,计算仍需执行的 prompt token,检查本轮 token 预算,然后分配 block、设置 num_scheduled_tokens,最后把请求移到 running。
当一个超长 prompt 超过剩余预算时,nano-vLLM 允许第一个请求做 Chunked Prefill,即只处理一部分 prompt;但若本轮已经选了其他请求,就不会再塞入一个无法完整容纳的长请求,以保持逻辑简单。
Decode 阶段
没有可调度 Prefill 后,Scheduler 从 running 队列选择请求。Decode 中每个序列通常只需要处理最后一个 token,但可能需要新 block。如果空闲 block 不足,调度器会回退或抢占序列,释放其缓存,之后让它重新进入等待。
为什么要连续批处理
静态 batching 必须等待整批请求一起结束;连续批处理允许某个请求完成后立即腾出位置,引入新请求。这样 GPU 不必被最慢序列拖住。
公平性与吞吐的取舍
Prefill 优先能让新请求尽快进入系统,但超长 prompt 可能占用大量 token 预算;Decode 批次越大,总吞吐通常越高,但单个用户的每 token 延迟可能变差。生产系统会加入更复杂的优先级、预算和延迟目标,nano-vLLM 保留了最小但清晰的核心。
动手验证:预测 → 运行 → 解释
不要直接点击按钮。先在脑中扮演一次 Scheduler,再用实验验证自己的解释。
| 步骤 | 你要做什么 | 你应该观察什么 |
|---|---|---|
| 1. 预测 | 加入长度为 8、24、60 token 的三个请求,先把预算设为 32。写下你认为本轮会完整处理谁、谁会被 Chunked Prefill。 | token 总预算而不是请求数量决定本轮能接纳多少 prompt。 |
| 2. 运行 | 点击 执行 schedule();随后把预算改为 64,点击“装载 / 重置对比任务”恢复同一批请求,再运行一轮。 | Waiting 与 Running 的变化、本轮已用 token,以及日志中的完整 Prefill 或 chunk。 |
| 3. 解释 | 比较两次运行:预算从 32 变为 64 后,哪一个请求改变了命运?为什么? | 用“队列、预算、阶段、状态迁移”四个词解释差异,而不是只报告结果。 |
为什么 `max_num_batched_tokens` 比“最大 batch size”更适合约束 Prefill?
先完成上面的预算 32 与 64 对比,再用一句话解释:你改了什么,Scheduler 为什么这样选,下一轮会发生什么。
下一节:分页 KV Cache