<<<CUDA C++ 学习路线grid · block · warp · lane
阶段 2 · 执行模型与内存05 / 15约 20 分钟

内存层级:决定性能上限的那张表

寄存器、共享内存、L1/L2、HBM 的延迟与带宽,以及计算强度这个判断工具

学完这一课你会
  • 记住各级存储的延迟与带宽数量级差异
  • 会用计算强度(arithmetic intensity)判断 kernel 是访存瓶颈还是算力瓶颈
  • 知道寄存器溢出(register spilling)的症状和后果
  • 理解为什么绝大多数真实 kernel 都是 memory-bound

如果只允许你记住 CUDA 优化的一件事,那应该是这张表。GPU 的算力早已过剩,真正稀缺的是把数据喂进去的能力。 一张 A100 有 19.5 TFLOPS 的 FP32 算力,但显存带宽只有 1.55 TB/s——也就是说,每从显存读一个 float(4 字节),你必须做够 50 次浮点运算才能让算力单元不闲着。绝大多数 kernel 远远达不到这个比例。

存储层级作用域延迟(周期)带宽量级容量
寄存器单线程私有~1~20 TB/s每 SM 256 KB
共享内存 / L1block 内共享~20–30~10 TB/s每 SM 最多 164 KB
L2 缓存全设备共享~200~4 TB/s40–50 MB
全局内存(HBM)全设备 + host~400–8001–3 TB/s16–80 GB
常量内存全设备只读~1(命中缓存)广播优化64 KB
本地内存单线程私有(实为显存)~400–800同全局内存受限于显存
查看寄存器用量和溢出
1nvcc -O3 -arch=sm_80 -Xptxas -v -c kernel.cu2 3# 典型输出:4# ptxas info : Used 38 registers, 8192 bytes smem, 380 bytes cmem[0]5#                   ↑ 每线程寄存器数     ↑ 每 block 共享内存6#7# 出现下面这行就说明溢出了,必须处理:8# ptxas info : 24 bytes spill stores, 24 bytes spill loads

计算强度:先判断瓶颈,再谈优化

计算强度 = 浮点运算次数 / 访问的字节数(单位 FLOP/Byte)。把它和硬件的「拐点」比一比,就能知道你的 kernel 卡在哪。拐点 = 峰值算力 / 峰值带宽,A100 FP32 约为 19500 / 1555 ≈ 12.5 FLOP/Byte。

运算计算强度瓶颈优化方向
向量加 c = a + b1 FLOP / 12 B ≈ 0.08严重访存受限只能优化访存模式,算力毫无意义
SAXPY y = a*x + y2 FLOP / 12 B ≈ 0.17严重访存受限同上,目标是打满带宽
朴素矩阵乘(无 tiling)2 FLOP / 8 B = 0.25访存受限用 shared memory 做数据复用
Tiled 矩阵乘(tile=32)≈ 8接近平衡继续加大 tile、用寄存器分块
大矩阵乘(寄存器分块)> 50算力受限上 Tensor Core,榨 FLOPS

各级存储的声明方式

五种存储在代码里长什么样
1__constant__ float c_filter[256];        // 常量内存:host 用 cudaMemcpyToSymbol 写入2 3__global__ void demo(const float* __restrict__ g_in,   // 全局内存4                     float* g_out) {5    __shared__ float s_tile[32][33];     // 静态共享内存(33 是防 bank conflict 的 padding)6    extern __shared__ float s_dyn[];     // 动态共享内存,大小由 <<<,,bytes>>> 给出7 8    float acc = 0.0f;                    // 寄存器9    float buf[8];                        // 索引若能编译期展开 → 寄存器;否则 → 本地内存(慢)10 11    #pragma unroll                       // 强制展开,保证 buf 留在寄存器里12    for (int i = 0; i < 8; ++i) buf[i] = g_in[i];13 14    for (int i = 0; i < 8; ++i) acc += buf[i] * c_filter[i];15    g_out[threadIdx.x] = acc;16}

共享内存和 L1 在物理上是同一块 SRAM,可以按比例切分。默认由驱动决定,但你可以给出偏好——重度使用 shared memory 的 kernel(比如 tiled matmul)应该主动多要一些。

调整 shared memory / L1 的切分
1// 偏好:尽量多给共享内存2cudaFuncSetCacheConfig(myKernel, cudaFuncCachePreferShared);3 4// Volta 及以后可以精确指定每个 block 的共享内存上限(单位字节)5cudaFuncSetAttribute(myKernel,6                     cudaFuncAttributeMaxDynamicSharedMemorySize,7                     96 * 1024);

自测

先自己在心里回答一遍,再展开对照。答不上来的说明这一段值得重读。