vLLM 技术内幕——像操作系统管理内存一样管理 KV 缓存
vLLM 技术内幕——像操作系统管理内存一样管理 KV 缓存
基于 vLLM 主线(commit
8151f2ad,2026-08-12)与最新稳定版 v0.27.1(2026-08-11)撰写。文中涉及的源码位置均固定到该 commit。
核心论文:Kwon et al., Efficient Memory Management for Large Language Model Serving with PagedAttention, SOSP 2023(arXiv:2309.06180)。
1. 它解决什么问题
大模型推理的成本,很大一块不在算力,而在显存。一个 13B 模型,每个 token 的 KV 缓存约 800 KB(论文 §3);一段 2048 token 的序列,KV 缓存就要吃掉 1.6 GB。在线服务里,成百上千个请求并发,KV 缓存的总量往往比模型权重还大。
但传统推理引擎(FasterTransformer、Orca,以及直接用 HuggingFace Transformers 跑)把每个请求的 KV 缓存预先分配成一整块连续显存,长度按该请求可能达到的最大长度算。论文对真实负载(ShareGPT)的实测显示:KV 缓存里真正存了有效 token 状态的部分只有 20.4%–38.2%,也就是 60%–80% 的显存被白白浪费(内部碎片 + 外部碎片 + 为最长可能序列预留的空位)。
vLLM 由 UC Berkeley 提出,本质是用操作系统管理内存的那套思路——分页(paging)+ 页表(block table)+ 引用计数 + 写时复制——重做了 LLM 推理的 KV 缓存管理。它把"显存浪费"从根上压到不足 4%,把吞吐相对 HuggingFace Transformers 提升最高 24 倍,相对同期最优系统(FasterTransformer、Orca)提升 2–4 倍。
今天 vLLM 已经是一套完整的 LLM 推理与服务引擎,而不只是一个显存优化技巧。它提供 OpenAI 兼容的 API 服务、多种并行策略、量化、前缀缓存、投机解码、结构化输出、LoRA 等能力,并且支持从单卡到跨节点的大规模部署。
2. PagedAttention:把 KV 缓存变成"虚拟内存"
2.1 基本机制
PagedAttention 把每个序列的 KV 缓存切成固定大小的块(block),默认一块 16 个 token(论文 §7.2 验证过这是利用率与碎片之间的甜点)。每个块在物理显存里是一段不连续的连续空间,靠一张**块表(block table)**把"逻辑块序号"映射到"物理块 ID"——和操作系统用页表把虚拟页映射到物理页完全同构:块即页、token 即字节、序列即进程。
注意力计算时,不再把整段 KV 拼成一张大矩阵,而是逐块读取:对当前 query,依次与每个物理块里的 key 做点积,再按块累加 value。块表保证每个序列只看到自己的 token。
在 2026 年的 V1 代码里,这套结构依然清晰可见:
BlockPool为整块 GPU KV 显存预建一个KVCacheBlock元数据对象数组(vllm/v1/core/block_pool.py:174)。KVCacheBlock携带block_id、ref_cnt(共享引用计数)、满块时的前缀哈希、以及空闲链表指针(vllm/v1/core/kv_cache_utils.py:118)。- 每个请求的块表就是
req_to_blocks: dict[request_id, list[KVCacheBlock]],列表按逻辑块顺序排列,每个元素指向一个物理块对象(vllm/v1/core/single_type_kv_cache_manager.py:357)。 - 内核侧:
PagedAttention.write_to_paged_cache调用融合算子reshape_and_cache把新算出的 KV 写进对应物理块(vllm/v1/attention/ops/paged_attn.py:31);KV 张量按块布局为[2, num_blocks, num_kv_heads, head_size/16, block_size, 16](paged_attn.py:17)。
2.2 它带来的三个直接收益
显存几乎零浪费。 碎片被限制在每个序列最后一个不满的块里(小于一个块 = 16 token 的 KV),实测浪费不到 4%。结果是在相同显存下能并发处理的请求数,相对 Orca(Oracle) 多 2.2 倍、相对 Orca(Max) 多 4.3 倍(论文 §6.2)。
吞吐翻数倍。 相同延迟下,相对 FasterTransformer 和 Orca 提升 2–4 倍(论文摘要);在 ShareGPT 上相对 FasterTransformer 最高 22 倍;相对 HuggingFace Transformers 最高 24 倍,相对 TGI 最高 3.5 倍(官方基准)。
序列可共享 KV。 每个物理块带 ref_cnt。并行采样时,所有样本的 prompt 逻辑块指向同一批物理块,只在发生分叉的最后一个共享块上做写时复制(Copy-on-Write):某样本要写入一块 ref_cnt>1 的块时,分配新物理块、拷贝、原块引用计数减一(代码 _apply_cow,single_type_kv_cache_manager.py:403)。论文实测:并行采样省 6.1%–9.8% 显存,beam search 省 37.6%–55.2%(ShareGPT 上最高 66.3%)。
代价的量化:论文 §7.1 测出 PagedAttention 内核的相对延迟比 FasterTransformer 连续 KV 内核高约 20%–26%——但端到端仍然碾压,因为省的显存释放出的并发度远超这点内核开销。
3. 调度:连续批处理与分块预填充
3.1 连续批处理(Continuous Batching)
vLLM 沿用 Orca 的迭代级调度:每一步前向之后,完成的请求立刻移出批次,新请求立刻补进来。关键在于——PagedAttention 让"加一个请求 / 删一个请求"不再需要预分配连续显存,所以批次组成几乎零成本地在每次前向之间变化。
V1 的引擎主循环就是一遍遍 step()(vllm/v1/engine/core.py:581):
scheduler_output = scheduler.schedule(...) # 决定本步处理谁
future = executor.execute_model(...) # 非阻塞前向
model_output = future.result()
engine_core_output = scheduler.update_from_output(...) # 推进状态、回收块、生成输出
finish_requests 在 update_from_output 里触发 kv_cache_manager.free(),把请求占用的块按"尾部优先"的顺序归还给池(kv_cache_manager.py:568),下一轮的 schedule() 立刻可以把 waiting 队列里的请求补进来。增删都发生在两次前向之间,不在请求级边界上——这就是"连续"的含义。
把一轮 step() 从头到尾走一遍,连续批处理和抢占是怎么在同一个循环里发生的就一目了然了:
3.2 统一调度与分块预填充(Chunked Prefill)
V0 时代引擎内部区分"prefill 阶段"和"decode 阶段",一个长 prompt 的 prefill 是一整次前向,会独占一个 step,把 decode 和其他 prefill 全卡住。V1 做了一个关键简化:不再区分 prefill 和 decode,而是把 prompt token 和模型生成的 output token 一视同仁,用一张简单的字典来表示调度决策:{request_id: num_tokens}——本步给这个请求分配多少 token(V1 博客 §2)。
字典背后是两个预算(来自 scheduler.py:439 附近):
token_budget = max_num_scheduled_tokens(默认值 =max_num_batched_tokens= 2048);input_budget = max_num_batched_tokens,约束本步拼起来的总 token 数。
每个请求本步能处理的 token 数被夹在 剩余 token数 / token_budget / input_budget 三者之间(scheduler.py:882)。于是当一个 prompt 超过剩余预算时,它只是被切成"块"——剩下的块在后续 step 里接着算,而这一 step 里 decode 请求照样并行跑。这就是分块预填充,V1 里是默认开启、零配置的(docs/usage/v1_guide)。可选参数 long_prefill_token_threshold 还能对单个 prefill 块再设上限。
V0 与 V1 的本质差别:V0 只能在"全是 prefill"或"全是 decode"的同质 step 里工作;V1 的 scheduler 可以在同一个 step 里混跑 prefill 块和 decode。
分块预填充最怕"光看文字想不清"。假设有一个 3000 token 的长 prompt 进来,同时还有 A、B 两个正在 decode 的请求,预算是每步 2048 token——它实际是这样跨步铺开的:
长 prompt 被切成两块,块1 占满 Step 1 的预算、块2 在 Step 2 收尾;而 A、B 的 decode 每一步都照常跑,没有被长 prompt 卡住。TTFT 不再等于"等长 prompt 一次性算完",而是被预算切成多步完成,decode 的吞吐几乎不受影响——这正是分块预填充的收益所在。
4. 前缀缓存:让公共 prompt 不再重复算
很多场景里大量请求共享同一段 system prompt、同一段 few-shot 示例。前缀缓存(Prefix Caching)把这些公共前缀算过的 KV 块复用下来。
哈希是链式的:每个块的哈希 = 上一块的哈希 + 本块 token ID(+ 可选的 LoRA / 多模态额外键)。这样一块哈希唯一地标识"到该边界为止的整条前缀"(kv_cache_utils.py:577)。
查找:find_longest_cache_hit 从块 0 起顺着链式哈希往后走,遇到第一个 miss 就停(“链式哈希保证后面必然全 miss”),去 cached_block_hash_to_block 表里查(single_type_kv_cache_manager.py:729)。命中的块 touch()——引用计数 +1,若它原本在空闲队列(即淘汰候选)里则被摘掉(block_pool.py:702)。
写回:前向之后 cache_blocks 把新填满的块登记哈希、插入哈希表(single_type_kv_cache_manager.py:425)。只缓存已验证的 token,投机解码的草稿 token 不会被缓存(kv_cache_manager.py:555)。
淘汰:块释放时引用计数递减;仍带哈希、归零的块进空闲队列尾部(最后才被淘汰,等于保留作缓存),无哈希的块进头部(立刻复用)。空闲队列是 O(1) 双向链表,支持中间删除——即论文说的"常数时间缓存淘汰"。
V0 的前缀缓存因 Python 开销大默认关闭;V1 重做后开销压到 0% 命中率下吞吐损失 <1%,于是 V1 默认开启(config/cache.py:96)。论文对共享前缀的实测:LLaMA-13B 上一个 341-token 的 few-shot 前缀,相对 Orca(Oracle) 吞吐 3.58 倍。
命中一条公共前缀时,请求实际走的是下面这条路径——命中段直接复用物理块、跳过前向,只算未命中的后缀:
5. KV 缓存管理与抢占
KV 缓存管理器(vllm/v1/core/kv_cache_manager.py:118)是 PagedAttention 的"页表 + 分配器"。每步调度时 allocate_slots() 计算需要多少新块、检查池里够不够;不够就触发抢占。_preempt_request(scheduler.py:1284)的做法是把被抢占请求的 num_computed_tokens 直接归零、释放它所有块、退回 waiting 队列——也就是纯重算(recompute)。
这里有一个和原始论文、和 V0 都不同的重要事实:
- 论文 §4.5 实现了两种抢占恢复策略:Swap(把块搬到 CPU)和 Recompute(丢弃重算)。论文 §7.3 实测:重算开销随块大小基本不变,Swap 开销随小块急剧上升;重算延迟从不超过 Swap 的 20%,二者在块大小 16–64 时相当。所以即使当年也是重算为默认。
- V1 干脆移除了 Swap,只保留重算(
docs/usage/v1_guide的 Known Changes 明确标注 “GPU<>CPU KV Cache Swapping: Removed”)。理由就是上面那个实测:在 60–80% 显存浪费已被干掉的前提下,再维护一套 CPU 交换既复杂又不划算。被抢占的请求退回 waiting 后,如果它的前缀还缓存在池里,前缀缓存还能帮它少算一段。
6. 系统架构(V1):从 HTTP 到 GPU 的链路
6.1 进程拓扑
V1 用多进程把职责拆开,最大化吞吐并压低 CPU 开销。关键进程(来自 docs/design/arch_overview.md):
- API Server 进程:处理 HTTP 请求(OpenAI 兼容)、做输入处理(分词、多模态数据加载)、把结果流式回客户端。通过 ZMQ 与 EngineCore 通信。默认 1 个,DP>1 时自动扩到与 DP 数一致。
- EngineCore 进程:跑调度器、管 KV 缓存、协调 GPU worker。每个 DP rank 一个。内部是一个 busy loop,持续调度并把活派给 worker。
- GPU Worker 进程:每卡一个,加载权重、跑前向、管本卡显存。一个 EngineCore 下 worker 总数 =
DP × PP × TP。 - DP Coordinator 进程:仅当
DP>1时存在,做跨 DP rank 的负载均衡。
6.2 一次请求的端到端流转
关键代码锚点:
- 入口
api_server.py:110的build_async_engine_client()→AsyncLLM.from_vllm_config()(:168)。 - 异步引擎
AsyncLLM(vllm/v1/engine/async_llm.py:72)只接三样东西:InputProcessor(请求→EngineCoreRequest)、OutputProcessor(EngineCoreOutputs→RequestOutput)、EngineCoreClient(→ZMQ 客户端)。 - ZMQ 客户端
AsyncMPClient(core_client.py:977):input 用 ROUTER socket、output 用 PULL socket(:554)。 EngineCore(core.py:104)被EngineCoreProc(:1005)包了一层 ZMQ;run_engine_core(:1269)是后台进程入口,run_busy_loop(:1373)是那个不停转的循环。- 调度结果经
Executor下发到Worker.execute_model(gpu_worker.py:1026),再到GPUModelRunner.execute_model(gpu_model_runner.py:4277):更新状态 → 准备输入 → 构建注意力元数据 → 前向 → 算 logits → 采样(sample_tokens,:4656)。
6.3 执行层与并行
Executor 的选择在 vllm/v1/executor/abstract.py:48 的 get_class():
uni→UniProcExecutor:worker 跑在同一进程,调试用;mp(默认)→MultiprocExecutor:为每个 GPU 起一个WorkerProc,GPU 上的Worker就在其中运行;ray→RayExecutorV2/RayDistributedExecutor:跨节点大规模部署。
并行维度有四层,可任意组合:
- TP(张量并行):层内权重切分,单节点多卡最常见;
- PP(流水线并行):层间切分,配
max_concurrent_batches用 batch queue 填流水线气泡; - DP(数据并行):多份完整副本,配 DP Coordinator 做请求负载均衡;
- EP(专家并行):MoE 的专家切分到不同卡,
v0.9.0起引入 NIXL 集成,后续有elastic_ep动态扩缩。
7. 能力与生态(截至 v0.27.1)
7.1 模型覆盖
主线(main,2026-08)登记了 240 个原生模型架构:123 个纯文本生成 + 117 个多模态(含语音识别/转录、实时语音)。另有一组 池化(pooling)模型:embedding 39 个、分类 5 个、token 分类 7 个、打分/重排 12 个、奖励模型 7 个。
覆盖面横跨主流家族:Llama 2/3/4、Mistral / Mixtral / Ministral、Qwen2/2.5/3/3.5 及 Qwen2-VL/2.5-VL/3-VL/Qwen3-Omni、DeepSeek-V2/V3/V3.2/V4 与 R1、Gemma 2/3/3n/4、GPT-OSS、Phi、GLM-4.x/5.x、Kimi(K2/K3/VL)、MiniMax(M2/M3/VL)、Nemotron、Granite、InternLM、Jamba、Mamba2、Falcon、OLMo 等文本侧;视觉侧有 InternVL 2/2.5/3、LLaVA 系列、Pixtral、Molmo、Gemma 3/4 vision、Llama 4、Phi 视觉、GLM-4.5V;语音侧有 Whisper、Qwen3-ASR、Kimi-Audio、GLM-ASR、FireRedASR2;视频侧有 LLaVA-NeXT-Video、InternVideo。
对不在原生列表里的模型,vLLM 提供 Transformers 建模后端作兜底(支持 encoder-only / decoder-only / MoE、图像与语音多模态、配合 trust_remote_code 的自定义模型),并用插件系统补齐 encoder-decoder(BART、Florence-2 等)。注意:encoder-decoder 在核心里已不再原生支持,必须走插件——这是它成熟度上一个明确的边界。
7.2 量化
支持:GPTQ(AutoAWQ、GPTQModel)、BitsAndBytes(NF4/INT8)、LLM Compressor(FP8-W8A8、INT4-W4A16、INT8-W4A8、INT8-W8A8)、Marlin(GPTQ/AWQ/FP8/FP4 的高性能内核)、FP4 系列(NVFP4、MXFP4、MXFP6、MXFP8)、AMD Quark、TorchAO、Intel Neural Compressor 等;KV 缓存本身也能量化(FP8 KV Cache)。
7.3 硬件后端
- NVIDIA CUDA:主战场,Ampere 起的架构全支持;
- AMD ROCm:V1 自
v0.10.0起默认启用; - Intel XPU(Gaudi/GPU)、CPU(x86 / ARM / Apple / IBM Z);
- Google TPU:核心内置平台,文档独立维护;
- AWS Trainium/Inferentia:已移出核心,改由独立插件 vllm-neuron(beta,基于 vLLM 0.21.0)维护;
- Apple Silicon:通过 vLLM-Metal 插件支持。
一句话概括硬件状态:CUDA/ROCm/XPU/CPU/TPU 在核心,Neuron 与 Apple Silicon 在插件,且 vLLM 自 v0.12.0 起正式支持硬件插件系统,第三方后端不必再塞进核心代码。
7.4 服务与高级特性
-
OpenAI 兼容 API:
/v1/completions、/v1/chat/completions(含 batch)、/v1/responses(含 cancel)、/v1/embeddings、/v1/audio/transcriptions|translations;另有非 OpenAI 的/rerank、/score、/classify、/tokenize、/detokenize、/generate、/pooling等。 -
结构化输出:默认后端
xgrammar(auto),旧guided_*字段在v0.12.0移除,统一到structured_outputs。 -
投机解码:支持 EAGLE、MTP、Medusa 等;V1 用
num_tokens_with_spec把草稿 token 一并纳入统一调度,仅验证通过的 token 真正落盘缓存。草稿如何生成、又如何被验证采纳,流程如下: -
前缀缓存:默认开启(§4)。
-
LoRA:多 LoRA 并发服务,
max_loras控制上限。 -
分离式 prefill/decode(Disaggregated P/D):实验性,通过 KV 连接器把 prefill 实例算出的 KV 搬到 decode 实例,连接器有 NIXL、Mooncake、LMCache、offloading 等九种。
8. 与同类系统的差别
| 维度 | vLLM | SGLang | TGI (HF) | TensorRT-LLM |
|---|---|---|---|---|
| 实现语言 | Python 为主 | Python 为主 | Rust + Python | C++/Python(编译成引擎) |
| 显存管理 | PagedAttention(块表) | RadixAttention(radix 树缓存) | 连续分配(较简单) | 分页 + paged KV |
| 调度特色 | 统一调度 + 分块预填充 | N+1 调度 | 连续批处理 | in-flight 批处理 |
| 模型广度 | 极广(240+ 架构) | 广 | 中(HF 生态) | 窄(NVIDIA 认证模型) |
| 硬件 | CUDA/ROCm/XPU/CPU/TPU + 插件 | 以 NVIDIA 为主 | 多硬件 | 仅 NVIDIA |
| 定位 | 通用推理服务引擎 | 强推理/agent 框架 | HF 官方服务 | NVIDIA 极致性能 |
差异是结构性的:TGI 偏 HF 官方、简单稳妥但并行能力弱;TensorRT-LLM 追求 NVIDIA 上的极限性能,代价是模型覆盖窄、要编译成引擎、闭源二进制;SGLang 与 vLLM 路线最接近(都是 Python、都做分页/radix 缓存、都接 xgrammar),竞争焦点在调度细粒度与生态。vLLM 的核心护城河是 PagedAttention 奠定的显存效率和由此撑起的模型广度与并行组合。
9. 演进时间线
- 2023-06:vLLM 首发,提出 PagedAttention(arXiv 论文尚未出)。
- 2023-09:SOSP 2023 论文发表,2–4 倍吞吐的结论成为行业基准。
- 2024 下半年:连续批处理之外,引入分块预填充、前缀缓存、投机解码等。
- 2025-01:V1 引擎实验性发布(
VLLM_USE_V1=1),重做调度器/KV 管理器/worker/采样器/API server。 - 2025-03:v0.8.0,V1 对 CUDA 成为默认引擎(仍可用
VLLM_USE_V1=0回退)。 - 2025-05:v0.9.0,引入 DP/EP/PD 雏形、NIXL 集成、PyTorch 2.7 / CUDA 12.8。
- 2025-07:v0.10.0,开始清理 V0 代码、ROCm 默认 V1、Responses API、MXFP4 MoE。
- 2025-12:v0.12.0,PyTorch 2.9、
guided_*统一到structured_outputs、xformers 后端弃用、Model Runner V2 实验性。 - 2026 全年:改为约每两周一个发布;到 v0.27.1(2026-08-11),V0 引擎已从代码树中彻底移除,
LLMEngine/AsyncLLMEngine只是 V1 类的别名。
一个对写报告很重要的纠正:“V0 vs V1” 在今天已经是历史概念。2026 年的代码里没有 V0 引擎,
vllm/engine/llm_engine.py只有六行,把LLMEngine重新指向 V1 的LLMEngine。要做对比,只能描述 2025 年初之前的旧架构(单体LLMEngine把调度器和 worker 放在同进程、vllm/core/scheduler.py、vllm/worker/worker.py),并说明它已不在主干。
10. 小结
vLLM 的起点是一个看似局部的优化——把 KV 缓存从"一大块连续显存"改成"按页管理的块表"。但这个局部优化撬动了整条推理链路:因为显存不再碎片化,连续批处理才真正低成本;因为块可共享、可哈希,前缀缓存和并行采样才自然成立;因为调度不再被 prefill/decode 的阶段边界束缚,分块预填充才顺理成章。今天它已经长成一套覆盖模型广度、量化、多硬件、多并行、OpenAI 兼容服务与高级推理能力的完整引擎,而所有能力都建筑在 PagedAttention 这一根骨架上。
选型上,vLLM 最值当的场景是通用、大规模、模型杂、还要 OpenAI 兼容 API 的-service;负载锁死在 NVIDIA、只追单模型极限延迟时,该把 TensorRT-LLM 拉进来比;规模不大又重度依赖 HF 生态,TGI 更轻;要做复杂推理或 agent 流程,SGLang 是同量级对手。而无论哪种用法,把 vLLM 真正跑顺,终究绕不开它的基石:KV 块大小、max_num_batched_tokens 预算、前缀缓存开关,以及分块预填充在 TTFT 与吞吐之间做的那笔权衡。
参考来源
- Kwon et al., Efficient Memory Management for LLM Serving with PagedAttention, SOSP 2023. arXiv:2309.06180. DOI 10.1145/3600006.3613165
- vLLM 官方文档(docs.vllm.ai,arch_overview / v1_guide / quantization / disaggregated_prefill 等),访问于 2026-08
- vLLM 博客:2023-06 发布帖、2025-01 V1 公告、2025-09 “Anatomy of vLLM”
- vLLM 源码(vllm-project/vllm,commit
8151f2ad,2026-08-12):v1/engine/core.py、v1/core/sched/scheduler.py、v1/core/kv_cache_manager.py、v1/core/block_pool.py、v1/attention/ops/paged_attn.py、v1/executor/、entrypoints/openai/api_server.py - 版本里程碑:
v0.8.0–v0.27.1发布说明
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)