3. vLLM架构与显存使用:PagedAttention、KV Cache管理与显存池化技术
3.1 从传统显存分配到PagedAttention的范式转变
在理解vLLM的显存治理能力之前,必须先看清传统推理框架的显存分配方式。以HuggingFace Transformers为代表的常规实现,会在请求到来时为每条序列的KV Cache预先分配连续显存。这种方式的致命缺陷在于:显存大小必须按最大序列长度预留,而实际生成过程中,KV Cache是逐步增长的。这导致两个后果——显存利用率低(平均利用率往往不足60%),以及当多个请求并发时,因无法预知各自最终长度而产生大量碎片。
vLLM的核心创新在于引入了操作系统虚拟内存的分页思想,将KV Cache切分为固定大小的块(Block),每个块默认大小为16个token的KV数据。这种设计使得显存分配不再依赖序列的连续性,而是按需分配、按块回收。下图展示了传统连续分配与PagedAttention块式分配的核心差异:
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池。池化过程遵循以下步骤:
- 显存预算计算:读取GPU总显存,乘以利用率系数,得到可用显存上限。
- 模型权重加载:加载模型参数后,剩余显存即为KV Cache可用空间。
- 块数量确定:将剩余显存除以单个块的大小(
block_size × num_attention_heads × head_dim × 2 × dtype_size),得到物理块总数。 - 块池初始化:建立空闲块链表,所有块初始状态为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还通过减少显存碎片间接提升了吞吐量。碎片减少意味着同一时刻可驻留的序列更多,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来匹配实际显存容量。