vLLM 引擎解剖

vLLM 引擎解剖

vLLM 是部署最广泛的开源大语言模型推理引擎,它的核心思想几乎被后来所有的 serving 框架默默借鉴。本文从内到外拆解这个引擎:预填充/解码的执行模型及其时延指标、分页式 KV 缓存(PagedAttention)、当前 V1 引擎的统一调度器、抢占与前缀缓存,以及 vLLM 为生产监控暴露的 Prometheus 指标。最后讨论它与邻近系统——FlashAttention、SGLang 与分离式预填充——的边界,并给出在单块 TITAN RTX 上如实复现本文内容的指南。

文中一切内容均引自一手来源(PagedAttention 论文、vLLM 官方文档与仓库,以及相邻系统的原始论文),信息截至 2026-08-09;文末附有带日期的版本说明。本文作者没有运行任何基准测试:文中每个性能数字都引自测量它的原始来源。

预填充、解码与三个时延指标#

Transformer 的文本生成是自回归的:模型一次只产出一个词元,每个新词元都要与之前的所有词元做注意力计算。serving 系统把这一过程切成两个形态截然不同的阶段:

  • 预填充(prefill)处理提示词。长度为 $P$ 的提示词在一次(或少数几次)前向传播中并行跑完整个模型,每个提示词元组的键(key)和值(value)都被计算出来并写入 KV 缓存。预填充是计算密集型的,每一层都要全量计算。
  • 解码(decode)逐个生成输出词元。每一步只对最新词元跑一次模型,此前所有词元的键/值直接从 KV 缓存读取。解码是内存带宽密集型的:每一步几乎不重复计算注意力状态,但必须把整份 KV 缓存流过 GPU。

这两个阶段对应着所有 serving 基准测试都会引用的三个时延数字:

  • TTFT——首词元时延(time to first token):从请求提交(vLLM 从被调度算起)到返回第一个生成词元。它由预填充主导。
  • ITL——词元间隔时延(inter-token latency):解码阶段相邻输出词元之间的时间,即解码阶段每步的时延。
  • TPOT——每输出词元耗时(time per output token):解码阶段产出一个输出词元的平均时间。vLLM 官方的参考 Grafana 面板把 vllm:inter_token_latency_seconds 指标当作 TPOT 使用,因此实践中两者经常混用。

图 1 把它们放在一条时间线上。TTFT 期间请求是不可交互的,所以交互型用户最关心 TTFT;吞吐型负载关心的是 ITL/TPOT,以及有多少请求在并发解码。

请求到达                   首词元                    末词元
  |                          |                          |
  |<------- 预填充 -------->|<---- 解码(每步一个词元)---->|
  |<------- TTFT --------->|
                             |<-- ITL -->|<-- ITL -->|<-- ITL -->|
                             |<--------- 生成时间 --------->|
  |<---------------------- 端到端请求时延 -------------------->|

图 1 —— TTFT、ITL 与 TPOT 在请求时间线上的位置。TTFT 覆盖预填充;ITL/TPOT 描述解码阶段。定义遵循 vLLM 的 per-request 指标文档。

KV 缓存需要多少内存?#

注意力计算中,每个词元都会产生一个查询(query),与之前所有词元的键做比较,再用得到的权重对值加权求和。每个已处理词元的键和值必须在序列的整个生命周期内保留,因为之后每个词元都要与它们做注意力。这份存储就是 KV 缓存,它是 LLM serving 的核心内存难题。

KV 缓存在给定精度下每个词元占用的字节数为:

每词元字节数 = 2(键+值)x 每元素字节数 x 层数 x KV 头数 x 每头维度

第一个因子 2 是键值对;第二个是元素大小(FP16 为 2 字节,FP8 为 1 字节)。使用分组查询注意力(GQA)时,KV 头数少于查询头数——这正是 GQA 存在的原因:它把 KV 缓存内存按分组数成倍压缩。

模型 层数 KV 头数 每头维度 每词元 KV 字节数(FP16) 8k 上下文
Llama-2-7B(MHA) 32 32 128 512 KiB 4 GiB
Llama-3.1-8B(GQA) 32 8 128 128 KiB 1 GiB
Llama-3.1-70B(GQA) 80 8 128 320 KiB 2.5 GiB

