3、CUDA内存管理:cudaMalloc/cudaFree、cudaMemcpy、统一内存(Unified Memory)、内存带宽测试
内存管理,说白了就是CUDA编程的命脉。我见过太多新手,核函数写得飞起,结果一跑就崩,十有八九是内存没管好。这一章,咱们就把CUDA内存的四大金刚——分配、释放、拷贝、统一内存,以及带宽测试,彻底讲透。
3.1 从cudaMalloc说起:显存分配的艺术
在GPU上干活,第一步就是找地方放数据。CPU用malloc,GPU就用cudaMalloc。我个人习惯,每次写cudaMalloc的时候,都会顺手把错误检查写上。为什么?因为显存分配失败太常见了。
float *d_data;
cudaError_t err = cudaMalloc(&d_data, N * sizeof(float));
if (err != cudaSuccess) {
printf("显存分配失败: %s\n", cudaGetErrorString(err));
return -1;
}
你想想看,一个模型动辄几个GB,显存不够用是家常便饭。我曾经在压测一个推荐系统时,就因为没检查cudaMalloc的返回值,程序跑了两小时突然崩了,查了半天才发现是显存碎片化导致分配失败。嗯,从那以后,我每条内存操作后面都跟着错误检查。
3.2 cudaFree:释放要干净利落
有借有还,再借不难。cudaFree就是还显存的操作。但这里有个坑——你必须在正确的CUDA流(stream)里释放。我记得有一次,我在默认流里分配,却在另一个流里释放,结果程序直接挂掉。
cudaFree(d_data); // 简单,但别忘了置空
d_data = NULL; // 防止野指针
3.3 cudaMemcpy:数据搬运的三种模式
数据在CPU和GPU之间搬来搬去,全靠cudaMemcpy。它有三种模式,我当年刚学时经常搞混:
| 模式 | 方向 | 典型场景 |
|---|---|---|
| cudaMemcpyHostToDevice | CPU → GPU | 输入数据上传 |
| cudaMemcpyDeviceToHost | GPU → CPU | 结果回传 |
| cudaMemcpyDeviceToDevice | GPU → GPU | 显存内部拷贝 |
这里有个性能要点:cudaMemcpy是同步操作。也就是说,它会把CPU线程卡住,直到数据拷贝完成。我在做流水线优化时,就尽量用异步拷贝(cudaMemcpyAsync)来避免阻塞。
// 同步拷贝(阻塞)
cudaMemcpy(d_data, h_data, size, cudaMemcpyHostToDevice);
// 异步拷贝(非阻塞,需要流)
cudaStream_t stream;
cudaStreamCreate(&stream);
cudaMemcpyAsync(d_data, h_data, size, cudaMemcpyHostToDevice, stream);
3.4 统一内存(Unified Memory):偷懒但别滥用
统一内存(UM)是CUDA 6.0引入的“黑科技”。它让你不用手动管理cudaMalloc和cudaMemcpy,系统自动在CPU和GPU之间迁移数据。说白了,就是写代码更爽了。
float *data;
cudaMallocManaged(&data, N * sizeof(float));
// 在CPU上初始化
for (int i = 0; i < N; i++) data[i] = i;
// 在GPU上使用(自动迁移)
kernel<<<grid, block>>>(data);
但是!我劝你谨慎使用。为什么?因为统一内存的自动迁移是有代价的。它会在页面级别触发缺页中断,然后按需迁移。如果数据访问模式不好,性能会惨不忍睹。
我记得有一次压测,用统一内存跑一个矩阵乘法,结果比手动管理慢了5倍。查了半天,发现是因为数据在CPU和GPU之间来回“抖动”。嗯,从那以后,我只在原型开发或者数据量小的场景用UM。
3.5 内存带宽测试:用数据说话
理论带宽和实际带宽,永远是两码事。我每次拿到新GPU,第一件事就是跑带宽测试。CUDA官方提供了bandwidthTest示例,但咱们也可以自己写一个简单的。
// 简单的带宽测试核心代码
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
float *d_src, *d_dst;
cudaMalloc(&d_src, size);
cudaMalloc(&d_dst, size);
cudaEventRecord(start);
cudaMemcpy(d_dst, d_src, size, cudaMemcpyDeviceToDevice);
cudaEventRecord(stop);
cudaEventSynchronize(stop);
float ms;
cudaEventElapsedTime(&ms, start, stop);
float bandwidth = (size / 1e9) / (ms / 1000); // GB/s
printf("实测带宽: %.2f GB/s\n", bandwidth);
你想想看,这个测试能帮你发现很多问题。比如,如果实测带宽远低于理论值,可能是内存访问模式不对,或者显存频率没跑满。
3.6 知识体系总览
下面这张图,是我自己总结的CUDA内存管理知识体系。你看一眼,就能把这一章的内容串起来。
这张图把四个核心知识点串起来了。你仔细看,每个分支下面还有子节点,都是我在实际项目中踩过的坑。比如“避免碎片化”、“缺页中断代价”,这些才是真正影响性能的关键。
3.7 总结一下
CUDA内存管理,说白了就是三件事:分配、搬运、释放。统一内存是锦上添花,但别依赖它。带宽测试是检验真理的唯一标准。我建议你每次写CUDA代码前,先跑一遍带宽测试,摸清硬件的底细。
嗯,这一章的内容就到这里。记住:内存管不好,性能好不了。