vLLM基础:架构、推理流程与关键组件
在深入延迟调优之前,必须先理解 vLLM 的内部运作机制。vLLM 之所以能在高并发下保持低延迟,核心在于它对 KV Cache 的管理方式和请求调度策略进行了根本性重构。本节将拆解 vLLM 的架构分层、一次推理请求的完整生命周期,以及 PagedAttention 与 Continuous Batching 这两个关键组件的实现原理。
2.1 vLLM 整体架构分层
vLLM 采用模块化设计,从上到下可分为 API 层、调度层、执行层和显存管理层。理解各层的职责边界,有助于在后续调优时快速定位瓶颈。
- API 层:提供 OpenAI 兼容的 HTTP 接口(
/v1/chat/completions),负责请求解析、鉴权与响应封装。 - 调度层:核心是
Scheduler,决定哪些请求进入当前迭代(iteration)执行,并管理序列(Sequence)的状态。 - 执行层:调用模型前向计算(
ModelRunner),将多个序列的 token 拼成 batch 送入 GPU。 - 显存管理层:基于 PagedAttention 的
BlockManager,以“块”为单位分配和回收 KV Cache 显存。
图中箭头表示请求自上而下的流动方向。实际调优时,如果发现 GPU 利用率低但延迟高,问题往往出在调度层;如果显存充足但报 OOM,则需检查显存管理层的块分配策略。
2.2 一次推理请求的完整生命周期
一个 HTTP 请求从进入 vLLM 到返回结果,会经历以下阶段。每个阶段都有对应的可调参数,后续章节会逐一展开。
- 请求解析与预处理:将输入文本 tokenize 为 token ID 序列,并创建
Sequence对象。 - 序列加入等待队列:
Scheduler将新序列放入waiting队列,等待被调度。 - 调度决策(Continuous Batching):每个迭代步,调度器从 waiting 队列取新序列,从 running 队列中保留未完成的序列,组成一个
seq_group列表。 - KV Cache 分配:为新增序列分配物理块(block),记录逻辑块到物理块的映射表。
- 模型前向计算:将当前 batch 的所有 token 拼接为张量,执行一次 Transformer 前向传播,得到每个序列下一个 token 的 logits。
- 采样与解码:对 logits 做采样(如 top-p、temperature),生成新 token,追加到序列末尾。
- 状态更新:若序列生成
<eos>或达到最大长度,则标记为 finished,释放其占用的物理块;否则继续留在 running 队列。 - 响应返回:所有序列完成后,将 token 序列 detokenize 为文本,通过 HTTP 响应返回。
2.3 PagedAttention:显存管理的核心创新
传统推理框架(如 HuggingFace Transformers)在生成前会为每个序列预分配最大长度的 KV Cache 显存。假设序列最大长度 2048,模型层数 32,头数 32,维度 128,则每个序列需要约 2 × 32 × 2048 × 32 × 128 × 2 bytes ≈ 1GB 显存。实际生成往往远短于最大长度,导致 60%~80% 的显存浪费。
PagedAttention 借鉴操作系统虚拟内存的分页思想,将 KV Cache 划分为固定大小的物理块(默认块大小 16 个 token)。每个序列的逻辑 KV Cache 可以映射到不连续的物理块上,按需分配。
| 对比维度 | 传统预分配方式 | PagedAttention |
|---|---|---|
| 显存分配粒度 | 整个序列长度 | 16 token 的物理块 |
| 内部碎片 | 极大(预留未用) | 极小(仅最后一块可能不满) |
| 共享支持 | 不支持 | 支持(如并行采样时多个输出共享前缀) |
| 显存利用率 | 约 20%~40% | 可达 90% 以上 |
块分配的核心数据结构是 块表(Block Table),它记录了每个序列的逻辑块 ID 到物理块 ID 的映射。当序列需要新块时,BlockManager 从空闲块列表中分配;当序列完成时,回收其所有物理块。
# 块表示意图(伪代码)
block_table = {
"seq_1": [block_id=3, block_id=7, block_id=12], # 逻辑块0→物理块3
"seq_2": [block_id=5, block_id=9] # 逻辑块0→物理块5
}
# 物理块3和5可能不连续,但逻辑上连续
PagedAttention 还支持 写时复制(Copy-on-Write):当多个序列共享同一前缀(如 beam search 或并行采样)时,它们可以共享物理块,直到某个序列需要写入不同的 token,才复制该块。这大幅减少了显存占用。
2.4 Continuous Batching:动态调度策略
传统静态 batching 将请求分批,每批必须等所有序列生成完毕才释放资源,导致 GPU 在批尾出现大量空闲。Continuous Batching(连续批处理)在 每个迭代步 动态决定哪些序列参与计算:
- 新到达的序列可以立即加入当前 batch(只要显存允许)。
- 已完成的序列立即退出,释放的显存马上被新序列使用。
- batch 大小不再固定,而是每个迭代步动态变化。
这种机制让 GPU 始终处理尽可能多的 token,显著提升吞吐量。但需要注意,过大的 batch 会增加单个迭代步的计算延迟,因此延迟敏感场景需要限制最大 batch 大小(通过 --max-num-seqs 参数控制)。
--max-num-seqs 调小 batch 上限,或使用抢占策略(如 --preemption-mode)来保证高优先级请求的及时响应。延迟与吞吐之间存在权衡,必须根据业务场景明确目标。
2.5 案例:从显存碎片到吞吐翻倍
某团队部署 7B 模型(FP16,约 14GB 权重),使用 4 张 A10G(每张 24GB)。最初使用 HuggingFace 原生推理,并发 8 个请求时即出现 OOM。分析发现:
- 每个请求预分配 2048 token 的 KV Cache,实际平均只生成 300 token,显存浪费率约 85%。
- 8 个请求的预分配总量为
8 × 1GB = 8GB,加上权重和激活值,超出单卡显存。
切换到 vLLM 后,使用 PagedAttention 按需分配,8 个请求实际 KV Cache 仅占用约 8 × 300 × 2 × 32 × 32 × 128 × 2 bytes ≈ 1.2GB。显存余量允许将并发提升到 32 个请求,吞吐量从 12 req/s 提升到 45 req/s,同时 p95 延迟仅增加 15%。
这个案例说明:显存利用率直接决定了可并发请求数,而并发数又是吞吐量的决定性因素。后续章节的 KV Cache 量化、块大小调优等,都是在 PagedAttention 基础上进一步挖掘显存效率。
2.6 误区复盘:三个常见理解偏差
实际上,块分配和查找会增加少量 CPU 开销,但避免了显存不足导致的等待和抢占,整体延迟反而降低。在极端碎片化场景下,块表查找可能成为瓶颈,可通过增大块大小缓解。
当 batch 超过 GPU 计算能力的线性扩展区间后,单个迭代步的延迟会急剧上升。对于在线服务,应设置
--max-num-seqs 为经验值(如 64~128),并配合 --max-paddings 控制 padding 开销。
默认调度策略是 FCFS(先来先服务),新请求只能进入 waiting 队列。要实现优先级抢占,需要配置
--priority 参数并启用对应的调度策略,否则高优先级请求仍会排在长序列之后。