vLLM 大模型推理引擎调度机制:五个关键问题
问题一:显存够不够放下这个请求的 KV Cache?
为什么 KV Cache 是显存的大头
大模型推理不是"算完就扔"的。每处理一个 token,模型每一层都会产生一对 K 和 V 向量,后续的每个 token 都要用到它们。这些向量必须留在显存里,这就是 「KV Cache」。
一个 70B 模型、batch size 32、每个请求 2048 token 的场景下,KV Cache 可以轻松吃掉几十 GB 显存,比模型权重本身还大。所以调度器的第一个工作就是:「在接纳一个请求之前,确认显存够放它的 KV Cache」。
分页式管理:像操作系统管内存一样
vLLM 不要求一个请求的 KV Cache 连续存放,而是把显存切成固定大小的 「Block」(类似操作系统的内存页),每个 Block 存若干 token 的 KV 数据:
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
GPU 显存物理布局:
┌──────┬──────┬──────┬──────┬──────┬──────┐
│Block0│Block1│Block2│Block3│Block4│Block5│ ...
└──────┴──────┴──────┴──────┴──────┴──────┘
请求A: Block0 → Block2 → Block5 (通过 block_table 映射,逻辑连续)
请求B: Block1 → Block3 → Block4
请求 A 和请求 B 的 Block 完全可以交错排列。这消除了传统推理框架中最大的痛点——「内存碎片」。传统方案要求每个请求的 KV Cache 连续存放,一个长请求走了,中间空出一大块,但新请求放不进去,显存白白浪费。分页式没有这个问题,用多少占多少。
调度器怎么判断"够不够"
调度器维护一个 「BlockPool」(物理块池),记录哪些 Block 空闲、哪些被占用。当新请求到来时,调度器计算这个请求需要多少 Block,然后尝试从空闲池中分配。分配成功,请求进入运行状态;分配失败,请求继续等待,或者触发抢占(见问题四)。
BlockPool 内部用「引用计数」跟踪每个 Block 被多少请求使用——一个 Block 可以被多个请求共享(Prefix Caching 场景,见问题五),只有引用计数归零才会回收。
Watermark:留一点余量
如果调度器把显存用到最后一块才停,新请求刚进来就可能挤掉别人的资源,导致频繁抢占。所以 vLLM 设计了 「Watermark」 机制:预留一定比例的空闲 Block 作为缓冲。只有空闲 Block 超过 Watermark 线时,才接纳新的 WAITING 请求。这就像水库不会把水用到见底,总留一点安全水位。
问题二:是先处理新请求的 Prefill,还是继续已有请求的 Decode?
两种阶段
一个推理请求的生命周期分两段:
- 「Prefill」:处理用户输入的 prompt。如果 prompt 有 2000 个 token,这一步要一次性算 2000 个 token 的前向传播,计算量大,耗时较长。
- 「Decode」:逐个生成输出 token。每步只算 1 个 token,计算量小,但需要持续多步。
当 GPU 上已经有几个请求在 Decode(每步生成 1 个 token),同时等待队列里有一个新请求等着 Prefill(要算 2000 个 token),调度器该怎么选?
V1 的做法
老版本(V0)是互斥的——一个 step 要么全做 Prefill,要么全做 Decode。选了 Prefill,所有 Decode 请求都得等这 2000 个 token 算完;选了 Decode,新请求继续排队。
V1 的做法是「混合执行」:同一个 forward pass 里,既有正在 Prefill 的请求,也有正在 Decode 的请求:
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
同一个 step 的 GPU 批次:
┌──────────────────────────────────────────────┐
│ 请求A: 算 prompt 的第 0~1023 个 token │ ← Prefill(分块中)
│ 请求B: 生成第 15 个 output token │ ← Decode
│ 请求C: 生成第 8 个 output token │ ← Decode
│ 请求D: 算 prompt 的第 0~511 个 token │ ← Prefill(分块中)
└──────────────────────────────────────────────┘
怎么做到的:统一 Token 调度模型
V1 调度器最革命性的设计是:「没有"Prefill 阶段"或"Decode 阶段"的概念」。
每个请求只有两个计数器:
- 「num_computed_tokens」:已经算了多少 token
- 「num_tokens_with_spec」:目标是算到多少 token
调度器只做一件事:「让前者追上后者」。差距是多少,这一步就分配多少 token 的预算。
|
场景 |
差距 |
调度器分配 |
|
新请求 Prefill |
2000(prompt 还没算) |
最多 2000,受 budget 限制 |
|
Decode 中 |
1(每步多 1 个 output token) |
1 |
|
Chunked Prefill |
1500,但 budget 只剩 512 |
512,剩下的下一步继续 |
|
投机解码 |
5(1 正常 + 4 draft) |
5 |
"Prefill"和"Decode"不再是调度器需要判断的阶段,而是差距大小不同时自然产生的结果。差距大就多分配,差距小就少分配,不需要 if-else 分支。
优先级:RUNNING 先于 WAITING
虽然可以混合执行,但调度器有明确的优先级:
- 「先处理正在运行的请求」(RUNNING 队列):保证已开始生成的请求不会断火,Decode 连续性不被打断
- 「再处理等待中的请求」(WAITING 队列):用剩余的 token budget 接纳新请求
- 「如果这一步发生了抢占,就不再接纳新请求」:避免新请求挤占被抢占请求的恢复资源
问题三:长 prompt 能不能分块处理?
为什么要分块
假设 GPU 的 token budget 是 8192。一个新请求的 prompt 有 8000 个 token,而 GPU 上已经有 30 个请求在 Decode。如果一次性算完 8000 个 token,budget 全被占光,30 个 Decode 请求这一步全部停摆——延迟飙升。
Chunked Prefill:把长 Prefill 切成小块
V1 默认开启 Chunked Prefill。长 prompt 不会一次性算完,而是受 token budget 限制,每步只算一部分:
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
8000 token 的 prompt,budget 每步给 2048:
Step 1: 算 token 0~2047 ← 和其他人的 Decode 混在一起
Step 2: 算 token 2048~4095 ← 和其他人的 Decode 混在一起
Step 3: 算 token 4096~6143 ← 和其他人的 Decode 混在一起
Step 4: 算 token 6144~7999 ← Prefill 完成,下一步开始生成 output
每一步,这个请求和其他 Decode 请求共享 GPU。没有人因为一个长 prompt 而被完全阻塞。
分块不需要特殊机制
在统一 Token 调度模型下,Chunked Prefill 不是单独实现的特性。它只是 num_new_tokens 被 token budget 截断的自然结果:
- 请求需要算 8000 个 token
- 这一步 budget 只剩 2048
- 调度器分配 2048 个,剩余 5952 个留到下一步
num_computed_tokens从 0 变成 2048,差距缩小到 5952- 下一步继续追
不需要"判断这是 Chunked Prefill"的代码,差距被 budget 截断就自动分块了。
问题四:显存不够时,该抢占哪个请求?
抢占的触发
当一个 RUNNING 请求需要新的 KV Cache Block,但 BlockPool 里没有足够的空闲 Block 时,调度器必须抢占——把某个正在运行的请求踢出去,腾出空间。
抢占谁
「FCFS 策略」(默认):抢占最后加入的请求。
「Priority 策略」:抢占优先级最低的请求。适合需要 SLA 保障的场景,高优先级请求不会被低优先级请求挤掉。
抢占后怎么处理
V1 只支持「全量重算」(RECOMPUTE):
- 释放被抢占请求的所有 KV Cache Block
- 计算进度归零(
num_computed_tokens = 0) - 放回等待队列「头部」(下一步优先恢复)
是的,之前算的全白算了。但 V1 选择这种方式而不是 V0 的 SWAP(把 KV Cache 搬到 CPU 内存再搬回来),因为:
- 前向传播很快,重算比搬运便宜
- 被抢占的请求通常有 Prefix Cache 可以复用,重算没那么贵
- SWAP 需要额外的 CPU 内存和搬运带宽,复杂度高
减少抢占:Watermark
问题一提到的 Watermark 机制在这里发挥作用:为 WAITING 请求预留一定比例的空闲 Block,避免过度接纳新请求导致频繁抢占。这就像水库的安全水位——平时留一点余量,洪水来时才有缓冲。
问题五:多个请求能否共享相同的 KV Cache?
场景
两个请求用了相同的 system prompt(比如都是"你是一个有用的助手"加上 1000 token 的上下文),只是最后的问题不同:
ounter(lineounter(line
请求A: [system prompt 1000 tokens] + [问题A 20 tokens]
请求B: [system prompt 1000 tokens] + [问题B 30 tokens]
前 1000 个 token 完全一样,它们的 KV Cache 也完全一样。请求 A 算完后,请求 B 没必要再算一遍。
Prefix Caching:复用已计算的 KV Cache
vLLM 的做法是 「Prefix Caching」:
- 每个 Block 计算一个「链式哈希」——包含前序所有 token 的哈希,保证只有完全相同的前缀才会命中
- 当一个 Block 算满时,把它的哈希注册到全局哈希表
- 新请求到来时,逐个 Block 查找哈希命中
- 命中的 Block 直接复用,通过引用计数标记"有人在用",不会被驱逐
如果两个请求有 1000 个 token 的相同前缀,第二个请求的这 1000 个 token 「完全不需要做前向计算」,直接指向请求 A 留下的 Block。
与模型 API 的 Prompt Cache 有什么区别
|
vLLM Prefix Cache |
OpenAI/Anthropic Prompt Cache |
|
|
缓存什么 |
KV Cache 的物理显存块 |
服务端黑盒中的处理结果 |
|
存在哪 |
GPU 显存,调度器直接管理 |
服务端,用户不可见 |
|
粒度 |
Block 对齐(如每 16 token),可部分命中 |
通常要求前缀完全匹配 |
|
生命周期 |
LRU 策略,显存满了才驱逐 |
服务端管理,5~10 分钟过期 |
|
透明度 |
白盒,可看到哈希、引用计数、命中位置 |
黑盒,只知道"省了多少 token" |
本质上闭源 API 厂商的 Prompt Cache 缓存的也是 KV Cache。
Prefix Caching 与 Chunked Prefill 的配合
Prefix Caching 命中后,num_computed_tokens 直接跳到命中的位置。比如 1000 个 token 命中,num_computed_tokens 从 0 变成 1000,差距从 1020 缩小到 20。这 20 个 token 一步就能算完,请求几乎瞬间从"等待 Prefill"变成"开始 Decode"。
问题六:混合调度已经够好了,为什么还要 PD 分离?
混合调度的"最后一公里"问题
前面几个问题讲的都是「混合调度」(Prefill 和 Decode 在同一个 GPU 上混合执行)。V1 的统一 Token 调度 + Chunked Prefill 已经解决了大部分问题:长 prompt 被切成小块,不会一次性占光 budget,Decode 请求不会被完全阻塞。
但"不会被完全阻塞"不等于"没有影响"。考虑这个场景:
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
Step N 的 GPU 批次:
┌──────────────────────────────────────────────┐
│ 请求A: 算 prompt 的第 0~2047 个 token │ ← Prefill chunk,计算密集
│ 请求B: 生成第 15 个 output token │ ← Decode
│ 请求C: 生成第 8 个 output token │ ← Decode
└──────────────────────────────────────────────┘
请求 A 的 prefill chunk 有 2048 个 token 要算,计算量远大于 B 和 C 的各 1 个 token。这一步 GPU 的计算时间主要由 A 决定——本来 B 和 C 只需要 5ms 就能出 token,现在因为 A 在同一 batch 里,可能要等 15ms。
用户在等 B 和 C 的流式输出,感受到的就是:「token 生成速度不稳定,时快时慢」。大部分时候很快(没有 prefill chunk 的 step),偶尔卡一下(碰上 prefill chunk 的 step)。
Chunked Prefill 把这个问题从"严重"降到了"轻微",但没法完全消除——只要 prefill 和 decode 共享同一个 GPU forward pass,就一定有互相影响。
PD 分离:彻底解耦
「PD 分离」(Prefill-Decode Disaggregation)的思路很简单:「把 Prefill 和 Decode 放到不同的 GPU 上,各自独立运行」。
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
传统混合调度(单 GPU 池):
GPU 1: [Prefill chunk + Decode + Decode + Prefill chunk + ...]
GPU 2: [Prefill chunk + Decode + Decode + ...]
→ Prefill 和 Decode 在同一 GPU 上互相影响
PD 分离(双 GPU 池):
Prefill 节点池: [Prefill] [Prefill] [Prefill] ← 只做 prefill
Decode 节点池: [Decode] [Decode] [Decode] ← 只做 decode
→ 互不干扰
Prefill 节点专门处理长 prompt 的前向计算,算完后把 KV Cache 传给 Decode 节点。Decode 节点只管逐 token 生成,每个 step 的 batch 全是 decode 请求,计算量均匀,延迟稳定。
KV Cache 怎么传
PD 分离的关键挑战是:Prefill 算完的 KV Cache 要搬到 Decode 节点上。一个 70B 模型、2000 token prompt 的 KV Cache 可以有好几 GB,搬运开销不小。
vLLM 通过 「KV Connector」 插件接口实现跨节点 KV Cache 传输。这是一个可插拔的设计——不同的传输方式(RDMA、NCCL、共享存储等)可以实现不同的 Connector。
混合调度 vs PD 分离:什么时候用哪个
|
维度 |
混合调度(V1 默认) |
PD 分离 |
|
「GPU 利用率」 |
高(prefill 和 decode 填满同一 GPU) |
略低(两池各自可能有空闲) |
|
「Decode 延迟稳定性」 |
~80%(偶尔受 prefill chunk 影响) |
~95%(完全不受 prefill 影响) |
|
「吞吐量」 |
高 |
不一定提升,甚至略降 |
|
「额外开销」 |
无 |
KV Cache 跨节点传输 |
|
「适用场景」 |
通用场景、大多数应用 |
SLA 敏感、要求极低尾延迟的生产场景 |
一句话总结:「混合调度追求吞吐,PD 分离追求稳定」。PD 分离不提升吞吐量,但把 decode 的尾延迟从"偶尔卡一下"变成"几乎不卡"。对于需要严格 SLA 保障的线上服务(比如要求 P99 延迟 < 50ms),这个差异至关重要。
vLLM 对 PD 分离的支持
vLLM V1 通过 「KV Connector」 插件接口支持 PD 分离架构。KV Connector 是一个抽象层,定义了 KV Cache 在节点间传输的接口协议,具体传输实现可以由插件提供。这保持了调度器核心代码的通用性——同一个调度器既能跑混合调度,也能在 PD 分离架构中作为 Prefill 节点或 Decode 节点运行。
学习资源推荐
如果你想更深入地学习大模型,以下是一些非常有价值的学习资源,这些资源将帮助你从不同角度学习大模型,提升你的实践能力。
一、全套AGI大模型学习路线
AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取
二、640套AI大模型报告合集
这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取
三、AI大模型经典PDF籍
随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取
四、AI大模型商业化落地方案

作为普通人,入局大模型时代需要持续学习和实践,不断提高自己的技能和认知水平,同时也需要有责任感和伦理意识,为人工智能的健康发展贡献力量。
更多推荐


所有评论(0)