GPU 内存层次结构

GPU 的内存层次结构是其高性能的关键。理解 GPU 的内存层次结构对于编写高效的 CUDA 程序至关重要。本文将详细介绍 GPU 的内存层次结构及其优化策略。

GPU 内存层次概览

GPU 的内存层次结构从快到慢依次为:

内存类型 访问速度 容量 作用域 缓存
Registers(寄存器) 最快 极小(每个线程) 线程私有
Shared Memory(共享内存) 极快 小(每个 Block) Block 共享
L1 Cache 小(每个 SM) Block 共享
L2 Cache 中等 中(整个 GPU) 全局
Global Memory(全局内存) 大(整个 GPU) 全局 有(L1/L2)
Constant Memory(常量内存) 小(64KB) 全局
Texture Memory(纹理内存) 全局

寄存器(Registers)

寄存器是 GPU 上最快的存储器,每个线程都有自己的一组寄存器。

寄存器特性

  • 速度:访问延迟约 1 个时钟周期
  • 容量:每个 SM 有数千个寄存器,每个线程分配一部分
  • 作用域:线程私有,其他线程无法访问
  • 生命周期:与线程生命周期相同

寄存器使用示例

__global__ void kernel(float *data) {
    // 局部变量通常存储在寄存器中
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    float temp = data[idx];  // temp 可能存储在寄存器中
    temp *= 2.0f;
    data[idx] = temp;
}

寄存器优化

  • 减少寄存器使用可以提高占用率
  • 使用 __launch_bounds__ 指定最大线程数
  • 避免过多的局部变量
// 指定每个 Block 最多 256 个线程,每个 SM 最多 2 个 Block
__global__ void __launch_bounds__(256, 2) optimizedKernel(float *data) {
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    data[idx] = idx;
}

共享内存(Shared Memory)

共享内存是 SM 内部的高速存储器,同一个 Block 内的线程可以共享访问。

共享内存特性

  • 速度:比全局内存快 100 倍以上
  • 容量:通常 48KB-96KB(可配置)
  • 作用域:Block 内线程共享
  • 生命周期:与 Block 生命周期相同

共享内存使用示例

#define TILE_SIZE 16

__global__ void matrixMul(float *A, float *B, float *C, int N) {
    // 声明共享内存
    __shared__ float tileA[TILE_SIZE][TILE_SIZE];
    __shared__ float tileB[TILE_SIZE][TILE_SIZE];

    int bx = blockIdx.x, by = blockIdx.y;
    int tx = threadIdx.x, ty = threadIdx.y;

    int row = by * TILE_SIZE + ty;
    int col = bx * TILE_SIZE + tx;

    float sum = 0.0f;

    for (int t = 0; t < N / TILE_SIZE; ++t) {
        // 加载数据到共享内存
        tileA[ty][tx] = A[row * N + t * TILE_SIZE + tx];
        tileB[ty][tx] = B[(t * TILE_SIZE + ty) * N + col];

        // 等待所有线程完成加载
        __syncthreads();

        // 使用共享内存中的数据进行计算
        for (int k = 0; k < TILE_SIZE; ++k) {
            sum += tileA[ty][k] * tileB[k][tx];
        }

        // 等待所有线程完成计算
        __syncthreads();
    }

    C[row * N + col] = sum;
}

共享内存优化

  • Bank Conflict:避免 Bank 冲突,提高访问效率
  • Pad:使用 padding 避免 Bank 冲突
  • 异步拷贝:使用 __cp_async 异步加载数据
// 避免 Bank Conflict 的示例
__global__ void avoidBankConflict(float *data) {
    __shared__ float s_data[32];  // 32 个 Bank,每个 Bank 4 字节

    int tid = threadIdx.x;
    s_data[tid] = data[tid];  // 每个线程访问不同 Bank,无冲突

    __syncthreads();

    data[tid] = s_data[tid];
}

// 使用 Padding 避免 Bank Conflict
__global__ void withPadding(float *data) {
    __shared__ float s_data[32][33];  // 添加 Padding

    int tid = threadIdx.x;
    s_data[tid][0] = data[tid];  // 避免 Bank 冲突

    __syncthreads();

    data[tid] = s_data[tid][0];
}

