3. vLLM架构与显存使用:PagedAttention、KV Cache管理与显存池化技术

3.1 从传统显存分配到PagedAttention的范式转变

在理解vLLM的显存治理能力之前,必须先看清传统推理框架的显存分配方式。以HuggingFace Transformers为代表的常规实现,会在请求到来时为每条序列的KV Cache预先分配连续显存。这种方式的致命缺陷在于:显存大小必须按最大序列长度预留,而实际生成过程中,KV Cache是逐步增长的。这导致两个后果——显存利用率低(平均利用率往往不足60%),以及当多个请求并发时,因无法预知各自最终长度而产生大量碎片。

vLLM的核心创新在于引入了操作系统虚拟内存的分页思想,将KV Cache切分为固定大小的块(Block),每个块默认大小为16个token的KV数据。这种设计使得显存分配不再依赖序列的连续性,而是按需分配、按块回收。下图展示了传统连续分配与PagedAttention块式分配的核心差异:

传统方式:连续显存分配 序列A预留空间(最大长度) 已用 KV 空闲但被占用 序列B预留空间(碎片) vLLM:PagedAttention块式分配 显存块池(Block Pool) B1 B2 B3 B4 B5 B6 序列A: B1→B3→B5(逻辑连续,物理离散) 序列B: B2→B4(按需分配,无预留) 关键差异对比 ✗ 传统:预分配最大长度 → 内部碎片 + 外部碎片 ✗ 传统:序列间无法共享显存块 ✓ vLLM:按需分配块,碎片率趋近于零 ✓ vLLM:块可跨序列共享(如并行采样) ✓ vLLM:支持物理块到逻辑块的灵活映射

3.2 KV Cache的块级生命周期管理

vLLM的KV Cache管理由BlockManager统一调度,其核心数据结构是逻辑块表(Logical Block Table)。每个序列维护一张逻辑块表,记录其KV Cache对应的物理块编号。当生成新token时,BlockManager检查当前逻辑块是否还有剩余槽位;若已满,则从全局空闲块列表中分配一个新物理块,并更新逻辑块表。

这一机制带来了三个重要能力:

  • 按需精确分配:显存占用与实际生成的token数严格成正比,不再预留任何空闲空间。
  • 块级共享:当多个序列来自同一父序列(如beam search或并行采样)时,它们的前缀KV Cache可以共享同一物理块,通过引用计数管理生命周期。
  • 高效回收:序列结束时,其占用的物理块通过引用计数递减,归零后立即回收到空闲池,供其他序列使用。

下面通过一个具体案例展示块分配与共享的过程。假设显存块池共有6个物理块(B1~B6),初始全部空闲。两个请求序列Seq_A和Seq_B同时到达,且Seq_B的前缀与Seq_A完全相同(例如来自同一提示词的多次采样):

# 逻辑块表(Logical Block Table)示意
Seq_A: [B1] → [B3] → [B5]   # 引用计数: B1(2), B3(2), B5(1)
Seq_B: [B1] → [B3] → [B6]   # 引用计数: B1(2), B3(2), B6(1)

# 物理块状态
B1: 引用计数=2, 状态=共享
B2: 引用计数=0, 状态=空闲
B3: 引用计数=2, 状态=共享
B4: 引用计数=0, 状态=空闲
B5: 引用计数=1, 状态=独占
B6: 引用计数=1, 状态=独占

当Seq_B生成结束后,B6的引用计数降为0,立即回收到空闲池;而B1和B3因Seq_A仍在使用,引用计数降为1但不会被回收。这种精细的引用计数机制,使得显存利用率相比传统方式提升了2~4倍(取决于共享前缀的比例)。

3.3 显存池化与物理块分配策略

vLLM在初始化时会根据gpu_memory_utilization参数(默认0.9)预留GPU显存,并从中划分出KV Cache池。池化过程遵循以下步骤:

  1. 显存预算计算:读取GPU总显存,乘以利用率系数,得到可用显存上限。
  2. 模型权重加载:加载模型参数后,剩余显存即为KV Cache可用空间。
  3. 块数量确定:将剩余显存除以单个块的大小(block_size × num_attention_heads × head_dim × 2 × dtype_size),得到物理块总数。
  4. 块池初始化:建立空闲块链表,所有块初始状态为free。

