3、显存泄漏概念:定义、常见类型、与内存泄漏的区别

在vLLM这类大模型推理框架中,显存泄漏(VRAM Leak)是最隐蔽、最难以排查的故障之一。它的表象往往是“服务跑着跑着就OOM了”,但真正的原因并非流量突增,而是显存被某段逻辑持续占用且永不释放。要理解这个问题,我们首先需要给出一个精确的定义。

3.1 显存泄漏的定义与本质

显存泄漏是指:程序在运行过程中,通过CUDA、ROCm等接口向GPU显存申请了存储空间,但在使用完毕后,由于逻辑错误、引用未解除或异常分支未处理,导致这些显存块无法被回收或复用,从而造成可用显存持续下降的现象。与CPU内存泄漏类似,显存泄漏的本质是“分配与释放的不对称”——每一次分配都对应一个确切的释放时机,但程序在某个路径上丢失了这个时机。

在vLLM中,显存泄漏的严重性被进一步放大:因为vLLM使用PagedAttention管理KV Cache,显存被划分为固定大小的块(block),一旦某个block被标记为“已占用”但实际不再被任何序列引用,它就会成为“僵尸块”。更危险的是,vLLM的显存池(memory pool)通常预分配了绝大部分显存,泄漏并不表现为“分配失败”,而是表现为可用KV Cache块数量逐渐减少,最终导致新请求无法获得足够的KV块而触发OOM或请求排队超时。

关键判断标准:如果重启服务后显存恢复、运行一段时间后显存再次被占满,且占用增长与请求量不成线性关系,那么几乎可以断定存在显存泄漏。
显存泄漏 vs 正常显存回收 ✅ 正常:分配 → 释放 → 复用 请求到达 分配KV块 cudaMalloc 推理完成 free() 块回收到池 block_table更新 显存水位 稳定在 60% 左右 ❌ 泄漏:分配 → 丢失引用 → 无法回收 请求到达 分配KV块 异常退出 未走释放路径 引用丢失 block_table残留 僵尸块 永不释放 ✗ 显存水位 持续增长 → 最终 OOM 内存池复用 内存池耗尽

3.2 显存泄漏的常见类型

在vLLM及类似的大模型推理系统中,显存泄漏并非单一形态,而是分布在多个层面。根据我们的工程实践,可以归纳为以下四种典型类型:

泄漏类型 发生位置 典型特征 检测难度
KV Cache 块泄漏 PagedAttention 管理器 block_table 中残留已终止序列的块引用;可用块数持续下降 中
CUDA 张量泄漏 模型前向/后向传播 中间激活张量未释放;显存碎片化加剧 高
Python 对象引用泄漏 请求处理循环 请求对象被全局缓存或闭包引用,导致其持有的GPU张量无法释放 极高
CUDA Context 泄漏 多线程/多进程 每次创建新的CUDA context 未销毁;显存占用随线程数线性增长 低

3.3 与内存泄漏的区别

虽然显存泄漏与CPU内存泄漏在“分配-释放不匹配”这一本质上相同,但在vLLM场景下,两者存在几个关键差异,这些差异直接决定了排查手段的不同:

  • 分配方式不同:CPU内存通过 malloc/free 或GC管理;显存通过 cudaMalloc/cudaFree 或缓存池(如vLLM的block allocator)管理。vLLM的显存池预分配机制使得泄漏初期不报错,而是表现为可用块减少。
  • 回收机制不同:Python的CPU内存有引用计数和垃圾回收(GC)兜底,即使开发者忘记释放,只要引用解除,内存最终会被回收;而显存没有自动GC,一旦 cudaMalloc 分配的内存丢失指针,就永久泄漏,直到进程结束。
  • 可观测性不同:CPU内存泄漏可以通过 tracemalloc、objgraph 等工具追踪对象引用;显存泄漏则需要借助 nvidia-smi、torch.cuda.memory_summary() 或CUDA原生工具,且显存碎片化会干扰判断。
  • 影响范围不同:CPU内存泄漏通常只影响当前进程;显存泄漏在共享GPU的多人多卡环境中,会波及其他租户,甚至导致整卡OOM,影响同卡上的其他任务。
一个容易混淆的案例:某团队报告vLLM服务“显存泄漏”,但排查后发现是CPU内存泄漏导致Python进程RSS持续增长,进而触发了CUDA OOM。因为CPU内存不足时,PyTorch的缓存分配器会尝试扩展显存缓存,反而加剧了显存压力。这说明在排查时,必须同时监控CPU和GPU内存,不能只看 nvidia-smi。

3.4 典型泄漏场景复盘

我们以一个真实的vLLM生产事故为例,复盘显存泄漏的完整链路:

  1. 现象:服务运行约6小时后,新请求的TTFT(首token延迟)从200ms飙升至5s,随后报 CUDA out of memory。
  2. 初步排查:nvidia-smi 显示显存占用率99%,但 vllm.engine.metrics 中的 num_free_kv_blocks 从初始的 12000 降至 0。
  3. 根因定位:通过 torch.cuda.memory_snapshot() 发现大量 allocator 标记为 active 但无对应 tensor 引用的内存段。进一步追踪发现,请求处理函数中有一个 try...except 分支:当生成结果包含特殊token时,提前 return,跳过了 seq_group.free() 调用,导致该序列的KV块未被释放。
  4. 修复:在 finally 块中确保 seq_group.free() 必然执行,并增加 block_table 的完整性校验。
误区提醒:不要认为“显存泄漏一定是框架bug”。在vLLM中,自定义模型(如新增一个attention变体)或自定义采样逻辑是最常见的泄漏来源。框架本身对标准模型做了充分测试,但你的扩展代码可能破坏了内存管理的不变量。排查时,优先审查自己新增的代码路径。