BLOG

vLLM 的 PagedAttention:显存管理如何撑起大模型推理

Kael Zhang
AIvLLMInference
广告 · Advertisement

技术拆解:解析 AI 技术框架——说明、分析、技术评估、价值判断、落地使用。 作者:永亮


2023 年,大模型推理有个公认的痛:GPU 显存看着够大,真跑起来 batch 却开不大。原因不在模型权重——权重是死的,算一次就知道占多少——而在 KV cache:它随着对话变长不断膨胀,还每条请求长得不一样。UC Berkeley 的 Sky Computing Lab 在这一年给出了一个后来被反复引用的答案:把操作系统的虚拟内存分页搬进注意力计算,就有了 PagedAttention,以及它背后的服务引擎 vLLM。

两年多过去,vLLM 从一篇 SOSP 论文长成推理服务的事实标准:截至 2026 年 9 月,GitHub 约 9.2 万 star、PyPI 周下载约 41.5 万次、贡献者超过 2000 人。这篇文章照旧拆六件事:它是什么、核心机制一直到源码层、数据上怎么评估、和 SGLang 怎么选、值不值得用、怎么落地,以及——如果你想自己写一套类似的显存管理,最小思路是什么。

一、这是什么

一句话定位:vLLM 是一个高吞吐、显存高效的 LLM 推理与服务引擎(GitHub 仓库描述原文),它解决的核心问题是——KV cache 占着推理显存的大头,管理粗放就会浪费,浪费就会压低 batch 大小,batch 大小直接决定吞吐。

关键事实:仓库 2023 年 2 月创建,最初来自 UC Berkeley 的 Sky Computing Lab;论文《Efficient Memory Management for Large Language Model Serving with PagedAttention》发表于系统领域顶会 SOSP 2023,arXiv 编号 2309.06180,九位作者里有 Ion Stoica、Joseph Gonzalez 这类分布式系统领域的大牛;协议 Apache-2.0;约 91,598 star、22,113 fork。README 自述「由数十家学术机构和公司组成的社区维护」,贡献者名单按提交量排,前 100 名开发者合计提交超过 1.2 万次——star 高不是刷出来的,是真团队在长期投入。

二、核心机制

2.1 先把账算清楚:KV cache 到底吃多少显存

PagedAttention 不是凭空想出来的花活,是先把账算清楚了。这笔账今天依然值得每个做推理的人背下来:

每 token 的 KV cache 字节数 = 2 × 层数 × KV 头数 × 头维度 × 每参数字节数(乘 2 是因为 K 和 V 各一份)。

拿 Qwen2.5-72B 的官方配置代入(Hugging Face config.json):80 层、GQA 架构 8 个 KV 头、头维度 128、fp16 精度两字节——每个 token 要占 327,680 字节,约 0.31 MB。一个 8k 上下文的请求,光 KV cache 就要 2.5 GB;一百并发就是 250 GB。这里 GQA 立了大功:64 个注意力头共享 8 个 KV 头,要是还是多头注意力老架构,这个数字再翻八倍。

这笔账解释了论文里三类浪费为什么致命。预留:旧系统按预估的最大长度一次给每条请求划满连续显存,多数位置空着。内部碎片:实际长度到不了预估长度,尾巴作废。外部碎片:显存被切成大小不一的空洞,凑不出连续大块——旧系统要求显存连续,这是它自己给自己挖的坑。论文的实验结论是:旧系统里 KV cache 的有效利用率可以低到约 20%,八成显存白占着。而 serving 是显存受限场景,白占的直接后果就是 batch 开不大、吞吐上不去。

2.2 PagedAttention:操作系统分页思想的三个零件

PagedAttention 的想法直白得惊人:操作系统管虚拟内存,把进程看到的连续地址空间切成固定大小的页,靠页表映射到物理内存——为什么不把 KV cache 也这么管?

