显存管理基础:GPU显存层次、CUDA显存分配机制、显存碎片定义与分类

在深入多卡vLLM的显存碎片治理之前,必须先建立一套统一的“显存世界观”。很多性能问题表面上是OOM(Out of Memory)或吞吐低下,本质上是对显存层次理解错位、对CUDA分配器行为误判、以及对碎片类型缺乏分类能力所致。本节将依次拆解GPU显存的物理与逻辑层次、CUDA运行时分配器的真实工作方式,以及显存碎片的精确定义与分类。

2.1 GPU显存层次:从物理带宽到逻辑池

现代GPU(以NVIDIA Hopper/Ampere架构为例)的显存体系可以划分为三个关键层次,每一层对vLLM的KVCache和权重驻留策略都有直接影响:

  • 物理显存(HBM):高带宽显存,带宽可达2~3TB/s,容量为40GB/80GB(A100/H100)。这是所有张量数据的最终归宿,也是碎片产生的物理载体。
  • L2缓存:片上缓存,容量约40~50MB,带宽远高于HBM。vLLM的算子若能在L2中命中(如重复读取同一Attention分块),可显著降低HBM压力,但L2无法被软件直接管理,只能通过访问模式优化。
  • CUDA虚拟地址空间:每个进程拥有独立的虚拟显存地址空间,CUDA驱动负责将虚拟页映射到物理HBM页。碎片问题本质上是虚拟地址空间中的“空洞”无法被有效复用。
关键认知:vLLM的PagedAttention之所以高效,正是因为它将KVCache切分为固定大小的“页”(通常为16个token的block),在虚拟地址空间中按需映射,从而绕过了传统连续分配导致的物理碎片问题。但页表本身也会占用显存,且页的分配/释放仍受CUDA分配器行为影响。
GPU显存层次与CUDA分配机制 物理显存 (HBM) 容量: 40~80GB | 带宽: 2~3TB/s | 页粒度映射 碎片产生地: 连续分配失败 → 空洞 L2 缓存 (片上) 容量: 40~50MB | 软件不可直接管理 | 访问模式优化 CUDA 虚拟地址空间 每个进程独立 | 按需映射到物理页 | 碎片本质: 虚拟地址空洞 CUDA Runtime 分配器 cudaMalloc → 缓存池 → 复用 碎片分类 内部碎片: 分配粒度浪费 外部碎片: 地址空洞

2.2 CUDA显存分配机制:cudaMalloc的“缓存池”陷阱

许多开发者误以为cudaMalloc每次调用都会直接向驱动申请物理显存,实际上CUDA Runtime维护了一个显存缓存池。当调用cudaFree释放显存时,内存块并不会立即归还给驱动,而是被放入缓存池中以供后续复用。这种设计减少了与驱动的交互开销,但也带来了两个关键问题:

  1. 缓存池碎片化:频繁分配/释放不同大小的张量(如不同batch size下的KVCache),缓存池中会出现大量不连续的空闲块,即使总空闲容量充足,也无法满足大块连续分配请求。
  2. 显存占用虚高:cudaFree后显存占用(通过nvidia-smi查看)可能不会下降,因为缓存池仍持有该内存。这会导致多卡场景下其他进程无法使用该显存。
常见误区:在vLLM中,如果直接使用torch.cuda.memory_summary()观察显存,会发现“reserved”远大于“allocated”。这是PyTorch缓存分配器(基于CUDA缓存池)的正常行为,但若reserved持续增长且不回落,说明缓存池中存在大量碎片或未释放的block。

CUDA分配器的工作流程可以概括为:

cudaMalloc(ptr, size)
  → 检查缓存池中是否有 ≥ size 的空闲块
    → 有: 从池中取出并返回(可能产生内部碎片)
    → 无: 向驱动申请新物理页 → 加入缓存池 → 返回
cudaFree(ptr)
  → 将块标记为空闲 → 放入缓存池
  → 不立即归还驱动(除非超过阈值或显式调用 cudaDeviceReset)

2.3 显存碎片定义与分类:内部碎片与外部碎片

显存碎片是指显存空间中无法被有效利用的“空洞”或“浪费”。在vLLM多卡部署中,碎片直接影响KV Cache的可用容量和吞吐上限。碎片分为两大类:

碎片类型 定义 产生原因 vLLM中的典型表现
内部碎片 分配块内部未被使用的空间 分配粒度(如256B对齐)、请求大小与分配块大小不匹配 KV Cache block大小为16 token,但实际序列长度不足16的整数倍时,最后一个block部分浪费
外部碎片 空闲块之间被已分配块隔断,无法合并成大块 不同大小的张量交替分配/释放,缓存池无法合并相邻空闲块 多卡并行时,各卡显存分配不均,某卡出现“总空闲>需求但无法分配”
量化示例:假设GPU显存为80GB,vLLM配置--gpu-memory-utilization 0.9,即可用显存为72GB。若KV Cache block大小为16 token(约2KB),而实际请求的平均序列长度为1000 token(62.5个block),则每个序列最后一个block平均浪费约50%空间。若同时有1000个并发序列,内部碎片浪费约1000 × 1KB = 1MB,看似不大,但若block大小设置为128 token(约16KB),浪费将放大8倍。

2.4 案例:多卡场景下的碎片放大效应

考虑一个4卡A100(80GB)的vLLM部署,使用张量并行(TP=4)加载一个70B模型。模型权重约140GB,每卡需35GB。剩余每卡45GB用于KV Cache。假设采用默认的cudaMalloc行为:

  • 每卡独立运行PyTorch缓存分配器,缓存池互不共享。
  • 当请求到达时,每卡为KV Cache分配相同大小的block(因为TP需要保持各卡张量形状一致)。
  • 若某卡的缓存池因之前处理不同长度请求而产生外部碎片,该卡可能无法分配新的KV block,导致整个请求失败(即使其他卡仍有空闲)。

这种“木桶效应”在多卡场景中被放大:任何一张卡的碎片问题都会导致整体请求失败。因此,vLLM引入了--kv-cache-dtype、--block-size等参数来精细控制分配粒度,并通过PagedAttention的页表机制减少外部碎片的影响。

治理思路预览:解决碎片问题可以从三个层面入手:① 调整block大小以减少内部碎片;② 使用torch.cuda.memory.set_per_process_memory_fraction限制每卡缓存池上限,强制释放;③ 在vLLM中启用--enable-prefix-caching复用KV Cache块,减少分配/释放频率。

2.5 误区与复盘:显存碎片治理的常见错误

误区一:认为“显存占用高”等于“显存碎片多”。实际上,高占用可能是缓存池未释放所致,而非碎片。应通过torch.cuda.memory_snapshot()分析块分布,区分“已分配”与“保留未用”。
误区二:盲目调大--gpu-memory-utilization。该参数只是设置可用显存上限,并不会减少碎片。若碎片严重,即使利用率设到0.95,仍可能OOM。
误区三:忽略多卡间的碎片差异。每卡的分配历史不同,碎片状态也不同。治理时应逐卡检查nvidia-smi的“FB Memory Usage”和PyTorch的memory_summary(),而非只看全局均值。

复盘建议:在部署vLLM前,先使用torch.cuda.memory._dump_snapshot()生成显存快照,分析block大小分布和空闲块位置。若发现大量小空闲块(<2MB),说明外部碎片严重,应考虑调整block大小或启用内存池合并策略。若发现分配块内部浪费超过20%,则应减小block size或启用动态block大小调整。