3、显存池性能指标:分配延迟、碎片率、吞吐量、内存利用率
聊显存池,绕不开四个核心指标。
分配延迟、碎片率、吞吐量、内存利用率。
这四个指标,说白了就是衡量一个显存池「好不好用」的尺子。我这些年调优踩过的坑,十有八九都跟它们有关。今天咱们一个一个掰开揉碎了讲。
3.1 分配延迟:你的模型在等内存吗?
分配延迟,就是申请一块显存需要花多长时间。
你想想看,如果每次 new 一个 Tensor 都要等上几毫秒,那训练速度肯定被拖死。我见过一个项目,模型结构特别复杂,每步都要动态创建几十个临时 buffer。结果呢?GPU 算力利用率不到 30%,全耗在 cudaMalloc 上了。
怎么衡量?我一般用这个公式:
分配延迟 = 单次分配耗时 / 分配次数
实际测试时,我会写一个简单的 benchmark:
// 伪代码示例
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
cudaEventRecord(start);
for (int i = 0; i < 10000; i++) {
void* ptr = pool->allocate(1024); // 从池中分配 1KB
pool->deallocate(ptr);
}
cudaEventRecord(stop);
cudaEventSynchronize(stop);
float ms;
cudaEventElapsedTime(&ms, start, stop);
printf("平均分配延迟: %.3f us\n", ms * 1000 / 10000);
3.2 碎片率:显存里的「黑洞」
碎片率,是显存池最头疼的问题。
什么叫碎片?就是你明明还有 8GB 空闲显存,但申请一个 2GB 的连续块却失败了。这些空闲空间被分割成无数小块,像棋盘一样散落在显存里。
我习惯用这个公式计算碎片率:
碎片率 = 1 - (最大连续空闲块大小 / 总空闲大小)
举个例子:
| 场景 | 总空闲 | 最大连续块 | 碎片率 |
|---|---|---|---|
| 理想状态 | 8 GB | 8 GB | 0% |
| 轻度碎片 | 8 GB | 6 GB | 25% |
| 重度碎片 | 8 GB | 1 GB | 87.5% |
碎片率高了会怎样?
- 大块分配失败,触发 fallback 到 cudaMalloc,延迟飙升
- 显存利用率虚高,实际可用空间缩水
- 严重时直接 OOM,进程崩溃
3.3 吞吐量:池子能扛住多大压力?
吞吐量,衡量的是显存池在单位时间内能处理多少分配/释放请求。
这个指标跟分配延迟有点像,但侧重点不同。延迟看的是单次,吞吐量看的是并发压力下的整体表现。
我一般用这个公式:
吞吐量 = 总分配次数 / 总耗时
单位通常是「百万次/秒」或者 MOPS(Million Operations Per Second)。
我遇到过最夸张的一次:一个显存池单线程吞吐量 50 MOPS,换成 8 线程后掉到了 8 MOPS。原因就是内部用了全局锁,所有线程都在等锁释放。
怎么优化?
- 无锁设计: 用原子操作替代锁,或者用 per-thread cache
- 分桶策略: 不同大小的分配走不同的 freelist,减少竞争
- 批量处理: 把多次小分配合并成一次大分配
3.4 内存利用率:别让显存「空转」
内存利用率,就是池子里有多少显存被真正用上了。
公式很简单:
内存利用率 = 已分配大小 / 池子总大小
但这里有个坑——利用率不是越高越好。
你想想看,如果把利用率压到 99%,那意味着池子里几乎没有空闲块。一旦有新的分配请求进来,要么等别人释放,要么触发昂贵的整理操作。结果就是延迟飙升。
我见过一个团队把利用率推到 98%,结果线上频繁出现分配超时。后来我帮他们加了 10% 的预留空间,问题就解决了。
3.5 四个指标的权衡关系
这四个指标不是孤立的,它们互相牵制。
我画了一张图,帮你理清它们的关系:
从图上你能看到:
- 分配延迟 vs 吞吐量: 延迟越低,吞吐量越高。但优化延迟可能会增加锁竞争,反而降低吞吐量
- 碎片率 vs 内存利用率: 利用率越高,碎片越容易产生。这是个天然的矛盾
- 碎片率 vs 分配延迟: 碎片多了,找合适块的时间变长,延迟自然就上去了
- 吞吐量 vs 内存利用率: 池子太满,分配时容易触发整理或等待,吞吐量会掉
嗯,这四个指标讲完了。下次你评估一个显存池好不好,就从这四个维度去测。保证能看出门道来。
想要更多资料,联系可可