落到实现上是三件套。第一,分页:KV cache 不再按请求预留一整条连续空间,而是切成固定大小的块(论文实验里常用 16 个 token 一块),需要时才分配一块。第二,块表(block table):每个请求维护一张逻辑块号到物理块号的映射表,注意力内核按表去 gather 对应的物理块,物理块不要求连续。这一步是全文的关键——「连续」假设正是外部碎片的来源,块表把它彻底取消了。第三,按需分配:块从左到右填满才申请新块,每条请求的浪费被压在一个块以内,接近零浪费(论文口径)。

2.3 Copy-on-write:共享块的引用计数

分页还带来一个计划外的好处:共享。并行采样(同一个 prompt 生成 n 条候选)、beam search 的多条路径,前缀 KV 完全一样——旧系统要么各存一份,要么靠特殊逻辑硬共享。vLLM 的做法和操作系统一模一样:共享块的引用计数加一,谁要写入且引用计数大于一,先复制再写(copy-on-write)。结果是前缀 KV 在请求内和请求间都能复用,论文把这条列为 PagedAttention 的第二大贡献:near-zero waste 之外,再加 flexible sharing。

2.4 源码层:V1 的块池与哈希链

2025 年 vLLM 完成 V1 引擎重写,如今全部调度逻辑都在 vllm/v1 目录下,旧引擎代码已删除。块管理的入口是 vllm/v1/core/block_pool.py 的 BlockPool,它管两张结构:

第一张是空闲块队列 free_block_queue——一个双链表,按「最近使用」排序。被复用过的块释放后排回队尾,新分配从队首拿;队首的块如果还带着前缀缓存内容,就先把它从缓存里逐出再分配。LRU 逐出不需要任何额外数据结构,队列顺序本身就是逐出顺序。第二张是哈希到块的映射 cached_block_hash_to_block,支撑前缀缓存的查找。

块的哈希不是简单的「本块内容求哈希」,而是链式的。kv_cache_utils.py 里的 hash_block_tokens 长这样:sha256(父块哈希,本块 token 序列,额外键)。父块哈希做盐——前缀内容只要差一个 token,后面整条链的哈希全部不同,不同前缀不可能撞出同一个键;函数本身带 LRU 缓存,同一块内容不用重复算。新请求进来,kv_cache_manager.py 的 get_computed_blocks 拿自己的 prompt 逐块比对这条哈希链,命中的块引用计数加一、直接接管,对应部分的 prefill 整个跳过。

源码里有个细节值得说:即使 prompt 的每个块都命中缓存,最后一个 token 也必须重算——因为拿 logits 要靠现算(源码注释原话:When all tokens hit the cache, we must recompute the last token to obtain logits)。这个「全命中也要算一步」的小设计,保证了缓存命中不改变输出语义。

2.5 调度器:一个循环吃掉 prefill、decode 和抢占

调度入口在 vllm/v1/core/sched/scheduler.py。这个文件的顶部有一段作者 Woosuk 写的注释,值得全文读:调度器里没有「decode 阶段」和「prefill 阶段」之分,每个请求只维护两个数——num_computed_tokens(已算过的 token 数)和 num_tokens_with_spec(要算到的位置,等于 prompt 长加已生成长加投机 token)。调度器每步做的事,就是给每个请求分 token 额度,帮前者追上后者。

这个抽象的好处是三种场景被同一个循环统一吃掉:长 prompt 切块混进批次(chunked prefill,一条长请求不会饿死一片 decode);前缀缓存命中的请求直接从中间位置继续;投机解码的多算几个候选 token 也只是 num_tokens_with_spec 多加一截。每步的总额度由 max_num_scheduled_tokens 卡住。

显存不够时的手段是抢占:_preempt_request 把 running 队列尾部的请求撤回 waiting 队列,释放它的全部块和编码器缓存。V1 的抢占只有重算(recompute)一种——被撤回的请求下次轮到它时从头再 prefill 一遍,不做换出(swap)到 CPU 内存。这是明摆着的取舍:重算浪费算力,但换来实现简单,也避免 CPU 与 GPU 之间搬运 KV cache 的抖动。真正的线上系统里,抢占次数是个该盯的指标——频繁抢占说明容量配置或调度参数有问题。