表 1 —— 三种模型配置在 FP16 下的每词元 KV 缓存大小(层数/KV 头数/每头维度取自 Llama 3 模型论文)。MHA 与 GQA 的差异解释了为什么两个 7–8B 模型相差 4 倍。Llama-3.1-8B 的 128k 上下文仅 KV 缓存就需要 16 GiB——与模型权重相当。

KV 缓存还是动态的:解码过程中它逐词元增长,每个序列最终有多长事先无法预知,请求也在持续到达和结束。推理引擎必须为一个体量巨大、持续增长且不可预测的对象分配内存。这正是 PagedAttention 要解决的问题。

PagedAttention:为 KV 缓存分页#

PagedAttention 的原始设计出自 SOSP 2023 论文《Efficient Memory Management for Large Language Model Serving with PagedAttention》(Kwon 等,arXiv:2309.06180)。论文指出,既有系统把每个请求的 KV 缓存分配成一段连续内存,按可能的最大序列长度预留。论文的剖析实验(其图 2)发现,既有系统中只有 20.4%–38.2% 的 KV 缓存内存真正存放了词元状态:其余都被超额预留、过度供给带来的内部碎片以及分配器的外部碎片浪费掉了。

修法借鉴自操作系统:把 KV 缓存当作分页虚拟内存来管理。

  • 缓存被划分为固定大小的物理块——vLLM 的默认块容纳 16 个词元(CacheConfig.DEFAULT_BLOCK_SIZE)。
  • 每个序列拥有一段逻辑块序列;每序列一张块表(block table)把逻辑块号映射到物理块号。
  • 物理块按需分配,词元产生多少就分配多少,因此一个序列的各块散落在缓存各处而非连续排列。不会为也许永远不会到达的词元预留任何块。

图 2 展示了这种布局。结果是近乎零内存浪费:唯一浪费的空间是每个序列末尾未填满的半个块,而且块只在真正需要时才分配。

GPU KV 缓存,12 个物理块(每块 16 词元):

  服务前:           [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11]
                    全部空闲

  两个序列之后(A = 40 个提示词元,B = 20 个):

                    [0]=A t0-15   [1]=A t16-31  [2]=A t32-39
                    [3]=B t0-15   [4]=B t16-19  [5..11]=空闲

  块表:             A: [0, 1, 2]          B: [3, 4]

图 2 —— 分页式 KV 缓存。每个序列的块表把逻辑块映射到不连续的物理块。0、1 号块已填满,可被具有相同前缀的其他请求共享;空出的 5–11 号槽可被任意序列复用,外部碎片由此消除。

分页还有两个额外的价值:

  • 共享。 块的内容是其词元的纯函数,因此前缀相同的两个序列可以指向同一批物理块。vLLM 最初的用途是并行采样(n > 1)和束搜索(beam search)——多个输出共享同一个提示词。
  • 写时复制(copy-on-write, CoW)。 共享块是只读的。当某个序列需要向其他序列仍在引用的块中写入新词元时,分配器复制该块,只改写写者的块表;其他序列继续使用原块。论文原文正是如此描述:*“copy-on-write mechanism at the block granularity for the physical blocks that need modification by multiple sequences, similar to the copy-on-write technique in OS virtual memory”*(在块粒度上为需要被多个序列修改的物理块实现写时复制机制,类似操作系统虚拟内存中的写时复制技术)。

论文的评估(在其时代的 A100 GPU、模型与负载上)报告,与当时的先进 serving 系统(FasterTransformer、Orca)相比,在相同时延下吞吐提升 2–4 倍,序列越长、模型越大收益越明显。

有一个历史性说明很重要。vLLM 官方文档中仍保留着一份 Paged Attention 设计文档,但它被明确标注为:*“This is a historical document based on the original paper… It no longer describes the code used in vLLM today”*(本文档基于原始论文,属历史文档……它已不再描述 vLLM 如今的代码)。块表的思想延续了下来,但当前 V1 引擎是在KV 缓存管理器中实现它的:预分配的块池、引用计数、每个请求只追加的块表。阅读关于 vLLM 的旧博客时请记住,它们描述的是 v0 时代的代码。

