1. 课程导入与显存治理全景
GPU 推理服务的稳定性,往往不是被算力卡住,而是被显存卡住。一个 7B 模型在 FP16 下权重约 14 GB,看似能塞进 24 GB 显卡,但真正上线后,KV Cache、激活值、CUDA 上下文、通信缓冲、框架预留会一起挤占空间,最终在并发升高时突然 OOM。要治理显存,第一步不是调参,而是建立一张完整的"显存地图":钱花在哪里、谁在分配、为什么会碎、OOM 到底由谁触发、隔离又该隔离什么。
一、GPU 推理显存的层级结构
从物理到逻辑,GPU 显存可以分成四层来看。最底层是 HBM 物理显存,由 GPU 板载,容量固定,例如 A100 80GB、L20 48GB、RTX 4090 24GB。第二层是 CUDA 驱动与运行时管理的设备内存池,包括 CUDA Context、驱动预留、显存对齐开销,这部分通常几百 MB 到 1 GB 不等,且每个进程独立占用。第三层是框架级分配器,如 PyTorch 的 Caching Allocator、TensorRT 的显存池、vLLM 的 KV Cache 管理器,它们向驱动申请大块显存后自行切分复用。第四层才是业务可见的对象:模型权重、KV Cache、中间激活、临时张量、通信缓冲。
理解这个层级非常关键:OOM 报错发生在第四层,但根因常常在第三层的碎片或第二层的预留。例如 PyTorch 报 "CUDA out of memory. Tried to allocate 20.00 MiB",并不代表真的只剩 20 MiB,而是缓存分配器找不到一块连续的 20 MiB 空闲块。
二、分配器与碎片化
PyTorch 的 Caching Allocator 采用"按块申请、按流复用"的策略:向驱动申请时以较大粒度(如 2 MB、20 MB)为单位,内部再切成 small pool 与 large pool。释放的张量不会立刻还给驱动,而是回到缓存池等待复用。这带来两个后果:一是 torch.cuda.memory_allocated() 与 torch.cuda.memory_reserved() 出现差异,前者是真实使用,后者是已向驱动占用的总量;二是长时间运行后,缓存池中会留下大量大小不一的空闲块,形成外部碎片——总空闲足够,但没有一块能满足新请求。
碎片化的典型诱因包括:变长序列导致 KV Cache 反复扩缩、不同 batch size 交替、动态 shape 的中间张量、频繁创建销毁的临时 buffer。工程上常用三种手段缓解:预分配固定大小的 KV Cache 池(如 vLLM 的 PagedAttention 把 KV 切成固定 block)、设置 PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True 让分配器可扩展段、以及限制 max_split_size_mb 减少大块被切碎。
三、OOM 的根因分类
把 OOM 简单归为"显存不够"会掩盖真正问题。按触发位置可分为四类:
- 容量型 OOM:模型权重 + KV Cache + 激活的峰值确实超过物理显存,属于真不够,需要量化、切分或换卡。
- 碎片型 OOM:reserved 远大于 allocated,空闲块不连续,属于分配器问题,靠整理策略或预分配解决。
- 泄漏型 OOM:显存随请求数单调上升不回落,常见于未释放的中间张量、缓存了带梯度的引用、异常路径未清理。
- 突发型 OOM:并发瞬时冲高,KV Cache 一次性扩张,峰值超过预留,属于容量规划与限流问题。
判断方法很直接:连续打印 allocated、reserved、max_memory_allocated,观察三者关系。若 reserved 稳定但 allocated 持续上涨,是泄漏;若 reserved 远高于 allocated 且请求失败,是碎片;若两者同步冲高后回落,是突发。
四、资源隔离的目标与工程挑战
单卡多模型、多租户、多副本共存时,显存必须被隔离,否则一个模型的峰值会拖垮同卡的其他服务。隔离目标有三个层次:容量隔离(每个实例有上限,不能越界)、故障隔离(一个实例 OOM 不导致整卡崩溃)、性能隔离(一个实例的显存压力不显著影响其他实例的延迟)。
工程挑战在于:CUDA 本身没有原生的显存配额机制,MPS、MIG、时间片调度各有取舍。MIG 能提供硬件级切分,但粒度粗、配置不灵活;MPS 共享显存池,隔离弱;应用层配额(如 vLLM 的 gpu_memory_utilization)灵活但依赖框架自觉。因此实际方案通常是"框架配额 + 监控告警 + 限流熔断"的组合。
五、从观测到治理的闭环
治理显存不是一次性动作,而是一个闭环:先观测,再定位,然后干预,最后验证。观测阶段至少采集四个指标:allocated、reserved、max_memory_allocated、num_alloc_retries。定位阶段用 torch.cuda.memory_summary() 看分块分布,用 torch.cuda.memory_snapshot() 看碎片形态。干预阶段按根因选择手段:容量型做量化或张量并行,碎片型调分配器参数或改 KV 管理,泄漏型查引用链,突发型加限流与预留。验证阶段对比干预前后的峰值与 P99 延迟,确认没有把 OOM 转移成性能退化。
allocated 与 reserved 的持续曲线,任何调参都是盲调。建议在服务启动时就挂上显存采样,把峰值、均值、重试次数写入监控,让 OOM 从"偶发事故"变成"可预测事件"。