vLLM基础:架构、推理流程与显存管理机制

在深入排查显存泄漏之前,必须先建立对 vLLM 运行机制的整体认知。vLLM 之所以能实现高吞吐推理,核心在于它对 KV Cache 的精细管理,而显存泄漏问题也几乎都围绕这一机制展开。本节将拆解 vLLM 的三大支柱:架构组成、推理流程、显存管理。

2.1 vLLM 核心架构

vLLM 采用经典的 客户端-服务端 分层架构,各模块职责清晰,任何一层的异常都可能表现为显存异常增长。其核心组件如下:

  • LLM Engine(推理引擎):负责模型加载、分词、前向计算与采样。它是显存消耗的主体,包含模型权重、激活值、KV Cache 三大部分。
  • Scheduler(调度器):vLLM 的“大脑”,决定每个 step 将哪些请求的 token 送入模型。它维护着连续的 token 序列状态,并管理 KV Cache 的物理块分配。
  • Block Manager(块管理器):显存分配的核心执行者。它将 KV Cache 划分为固定大小的物理块(默认 16 个 token/块),通过页表映射实现逻辑连续、物理离散的存储。
  • Executor(执行器):负责将调度器输出的 token 批次实际送入 GPU 计算。在张量并行或多 GPU 场景下,它负责跨设备通信。
  • API Server(HTTP 服务层):基于 FastAPI 实现,接收 OpenAI 兼容的请求,转换为内部 token 序列,并返回生成结果。
关键认知:vLLM 与传统 HuggingFace 推理的最大区别在于,它把 KV Cache 从“动态增长的临时变量”变成了“预先规划、按块管理的资源池”。这一转变是高性能的基础,也是显存泄漏的潜在温床。
vLLM 核心架构与数据流 API Server (FastAPI) 接收请求 / 流式返回 / 请求排队 LLM Engine (推理引擎) Tokenizer → 模型前向计算 → Sampler Scheduler (调度器) 连续批处理 / 序列状态跟踪 Block Manager (块管理器) 物理块分配 / 页表映射 / 引用计数 token批次 Executor (执行器) GPU 计算 / 张量并行 / 跨设备通信 GPU 显存池 权重 / 激活 / KV Cache

2.2 推理流程:从请求到输出

理解 vLLM 的推理流程,有助于定位显存增长发生在哪个阶段。整个流程分为 预填充(Prefill) 和 解码(Decode) 两个阶段,vLLM 通过 连续批处理(Continuous Batching) 将两者混合执行:

  1. 请求接入:API Server 接收请求,进行 tokenize,生成 Sequence 对象。
  2. 序列注册:Scheduler 将新序列加入等待队列,并向 Block Manager 申请初始 KV Cache 物理块。
  3. Prefill 阶段:对序列的 全部输入 token 并行计算,生成首个输出 token,同时填充 KV Cache。此阶段计算密集,显存占用随输入长度线性增长。
  4. Decode 阶段:每次迭代只计算 1 个新 token,并更新 KV Cache。此阶段显存增长缓慢,但需要持续占用已分配的物理块。
  5. 调度决策:Scheduler 在每个 step 检查所有序列状态,决定哪些序列继续解码、哪些新序列进入 Prefill、哪些序列已终止(输出 <EOS> 或达到最大长度)。
  6. 资源回收:终止序列的 KV Cache 物理块被释放,引用计数归零,归还给 Block Manager 的空闲池。
排查启示:如果显存泄漏发生在持续解码过程中,重点检查 Block Manager 的引用计数 是否在序列终止后正确递减;如果泄漏发生在高并发请求下,则需关注 预填充阶段的临时激活值 是否被正确释放。

2.3 显存管理机制:PagedAttention 与物理块

vLLM 显存管理的基石是 PagedAttention 算法,它借鉴操作系统虚拟内存的分页思想,将 KV Cache 划分为固定大小的物理块(默认 block_size=16,即每个块存储 16 个 token 的 KV 向量)。每个序列维护一张 逻辑页表,记录其 KV 数据在物理块中的实际位置。

概念 说明 显存泄漏风险点
物理块(Physical Block) GPU 显存中固定大小的连续区域,存储 KV 向量 块分配后未释放
逻辑页表(Page Table) 每个序列维护的映射表,逻辑 token 位置 → 物理块编号 页表条目未随序列销毁而清除
引用计数(Ref Count) 每个物理块被多少个序列共享(beam search 时多个序列可能共享块) 计数未递减导致块永久占用
空闲块列表(Free List) Block Manager 维护的可分配物理块集合 块泄漏后空闲列表逐渐缩短

Block Manager 的分配与回收逻辑如下:

# 伪代码:Block Manager 核心逻辑
class BlockManager:
    def allocate_block(self, sequence_id):
        if self.free_list.empty():
            raise OutOfMemoryError("GPU 显存不足")
        block = self.free_list.pop()
        block.ref_count = 1
        block.sequence_id = sequence_id
        return block

    def release_block(self, block):
        block.ref_count -= 1
        if block.ref_count == 0:
            self.free_list.push(block)  # 归还空闲池
            block.sequence_id = None

当序列终止时,vLLM 会遍历其页表,对每个物理块调用 release_block。如果引用计数归零,块被回收。若此处存在逻辑缺陷(例如异常路径未调用释放函数),物理块将永久滞留,表现为显存随请求数线性增长。

2.4 典型场景:连续批处理下的显存波动

假设 GPU 总显存为 40GB,其中模型权重占 14GB,KV Cache 池上限设为 24GB(通过 gpu_memory_utilization=0.6 配置)。在正常运行时,显存占用曲线应呈 锯齿状:请求到达时上升,请求完成后下降。

正常波动特征:显存峰值稳定在 38GB 左右,谷值不低于 36GB(因为权重和常驻 KV Cache 不会释放)。如果观察到谷值持续抬高,例如从 36GB → 37GB → 38GB → 39GB,说明存在 不可回收的显存占用,即泄漏。

一个典型的泄漏场景是:请求在生成过程中被客户端断开连接。此时 vLLM 的异步任务可能抛出异常,导致序列终止清理逻辑未完整执行。Block Manager 中该序列的物理块引用计数停留在 1,永远不会归零,块无法回到空闲列表。随着此类中断请求累积,可用 KV Cache 块逐渐减少,最终触发 OutOfMemoryError。

2.5 常见误区与排查方向

  • 误区一:显存增长一定是模型权重泄漏。实际上,权重在加载后是静态的,不会随请求变化。持续增长几乎总是 KV Cache 或激活值问题。
  • 误区二:重启服务就能解决,无需深究。重启只是暂时清空显存,如果根因是代码路径缺陷,泄漏会复现。必须定位到具体的未释放块来源。
  • 误区三:只关注 GPU 显存总量,忽略 CPU 内存。vLLM 的 Block Manager 在 CPU 上也维护页表元数据,如果页表条目泄漏,CPU 内存也会增长,并间接影响 GPU 分配效率。
  • 正确方向:使用 nvidia-smi 监控显存曲线,结合 vLLM 的 --log-stats 参数输出 Block Manager 的块分配统计,对比空闲块数量变化趋势。
重要提示:在排查显存泄漏前,务必先确认 vLLM 版本。不同版本的 Block Manager 实现差异较大,例如 v0.4.x 与 v0.6.x 的块分配策略完全不同。排查方法需与版本匹配,否则可能得出错误结论。