1. CUDA内存模型全景:全局内存、共享内存、寄存器、本地内存、常量内存、纹理内存的层次结构与带宽特性

做CUDA压测这些年,我见过太多人一上来就写cudaMemcpy,然后抱怨程序跑不快。其实说白了,你不了解GPU的内存体系,就像开赛车却不看仪表盘——能跑,但绝对跑不远。

今天咱们就把CUDA的内存模型彻底捋一遍。我个人习惯把这六个内存区域分成两类:程序员可控的和硬件自动管理的。前者是共享内存、常量内存、纹理内存;后者是全局内存、寄存器、本地内存。嗯,这个分类方式帮我避过不少坑。

1.1 全局内存:最大的池子,最慢的路

全局内存是GPU里容量最大的存储区域,通常有4GB到80GB不等。但大归大,它的延迟也是最高的——大约400到800个时钟周期。

我曾经在压测一个图像处理管线时,发现90%的时间都花在全局内存访问上。后来一查,原来是没做合并访问(coalesced access)。

关键特性:

  • 容量:最大(显存大小)
  • 延迟:400-800 cycles
  • 带宽:取决于显存类型(GDDR6/HBM2e)
  • 可见性:所有线程可见
  • 缓存:L1/L2缓存(取决于架构)

这里有个血泪教训:全局内存的带宽利用率取决于访问模式。如果相邻线程访问相邻地址,硬件会把多次访问合并成一次大事务。反之,如果线程访问的是分散的地址,带宽利用率会暴跌到个位数百分比。

// 好的访问模式:合并访问
__global__ void coalesced_kernel(float* data, int N) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < N) {
        data[idx] = data[idx] * 2.0f;  // 相邻线程访问相邻地址
    }
}

// 坏的访问模式:非合并访问
__global__ void strided_kernel(float* data, int N, int stride) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < N) {
        data[idx * stride] = data[idx * stride] * 2.0f;  // 线程间有间隔
    }
}

1.2 共享内存:程序员的手动缓存

共享内存是我个人最喜欢的特性。它位于芯片内部,延迟只有全局内存的十分之一左右(约20-30 cycles)。但容量很小,每个SM上通常只有48KB到164KB。

你想想看,如果能把频繁访问的数据从全局内存搬到共享内存里,性能提升是立竿见影的。我在做矩阵乘法压测时,用共享内存做分块(tiling),吞吐量直接翻了6倍。

使用技巧:

  • 共享内存按block分配,同一个block内的线程可以共享
  • 需要手动用__syncthreads()做同步
  • 小心bank conflict——多个线程同时访问同一个bank会导致串行化
// 共享内存分块示例
__global__ void tiled_matmul(float* A, float* B, float* C, int N) {
    __shared__ float As[TILE_SIZE][TILE_SIZE];
    __shared__ float Bs[TILE_SIZE][TILE_SIZE];
    
    int row = blockIdx.y * TILE_SIZE + threadIdx.y;
    int col = blockIdx.x * TILE_SIZE + threadIdx.x;
    float sum = 0.0f;
    
    for (int t = 0; t < N / TILE_SIZE; t++) {
        As[threadIdx.y][threadIdx.x] = A[row * N + t * TILE_SIZE + threadIdx.x];
        Bs[threadIdx.y][threadIdx.x] = B[(t * TILE_SIZE + threadIdx.y) * N + col];
        __syncthreads();
        
        for (int k = 0; k < TILE_SIZE; k++) {
            sum += As[threadIdx.y][k] * Bs[k][threadIdx.x];
        }
        __syncthreads();
    }
    C[row * N + col] = sum;
}

1.3 寄存器:最快但最稀缺

寄存器是GPU里最快的存储,延迟为0(一个周期内完成)。但每个线程能用的寄存器数量有限——通常是32到255个,取决于架构和编译器设置。

我记得有一次压测,kernel的寄存器使用量超过了限制,结果编译器把多余的变量溢出(spill)到了本地内存。性能直接腰斩。嗯,这就是为什么我总说:别在kernel里声明太多局部变量。