V1 还做了两层重叠:输出处理(detokenize、流式发送)与 GPU 计算重叠;异步调度模式下,准备下一 step 的调度决策与当前 step 的计算重叠。调度器纯 Python,这类重叠就是它的免费午餐。

2.6 从块表到 GPU:内核、后端与多进程

块表最终要变成 GPU 上的东西:每步调度把 block_table 以张量形式传给注意力内核。decode 用的是 vLLM 自己在 csrc/attention 下用 CUDA/CUTLASS 手写的 paged attention 内核,按表 gather 物理块;prefill 的注意力形状不同(一次性算整个 prompt),不走这条路径,直接接 FlashAttention、FlashInfer 等现成后端。两者之上是 AttentionBackend 抽象层,NVIDIA、AMD、CPU、TPU 各自实现自己的后端,内核细节对调度器不可见。

decode 步的形状高度规整(每请求每步恰好多一个 token),V1 把整个 decode step 用 CUDA Graph 捕获,消除 Python 与 CUDA 之间的发射开销;prefill 与 decode 混合的场景靠分片编译(piecewise compilation)把形状变化的部分留在图外。这一步和分页是相辅相成的:正因为每个请求按块增减显存,批次里谁走谁留都不影响别人的显存布局,整步计算才能被稳定地捕获成图。

最后是多进程骨架:API server 进程接 HTTP 请求、做 tokenize 和多模态加载,经 ZMQ 与 engine core 通信(多 API server 对多 engine core 的 many-to-many 拓扑);engine core 进程做调度决策;GPU worker 进程一卡一个,数量等于 DP×PP×TP,只负责跑前向;开了数据并行还有一个 DP coordinator 做负载均衡。单机 4 卡张量并行的标准部署,就是 1 个 API server + 1 个 engine core + 4 个 worker,共 6 个进程。

三、技术评估:2-4 倍是论文口径,先看它跟谁比

论文的数据:在同延迟水平下,vLLM 的吞吐比当时的最强系统 FasterTransformer 和 Orca 高 2-4 倍;序列越长、模型越大、解码算法越复杂,优势越明显(论文原话)。注意基线——这是 2023 年的对比,Orca 已经是当时最先进的迭代级调度系统,vLLM 赢它靠的是显存管理,不是算子。

我的解读分两层。第一层,论文里「旧系统有效显存低至约 20%」这个测量,比 2-4 倍更本质——它解释了为什么改进成立:把浪费从八成压到一个块以内,同样的卡能塞进数倍请求,吞吐自然翻倍。这个因果是硬的。第二层,放到 2026 年的坐标系里,2-4 倍不能再当宣传语抄,今天的对手是 SGLang、TensorRT-LLM 这一档,差距缩到了 workload 相关的百分之几十。

热度看三个数:star 约 9.2 万;贡献者前 100 人合计提交超 1.2 万次;PyPI 周下载约 41.5 万次。三个数互相咬合,真实使用规模毋庸置疑——它是推理服务领域的头号基础设施。

冷水也要泼。benchmark 是论文口径,自家复现要看自己的请求长度分布和并发模式;前缀缓存对短请求、高多样 prompt 的场景收益有限,白白多占显存;V1 的多进程架构对单卡小模型是有 CPU 开销的,官方文档自己都建议按 GPU 数量配置 CPU 资源。

四、和 SGLang 比,怎么选

SGLang 画像一句话:LMSYS 出身的高性能服务框架,2024 年 1 月创建,约 3.6 万 star,Apache-2.0,核心差异化是 RadixAttention——把前缀缓存组织成基数树而不是哈希表,官方口径「最高 5 倍推理加速」(2024 年 1 月官方 blog,自评数据),近一年在 agent 工作流和 RL 训练框架(verl、slime 等)里采用度很高。