块大小的选择直接影响显存效率。较小的块(如16)能降低内部碎片,但会增加块表开销和地址映射成本;较大的块(如32或64)则相反。vLLM默认使用16,在多数场景下表现最优。下表对比了不同块大小的影响:

块大小(token数) 内部碎片率 块表内存开销 适用场景
8 极低(<5%) 高(块数量多) 短序列、高并发
16(默认) 低(约8%) 中等 通用场景
32 中等(约15%) 低 长序列、低并发
64 较高(约25%) 极低 超长文本生成

值得注意的是,vLLM还引入了Copy-on-Write(写时复制)机制来处理共享块的修改场景。当某个共享块需要写入新内容时,BlockManager会先分配一个新块,复制原块数据,再在新块上执行写入,同时将原块的引用计数减1。这一机制保证了共享安全,同时避免了不必要的全量复制。

3.4 显存碎片治理的实际效果与调优实践

在真实的多卡部署场景中,vLLM的显存池化技术带来了可量化的收益。以8×A100(80GB)环境运行Llama-2-70B为例,使用传统HF推理时,单卡最多承载4个并发序列(序列长度2048),显存碎片率高达35%;而使用vLLM后,单卡可承载12个并发序列,碎片率降至3%以下。

以下是一个典型的vLLM启动配置,展示了显存池化相关参数的实际用法:

# vLLM 启动示例(多卡张量并行)
python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Llama-2-70b-chat-hf \
    --tensor-parallel-size 8 \
    --gpu-memory-utilization 0.92 \
    --max-num-seqs 32 \
    --max-model-len 4096 \
    --block-size 16 \
    --swap-space 16  # CPU内存交换空间(GB)

调优实践中,以下经验值得参考:

  • gpu_memory_utilization不宜超过0.95,否则会因CUDA context、激活值等额外开销导致OOM。
  • 当并发序列数较高时,适当调小block-size可降低碎片;当序列普遍较长时,调大block-size可减少块表开销。
  • 启用--enable-prefix-caching可缓存共享前缀的KV块,对多轮对话场景效果显著。
实践建议:在部署前,使用vllm.engine.arg_utils中的EngineArgs打印显存分配详情,观察KV Cache池大小和块数量。若发现KV Cache占比过低(<50%),说明模型权重或激活值占用过高,需检查是否开启了不必要的CUDA图优化或过大的batch size。

3.5 常见误区与复盘

在理解vLLM显存管理时,开发者常陷入以下误区:

误区一:认为PagedAttention只影响显存利用率。
实际上,PagedAttention还通过减少显存碎片间接提升了吞吐量。碎片减少意味着同一时刻可驻留的序列更多,GPU计算单元的空闲等待时间更短,从而提升了端到端的token生成速度。
误区二:忽视块共享带来的显存放大效应。
在beam search或并行采样场景中,如果未正确配置--max-num-seqs,共享块的引用计数管理可能成为瓶颈。建议在代码中监控num_free_gpu_blocks指标,若长期为0,说明块池已满,需降低并发或增大显存利用率。

一个典型的复盘案例:某团队在部署Qwen-72B时,将gpu-memory-utilization设为0.99,结果启动后立即OOM。排查发现,CUDA context本身占用约1.5GB显存,且PyTorch的CUDA缓存分配器会额外预留部分显存。将参数调回0.92后,系统稳定运行,KV Cache池大小从0变为约28GB,并发能力提升3倍。

另一个值得关注的细节是CPU Swap空间。当GPU显存不足时,vLLM会将不活跃的块交换到CPU内存(通过--swap-space配置)。但交换操作会引入显著的延迟(单次交换约5~10ms),因此仅适合作为应急手段,不应依赖它来支撑高并发。生产环境中,应优先通过调整max-num-seqs和max-model-len来匹配实际显存容量。