4. 显存碎片化问题:成因、影响与监控
显存碎片化,说白了就是显存这块「大蛋糕」被切得乱七八糟。你明明还有不少空闲空间,但就是找不到一块连续的区域来放你的模型或张量。这个问题,我在做实时推理服务时踩过不少坑。
4.1 碎片化的成因分析
为什么会碎片化?我总结下来,主要有三个推手。
4.1.1 频繁的分配与释放
TensorRT 在构建引擎和运行时,会反复申请和释放各种大小的显存块。比如,你加载一个模型,它先给权重分配一块,再给中间激活值分配一块。推理完释放掉激活值,下次推理又申请。一来二去,显存就像被狗啃过一样。
- 大小不一的分配请求:权重可能是 1GB,激活值可能是 200MB,临时缓冲区可能是 50MB。这种大小差异,是碎片化的温床。
- 生命周期交错:有些显存块活得久(比如权重),有些活得短(比如临时变量)。短的释放后,留下的空洞很难被后续的大块请求填满。
核心观点:碎片化的本质,是「分配模式」与「释放模式」不匹配。你想想看,如果每次分配的大小都一样,释放顺序也固定,那碎片化几乎不会发生。但现实是,深度学习推理的显存需求是动态的、不规则的。
4.1.2 TensorRT 的内存池机制
TensorRT 内部其实有自己的内存池(Memory Pool),它试图通过缓存已释放的显存块来减少碎片。但池子不是万能的。
- 池子大小有限:默认情况下,TensorRT 的池子不会无限膨胀。当池子满了,新释放的块会被真正归还给 CUDA。
- 块合并策略:TensorRT 会尝试合并相邻的空闲块。但如果你的分配模式导致相邻块大小差异巨大,合并就变得困难。我记得有一次,一个 1MB 的空洞夹在两个 500MB 的大块之间,死活合并不了,因为合并后的大小超过了池子的最大块限制。
4.1.3 多流与多上下文
当你使用多个 CUDA 流(Stream)或多个执行上下文(ExecutionContext)时,碎片化会加剧。每个流或上下文都有自己的临时显存需求,它们互相交织,让显存布局变得异常复杂。
避坑指南:我曾经在一个项目中,同时跑 4 个推理流,每个流处理不同的 batch size。结果跑了不到 10 分钟,显存碎片化率飙到 40%,直接导致 cudaMalloc 返回 out of memory。后来我强制统一了 batch size,并使用了 TensorRT 的共享上下文机制,才把问题压下去。
4.2 碎片化对推理延迟的影响
碎片化不只是浪费显存,它还会直接拖慢你的推理速度。怎么影响的?我分两点说。
4.2.1 分配延迟飙升
当显存碎片化严重时,cudaMalloc 需要遍历空闲链表,寻找一个足够大的连续块。这个过程的时间复杂度从 O(1) 退化到 O(n)。
- 正常情况:cudaMalloc 耗时约 1-5 微秒。
- 碎片化严重:cudaMalloc 可能耗时 100 微秒甚至 1 毫秒。对于延迟敏感的推理服务,这 1 毫秒可能就是压垮骆驼的最后一根稻草。
4.2.2 内存带宽利用率下降
碎片化还可能导致你的数据在显存中「东一块西一块」。GPU 在访问这些数据时,无法利用空间局部性,导致缓存命中率下降,内存带宽利用率降低。
- 连续内存:一次内存事务可以读取 128 字节的连续数据,带宽利用率高。
- 碎片化内存:数据分散在多个不连续的页中,GPU 需要发起多次内存事务,带宽利用率可能腰斩。
个人经验:我调试过一个模型,推理延迟从 5ms 涨到 8ms。排查了半天,发现是显存碎片导致张量数据跨页存储,L2 缓存命中率从 85% 掉到了 60%。后来通过预分配固定大小的内存池,才把延迟拉回来。
4.3 使用 cudaMemGetInfo 监控显存状态
要解决碎片化,第一步是能看见它。cudaMemGetInfo 就是你的「显存显微镜」。
4.3.1 函数原型与用法
cudaError_t cudaMemGetInfo(size_t *free, size_t *total);
这个函数返回两个值:当前空闲显存大小和总显存大小。用法很简单:
size_t free_bytes, total_bytes;
cudaMemGetInfo(&free_bytes, &total_bytes);
printf("Free: %.2f GB, Total: %.2f GB\n",
free_bytes / 1e9, total_bytes / 1e9);
4.3.2 如何判断碎片化程度
光看空闲大小不够。你需要结合「最大可分配块」来判断。CUDA 没有直接提供这个 API,但你可以通过尝试分配不同大小的块来间接测量。
// 尝试分配一个大的连续块,看能分配多大
size_t max_allocatable = 0;
for (size_t size = free_bytes; size > 0; size -= 1024*1024) {
void *ptr;
cudaError_t err = cudaMalloc(&ptr, size);
if (err == cudaSuccess) {
max_allocatable = size;
cudaFree(ptr);
break;
}
}
float fragmentation_ratio = 1.0f - (float)max_allocatable / free_bytes;
printf("Fragmentation ratio: %.2f%%\n", fragmentation_ratio * 100);
关键指标:当 fragmentation_ratio > 30% 时,你就该警惕了。我一般会在推理服务中定期(比如每 100 次推理)跑一次这个检查,一旦超过阈值就触发内存整理或重启上下文。
4.3.3 实战监控脚本
下面是我在项目中常用的一个监控片段,放在推理循环的外围:
void monitorMemory(const char* tag) {
size_t free, total;
cudaMemGetInfo(&free, &total);
// 尝试分配一个接近空闲大小的块
size_t test_size = free * 0.9;
void* test_ptr = nullptr;
cudaError_t err = cudaMalloc(&test_ptr, test_size);
float frag = 0.0f;
if (err == cudaSuccess) {
frag = 1.0f - (float)test_size / free;
cudaFree(test_ptr);
} else {
// 分配失败,说明碎片化严重
// 用二分法找最大可分配块
size_t lo = 0, hi = free;
while (lo < hi) {
size_t mid = (lo + hi + 1) / 2;
void* ptr;
if (cudaMalloc(&ptr, mid) == cudaSuccess) {
lo = mid;
cudaFree(ptr);
} else {
hi = mid - 1;
}
}
frag = 1.0f - (float)lo / free;
}
printf("[%s] Free: %.2f MB, Frag: %.2f%%\n",
tag, free / 1e6, frag * 100);
}
4.3.4 监控时机建议
| 时机 | 说明 |
|---|---|
| 引擎构建后 | 检查初始碎片化程度,判断是否需要调整内存池参数 |
| 每 N 次推理后 | N 建议取 100-1000,根据你的推理频率调整 |
| 出现 OOM 错误时 | 立即打印当前显存状态,辅助定位问题 |
小技巧:我习惯把监控结果输出到 Prometheus 或 Grafana 里,做成实时曲线。这样碎片化趋势一目了然,不用等到出问题才去查。
4.4 小结
显存碎片化是个隐形杀手。它不会直接报错,但会慢慢蚕食你的显存和性能。通过 cudaMemGetInfo 配合最大可分配块检测,你可以把碎片化量化出来。嗯,这一步很重要——看不见的问题,你永远解决不了。