三句话选型:没有明确理由时默认 vLLM——生态、文档、硬件适配(NVIDIA/AMD/Intel/CPU 一等支持,TPU/Gaudi/Ascend/Apple Silicon 走插件)最全,踩坑搜索成本最低;如果你的负载是多轮 agent 调用、复杂结构化输出、或要嵌进 RL 训练循环,值得把 SGLang 拉进来同场压测,它的激进优化在这些场景有实测优势;两个都是 Apache-2.0,迁移成本不高,拿自己的流量说话。

理念上两者同源:RadixAttention 和 PagedAttention 的前缀共享是同一条思路的两种数据结构(基数树对块表哈希),SGLang 项目本身也大量复用了 vLLM 的基础设施。这不是零和竞争,是整个行业在朝「KV cache 是可复用资源」这个共识收敛。

五、价值判断

真问题:推理成本等于显存效率乘 batch 大小。vLLM 把 KV cache 从「预留制」改成「分页制」,是我看来 2023 年以来推理栈最重要的一项单项改进——它不改变模型、不改变芯片,纯靠系统软件把同样的硬件多榨出几倍吞吐。今天几乎所有开源推理框架都在用它的块管理思想,这一点本身就是价值证明。

边界也清楚。单机跑小模型、并发个位数,分页和连续批处理带来的收益有限,简单方案可能更省心;它不解决首 token 延迟——prefill 的算力瓶颈要靠内核和并行策略,是另一个战场;对非标准架构(Mamba 这类状态空间模型)的支持在 V1 里刚长出 HybridKVCacheCoordinator 这层,还在演进。什么时候该用:任何要认真对外提供 LLM 服务的场景,它就是默认起点。什么时候不该用:一次性离线跑几条数据、或者要在训练里用——那是别的工具的地盘。

六、如何落地

安装一行:

uv pip install vllm    # 或 pip install vllm

对外起服务也是一行,vllm serve 起 OpenAI 兼容接口(同时支持 Anthropic Messages API 和 gRPC):

vllm serve Qwen/Qwen3-32B --tensor-parallel-size 2

离线批量推理用 Python 的 LLM 类,几行出结果;支持 200 多种模型架构——decoder-only、MoE、混合注意力/状态空间、多模态、embedding、奖励模型都能 serving,还覆盖 parallel sampling、beam search、结构化输出(xgrammar/guidance)、tool calling 解析、多 LoRA。分布式侧有张量、流水、数据、expert、context 五种并行可配。落地建议两条:prefix caching 按流量特征决定开关,多轮对话类负载开着、短请求高多样负载关掉;生产环境把 KV cache 利用率和抢占次数纳进监控,这两个指标比 GPU 利用率更早暴露容量问题。

七、如何自己做一套类似的方案

「自己写一套」不是假想题。GitHub 上的 nano-vllm(GeeeekExplorer/nano-vllm,2025 年 6 月开源,MIT,约 1.5 万 star)就是社区读 vLLM 源码后写出的最小可读复刻,整个引擎千行量级——它证明了一件事:PagedAttention 的核心思想一页纸讲得完。骨架六步:

  1. 块池加块表:启动时把 KV cache 显存切成固定大小的物理块池;每个请求维护一张逻辑块到物理块的表,注意力按表 gather。
# 最小骨架:块池
free_blocks = deque(range(num_gpu_blocks))   # 物理块空闲队列
block_table = {}                             # req_id -> [物理块号]
  1. 按需分配:每步 decode 后检查当前块是否写满,满了才从空闲队列取一块挂上——浪费天然不超过一块。

  2. 引用计数加写时复制:共享块 refcount 加一;写入前 refcount 大于一就先复制再写。并行采样和 beam search 的共享就靠这一条。

  3. 调度循环只追两个数:每个请求记 num_computed 和 num_total;每步在 token 预算内挑请求,让 computed 追上 total;谁说完谁退出,放不下就按优先级抢占(撤回请求、释放块,事后从头重算 prefill)。chunked prefill、前缀缓存、投机解码,都只是这个循环的特例。

  4. 前缀哈希缓存:按 sha256(父块哈希 + 本块 token)建链式哈希,逐块比对命中跳过 prefill,空闲队列队首即逐出候选。

  5. 内核别自己写:注意力直接接 FlashAttention,CUDA/CUTLASS 留给真有性能团队的时候。nano-vllm 就是这么干的。