V1:统一调度预填充与解码#

当前引擎 V1 于 v0.8 系列引入(2025 年 1 月官宣),在 2025 年的 v0.9–v0.10 版本中成为默认引擎;v0.10.0 发布说明宣告开始清理 V0 代码库,截至 v0.26.0,V1 就是引擎本身。架构上,V1 把 serving 拆分为多个进程:一个或多个 API server 进程(HTTP、分词、流式输出)、每个数据并行 rank 一个 engine core 进程(调度器与 KV 缓存管理器)、每块 GPU 一个 worker 进程。

有趣的部分是调度器。V0 用两套独立策略分别调度预填充和解码;V1 彻底取消了这个区分。按 V1 公告的说法,调度决策就是一张简单的字典 {request_id: num_tokens}:每一步,调度器决定每个请求为下一次前向传播贡献多少词元。处于预填充的请求贡献提示词元(可能只是其中一);处于解码的请求恰好贡献一个输出词元。分块预填充、前缀缓存、投机解码全都由同一机制覆盖——它们只是选择这些数字的不同策略。

这带来了 v0 中需要单独实现的两个行为:

  • 连续批处理(continuous batching)。 运行中的批次每一步都会重组:完成的请求立即离开,等待中的请求只要预算和 KV 块允许就加入。这正是 Orca(Yu 等,OSDI 2022)提出的迭代级调度;vLLM 自称就是一个连续批处理引擎。
  • 分块预填充(chunked prefill)。 长提示词被切成能塞进单步词元预算的块,预填充块与解码在同一前向传播中交错执行。没有分块时,一个长提示词会独占 GPU 直到预填充完成,所有等待请求的 TTFT 都会随之拉长。

图 3 展示了结果:预填充与解码共享计算步,请求彼此独立地进出批次。

步:           1        2        3        4        5        6        7
R1(40 词元) [P 0-15] [P 16-31][P 32-39][D]      [D]      [D]      [完成]
R2(20 词元) [等待]   [P 0-15] [P 16-19][D]      [D]      [完成]
R3(16 词元) [等待]   [等待]   [P 0-15] [D]      [D]      [D]      [完成]
R4(4 词元)  [等待]   [等待]   [等待]   [P 0-3]  [D]      [D]      [D]

P x-y = 覆盖提示词元 x..y 的预填充块
D     = 解码步(一个新输出词元)

图 3 —— V1 的统一调度。第 4 步同时混合了三个解码和一个预填充块(严格的阶段分离下不可能做到)。R1 在第 6 步完成而 R4 继续解码,体现连续批处理:没有请求需要等待批次中最慢的成员。

抢占:RECOMPUTE#

即使有了分页内存,请求突发仍可能耗尽 KV 缓存。此时调度器必须腾出空间,做法是抢占(preempt)运行中的请求:释放它的 KV 块,把它放回等待队列。在 V1 中这是唯一的抢占模式——RECOMPUTE(重计算)。被抢占的请求重新被调度时,它的提示词从头重新计算,所需 KV 块被重建,然后从序列中断处继续解码。调度器源码对此毫不含糊:抢占时 num_computed_tokens 被重置为零。

更老的 v0 引擎还支持 SWAP 抢占——把请求的 KV 块拷贝到 CPU 内存(--swap-space)——以及通过 SequenceGroup 实现束搜索式的显式块共享。这些在 V1 中全部消失:指标文档把 vllm:num_requests_swappedvllm:cpu_cache_usage_perc 列为遗留指标,--swap-space 参数已被移除,束搜索也移出了核心引擎。

为什么 RECOMPUTE 可以接受?因为重计算不一定是白做的:有了自动前缀缓存(下一节),重新计算的提示词可以命中缓存,只有缺失的尾部才真正重算。指标文档正是这么说的——前缀缓存在 V1 中默认开启,因此”抢占加重计算的策略应当表现更好”。抢占仍会让受害请求的 TTFT 变长,并反映在时延指标里:它是过载信号,不是免费的午餐。

自动前缀缓存#

