从云原生走向 AI 原生:一套面向未来的架构方法论 → 阅读《AI 原生基础设施》

GPU 显存管理:分配器、碎片与 OOM 排查树

草稿

Kubernetes 能限制 Pod 用多少内存,却管不住它用多少显存。这不是配置遗漏,是显存根本不经过内核的内存管理。

阅读位置:上一章《CUDA 执行模型》 · 下一章《互连与集合通信》。

GPU 基础认知给出了“算力是软约束,显存是硬约束”的一阶结论;本章把显存这条硬约束拆到底:分配路径、框架行为、碎片机制、OOM 排查。它是HAMi显存配额(nvidia.com/gpumem)的计量口径、vLLM显存水位线策略与MIG硬边界的共同地基。

铁律:显存绕过 cgroup

K8s 的 memory.limit 经 cgroup 管主机内存。显存则通过 NVIDIA 驱动的设备节点(/dev/nvidia0/dev/nvidia-uvm 等)分配,内核对此没有记账

容器视角
├── 主机内存 7.9Gi / limit 8Gi   ← cgroup 可见,超限则 OOMKilled
└── 显存   79Gi  / 80Gi         ← cgroup 不可见,再涨即 CUDA OOM

于是:给了 Pod 一块 GPU,它就能吃光整卡显存,原生 K8s 无任何手段限制。这正是共享方案(HAMi 的 gpumem 配额、vGPU 的显存分区、MIG 的物理切分)存在的根本原因:三种机制分别在API 层、驱动层、硬件层补这个洞。

分配为什么慢,以及人人都有“缓存分配器”

驱动级分配(cudaMalloc/cudaFree)是微秒到毫秒级操作,且释放还可能同步整个设备。训练循环每步分配/释放张量的话,时间全耗在驱动上。因此所有 DL 框架内建了缓存分配器(PyTorch 的 CUDACachingAllocator),思路与 jemalloc/tcmalloc 一致:

  • 按大小分池:小块(<1MB 请求,2MB 段)、大块(≥1MB,20MB 段);
  • 释放的张量不还给驱动,留在池内等待复用;
  • torch.cuda.empty_cache() 才把空闲段还给驱动,它是诊断工具,生产循环里调用通常是 bug。

由此得到平台必背的一对指标:

reserved(nvidia-smi 看到的) = allocated(真实张量) + 缓存 + 碎片

nvidia-smi 的进程显存“只涨不跌”不是泄漏,是分配器的设计。把它当成泄漏去杀 Pod,是运维最常见的误操作之一。

碎片:还有内存却 OOM

外部碎片:池里有 10GB 空闲,但都是 1GB 的洞,来一个 5GB 请求 → 放置失败 → OOM。识别特征是报错信息里“reserved but unallocated”数值很大。对症配置:

PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True   # 虚拟地址连续映射,基本消灭外部碎片
PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:512      # 保守方案:禁止大切小

内部碎片:大小取整(2MB/20MB 段、推理引擎的 KV 块取整)造成的零头,单看小、量大时可观。分页 KV 的块大小选择就是这个问题在推理层的重现(见LLM 推理的物理层)。

与软切分配额的口径联动

HAMi 一类软切分方案在分配调用上拦截并记账(见数据平面谱系)。这意味着两件事:

  1. 计的是 reserved 口径(分配量),不是活跃张量,框架缓存会被当成“已用”;
  2. 跑在配额 Pod 里的 PyTorch 若不调 PYTORCH_CUDA_ALLOC_CONF,可能在“真实张量没用满”时就撞注入的 OOM。

软切分 Pod 模板应默认带分配器配置,这是显存配额从“能声明”到“能用”的临门一脚。

两棵 OOM 排查树

Pod OOMKilled(exit 137)进程内 CUDA out of memory
资源主机内存(cgroup)显存(绕过 cgroup)
常见元凶pinned host buffer、NCCL 共享内存、数据加载权重+KV+ 激活超预算、碎片、配额口径
排查入口kubectl describe pod、容器内存曲线nvidia-smi 总量 → 进程 reserved−allocated → 内存快照
修复方向调 memory limit / 减少主机侧缓存容量公式 / expandable_segments / 配额口径
表 1: OOMKilled 与 CUDA OOM 的分流

CUDA OOM 一侧的树状顺序:先看卡上是否真有空(共享卡则查邻居与配额);再看 reserved−allocated(大 → 碎片,走分配器配置);再看 allocated 本身(用 torch.cuda.memory._record_memory_history() + memory_viz 定位张量来源);最后核对容量公式(权重 + KV 池 + 每进程上下文底噪 0.3–0.8GB + 框架开销)。

工程经验
“显存还有剩余但新任务起不来”与“报 OOM 但 nvidia-smi 显示远没满”是同一枚硬币的两面:前者多半是碎片或连续分配失败,后者多半是进程内池子或配额口径。两者都指向本章,而不是“加卡”。

精度是一份工程契约

内存预算的另一根调节旋钮是精度。FP32 提供数值范围和熟悉的参照;FP16 与 BF16 减少内存、可提升 Tensor Core 吞吐,但二者在指数范围和稳定性行为上不同;FP8 及更低比特格式能在受支持的架构上解锁更高吞吐和容量,但需要缩放、校准、kernel 与质量验证。

格式适用角色需要的验证
FP32参考运算与敏感操作数值基线与性能对比
TF32加速的兼容 FP32 风格训练路径框架默认值与架构支持
FP16 / BF16训练与推理损失缩放、范围、累加、质量
FP8现代训练/推理缩放策略、算子覆盖、回归测试
INT8量化推理校准与任务精度
FP4 / 微缩放新兴低比特 AI 路径精确的 Blackwell 时代软件与模型支持
表 2: 数值格式的角色与验证要求

正确的问题不是“可用的最低精度是什么”,而是“在这套架构和软件栈上,满足应用质量与稳定性目标的最低精度是什么”。对显存预算而言,精度直接改写占用表里的权重、激活与 KV 缓存三项,这也是LLM 推理的物理层中量化提速的内存侧解释。

常见误区

误区真相
调大 Pod memory limit 能解决 CUDA OOM无关,显存不经 cgroup
empty_cache() 能防 OOM只释放空闲缓存;循环里调用反而拖慢
nvidia-smi 显示 79GB 就是模型有 79GB含缓存与碎片,真实张量看 allocated
OOM 一定是模型太大多数是碎片、水位线或配额口径问题
多个小进程共用 GPU 更灵活每进程 0.3–0.8GB 上下文底噪,且无隔离
表 3: 显存层的常见误区

总结

显存是 GPU 治理的第一硬约束,四个事实各有推论:

  • 绕过 cgroup:所以需要配额层
  • 框架持有缓存:所以 reserved ≠ allocated
  • 会碎片化:所以有“还有内存却 OOM”
  • 有每进程底噪:所以进程数本身是成本

这四个事实分别决定了软切分的计量口径、vLLM的水位线设计、MIG 硬边界的价值,以及 OOM 排障的第一步永远是“分清哪棵树”。

参考资料

创建于 2026/08/28 更新于 2026/08/28 2269 字 阅读约 5 分钟