显存体系详解:全局显存、共享显存、L1/L2缓存、寄存器文件的带宽与延迟特性

做GPU编程这些年,我最大的感触就是——搞懂显存体系,你就掌握了性能优化的半壁江山。很多人一上来就写核函数,结果跑起来发现带宽利用率不到20%,还一脸懵。说白了,你连数据在哪儿、怎么搬的都没搞清楚,怎么可能写出高性能代码?

今天咱们就把GPU的存储体系掰开揉碎了讲。我会结合自己踩过的坑,带你彻底搞懂每一级存储的特性。

一、GPU存储层次全景图

先看一张我手绘的存储层次图。这张图我每次培训都会拿出来讲,因为它太重要了。

GPU 存储层次结构 寄存器文件 (Register File) ~20 TB/s 带宽 | ~1 cycle 延迟 | 每个SM ~256KB L1 缓存 / 共享显存 (Shared Memory) ~10 TB/s 带宽 | ~20-30 cycles 延迟 | 每个SM 48-128KB可配 L2 缓存 ~2-4 TB/s 带宽 | ~200-400 cycles 延迟 | 芯片共享 4-40MB 全局显存 (Global Memory / HBM/GDDR) ~1-2 TB/s 带宽 | ~400-800 cycles 延迟 | 容量 4-80GB 速度↑ 延迟↓ 容量↓ 速度↓ 延迟↑ 容量↑ 每个线程私有 每个线程块共享 芯片全局共享 所有线程可见 典型带宽对比 (TB/s) 寄存器: ~20 L1/SM: ~10 L2: ~3 GM: ~1.5

这张图你看懂了吗?从上到下,速度越来越慢,容量越来越大。我当年刚接触CUDA时,总觉得全局显存够快了啊,结果一测延迟,好家伙,800个时钟周期!从那以后,我再也不敢随便乱访存了。

二、寄存器文件:最快的存储,但也是最稀缺的

寄存器文件是离计算单元最近的存储。它的速度有多快?一个时钟周期就能完成读写。说白了,这就是GPU的"贴身侍卫"。

关键数据:

  • 带宽:约20 TB/s(每个SM内部)
  • 延迟:1个时钟周期
  • 容量:每个SM约256KB(以A100为例)
  • 分配方式:每个线程私有,编译器自动分配

我在项目中遇到过一个问题:一个简单的向量加法,性能死活上不去。后来一查,每个线程用了32个寄存器,SM的occupancy被压到了25%。你想想看,本来能跑2048个线程的SM,现在只能跑512个,性能能好吗?

我的经验:用 --ptxas-options=-v 编译选项查看寄存器使用量。如果每个线程超过32个寄存器,就要考虑优化了。我曾经把一个kernel的寄存器从48压到24,性能直接翻倍。

三、L1缓存与共享显存:可编程的"快车道"

L1缓存和共享显存在物理上是同一块硬件,但用法完全不同。L1是自动的,你只管读数据,它自己缓存。共享显存则需要你手动管理——显式地把数据从全局显存搬进来。

我个人习惯把共享显存当作"程序员可控的缓存"。为什么这么说?因为它的延迟只有20-30个周期,比全局显存快了一个数量级。

特性 L1缓存 共享显存
控制方式 硬件自动 程序员手动
延迟 ~30 cycles ~20 cycles
带宽 ~10 TB/s ~10 TB/s
容量配置 可配(如48KB L1 + 16KB SM) 可配(如16KB L1 + 48KB SM)
典型用途 全局显存访问的自动缓存 线程块内数据共享、归约

嗯,这里要注意:共享显存虽然快,但用不好就是灾难。我曾经见过一个同事,把整个矩阵都塞进共享显存,结果每个SM只能跑一个线程块,occupancy直接归零。

避坑指南:共享显存不是越大越好。我曾经为了省事,把共享显存配到最大(48KB),结果L1只剩16KB,全局显存的缓存命中率暴跌。后来我学乖了,根据访存模式动态调整:

  • 如果数据重用率高 → 多分给共享显存
  • 如果随机访问多 → 多分给L1缓存

四、L2缓存:芯片的"中转站"

L2缓存是所有SM共享的。它的带宽大概在2-4 TB/s,延迟200-400个周期。虽然比不上L1,但比全局显存还是快不少。

我记得有一次做矩阵乘法优化,发现性能瓶颈不在计算,而在L2缓存的bank冲突。每个SM都在抢L2的同一块数据,结果带宽利用率只有30%。后来我调整了数据布局,让每个SM访问不同的L2 slice,性能提升了2倍。

L2缓存的关键特性:

  • 芯片全局共享,所有SM都能访问
  • 容量:4MB-40MB(取决于GPU型号)
  • 缓存行大小:128字节(和全局显存一致)
  • 写策略:写回(write-back)

五、全局显存:容量最大,但也是最慢的

全局显存就是GPU的"主存"。HBM2e的带宽能做到2 TB/s,GDDR6X也能到1 TB/s。但延迟嘛...400-800个周期。你想想看,CPU的L1缓存延迟才几个周期,这差距有多大。

但是!全局显存有一个隐藏特性:合并访问。如果相邻线程访问相邻地址,硬件会把多次访问合并成一次。我见过太多人没利用好这个特性,导致带宽利用率不到10%。

// ❌ 错误示范:非合并访问
__global__ void bad_access(float *data) {
    int tid = threadIdx.x;
    float val = data[tid * 1024];  // 每个线程间隔1024个元素
}

// ✅ 正确示范:合并访问
__global__ void good_access(float *data) {
    int tid = threadIdx.x;
    float val = data[tid];  // 相邻线程访问相邻地址
}

我的建议:写全局显存访问代码时,脑子里要有一张"线程-地址"映射图。保证同一个warp内的32个线程访问的地址是连续的、对齐的。我曾经用这个技巧,把带宽利用率从15%拉到了85%。

六、各存储层次的实战对比

说了这么多,咱们来个实战对比。假设你要处理一个1GB的数据集:

存储层次 理论带宽 实际可用带宽 处理1GB耗时 适用场景
寄存器 20 TB/s ~15 TB/s ~0.07ms 局部变量、中间结果
共享显存 10 TB/s ~8 TB/s ~0.13ms 线程块内数据共享
L2缓存 3 TB/s ~2 TB/s ~0.5ms 数据重用、缓存友好
全局显存 1.5 TB/s ~1 TB/s ~1ms 大容量数据存储

看到差距了吧?寄存器比全局显存快了200多倍。所以优化的核心思路就是:让数据尽量待在上层存储中。

七、总结:我的"三板斧"

做GPU性能优化这么多年,我总结了三句话:

  1. 能放寄存器的,绝不放共享显存——编译器会自动优化,但你要注意寄存器压力
  2. 能放共享显存的,绝不放全局显存——手动管理虽然麻烦,但收益巨大
  3. 访问全局显存时,一定要合并——这是最容易被忽视的性能杀手

说白了,GPU的存储体系就像一座金字塔。你越靠近塔尖,速度越快,但容量越小。作为工程师,你的任务就是在容量和速度之间找到最佳平衡点。

好了,这一章的内容就到这里。记住我今天讲的这些数字和特性,下次写kernel的时候,多想想你的数据到底在哪儿、怎么搬的。相信我,性能提升会让你惊喜的。

可可精品资料库