真实流量中有很大比例在重复前缀:聊天会话每次重发完整对话历史,RAG 负载反复发送同一批文档,agent 反复携带相同的系统指令。前缀缓存把已处理提示词的 KV 块存起来,新请求的前缀匹配时直接复用。

vLLM 的实现基于哈希。每个已填满的块由三部分内容共同哈希标识(出自 V1 KV 缓存管理器设计文档):

  1. 父块的哈希(即它的前缀);
  2. 块内确切的词元 ID;
  3. 使块唯一的附加哈希——LoRA ID、多模态输入哈希,以及可选的每请求 cache_salt(用于隔离租户、抵御基于缓存时延的计时攻击)。

只有完整块才会被缓存;末尾未填满的块不会。请求到达时,调度器查询的就是这个块哈希:命中的前缀块是”已计算块”,其 KV 直接复用而无需重算。图 4 展示这条哈希链。

请求 1:  "The capital of France is"
         [b0: The capital of ]  [b1: France is]        <- 都已填满,已缓存

请求 2:  "The capital of France is Paris, and its largest city is Lyon"
         [b0] 命中(复用)  [b1] 命中(复用)
         [b2: Paris, and it] [b3: s largest city is]   <- 全新计算

块哈希:
  H(b0) = hash( parent=None, tokens="The capital of ", extra={} )
  H(b1) = hash( parent=H(b0), tokens="France is",       extra={} )

图 4 —— 哈希链前缀缓存。请求复用哈希链匹配的每个完整块,只计算分叉的尾部。根据 V1 KV 缓存管理器设计文档绘制。

缓存管理器为每个块维护引用计数,把已缓存但未被引用的块放入空闲队列,内存紧张时按 LRU 淘汰。V1 默认开启前缀缓存——V1 公告报告,在他们的基准中即使命中率为 0%,吞吐损失也不到 1%,这让默认开启变得安全;命中率高时,同一批基准报告了数倍的吞吐提升。自 v0.11 起默认块哈希算法为 SHA-256(抗碰撞的选择);--prefix-caching-hash-algo sha256_cbor 则选用可复现、跨版本稳定的序列化方式。

前缀缓存还与抢占协同:被抢占后重算的请求可以命中自己提示词的前缀缓存,因此 RECOMPUTE 抢占往往比重算一切便宜得多。

指标告诉我们什么#

V1 暴露一个 Prometheus 端点(/metrics),提供一套以 vllm: 为前缀的丰富指标。指标文档给出的心智模型是:服务器级的 gauge 与 counter 用来解释请求级直方图为什么是那个样子。表 2 列出运维部署时最值得关注的指标。

指标 类型 含义
vllm:num_requests_running / _waiting gauge 处于 RUNNING / WAITING 调度状态的请求数
vllm:kv_cache_usage_perc gauge KV 缓存块使用比例(0–1)
vllm:prefix_cache_queries / vllm:prefix_cache_hits counter 前缀缓存查询次数与命中次数;两者之比即命中率
vllm:prompt_tokens_total / vllm:generation_tokens_total counter 累计处理的提示词元 / 生成词元数
vllm:request_success_total counter 按结束原因统计的已完成请求数
vllm:time_to_first_token_seconds histogram TTFT
vllm:inter_token_latency_seconds histogram ITL;参考 Grafana 面板将其视为 TPOT
vllm:e2e_request_latency_seconds histogram 请求端到端时延
vllm:request_queue_time_seconds histogram 首次被调度前的排队时间
vllm:request_prefill_time_seconds / vllm:request_decode_time_seconds histogram 预填充 / 解码阶段耗时

表 2 —— 关键 V1 指标(名称与语义取自官方指标文档;参考 Grafana 面板使用其中一部分)。

内部地看,engine core 为每个请求记录事件——QUEUEDSCHEDULEDPREEMPTEDNEW_TOKENS——前端据此推导各段时间间隔:排队时间、预填充时间(首次 SCHEDULED 到首个 NEW_TOKENS)、解码时间、推理时间与词元间隔时间。两点运维提示:解码期间发生抢占会拉长受害请求的解码/ITL 区间(指标设计中已说明),所以 ITL 尖峰与 vllm:num_requests_waiting 或 KV 缓存压力上升同时出现,就是抢占引发时延的特征信号;而前缀命中率——hits/queries——能告诉你流量是否真的在重复前缀,也就是前缀缓存是否物有所值。

