一、显存碎片全景:从现象到本质
大家好,我是你们的老朋友。今天咱们聊聊显存碎片——这个让无数GPU开发者头疼的问题。
先问个问题:你有没有遇到过这种情况?明明显存还有几十GB空闲,但一分配新张量就报OOM。我当年第一次遇到时,差点把键盘砸了。后来才明白,这背后就是显存碎片在作祟。
1.1 什么是显存碎片?
说白了,显存碎片就是显存空间被分割成许多不连续的小块。就像一块完整的蛋糕,被切得七零八落,虽然总重量没变,但你想切一块完整的方形出来,却发现哪块都不够大。
显存碎片分两种:
- 内部碎片:分配器给你一块内存,但你用不完。比如你申请100MB,分配器给了128MB的块,多出来的28MB就是内部碎片。
- 外部碎片:空闲内存总量够,但分散在各处,无法满足连续大块分配。这是最坑爹的。
核心区别:内部碎片是分配粒度问题,外部碎片是内存布局问题。实际项目中,外部碎片才是显存治理的头号敌人。
1.2 碎片产生的根本原因
为什么会碎?我总结了三座大山:
- 分配释放模式不匹配:训练过程中,张量不断创建和销毁。比如前向计算时分配大量中间结果,反向传播时又释放掉。这种频繁的分配-释放操作,就像在内存里打地鼠,打完一个洞又冒出一个。
- 张量生命周期差异大:模型参数活在整个训练周期,优化器状态活几个step,中间激活值可能只活几毫秒。不同生命周期的对象交错分配,内存自然被切得稀碎。
- 分配器策略局限:CUDA默认分配器是贪心的——它尽量复用最近释放的块。这导致大块空闲被切碎,后续大分配只能去申请新内存。
我记得有一次调一个LLM训练脚本,发现显存占用曲线像锯齿一样上下跳动。一查,原来是某个中间张量的生命周期没控制好,导致分配器反复切分大块内存。嗯,这就是典型的碎片制造机。
1.3 碎片对训练推理的影响
碎片不是小事,它带来的影响很实在:
| 影响维度 | 具体表现 | 严重程度 |
|---|---|---|
| 训练吞吐 | 频繁OOM导致batch size被迫缩小,训练效率下降30%-50% | 高 |
| 推理延迟 | 碎片导致内存分配耗时增加,单次推理延迟可能翻倍 | 中 |
| 模型规模 | 碎片浪费20%-40%显存,同等硬件下能跑的模型变小 | 高 |
| 稳定性 | 碎片积累到临界点,训练中途崩溃,白跑几小时 | 极高 |
我曾经在一个8卡A100集群上跑175B模型训练,因为碎片问题,每跑12小时就得重启一次。你想想看,重启一次光加载checkpoint就要半小时,这谁受得了?
1.4 碎片治理的宏观策略
治理碎片,说白了就三板斧:
- 预防:从源头减少碎片产生。比如统一张量生命周期、预分配大块内存池。
- 整理:碎片已经产生了,就主动整理。比如定期压缩、迁移活跃张量。
- 规避:绕开碎片问题。比如用更聪明的分配器、调整分配策略。
下面这张图是我自己画的碎片治理全景图,帮你快速建立整体认知:
我的建议:不要一上来就搞花里胡哨的整理策略。先做好预防——统一张量生命周期、预分配内存池,这两招能解决80%的碎片问题。我在实际项目中,光靠这两招就把OOM频率从每天3次降到了每周1次。
避坑指南:我曾经在某个项目里,为了追求极致的内存利用率,自己写了个内存整理模块。结果整理过程中频繁触发cudaMemcpy,反而把训练速度拖慢了40%。后来我学乖了——整理策略要权衡收益和开销,不是所有场景都适合做内存整理。
好了,这一章我们建立了显存碎片的全局认知。下一章,我会带你深入CUDA内存分配器的源码,看看它到底是怎么工作的——知己知彼,才能百战不殆。