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 显存。
API 层 OpenAI 兼容接口 · 请求解析 · 响应流式返回 /v1/chat/completions 调度层 (Scheduler) Continuous Batching · 序列状态管理 · 抢占与恢复 迭代级调度 执行层 (ModelRunner) 批量前向计算 · CUDA Graph 捕获 · 算子融合 GPU Kernel 执行 显存管理层 (BlockManager) PagedAttention · 物理块分配 · 写时复制 (Copy-on-Write) KV Cache 块表 请求入口 GPU 显存

图中箭头表示请求自上而下的流动方向。实际调优时,如果发现 GPU 利用率低但延迟高,问题往往出在调度层;如果显存充足但报 OOM,则需检查显存管理层的块分配策略。

2.2 一次推理请求的完整生命周期

一个 HTTP 请求从进入 vLLM 到返回结果,会经历以下阶段。每个阶段都有对应的可调参数,后续章节会逐一展开。

  1. 请求解析与预处理:将输入文本 tokenize 为 token ID 序列,并创建 Sequence 对象。
  2. 序列加入等待队列:Scheduler 将新序列放入 waiting 队列,等待被调度。
  3. 调度决策(Continuous Batching):每个迭代步,调度器从 waiting 队列取新序列,从 running 队列中保留未完成的序列,组成一个 seq_group 列表。
  4. KV Cache 分配:为新增序列分配物理块(block),记录逻辑块到物理块的映射表。
  5. 模型前向计算:将当前 batch 的所有 token 拼接为张量,执行一次 Transformer 前向传播,得到每个序列下一个 token 的 logits。
  6. 采样与解码:对 logits 做采样(如 top-p、temperature),生成新 token,追加到序列末尾。
  7. 状态更新:若序列生成 <eos> 或达到最大长度,则标记为 finished,释放其占用的物理块;否则继续留在 running 队列。
  8. 响应返回:所有序列完成后,将 token 序列 detokenize 为文本,通过 HTTP 响应返回。
延迟调优切入点:第 3 步的调度策略决定了 batch 大小和 GPU 利用率;第 4 步的块分配效率影响显存碎片;第 5 步的算子融合和 CUDA Graph 决定了单次前向的耗时。这三处是后续所有优化手段的着力点。

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 参数控制)。

常见误区:初学者常认为 Continuous Batching 会自动优化延迟,但实际上它优化的是吞吐量。如果追求单请求低延迟,需要配合 --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 误区复盘:三个常见理解偏差

误区一:PagedAttention 只影响显存,不影响延迟。
实际上,块分配和查找会增加少量 CPU 开销,但避免了显存不足导致的等待和抢占,整体延迟反而降低。在极端碎片化场景下,块表查找可能成为瓶颈,可通过增大块大小缓解。
误区二:Continuous Batching 的 batch 越大越好。
当 batch 超过 GPU 计算能力的线性扩展区间后,单个迭代步的延迟会急剧上升。对于在线服务,应设置 --max-num-seqs 为经验值(如 64~128),并配合 --max-paddings 控制 padding 开销。
误区三:vLLM 的调度是抢占式的,高优先级请求总能插队。
默认调度策略是 FCFS(先来先服务),新请求只能进入 waiting 队列。要实现优先级抢占,需要配置 --priority 参数并启用对应的调度策略,否则高优先级请求仍会排在长序列之后。