1. 显存碎片问题概述
大家好,我是你们的老朋友。今天咱们来聊聊显存碎片——这个在高并发场景下让人头疼的问题。
先问个问题:你遇到过模型推理时突然OOM,但nvidia-smi一看显存还剩不少的情况吗?
嗯,我遇到过。而且不止一次。
有一次线上服务半夜告警,说推理请求大面积超时。我登录上去一看,显存占用率才70%,但新请求就是分配不出内存。当时我就意识到——显存碎片来了。
什么是显存碎片
显存碎片,说白了就是显存空间被切割成许多小块。这些小块单独看都不大,但加起来总量不少。问题是,新来的大块请求找不到连续的空间。
就像你有一块完整的空地,被人这里挖个坑、那里堆个包。最后空地还剩不少,但想停一辆大卡车?没门。
在GPU里,显存分配以页(page)为单位。CUDA默认用4KB的页。频繁分配和释放,就会产生大量不连续的空闲页。
核心定义:显存碎片是指显存中可用但非连续的空闲内存块。这些碎片无法满足大块连续内存的分配请求,导致实际可用显存远小于理论值。
碎片产生的根本原因
为什么会碎?我总结了三层原因。
1. 分配与释放模式不匹配
你分配一块128MB的显存,用完后释放。但下一个请求要256MB。128MB的空闲块就卡在那里,成了碎片。
我做过一个实验:连续分配不同大小的tensor,然后随机释放。仅仅1000次操作后,显存碎片率就超过了30%。
2. CUDA内存管理机制
CUDA驱动本身会缓存已释放的显存,避免频繁与内核交互。这个缓存策略,说白了就是「先留着,万一有人用呢」。
但问题来了:缓存块的大小和位置是固定的。新请求的大小如果和缓存块不匹配,就得重新分配。旧的缓存块就成了碎片。
// 一个典型的碎片产生场景
cudaMalloc(&ptr1, 1024 * 1024); // 分配1MB
cudaMalloc(&ptr2, 512 * 1024); // 分配512KB
cudaFree(ptr1); // 释放1MB,产生空洞
cudaMalloc(&ptr3, 768 * 1024); // 需要768KB,但空洞只有1MB
// 1MB的空洞被分割:768KB被使用,剩下256KB碎片
3. 内存对齐要求
GPU对内存对齐有严格要求。通常是256字节或512字节对齐。这意味着你申请100字节,实际占用的是256字节。多出来的156字节,别人用不了。
这些小缝隙日积月累,就成了大问题。
避坑指南:我曾经在项目里用cudaMalloc频繁分配小tensor(几十KB级别),结果显存碎片率一周内飙到45%。后来改用内存池,问题才解决。
为什么高并发场景下更严重
这个问题,我当年在优化一个推荐系统推理服务时深有体会。
单线程场景下,分配释放是有序的。就像排队买饭,一个一个来,窗口始终整洁。
高并发场景呢?多个请求同时进来,同时分配,同时释放。顺序完全乱了。
我画了一张图,帮你理解这个过程:
你看,单线程下释放的块是相邻的,可以合并成一个大块。高并发下,释放的块被其他请求「插队」占用,碎片就散落在各处。
具体来说,高并发场景有三个「杀手」:
- 分配释放顺序随机化:多个请求的生命周期不同。有的请求快,有的慢。释放顺序和分配顺序完全错位。
- 内存分配器压力增大:CUDA的内存分配器需要处理更多并发请求。它不得不频繁分裂大块来满足小请求,导致碎片增多。
- 请求大小分布更广:高并发下,不同请求处理的数据量差异大。从几KB到几百MB都有。这种大小混杂的情况,最容易产生碎片。
注意:我见过一个极端案例——某公司的推理服务,8张A100显卡,每张80GB显存。高并发下实际可用显存只有55GB左右。碎片率超过30%!
这意味着你花了80GB的钱,只买到55GB的性能。亏不亏?
碎片带来的实际影响
碎片不只是浪费显存。它还会带来性能问题。
| 影响维度 | 具体表现 | 严重程度 |
|---|---|---|
| 显存利用率 | 实际可用显存减少20%-40% | 高 |
| 分配延迟 | cudaMalloc耗时增加3-10倍 | 中 |
| 服务稳定性 | 突发OOM导致请求失败 | 高 |
| 吞吐量 | 因内存不足被迫降低并发数 | 中 |
说白了,显存碎片就是「隐形杀手」。它不会直接报错,但会慢慢蚕食你的系统性能。
我的经验:排查显存碎片问题,我一般先看两个指标:
- nvidia-smi显示的已用显存 vs 实际分配总量
- cudaMalloc的调用耗时是否异常增加
如果前者差距大,后者耗时波动明显,十有八九是碎片问题。
好了,这一章我们搞清楚了什么是显存碎片、它怎么来的、为什么高并发下更严重。下一章我会讲具体的检测方法和量化指标。到时候咱们用代码说话。