GPU内存模型速览:全局内存、共享内存、寄存器、本地内存、常量内存、纹理内存的容量与特性

做GPU优化,说白了就是在跟内存打交道。

我刚开始接触CUDA时,总觉得只要把数据扔到显存里,核函数就能跑得飞快。结果呢?第一次调优就被现实狠狠教育了一顿——内存访问模式不对,性能直接腰斩。后来我才明白,搞清楚GPU的每一级内存长什么样、能装多少东西、访问速度如何,是写出高效核函数的前提。

今天咱们就把GPU内存模型从头到尾捋一遍。我会结合自己踩过的坑,帮你建立起这张内存地图。

核心观点:GPU内存不是铁板一块,而是一个层次分明的金字塔。越靠近计算单元的内存,容量越小、速度越快;越远离的,容量越大、延迟越高。你的任务,就是把数据放在最合适的位置上。

一、全局内存(Global Memory)—— 容量最大,但最慢

全局内存是GPU的“主存”。所有线程都能读写它,容量最大——从几GB到几十GB不等(比如A100有40GB或80GB)。

但代价也很明显:延迟高。访问一次全局内存,大约需要几百个时钟周期。我习惯把它比作“仓库”——东西多,但取一趟很费时。

特性 说明
容量 最大(GB级别)
延迟 高(400~800周期)
可见性 所有线程 + 主机端
缓存 有L2缓存,部分架构有L1

我的经验:全局内存访问一定要合并。什么意思?就是让相邻线程访问相邻地址。我曾经有个项目,没注意合并访问,带宽利用率只有10%。改成连续访问后,直接快了8倍。你想想看,同样的计算量,就因为内存访问方式不同,差距就这么大。

二、共享内存(Shared Memory)—— 线程块的“私房钱”

共享内存是每个线程块私有的。同一块内的线程都能读写它,但不同块之间互相隔离。

容量很小——通常只有几十KB到一百多KB(比如V100是96KB,A100可配置到164KB)。但速度极快,延迟只有几个时钟周期,跟寄存器差不多。

关键特性:共享内存是可编程缓存。你可以手动控制数据什么时候加载进来、什么时候写回去。这是GPU优化中最灵活、也最容易出彩的地方。

我个人的习惯是:能用共享内存就别用全局内存。比如做矩阵乘法时,把子矩阵先搬到共享内存里,反复使用,能省下大量全局内存访问。

注意:共享内存用多了会降低线程块并行度。每个线程块占用的共享内存越多,一个SM上能同时运行的块就越少。我曾经为了省事,把一个大数组全塞进共享内存,结果SM occupancy掉到25%,性能反而更差了。嗯,这里要权衡。

三、寄存器(Register)—— 最快,但最稀缺

寄存器是每个线程私有的,速度最快——零延迟。但数量极其有限:每个线程最多255个寄存器(实际可用通常更少,比如64或128个)。

为什么说它稀缺?因为一个SM上有成千上万个线程在跑,每个线程分到的寄存器就那么几十个。一旦你用超了,编译器会把多余的数据“溢出”到本地内存——性能直接崩盘。

避坑指南:我曾经写一个核函数,用了很多临时变量,没注意寄存器数量。编译后一看,每个线程用了120个寄存器,occupancy掉到33%。后来我把一些中间结果放到共享内存里,寄存器降到48个,occupancy回到67%,性能翻了一倍。所以,寄存器不是越多越好。

四、本地内存(Local Memory)—— 寄存器的“替补”

本地内存听起来像“本地”的,其实它物理上就在全局内存里。只是每个线程私有的,编译器用它来存放放不下的寄存器数据。

为什么会用到本地内存?

  • 寄存器不够用(最常见原因)
  • 数组索引不是编译期常量
  • 数据结构太大,放不进寄存器

性能陷阱:本地内存访问延迟跟全局内存一样高。如果你发现核函数性能异常,可以用--ptxas-options=-v编译选项看看local memory的使用量。如果超过0,就得优化了。

五、常量内存(Constant Memory)—— 只读,但带广播

常量内存是只读的,容量小(64KB),但有专门的缓存。它的最大特点是:当一个线程束(warp)内所有线程访问同一个地址时,硬件会广播,一次读取搞定。

我一般在以下场景用它:

  • 查找表(LUT)
  • 系数数组(比如滤波器系数)
  • 不会改变的配置参数

性能要点:常量内存适合所有线程读同一个值的场景。如果每个线程读不同地址,那就退化成普通全局内存访问了,甚至更慢。

六、纹理内存(Texture Memory)—— 为空间局部性而生

纹理内存最初是为图形学设计的,但用在通用计算里也有奇效。它也是只读的,有专门的纹理缓存,对二维/三维空间局部性访问特别友好。

它的几个独特能力:

  • 硬件插值(比如做图像缩放时自动算像素值)
  • 边界处理(超出范围返回0或边界值)
  • 数据格式转换(自动把整数转成浮点)

我记得有个项目做图像滤波,用全局内存读图像数据,缓存命中率很低。换成纹理内存后,因为空间局部性好,性能提升了30%。

适用场景:如果你的数据访问模式是随机但局部的(比如图像处理、体数据渲染),纹理内存值得一试。但如果是规整的连续访问,全局内存+合并访问就够用了。

七、一张图看懂GPU内存层次

下面这张SVG图,把上面讲的内存层次画出来了。你可以看到从寄存器到全局内存,容量越来越大,速度越来越慢。

GPU内存层次结构 寄存器 ~256B/线程 共享内存 ~48-164KB/块 常量/纹理内存 64KB / 设备级别 全局内存 GB级别 速度:快 速度:慢 容量:小 容量:大 越往上,速度越快,容量越小 越往下,速度越慢,容量越大

八、总结:怎么选?

好了,六种内存都过了一遍。最后给你一个我自己的决策流程:

  1. 数据是否只读且所有线程读同一个值? → 用常量内存
  2. 数据是否只读且访问有空间局部性? → 用纹理内存
  3. 数据是否被同一块内多个线程反复使用? → 用共享内存
  4. 数据是否每个线程私有且量很小? → 用寄存器
  5. 以上都不满足? → 用全局内存,但务必注意合并访问

最后一句:GPU优化的本质,就是让数据尽量靠近计算单元。你离寄存器越近,性能就越好。但也要记住,没有银弹——每种内存都有它的适用场景和代价。多实践、多分析,慢慢你就会有感觉了。

想要更多资料,联系可可