1. 显存碎片概述:什么是显存碎片、分类与根本原因
大家好,我是你们的老朋友。今天咱们来聊聊显存碎片这个话题。说实话,我在做GPU推理优化之前,一直觉得显存管理是操作系统该操心的事。直到有一次,一个模型在A100上跑着跑着突然OOM了,我一看显存使用率才70%多……嗯,从那以后,我再也不敢小看显存碎片了。
1.1 什么是显存碎片?
显存碎片,说白了就是显存空间被切割成一块块不连续的小区域。你想想看,就像你有一整块完整的土地,但被各种小建筑占得七零八落,剩下的空地虽然总面积不小,但每一块都太小,盖不了大房子。
在GPU推理场景下,情况类似。模型加载、张量分配、临时缓冲区申请……这些操作会不断占用和释放显存。久而久之,显存里就出现了很多「小洞」——单个看都不大,但加起来可能很可观。
核心问题:显存碎片导致明明有足够的空闲总容量,却无法分配出一块连续的大内存块。推理时如果遇到大张量分配失败,轻则触发显存整理(性能下降),重则直接OOM崩溃。
我在项目中遇到过最典型的情况:一个7B模型在FP16下理论只需要14GB显存,但在碎片严重的环境下,16GB的卡愣是跑不起来。排查了半天,发现碎片率高达30%多。
1.2 显存碎片的分类
显存碎片主要分两类:内部碎片和外部碎片。这两者的成因和影响完全不同,咱们分开说。
1.2.1 内部碎片
内部碎片,指的是分配出去的内存块中,没有被实际使用的部分。为什么会这样?
- 对齐要求:GPU对内存对齐有严格要求,通常是256字节或512字节对齐。你申请100字节,实际分配的可能就是512字节块,多出来的412字节就是内部碎片。
- 固定大小池:很多推理框架会预分配固定大小的内存池(比如16KB、64KB、256KB的块)。你申请一个17KB的缓冲区,它可能给你一个64KB的块,多出来的47KB就浪费了。
- 张量形状不匹配:模型推理时,中间激活值的形状经常变化。如果框架没有动态调整,就会产生内部碎片。
我的经验:内部碎片虽然看起来浪费不大,但累积起来很可观。我曾经优化过一个推理服务,光是调整内存对齐策略,就回收了约8%的显存。别小看这8%,在边缘设备上可能就是能不能跑起来的区别。
1.2.2 外部碎片
外部碎片才是真正的「杀手」。它指的是空闲显存被分割成多个不连续的小块,每个小块单独看都太小,无法满足大块分配请求。
为什么会这样?
- 分配与释放顺序不匹配:比如你先分配A(10MB)、B(20MB)、C(30MB),然后释放B。此时空闲区域是10MB(A前)、20MB(B处)、30MB(C后)。如果你想分配一个25MB的块,虽然总空闲有60MB,但没有一块连续空间够用。
- 生命周期交错:不同张量的生命周期不同。有些是模型权重(长期存活),有些是中间激活(短期存活),有些是临时缓冲区(极短存活)。这种交错释放很容易产生碎片。
- 多流并发:在多流(stream)并行执行时,不同流上的内存分配和释放相互交织,碎片化程度会急剧上升。
避坑指南:我曾经在一个多模型部署的项目中,同时加载了3个模型到同一张卡上。每个模型单独跑都没问题,但一起跑就频繁OOM。后来用nvidia-smi看了显存映射,发现外部碎片率高达45%。解决方案是改用统一内存池,并调整了模型的加载顺序。
1.3 显存碎片产生的根本原因
说到根本原因,其实就三个字:不匹配。具体来说:
- 分配大小与释放大小的不匹配:每次分配的大小不同,释放后留下的空洞大小各异,很难被后续分配完美复用。
- 分配时间与释放时间的不匹配:不同内存块的生命周期长短不一,长生命周期的块会把短生命周期的块「夹在中间」,导致碎片无法合并。
- 分配模式与释放模式的不匹配:推理过程中的内存分配模式是动态变化的。前向传播时大量分配,反向传播时大量释放(虽然推理没有反向,但中间激活的释放顺序依然复杂)。这种模式变化会加剧碎片化。
你想想看,这就像你住酒店,每天不同客人入住退房,房间大小不一、住的时间长短不一。时间一长,酒店里就全是零零散散的空房,想安排一个大型旅行团入住?难了。
下面这张图可以帮你直观理解显存碎片的形成过程:
从这张图可以看得很清楚:释放B之后,空闲区域被A和C分割成了两块。虽然总空闲有375MB,但最大的连续块只有200MB。这时候你想分配一个250MB的缓冲区?对不起,做不到。
一个小技巧:我习惯在模型加载后,用torch.cuda.memory_summary()看一眼显存分配情况。重点关注「Large free blocks」和「Fragmentation ratio」这两个指标。如果碎片率超过20%,就要考虑优化策略了。
好了,这一章的内容就到这里。显存碎片是个看似简单、实则坑很多的问题。理解了它的定义、分类和根本原因,后面我们才能对症下药,找到有效的优化手段。