vLLM 源码精读——高吞吐 LLM 推理引擎全景
vLLM 源码精读——高吞吐 LLM 推理引擎全景
本文基于 vLLM 源码
vllm/v1/(V1 引擎架构)。目标:让你在面试时能从"为什么传统 serving 显存利用率只有 20%“讲到"PagedAttention 的 block 怎么管”、从"prefill/decode 不再分阶段"讲到"continuous batching 的 token-budget 调度"、从"prefix caching + 拷贝写"讲到"V1 的 async scheduler + CUDA Graph + spec decode"。读完这篇,vLLM 这关基本通杀。
一、vLLM 要解决什么问题、为什么面试必问
传统 LLM serving(朴素 batch)有两个致命问题:
- KV cache 显存碎片化:为每个请求预分配一段连续 KV 显存,请求长短不一→产生大量内部碎片+外部碎片,实测显存利用率常低于 20%;且无法在请求间复用相同前缀。
- 批处理不连续:朴素做法是"攒满一个 batch 一齐 prefill,全部 decode 完再换下一批",prefill 和 decode 阶段割裂,新请求要等整批结束才能进→排队延迟高、GPU 空转。
vLLM 用两件套解决,也正是面试的核心:
- PagedAttention:把 KV cache 像操作系统虚拟内存一样按固定 block(默认 16 token)分页管理,按需分配、消除碎片、显存利用率从 ~20% 拉到 ~95%。
- Continuous Batching(连续批处理 / iteration-level batching):在每次 decode 迭代级别动态增删请求,prefill 和 decode 混在同一 batch,新请求即时加入、完成的即时退出,GPU 不空转。
源码鸟瞰(vllm/v1/):
1 | engine/ # 引擎层: AsyncLLM(API 入口) / EngineCore(调度核心) / core_client |
主线:AsyncLLM 收请求 → EngineCore 驱动 Scheduler 每步算 token 预算分配给各请求 → KVCacheManager 经 BlockPool 按 block 管 KV → GPUModelRunner.execute_model 把 batch 喂给模型 + 跑 attention + 采样 → 输出回流。
二、PagedAttention:KV cache 的虚拟内存(地基)
2.1 类比 OS 虚拟内存
vLLM 把 KV cache 当作"显存里的内存系统"来管:
| OS 虚拟内存 | vLLM PagedAttention |
|---|---|
| 进程的虚拟地址空间 | 一个请求的逻辑 KV 序列 |
| page(4KB) | block(默认 16 token 的 K/V) |
| 页表(虚拟→物理) | block table(逻辑 block → 物理 block id) |
| 物理 page frame | 显存里的 KV block |
| 按 page 分配 | 按 block 按需分配 |
每个请求持有一张 block_table(逻辑 block 序号 → 物理 block id),逻辑上连续的 KV,物理上可以离散分布在不同物理 block。所以:
- 消除外部碎片:任何空闲 block 都能用,不必连续。
- 消除内部碎片:只有最后一个 block 可能浪费一点,浪费 ≤ 一个 block(16 token)。
- 按需分配:用多少 token 才分多少 block,不预先占满。
- 复用/共享:相同前缀可指向同一组物理 block(prefix caching)。
2.2 源码对应
core/block_pool.py的BlockPool:物理 block 池,负责分配/回收/驱逐,get_new_blocks(n)、free_blocks、touch(LRU)、evict_blocks。core/kv_cache_manager.py的KVCacheManager:每个请求的逻辑→物理映射,allocate_slots(为请求新增 token 分 block)、free、get_computed_blocks(prefix 命中查询)、cache_blocks(把 block 哈希存进 prefix 缓存)。worker/gpu/block_table.py:GPU 上的 block_table 张量,attention kernel 据此查物理 block。worker/gpu/model_runner.py的prepare_attn:把每请求的slot_mapping(token → 物理 KV slot)算好,attention kernel 用它把新 K/V 写进正确位置。
2.3 block 的物理布局
一个 block 存 (如 16)个 token 的 K 和 V。attention kernel(v1/attention/backends/flash_attn.py)按 block_table 索引到这 个 token 的 K/V 做 attention。对 query token 要 attention 到序列里所有已计算的 K/V,跨 block 拼起来——这正是 PagedAttention kernel 相对普通 attention 多做的事,但换来了零碎片 + 复用。
2.4 prefix caching + copy-on-write
相同前缀(system prompt、few-shot、共享文档)的 KV 可以被多请求共享。源码 BlockPool.get_cached_block / cache_full_blocks / cache_partial_block:每个 block 按其内容的 hash 作 key 存进 BlockHashToBlockMap。新请求来了 KVCacheManager.get_computed_blocks 先算它能命中哪些已缓存的 block,命中则直接引用同一组物理 block,不重算 KV——这就是 vLLM 的 prefix caching(v1 默认开启)。
Copy-on-write:当某请求要往一个"共享" block 写新 token(它在前缀后又续了内容),不能直接改共享 block(会污染别的请求),而是复制一份再写——BlockPool 维护引用计数(ref_cnt),共享 block 被写时复制。这就是源码里 cache_partial_block、_maybe_evict_cached_block 处理的逻辑。
面试金句:“PagedAttention = 把 KV cache 当虚拟内存,逻辑 block 经 block_table 映射到离散物理 block,按需分配消除碎片、显存利用率从 20% 到 95%;相同前缀的 block 按内容 hash 共享,写时复制,这就是 prefix caching。”
三、调度器:没有"prefill 阶段 / decode 阶段"之分(V1 核心革新)
3.1 V0 的两阶段 vs V1 的统一
- V0:有明确的 prefill 阶段和 decode 阶段,一个 iteration 要么全 prefill 要么全 decode,切换有开销。
- V1:源码
scheduler.py的schedule()开头注释明确写——“There’s no decoding phase nor prefill phase in the scheduler”。每个请求只有num_computed_tokens(已算)和num_tokens_with_spec(目标),调度器每步就是"把 token 预算分给各请求,让它俩差值变小"。
这个抽象统一了 chunked prefill、prefix caching、speculative decoding、jump decoding——它们都只是"让 num_computed_tokens 追上 num_tokens_with_spec" 的不同形态。
3.2 token-budget 调度(每步怎么分)
Scheduler.schedule() 的核心是 max_num_scheduled_tokens 这个全局 token 预算,按优先级分配:
- RUNNING 请求优先:先给正在跑的请求续命——每个 running 请求需要的
num_new_tokens = num_tokens_with_spec - num_computed_tokens,从预算扣。 - chunked prefill:长 prompt 一次算不完?切 chunk,每步算一段(
long_prefill_token_threshold),剩下的下步再算——于是 prefill 和 decode 可在同一 batch 混跑。 - WAITING 请求:预算还有剩,从 waiting 队列拉新请求做 prefill(FCFS,但允许低优先级抢占以避免饿死——源码注释"by doing continue instead of break, allow lower-priority requests")。
- 预算耗尽或显存不够→preempt:当 KV 显存不够塞下新请求,
_preempt_request把 running 请求的 KV 换出/重算(preempt)给高优先级腾地方。
3.3 preemption(抢占)策略
KV 显存紧张时调度器会 preempt 请求。两种方式:
- Recompute:丢弃某请求所有 KV block,把它退回 waiting 队列,下步从 prompt 重新 prefill(损失算力但简单)。
- Swap:把 KV 临时换到 CPU(V0 有,V1 倾向 recompute + 新的 KV offload/tiering)。
源码 _preempt_request / _free_blocks / free 处理。抢占是 vLLM 在显存不足时的兜底,保证吞吐不崩。
面试金句:“V1 调度器取消了 prefill/decode 阶段,每步就是给每个请求分配 token 预算让 num_computed_tokens 追上目标——这就是 continuous batching 的本质:iteration 级动态混跑 prefill 和 decode。显存不够就 preempt(重算或换出)兜底。”
四、EngineCore 与 AsyncLLM:V1 的双进程异步架构
4.1 为什么拆异步
朴素架构里调度器(CPU)和 worker(GPU)串行:CPU 调度→GPU 算→CPU 收输出→CPU 再调度……CPU 调度时间和 GPU 计算时间互不重叠,互相等。GPU 在 CPU 调度时空闲。
V1 的解法:把"调度"和"执行"解耦成两条流水线,用队列 + 双进程(或双线程)异步重叠。
4.2 源码结构
engine/async_llm.py的AsyncLLM:用户面向的异步 API(add_request/generate),把请求丢进 input queue,从 output queue 拉结果。output_handler协程持续回流。engine/core.py的EngineCore:调度核心。step()/step_with_batch_queue()每步:调scheduler.schedule()算出SchedulerOutput,丢给 worker 执行,收ModelRunnerOutput,更新请求状态。v1/worker/gpu/model_runner.py的GPUModelRunner.execute_model:真正跑一次 forward。
4.3 async scheduler 重叠
V1 用 asyncio 让 scheduler 的一步和 worker 的下一步重叠:worker 在 GPU 上跑第 步 forward 时,scheduler 已经在 CPU 上算第 步的 SchedulerOutput。源码 core/sched/async_scheduler.py 实现这种 overlap 调度。这就是 V1 比 V0 高吞吐的关键之一——CPU 调度开销藏进 GPU 计算里。
4.4 sleep/wake 与 disaggregated prefill
EngineCore.sleep()/wake_up():让 GPU KV cache 卸载(sleep level 1/2)以腾显存给别的任务,再唤醒恢复——服务于 disaggregated prefill(prefill 节点和 decode 节点分离,KV 跨机迁移)。core/kv_offload/、worker/gpu/kv_connector.py、coordinator.py 实现跨节点 KV 传输(Mooncake 风格的 KV 池化)。
五、GPUModelRunner.execute_model:一次 forward 怎么跑
worker/gpu/model_runner.py 的 execute_model 是 worker 侧主入口,源码流程(精简):
1 | def execute_model(self, scheduler_output, ...): |
5.1 关键设计
prepare_attn算slot_mapping:每个新 token 要写到哪里——把逻辑 token 位置映射到物理 KV block 的具体 slot,attention kernel 据此 scatter 写入新 K/V、gather 旧 K/V 算注意力。这是 PagedAttention 的运行时落地。- block_table apply_staged_writes:调度器本步分配的新 block id 先"暂存"(staged),execute_model 时统一 apply 到 GPU 上的 block_table 张量,减少 CPU↔GPU 同步次数。
- 多后端 attention:
v1/attention/backends/下 flash_attn、flashinfer、mla(DeepSeek MLA)、mamba(SSM)、linear_attn 等,registry.py按 model_config 选后端。vLLM 能跑这么多模型全靠这层抽象。
5.2 CUDA Graph 抓图(decode 加速)
decode 阶段每步只产一个 token、算子小、kernel launch 开销占比高。vLLM 用 cudagraph_utils.py:对若干个"固定 batch 大小/token 数"组合预先抓 CUDA Graph,运行时 dispatch_cg_and_sync_dp 按实际 num_tokens 选一个匹配的图直接 replay,跳过 kernel launch。capture_model 在启动时做这些抓图。这就是 decode 阶段 vLLM 低延迟的关键。
代价:图的种类有限、要为每种 num_tokens 抓一份图(占显存);EAGLE 等 spec decode 下还要为 draft token 数额外抓图。所以有 dispatch_cg_and_sync_dp 在 DP 各 rank 间同步选同样的图。
5.3 DP(数据并行)与统一 token 数
源码里反复出现 get_uniform_token_count / num_tokens_across_dp:多 DP rank 时,为能复用同一 CUDA Graph、做一致 attention,要把各 rank 的 token 数对齐(padding 到 uniform count)。这是 V1 对多卡 serving 的工程细节。
六、采样、约束输出、投机解码
6.1 采样 model_runner.sample
- 把模型 logits 过 logits_processor(temperature、top_p、top_k、min_p、penalty)→ 采样 token id。
- 支持束搜索(best_of、beam search)、多候选取样(n)。
v1/sample/下 logits_processor 的各种算子,很多是 fused kernel(v1/sample/ops)。- 约束输出 / 结构化输出(
v1/structured_output/):JSON schema、regex、grammar——通过 grammar 编译成 FSM bitmask,采样前 mask 掉不合法 token,保证输出符合 schema(xGrammar/AICI 思路)。
6.2 投机解码(v1/spec_decode/)
V1 把 spec decode 做成"proposer + target 验证"的统一抽象,多种 proposer:
| Proposer | 文件 | 原理 |
|---|---|---|
| EAGLE/EAGLE-3 | eagle.py |
自回归草稿头(与大模型共享 embedding/lm_head),一次 forward 出多个 draft token |
| Medusa | medusa.py |
多头并行猜下一 k 个 token,树形验证 |
| draft_model | draft_model.py |
独立小模型起草稿(经典 spec decode) |
| n-gram | ngram_proposer.py |
纯启发式,用 prompt 里的 n-gram 猜 draft,零草稿模型开销 |
| dflash | dflash.py |
Flash 风格自回归草稿 |
| suffix decoding | suffix_decoding.py |
后缀树匹配猜 draft |
机制(呼应投机解码那篇):proposer 出 个 draft token,target 一次 forward 并行验证,按接受率接受前缀、拒绝处从修正分布补采。源码里 Scheduler.update_draft_token_ids 把 draft token 并进 num_tokens_with_spec,统一进调度器——这就是 V1 "spec decode 只是让 num_computed_tokens 追上一个更大的目标"的具体体现。take_draft_token_ids 在 worker 取 draft、sample_tokens 收验证。
面试金句:“V1 把 spec decode 做成 proposer 抽象(EAGLE/Medusa/draft/n-gram…),draft token 并进 num_tokens_with_spec 让调度器统一调度,target 一次 forward 验证、拒绝采样补采——无损且把 decode 的算力喂饱。”
七、KV cache offload / 分层 / disaggregated prefill
v1/core/kv_offload/(cpu/、tiering/)+ simple_kv_offload/:
- KV offload 到 CPU:显存紧张时把冷 KV block 换到 host 内存(pin 的),要时再换回。把"GPU 显存"扩展成"GPU + CPU 两级"。
- tiering:多级 KV 池(GPU hot / CPU warm / SSD cold),按访问热度迁移。
- disaggregated prefill:把 prefill(算力密集、可并行)和 decode(访存密集、串行)拆到不同节点池。prefill 节点算完 KV,通过
kv_connector把 KV 迁到 decode 节点继续生成。engine/coordinator.py、worker/gpu/kv_connector.py、_update_from_kv_xfer_finished实现 KV 跨节点传输。Mooncake/NVIDIA Dynamo 这套都对接 vLLM 这层。
这层是当前推理系统最热的方向(PD 分离),面试讲到"你怎么看推理架构演进"必谈。
八、LoRA / 多模态 / 量化 / 分布式
- LoRA(
vllm/lora/+worker/gpu/lora_utils.py):多个 LoRA adapter 共享 base 权重,按请求激活。num_active_loras在execute_model里决定走哪条 fused LoRA kernel 路径;CUDA Graph 也要按 active lora 数抓图。 - 多模态(
vllm/multimodal/+worker/gpu/mm):图像/音频输入经 encoder 出 embedding,作为 prefix 注入。encoder_cache_manager管 encoder 缓存,_try_schedule_encoder_inputs给 encoder 算预算。 - 量化(
vllm/model_executor/layers/quantization/+csrc/quantization/):W8A8/W4A16/AWQ/GPTQ/FP8 等。把nn.Linear换成量化版本,attention/MLP 走量化 kernel。 - 分布式(
vllm/distributed/+worker/gpu/dp_utils.py):TP(切权重,跨 GPU all-reduce)、PP(切层)、DP(多副本)。v1/executor/编排多 worker。dispatch_cg_and_sync_dp在 DP 各 rank 同步 token 数以复用 CUDA Graph。 - 多种硬件(
vllm/platforms/、csrc/cpu/、csrc/rocm/):CPU/ROCm/TPU/XPU 后端,attention 后端按平台选。
九、怎么读源码(给你的路线)
vllm/v1/request.py+vllm/v1/engine/async_llm.py:看一个请求从 API 进来怎么流转。vllm/v1/core/sched/scheduler.py的schedule():看 token 预算怎么分、continuous batching 的本质。这是 V1 最重要的一个函数。vllm/v1/core/block_pool.py+kv_cache_manager.py:看 PagedAttention 的 block 分配/回收/命中/驱逐、prefix caching、COW。vllm/v1/worker/gpu/model_runner.py的execute_model+prepare_attn:看一次 forward 怎么把 batch + block_table + slot_mapping 拼好喂给 attention。vllm/v1/attention/backends/flash_attn.py:看 PagedAttention kernel 怎么按 block_table 算注意力。vllm/v1/engine/core.py的step+core/sched/async_scheduler.py:看 V1 怎么用 asyncio 让调度和执行重叠。vllm/v1/spec_decode/eagle.py:看 spec decode 怎么并进调度器统一处理。- 跑一个最小例子(
examples/basic/)+VLLM_USE_V1=1打开调试日志对照。
十、面试速答清单
Q1:vLLM 为什么比朴素 serving 快?两个核心是什么?
两个核心:PagedAttention 把 KV cache 按固定 block(16 token)分页管理,逻辑 block 经 block_table 映射到离散物理 block,按需分配消除碎片、显存利用率从 20% 到 95%;continuous batching 在每次 decode 迭代级别动态增删请求,prefill/decode 混跑、新请求即时加入、GPU 不空转。叠加 prefix caching 复用相同前缀、CUDA Graph 加速 decode、async scheduler 重叠 CPU 调度与 GPU 计算。
Q2:PagedAttention 怎么消除碎片?怎么复用前缀?
KV cache 当虚拟内存:请求持 block_table(逻辑→物理 block id),物理 block 可离散,外部碎片没了,内部碎片最多一个 block。block 按内容 hash 作 key 存 prefix 缓存,新请求
get_computed_blocks命中已缓存 block 直接引用不重算 KV;要往共享 block 写新 token 时用引用计数 + copy-on-write 复制一份再写,避免污染别的请求。
Q3:continuous batching 和朴素 static batching 区别?V1 调度怎么做的?
static batching 攒满一批一起跑、整批结束才换;continuous batching 在 iteration 级别动态增删。V1 调度器取消 prefill/decode 阶段,每个请求有 num_computed_tokens 和 num_tokens_with_spec,每步把 max_num_scheduled_tokens 这个 token 预算按 running 优先、waiting 次之分配,让前者追上后者——这个抽象统一了 chunked prefill、prefix caching、spec decode。显存不够就 preempt(重算或换出)兜底。
Q4:V1 比 V0 强在哪?
三点:①async scheduler 用 asyncio 把 CPU 调度和 GPU 计算重叠,消除互相等待;②统一 prefill/decode 抽象,chunked prefill 让两者同 batch 混跑;③CUDA Graph 按 token 数抓图 replay,decode 阶段 kernel launch 开销大幅降低;④prefix caching 默认开、spec decode 多 proposer(EAGLE/n-gram/…)、KV offload/PD 分离原生支持。
Q5:CUDA Graph 在 vLLM 怎么用?为什么 decode 受益大?
decode 每步只产一两个 token、算子小,kernel launch 开销占比高。vLLM 在启动时
capture_model为若干个固定 num_tokens/batch 组合抓 CUDA Graph,运行时dispatch_cg_and_sync_dp按实际 token 数选匹配图直接 replay,跳过 launch。代价是图要占显存、且种类有限,DP 各 rank 要同步选同一图以复用。prefill 算子大 launch 占比低、且 shape 多变,所以 Graph 对 prefill 收益小。
Q6:prefill 和 decode 为什么要拆开调度、disaggregated prefill 是什么?
prefill 算力密集可并行、长 prompt 一次算很多 token;decode 访存密集、串行、每步少 token。混跑时两者对算力/带宽需求不同会互相拖累。disaggregated prefill 把 prefill 和 decode 拆到不同节点池:prefill 节点算完 KV 通过 kv_connector 把 KV 迁到 decode 节点继续生成。vLLM V1 的 sleep/wake、kv_offload、coordinator 实现这层。Mooncake/Dynamo 都对接这层。
Q7:vLLM 的 spec decode 怎么和调度器结合?
V1 把 spec decode 做成 proposer 抽象(EAGLE/Medusa/draft_model/n-gram/suffix)。proposer 出 k 个 draft token 后,
update_draft_token_ids把 draft 并进请求的 num_tokens_with_spec,于是调度器把它当成"要算更多 token"统一处理,target 一次 forward 验证这 k+1 个位置、按接受率接受前缀、拒绝处修正分布补采——无损。CUDA Graph 还要为 draft token 数额外抓图。
Q8:preemption 怎么工作?什么时候触发?
当某步要调度但 KV 显存不够塞下新请求/继续 running 请求时,调度器从 running 尾部 preempt——
_preempt_request把请求的 KV block 释放、退回 waiting 队列,下步从头 recompute(或 swap 到 CPU)。这是显存不足的兜底,保证吞吐不崩但牺牲被抢占请求的算力。源码用 recompute 为主(简单、prefix caching 能缓解重算成本)。
十一、一张图收口
1 | 请求流: AsyncLLM.add_request ─► input queue |
主线一句话:vLLM = PagedAttention(KV 分页 + prefix 共享 + COW)把显存利用率拉满 + continuous batching(token 预算统一调度、prefill/decode 混跑)把 GPU 喂饱 + V1 三件套(async scheduler 重叠 CPU/GPU、CUDA Graph 跳 launch、spec decode 喂 decode 算力)把延迟和吞吐再推一格。 把这条主线、PagedAttention 的 block 机制、V1 调度器的 token 预算抽象讲顺,vLLM 这关就稳了。
参考资料
- Kwon et al., Efficient Memory Management for Large Language Model Serving with PagedAttention (vLLM), SOSP 2023
- vLLM V1 设计文档:
docs/design/、vllm/v1/源码 - EAGLE: EAGLE: Speculative Sampling Requires Rethinking Feature Uncertainty, ICML 2024
- 与本文显存计算法则、投机解码、RDMA(disaggregated prefill 跨机 KV)、CUDA Graph/x86 体系结构篇交叉对照