全局内存(Global Memory)

全局内存是 GPU 上最大的存储空间,但访问速度最慢。

全局内存特性

  • 速度:访问延迟高(数百个时钟周期)
  • 容量:大(几 GB 到几十 GB)
  • 作用域:全局,所有线程都可以访问
  • 缓存:有 L1/L2 缓存

内存合并访问

当相邻线程访问连续的内存地址时,GPU 可以将多个内存请求合并为一个事务。

// 好的内存合并
__global__ void coalescedAccess(float *data) {
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    data[idx] = idx;  // 连续访问,合并
}

// 不好的内存访问
__global__ void stridedAccess(float *data, int stride) {
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    data[idx * stride] = idx;  // 跨步访问,不合并
}

全局内存优化

  • 使用行优先存储(Row-major)
  • 避免跨步访问(Strided Access)
  • 使用共享内存减少全局内存访问
  • 考虑数据对齐(Alignment)

常量内存(Constant Memory)

常量内存是只读内存,有缓存优化,适合存储常量数据。

常量内存特性

  • 速度:快,有缓存
  • 容量:64KB
  • 作用域:全局
  • 访问:只读

常量内存使用示例

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

// 从主机复制数据到常量内存
cudaMemcpyToSymbol(constData, hostData, 256 * sizeof(float));

// 在 Kernel 中使用
__global__ void useConstant(float *data) {
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    data[idx] = constData[idx % 256];  // 从常量内存读取
}

纹理内存(Texture Memory)

纹理内存是专门为图形渲染优化的内存,也适用于某些计算场景。

纹理内存特性

  • 速度:快,有缓存
  • 容量:中
  • 作用域:全局
  • 特性:支持 2D/3D 寻址、插值、边界处理

纹理内存使用示例

// 声明纹理引用
texture texRef;

// 绑定纹理到全局内存
cudaBindTexture(NULL, texRef, d_data, width * height * sizeof(float));

// 在 Kernel 中使用纹理
__global__ void useTexture(float *output) {
    int x = threadIdx.x + blockIdx.x * blockDim.x;
    int y = threadIdx.y + blockIdx.y * blockDim.y;

    // 使用纹理坐标读取
    float u = x / (float)width;
    float v = y / (float)height;
    output[y * width + x] = tex2D(texRef, u, v);  // 纹理读取
}

// 解绑纹理
cudaUnbindTexture(texRef);

内存访问模式优化

1. 内存合并

确保相邻线程访问连续的内存地址。

2. 避免 Bank 冲突

在共享内存中,避免多个线程同时访问同一个 Bank。

3. 使用共享内存

利用共享内存减少全局内存访问次数。

4. 数据重用

将频繁使用的数据加载到共享内存或寄存器中。

内存管理函数

// 分配全局内存
float *d_data;
cudaMalloc(&d_data, N * sizeof(float));

// 释放全局内存
cudaFree(d_data);

// 内存拷贝:主机到设备
cudaMemcpy(d_data, h_data, N * sizeof(float), cudaMemcpyHostToDevice);

// 内存拷贝:设备到主机
cudaMemcpy(h_data, d_data, N * sizeof(float), cudaMemcpyDeviceToHost);

// 内存拷贝:设备到设备
cudaMemcpy(d_dest, d_src, N * sizeof(float), cudaMemcpyDeviceToDevice);

// 内存填充
cudaMemset(d_data, 0, N * sizeof(float));

// 异步内存拷贝
cudaMemcpyAsync(d_data, h_data, N * sizeof(float), cudaMemcpyHostToDevice, stream);

总结

GPU 的内存层次结构是其高性能的关键。理解不同内存类型的特性和访问模式,可以帮助我们编写高效的 CUDA 程序:

  • 寄存器:最快的存储,用于局部变量
  • 共享内存:Block 内共享,用于数据重用
  • 全局内存:最大的存储,但访问慢,需要优化访问模式
  • 常量/纹理内存:只读内存,有缓存优化

通过合理使用不同层次的内存,可以充分发挥 GPU 的计算能力,实现高性能的并行计算。