这六步加起来几百行 Python 加现成内核就能走通——PagedAttention 论文的核心洞察从来不复杂,难的是两年如一日把它维护成生产级。这也是 vLLM 源码比论文更值得读的原因:架构思想一页纸能说清,工程完成度才是护城河。

结论

PagedAttention 的贡献可以浓缩成一句话:KV cache 不是「每条请求一条连续数组」,而是「按需分配、按块共享的池化资源」。vLLM 用操作系统的老办法解决了大模型推理的新问题,凭这一件事成为事实标准。对大多数团队,选型不需要犹豫——默认 vLLM,有特殊负载再拉 SGLang 同场压测;对想做推理系统的工程师,它的源码是比论文更好的教材。

参考来源

  • vLLM GitHub 仓库(README、架构文档):https://github.com/vllm-project/vllm
  • 论文:Efficient Memory Management for Large Language Model Serving with PagedAttention,SOSP 2023,arXiv:2309.06180(摘要、§2 浪费分析、§6 评测)
  • vLLM 官方架构文档 Architecture Overview(docs.vllm.ai,V1 多进程架构与源码索引)
  • vLLM V1 源码:vllm/v1/core/block_pool.py(块池与空闲队列)、vllm/v1/core/kv_cache_utils.py(hash_block_tokens 链式哈希)、vllm/v1/core/kv_cache_manager.py(get_computed_blocks 前缀命中)、vllm/v1/core/sched/scheduler.py(调度主循环与 _preempt_request 抢占)、csrc/attention(paged attention CUDA/CUTLASS 内核)
  • Qwen2.5-72B 模型配置(Hugging Face config.json:80 层 / GQA 8 KV 头 / 头维度 128,KV cache 显存账计算依据)
  • GitHub 仓库元数据与贡献者列表、pypistats.org vllm 下载量(2026 年 9 月)
  • nano-vllm 最小复刻(GitHub: GeeeekExplorer/nano-vllm,MIT):https://github.com/GeeeekExplorer/nano-vllm
  • 源码解读二手资料(交叉参考用):博客园「Nano-vLLM 源码解读」系列、掘金「nano-vllm 的 KV Cache 与 Paged Attention」系列、CSDN「大模型推理引擎 vLLM 学习笔记」系列(V1 架构与 Prefix Caching 篇)
  • SGLang GitHub 仓库(README 与元数据):https://github.com/sgl-project/sglang;LMSYS blog 2024-01-17(RadixAttention 官方口径)
广告 · Advertisement

常见问题

PagedAttention如何解决KV cache显存浪费问题?

PagedAttention将KV cache分成固定大小的块,按需求动态分配,避免了传统方法中为每条请求预留连续显存导致的预留浪费、内部碎片和外部碎片。实测显示传统方法的显存有效利用率可低至20%,而PagedAttention将浪费压缩到一个块以内,使同一GPU能服务更多并发请求。

vLLM的前缀缓存是如何工作的?

vLLM使用链式哈希算法对KV cache块进行索引,每个块的哈希值由父块哈希和当前块token计算得出。当新请求的prompt与缓存中的前缀匹配时,可直接复用已计算的KV cache,跳过部分prefill计算,降低首token延迟。缓存块采用LRU策略管理,最近未使用的块优先被驱逐。

vLLM和SGLang相比该如何选择?

默认选择vLLM,因为它拥有更全面的生态、文档和硬件适配支持,社区活跃度高,问题排查成本低。如果负载主要是多轮agent调用、复杂结构化输出或需要嵌入RL训练循环,可以考虑SGLang,它在这些场景有针对性优化。两者都是Apache-2.0协议,建议用实际流量进行压测对比后再做决定。