边界:FlashAttention、SGLang 与分离式预填充#

vLLM 并未包揽自家技术栈里的每一项优化,几个相邻系统也在用不同方式解决重叠的问题。这些边界值得精确厘清。

FlashAttention 是内核,PagedAttention 是内存布局。 FlashAttention(Dao 等,2022)与 FlashAttention-2(Dao,2023)是融合注意力内核:它们分块执行注意力计算并用在线 softmax,完整得分矩阵从不落地,从算术层面同时削减了时间与内存。PagedAttention 决定 KV 值存在哪里、如何索引;FlashAttention 决定针对这些值的注意力数学跑多快。vLLM 把注意力后端做成可插拔的:FLASH_ATTN(flash-attn 包)、FLASHINFER、面向 MLA 模型的 FLASHMLA,以及一个 Triton 注意力后端,按优先级与硬件能力自动选择。这也是 GPU 代数如此重要的原因:vLLM 的 FlashAttention 后端校验计算能力 ≥ 8.0,FlashInfer 面向 sm80+,FlashAttention-3 仅限 Hopper,FlashAttention-4 面向 Hopper/Blackwell——官方 flash-attn 明确只支持 Ampere 及更新架构(Turing 上的子集由独立的社区项目覆盖)。

SGLang 是结构不同的另一套系统。 SGLang(Zheng 等,2023)是独立的 serving 系统,用 RadixAttention 解决同一个前缀复用问题:KV 块存放在以词元序列为键的字典树(radix tree)中,共享前缀即共享节点,配 LRU 淘汰与写时复制。vLLM V1 的哈希块缓存与 SGLang 的字典树是同目标(复用共享前缀请求间的 KV)的两种设计,两个项目都宣称在前缀密集负载下命中率很高。它们是替代关系,不是组件关系;实际选择通常取决于生态与负载契合度,而非决定性的技术差距。(两个项目作者亦有重叠、思想同源:它们出自同一个研究小组的谱系。)

分离式预填充把两个阶段搬到不同的机器上。 这一思想由 DistServe(Zhong 等,2024)提出:预填充与解码在分离的实例上运行,通过 KV 传输路径连接,两个阶段各自独立伸缩与调优。vLLM 以实验特性实现之:一个预填充实例与一个解码实例通过连接器(NIXL、Mooncake、LMCache 等)交换 KV 块。文档给出的动机正是本文的指标:分别调优 TTFT 与 ITL,并通过把预填充任务从解码实例上移走来控制尾部 ITL——文档明确说明,分离式预填充不会提升吞吐。在单实例内,分块预填充是解决同一尾部时延问题的更简单工具。

在 TITAN RTX 上复现本文#

撰写本文的工作站配备一块 NVIDIA TITAN RTX:24 GB GDDR6、672 GB/s 带宽、Turing TU102 架构、计算能力 7.5(NVIDIA 产品规格)。这意味着它恰好处于 vLLM 支持的最低架构线上(安装文档要求计算能力 ≥ 7.5),由此带来几个具体后果:

  • vLLM 的 FlashAttention 后端要求计算能力 ≥ 8.0,FlashInfer 要求 sm80+,FlashAttention-3 需要 Hopper。在 TITAN RTX 上,V1 会自动选择 Triton 注意力后端。正确性与指标管线不受影响;但论文里 A100/H100 的内核数字不能照搬。
  • 容量刚好够用。Llama-3.1-8B 的 FP16 权重约 16 GiB;按 vLLM 默认的 gpu_memory_utilization=0.92,留给 KV 缓存与临时内存的约 6 GiB——按表 1 的 128 KiB/词元计算,缓存容量在 4–5 万词元量级。这些是算术估算,不是实测。
  • NVIDIA 为这张卡宣传的 FP16 张量算力(约 130 TFLOPS)是峰值内核数字,端到端 serving 永远达不到;不要用它预测 TTFT 或 ITL。

