一、CUDA内存模型全景:一张图看懂层级结构

做GPU加速这些年,我见过最多的坑就是显存溢出。说白了,很多人把GPU当成一个黑盒子,数据往里一扔就完事。结果呢?要么跑着跑着崩了,要么性能惨不忍睹。

要真正用好CUDA,你得先搞清楚GPU的内存长什么样。嗯,今天我们就来彻底拆解一下。

1.1 为什么内存模型这么重要?

你想想看,CPU访问内存,大概几十纳秒。GPU呢?全局内存访问延迟是几百个时钟周期。差了一个数量级还不止。

我在项目中遇到过这样一个场景:一个简单的矩阵乘法,naive实现跑了一整天没出结果。后来优化了内存访问模式,十分钟搞定。差别在哪?就在内存。

核心观点:CUDA程序的性能瓶颈,90%都在内存访问上。计算本身很快,数据搬运才是大头。

1.2 六种内存类型,一张图看懂

先上干货。CUDA内存模型分六层,从快到慢、从少到多。我习惯用这张图来跟团队新人讲解:

CUDA内存层级结构全景图 寄存器 (Register) 每个线程私有,~256个/线程 延迟:1个时钟周期 本地内存 (Local Memory) 寄存器溢出时使用,线程私有 延迟:~100个时钟周期 共享内存 (Shared Memory) 线程块内共享,~48KB/块可配置 延迟:~5个时钟周期 常量内存 (Constant Memory) 只读,所有线程可访问,64KB 延迟:~100周期(有缓存) 纹理内存 (Texture Memory) 只读,支持硬件插值与边界处理 延迟:~100周期(有缓存) 全局内存 (Global Memory) 所有线程可访问,GB级别,延迟400-800周期 快 慢 小 大

这张图我每次培训必讲。从上到下,速度越来越慢,容量越来越大。寄存器最快但最少,全局内存最慢但最大。

1.3 逐层拆解

寄存器(Register)

这是GPU最快的内存,没有之一。每个线程独享,编译器自动分配。我见过新手写kernel,一上来就定义几十个大数组,结果寄存器溢出到本地内存,性能直接腰斩。

避坑指南:我曾经调试过一个kernel,发现占用率只有25%。查了半天,原来是每个线程用了太多寄存器。后来拆成多个小kernel,占用率直接拉到75%。记住:寄存器不是越多越好。

本地内存(Local Memory)

说白了就是寄存器的"溢出区"。当你的线程需要太多寄存器时,编译器会把一部分变量放到本地内存。嗯,这里要注意:本地内存物理上在全局内存里,所以访问很慢。

怎么避免?我个人的经验是:每个线程的局部变量控制在32个以内。超过这个数,就要考虑用共享内存来分担了。

共享内存(Shared Memory)

这是CUDA优化的核心武器。同一个线程块内的线程可以共享数据,延迟只有几个时钟周期。我在做矩阵分块乘法时,就是靠共享内存把性能提升了10倍。

关键点:共享内存需要手动管理。用好了是神器,用不好就是瓶颈。记得处理bank conflict,否则性能会大打折扣。

常量内存(Constant Memory)

只读的,所有线程都能访问。适合存放查找表、系数矩阵这类不变的数据。它有专门的缓存,如果所有线程访问同一个地址,效率极高。

为什么会这样?因为常量内存的缓存是广播机制。所有线程读同一个地址,一次搞定。但如果每个线程读不同地址,那就惨了,串行化。

纹理内存(Texture Memory)

这个用得相对少一些,但在图像处理、空间局部性强的场景下特别好用。它支持硬件插值和边界处理,做图像滤波时能省不少事。

我记得有一次做遥感图像处理,用纹理内存做双线性插值,比手动实现快了3倍。嗯,如果你做的是二维数据访问,可以考虑它。

全局内存(Global Memory)

GPU的主存,所有线程都能读写。容量最大,但延迟也最高。访问全局内存时,一定要遵循合并访问原则——相邻线程访问相邻地址。

警告:全局内存的随机访问是性能杀手。我曾经接手过一个项目,代码里到处都是随机索引的全局内存访问。重构后改成连续访问,速度提升了8倍。记住:顺序访问,合并访问,这是铁律。

1.4 各内存对比一览

内存类型 作用域 生命周期 延迟 典型大小 是否可缓存
寄存器 单个线程 线程内 1周期 256个/线程 N/A
本地内存 单个线程 线程内 ~100周期 取决于寄存器溢出 L1/L2
共享内存 线程块 块内 ~5周期 48KB/块(可配置) N/A
常量内存 全局 应用程序 ~100周期 64KB 专用缓存
纹理内存 全局 应用程序 ~100周期 取决于显存 专用缓存
全局内存 全局 应用程序 400-800周期 GB级别 L1/L2

1.5 我的优化原则

做了这么多年CUDA优化,我总结出三条原则:

  1. 能放寄存器不放共享内存,能放共享内存不放全局内存。数据越靠近计算单元越好。
  2. 全局内存访问必须合并。这是底线,不遵守的话性能没救。
  3. 共享内存要手动管理bank conflict。别偷懒,该做padding就做padding。

小技巧:用nvcc --ptxas-options=-v编译时,会打印出每个kernel用了多少寄存器、共享内存。我每次写完kernel都会看一眼这个输出,心里有个底。

好了,CUDA内存模型的全景就讲到这里。记住这张层级图,后面我们讲优化方案时,会反复用到这些概念。下一章我们深入全局内存,聊聊怎么写出高效的全局内存访问模式。

可可精品资料库