2. 显存管理基础:GPU显存架构、CUDA内存模型、显存分配与释放机制
好,咱们正式开始聊显存管理。这一章是地基,地基不牢,后面做大模型推理优化的时候,你会踩很多坑。我个人习惯,每次接手一个新项目,第一件事就是先把显存摸透。你想想看,大模型动辄几十GB,显存要是管不好,别说推理了,加载都加载不进去。
2.1 GPU显存架构:从物理到逻辑
先说说GPU显存到底长什么样。说白了,它就是一块焊在显卡上的高速DRAM。但跟CPU内存不一样,GPU显存有自己的一套脾气。
核心要点:GPU显存是HBM(高带宽内存)或GDDR(图形双倍数据率内存),带宽极高,但延迟也高。大模型推理时,瓶颈往往不在算力,而在显存带宽。
我在项目中遇到过一件事:用A100跑一个70B的模型,算力利用率死活上不去。后来一查,发现是显存带宽被吃满了,计算单元在干等数据。嗯,这就是典型的「内存墙」问题。
GPU显存架构有几个关键层级:
- 全局内存(Global Memory):最大的存储区域,所有线程都能访问。大模型的权重、KV Cache都放这儿。延迟最高,约400-800个时钟周期。
- 共享内存(Shared Memory):每个线程块独享,延迟极低(约20-30个周期)。我经常用它来做Attention计算的中间缓存。
- 寄存器(Registers):每个线程私有,最快。但数量有限,一个SM通常只有65536个寄存器。
- L1/L2缓存:自动管理,对开发者透明。但你可以通过调整缓存分区来影响性能。
为什么会这样设计?因为GPU要同时处理成千上万个线程,必须用分层存储来平衡容量和速度。你想想看,如果所有数据都走全局内存,那延迟会直接把吞吐量拖垮。
2.2 CUDA内存模型:你该怎么管这些显存
CUDA给开发者提供了一套内存管理API。说白了,就是让你能手动控制数据在显存和主机内存之间的流动。我个人觉得,理解CUDA内存模型是成为GPU优化高手的第一步。
常用的CUDA内存操作函数就那么几个:
| 函数 | 作用 | 注意事项 |
|---|---|---|
cudaMalloc |
在GPU上分配全局内存 | 返回设备指针,不能直接在主机端解引用 |
cudaFree |
释放GPU内存 | 必须配对使用,否则内存泄漏 |
cudaMemcpy |
主机与设备间数据传输 | 同步操作,大块数据拷贝会阻塞 |
cudaMallocHost |
分配页锁定主机内存 | 传输速度更快,但占用系统内存 |
小技巧:我建议你在做显存池之前,先用cudaMemGetInfo把显存的空闲总量和总容量打印出来。这能帮你快速定位是不是有内存泄漏。我曾经在一个项目里,就是因为没做这个检查,排查了整整两天才发现有个cudaFree没调用。
来看一个简单的分配与释放示例:
// 分配显存
float* d_data;
size_t size = 1024 * 1024 * 1024; // 1GB
cudaError_t err = cudaMalloc(&d_data, size);
if (err != cudaSuccess) {
printf("分配失败: %s\n", cudaGetErrorString(err));
return -1;
}
// 使用显存...
// 你的计算逻辑
// 释放显存
cudaFree(d_data);
d_data = nullptr; // 好习惯:释放后置空指针
嗯,这里要注意:cudaMalloc和cudaFree都是同步操作,开销不小。在大模型场景下,频繁分配释放显存会导致严重的性能抖动。这就是为什么我们需要显存池——提前分配好一大块,然后自己管理。
2.3 显存分配与释放机制:别让系统替你操心
CUDA驱动底层是怎么管理显存的?说白了,它维护了一个空闲列表和已分配列表。当你调用cudaMalloc时,驱动会从空闲列表中找一块足够大的连续空间。如果没有,就触发一次垃圾回收(如果启用了cudaMallocManaged),或者直接返回失败。
避坑指南:我曾经在一个推理服务里,每来一个请求就cudaMalloc一次KV Cache的显存,用完就cudaFree。结果并发一高,显存碎片化严重,明明还有20GB空闲,却分配不出一个连续的1GB块。嗯,从那以后,我再也不敢裸调cudaMalloc了。
显存分配有几个关键特性你需要记住:
- 连续性要求:CUDA分配的内存必须是物理连续的。碎片化是最大的敌人。
- 对齐要求:默认256字节对齐,但大模型通常用512字节或更大对齐。
- 分配粒度:底层分配以2MB或64KB为单位(取决于架构)。你申请1字节,实际占用2MB。
- 延迟开销:一次
cudaMalloc大约需要10-100微秒,看起来不多,但推理服务每秒处理几百个请求时,这个开销就不可忽视了。
为什么会这样?因为GPU驱动要维护页表、做TLB刷新、还要检查权限。你想想看,每次分配都走一遍这个流程,性能能好吗?
所以,大模型推理框架(比如vLLM、TensorRT-LLM)的做法是:
- 启动时一次性分配一大块显存(比如预留80%的显存)。
- 自己实现内存池,管理分配和释放。
- 用块分配(Block Allocation)代替字节分配,减少碎片。
- 支持内存复用,比如KV Cache的显存在不同请求间共享。
核心结论:显存管理的本质是「用空间换时间,用预分配换低延迟」。大模型推理场景下,裸调cudaMalloc是下下策,显存池才是正道。
最后,我建议你记住一个数字:一次cudaMalloc的开销大约相当于1000次浮点运算。所以,当你优化推理延迟时,先看看显存分配次数是不是太多了。很多时候,砍掉几次不必要的分配,比优化算子本身效果更明显。
好,这一章就到这里。显存管理的基础打牢了,后面讲显存池的实现原理时,你会觉得豁然开朗。
课后练习:写一个小程序,用cudaMemGetInfo监控显存使用情况,然后反复cudaMalloc和cudaFree不同大小的块,观察碎片化对分配成功率的影响。