避坑指南:

  • 用--maxrregcount编译选项控制寄存器数量
  • 寄存器溢出到本地内存,性能损失巨大
  • 每个SM的寄存器总数固定(如65536个),线程越多,每个线程分到的越少

1.4 本地内存:寄存器的备胎

本地内存听起来像"本地",但其实它物理上位于全局内存里。当寄存器不够用时,编译器会自动把变量放到本地内存。它的延迟和全局内存一样高。

我曾经在分析一个kernel的性能瓶颈时,发现local memory traffic占了总内存访问的30%。一看代码,里面有个大数组float temp[256]——这玩意儿根本放不进寄存器,全被溢出到本地内存了。

1.5 常量内存:只读且带缓存

常量内存是全局内存的一个特殊区域,大小为64KB。它有两个特点:一是只读,二是硬件会为它提供常量缓存(constant cache)。

为什么要有常量缓存?因为当同一个warp内的所有线程访问同一个常量地址时,硬件只需要一次读取就能广播给所有线程。这在做查找表(LUT)或者参数传递时特别有用。

// 常量内存声明
__constant__ float params[256];

__global__ void kernel_with_const(float* data, int N) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < N) {
        data[idx] = data[idx] * params[0] + params[1];  // 所有线程访问相同地址
    }
}

1.6 纹理内存:为空间局部性而生

纹理内存最初是为图形学设计的,但它对某些计算场景也很有用。它利用空间局部性——如果线程访问了地址A,那么附近的地址很可能也会被访问。硬件会预取这些数据到纹理缓存中。

我在做图像处理压测时,用纹理内存代替全局内存做双线性插值,带宽利用率提升了40%。不过要注意,纹理内存的写入操作很慢,它更适合只读且访问模式不规则的场景。

1.7 带宽特性对比

下面这张表是我压测时常用的参考数据。注意,具体数值因GPU架构而异,但相对关系是固定的。

内存类型 典型容量 延迟 带宽(相对) 缓存 作用域
寄存器 256KB/SM 1 cycle 最高 无 线程私有
共享内存 48-164KB/SM 20-30 cycles 高 无 Block内共享
常量内存 64KB ~200 cycles(缓存命中) 中 常量缓存 全局只读
纹理内存 取决于显存 ~200 cycles(缓存命中) 中 纹理缓存 全局只读
本地内存 取决于显存 400-800 cycles 低 L1/L2 线程私有
全局内存 4-80GB 400-800 cycles 低 L1/L2 全局可见

1.8 内存层次结构全景图

下面这张SVG图是我自己画的,把整个内存层次结构串起来了。你看,从芯片内部的寄存器到片外的全局内存,延迟和容量呈反比——这就是经典的存储层次。

CUDA内存层次结构 寄存器 延迟: 1 cycle | 容量: 256KB/SM 共享内存 延迟: 20-30 cycles | 容量: 48-164KB/SM 常量缓存 / 纹理缓存 延迟: ~200 cycles (命中时) L1 / L2 缓存 延迟: 100-300 cycles 全局内存 / 本地内存 / 常量内存 / 纹理内存 延迟: 400-800 cycles | 容量: 4-80GB 片内 片内 片内 片内 片外 最快 最慢

从这张图你能看到,越靠近芯片核心(顶部),速度越快但容量越小;越往外(底部),容量越大但速度越慢。做压测优化的核心思路就是:把热点数据尽量往顶部搬。

我的经验总结:

  • 全局内存访问一定要做合并,否则带宽利用率惨不忍睹
  • 共享内存是性价比最高的优化手段,但要注意bank conflict
  • 别让寄存器溢出到本地内存——用--ptxas-options=-v检查
  • 常量内存适合warp内所有线程访问同一地址的场景
  • 纹理内存适合二维空间局部性强的只读访问

好了,这一章的内容就到这里。记住,理解内存模型是CUDA优化的第一步,也是最关键的一步。下一章咱们会深入聊全局内存的合并访问——那才是真正拉开性能差距的地方。

可可精品资料库