要如实复现本文的定性行为,几分钟即可,无需基准框架:用 vllm serve 起一个小模型(当前版本默认 Qwen3-0.6B),然后:

  1. TTFT/ITL:发送不同提示词长度的请求,观察 /metrics 上的 vllm:time_to_first_token_secondsvllm:inter_token_latency_seconds。预期 TTFT 随提示词长度增长,ITL 对每个请求大致持平。
  2. 前缀缓存:反复发送同一段长提示词。vllm:prefix_cache_hits 应持续上升,第一个请求之后预填充时间应大幅缩短。
  3. 抢占:把并发拉高直到 vllm:kv_cache_usage_perc 饱和;vllm:num_requests_waiting 上升,被抢占请求的 ITL 直方图被拉长。

这块卡上你不会得到任何能与厂商或论文基准相比的数字——负载、GPU 与软件版本都不同。如果需要可发表的数字,请在你打算交付的硬件上运行官方基准工具,并在方法学里如实说明。本文引用的每个性能数字都由来源作者在自己的硬件上测得,本文一处都没有复现。

版本说明#

本文描述的是截至 2026-08-09 的 vLLM:最新发布版为 v0.26.0(2026-07-27 发布)。V1 就是引擎本身;它在 v0.8 系列引入(2025 年 1 月官宣),在 v0.9–v0.10 版本期间成为默认,v0.10 起开始清理 V0 代码库。官方文档把原始的 Paged Attention 设计文档标注为历史文档。参数名、指标名与默认值会随版本变化——例如 --prefix-caching-hash-algo 与 SHA-256 默认值在 v0.11 才落地。请以你所部署版本的文档为准。

参考文献#

  1. W. Kwon, Z. Li, S. Zhuang, Y. Sheng, L. Zheng, C. H. Yu, J. E. Gonzalez, H. Zhang, I. Stoica. Efficient Memory Management for Large Language Model Serving with PagedAttention. SOSP 2023. arXiv:2309.06180. https://arxiv.org/abs/2309.06180
  2. vLLM project — official documentation (Architecture Overview; Paged Attention [historical]; Prefix Caching; Metrics; Automatic Prefix Caching; Disaggregated Prefill; Per-request Metrics; Installation). https://docs.vllm.ai/en/latest/
  3. vLLM project — source repository, release v0.26.0. https://github.com/vllm-project/vllm
  4. vLLM Team. vLLM V1: A Major Upgrade to vLLM’s Core Architecture. vLLM Blog, 2025-01-27. https://blog.vllm.ai/2025/01/27/v1-alpha-release.html
  5. T. Dao, D. Fu, S. Ermon, A. Rudra, C. Ré. FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness. NeurIPS 2022. arXiv:2205.14135.
  6. T. Dao. FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning. ICLR 2024. arXiv:2307.08691.
  7. L. Zheng, L. Yin, Z. Xie, C. Sun, J. Huang, C. H. Yu, S. Cao, C. Kozyrakis, I. Stoica, J. E. Gonzalez, C. Barrett, Y. Sheng. SGLang: Efficient Execution of Structured Language Model Programs. NeurIPS 2024. arXiv:2312.07104.
  8. Y. Zhong, S. Liu, J. Chen, J. Hu, Y. Zhu, X. Liu, X. Jin, H. Zhang. DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving. OSDI 2024. arXiv:2401.09670.
  9. G.-I. Yu, J. S. Jeong, G.-W. Kim, S. Kim, B.-G. Chun. Orca: A Distributed Serving System for Transformer-Based Generative Models. OSDI 2022. arXiv:2109.14456.
  10. A. Grattafiori et al. The Llama 3 Herd of Models. arXiv:2407.21783. (Layer/KV-head/head-dim configurations used in Table 1.)
  11. NVIDIA. NVIDIA TITAN RTX product specifications. https://www.nvidia.com/en-us/geforce/graphics-cards/titan-rtx/
  12. Dao-AILab. flash-attention repository README (supported-GPU statement). https://github.com/Dao-AILab/flash-attention
字体
阴影
滤镜
圆角
主题色