第一次在CUDA代码里看到cudaMalloc的时候,我天真地以为GPU内存管理和malloc没什么区别。直到我的矩阵乘法kernel跑得比CPU还慢时,才意识到GPU的内存体系完全是个新世界。NVIDIA的GPU包含四种物理内存:全局内存(global memory)、共享内存(shared memory)、寄存器内存(register)和本地内存(local memory),每种内存的延迟和带宽差异能达到两个数量级。
举个实际案例:在图像处理中,对1920x1080的图像做3x3卷积运算。如果直接读取全局内存,需要约200ms;而合理使用共享内存后,时间骤降到8ms。这种性能差异就是源于全局内存的延迟高达400-800个时钟周期,而共享内存的延迟只有1-2个时钟周期。
全局内存就是大家常说的显存,典型容量在几GB到几十GB。它的物理本质是GDDR6/GDDR6X颗粒,通过512位宽的总线与GPU相连。虽然理论带宽可达900GB/s(如RTX 4090),但实际有效带宽往往只有理论值的60%-70%。
关键性能指标:
优化技巧:
cpp复制// 错误示范:非合并访问
__global__ void bad_access(float* data) {
int tid = threadIdx.x + blockIdx.x * blockDim.x;
float val = data[tid * 32]; // 跨步访问导致带宽浪费
}
// 正确做法:连续合并访问
__global__ void good_access(float* data) {
int tid = threadIdx.x + blockIdx.x * blockDim.x;
float val = data[tid]; // 连续访问触发合并
}
实测数据:在RTX 3090上,合并访问相比非合并访问有5-8倍的带宽利用率提升
共享内存本质上是SRAM,位于每个SM(流式多处理器)内部。每个SM通常有128KB共享内存,被其下的所有线程块共享。它的神奇之处在于:
典型应用场景:
cpp复制__global__ void matrixMul(float* C, float* A, float* B, int N) {
__shared__ float As[BLOCK_SIZE][BLOCK_SIZE];
__shared__ float Bs[BLOCK_SIZE][BLOCK_SIZE];
// 从全局内存加载数据块到共享内存
As[threadIdx.y][threadIdx.x] = A[row*N + col];
Bs[threadIdx.y][threadIdx.x] = B[row*N + col];
__syncthreads();
// 使用共享内存计算
float sum = 0;
for (int k = 0; k < BLOCK_SIZE; ++k)
sum += As[threadIdx.y][k] * Bs[k][threadIdx.x];
C[row*N + col] = sum;
}
避坑指南:共享内存存在bank冲突问题。当多个线程同时访问同一个bank的不同地址时,访问会串行化。解决方案是采用错位存储(padding)。
每个线程有自己专属的寄存器组,访问延迟几乎为零。但寄存器资源非常有限:
寄存器优化案例:
cpp复制// 寄存器优化前:使用数组
__global__ void compute() {
float arr[10];
for(int i=0; i<10; i++)
arr[i] = i * 2.0f;
}
// 优化后:显式使用寄存器变量
__global__ void compute_opt() {
float r0=0.0f, r1=2.0f, r2=4.0f, r3=6.0f;
float r4=8.0f, r5=10.0f, r6=12.0f, r7=14.0f;
float r8=16.0f, r9=18.0f;
}
使用nvcc --ptxas-options=-v可查看寄存器使用情况。当寄存器不足时,编译器会自动将变量溢出到本地内存。
本地内存其实不是独立硬件,而是全局内存的一部分。当出现以下情况时,变量会被分配到本地内存:
性能特点:
现代GPU的全局内存访问以32字节为最小单位(称为cache line或memory transaction)。当线程束(warp)的所有32个线程访问连续对齐的32字节数据时,只需要一次内存事务。
合并访问的等级:
实测性能对比(RTX 3080):
| 访问模式 | 有效带宽 | 利用率 |
|---|---|---|
| 理想合并 | 760GB/s | 84% |
| 部分合并 | 320GB/s | 35% |
| 非合并 | 95GB/s | 10% |
共享内存被划分为32个bank(计算能力5.0+)。当多个线程访问同一个bank的不同地址时,会发生bank冲突。解决方案包括:
cpp复制// 原始有冲突的访问
__shared__ float data[32][32];
float val = data[threadIdx.y][threadIdx.x]; // 同一列访问相同bank
// 解决方案:添加padding
__shared__ float data[32][33]; // 每行多一个元素
float val = data[threadIdx.y][threadIdx.x]; // 现在无冲突
cpp复制// 转置访问改为广播式
if(threadIdx.x == 0) {
float val = data[threadIdx.y][0];
// 广播给其他线程
}
cpp复制// 优化前
for(int i=0; i<4; i++) {
sum += a[i] * b[i];
}
// 优化后:手动展开
sum += a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3];
cpp复制// 优化前
float t1 = a + b;
float t2 = a - b;
float t3 = a * b;
// 优化后
float tmp = a;
float t1 = tmp + b;
float t2 = tmp - b;
float t3 = tmp * b;
cpp复制__global__ void transpose_naive(float *odata, float *idata, int width) {
int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
odata[x * width + y] = idata[y * width + x]; // 非合并写入
}
问题:写入odata时是完全非合并的,性能极差。
cpp复制__global__ void transpose_shared(float *odata, float *idata, int width) {
__shared__ float tile[BLOCK_SIZE][BLOCK_SIZE];
int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
// 合并读取到共享内存
tile[threadIdx.y][threadIdx.x] = idata[y * width + x];
__syncthreads();
// 转置写入
x = blockIdx.y * blockDim.y + threadIdx.x; // 注意blockIdx.y和x交换
y = blockIdx.x * blockDim.x + threadIdx.y;
odata[y * width + x] = tile[threadIdx.x][threadIdx.y];
}
优化点:
cpp复制__global__ void transpose_opt(float *odata, float *idata, int width) {
__shared__ float tile[BLOCK_SIZE][BLOCK_SIZE+1]; // padding避免bank冲突
int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
tile[threadIdx.y][threadIdx.x] = idata[y * width + x];
__syncthreads();
x = blockIdx.y * blockDim.y + threadIdx.x;
y = blockIdx.x * blockDim.x + threadIdx.y;
odata[y * width + x] = tile[threadIdx.x][threadIdx.y]; // 现在无bank冲突
}
性能对比(1024x1024矩阵,RTX 3090):
| 版本 | 执行时间(ms) | 带宽利用率 |
|---|---|---|
| 基础版 | 2.56 | 12% |
| 共享内存版 | 0.87 | 35% |
| 终极优化版 | 0.52 | 58% |
使用Nsight Compute可以精确分析内存访问模式:
bash复制ncu --set full -o profile ./my_kernel
关键指标:
l1tex__t_sectors_pipe_lsu_mem_global_op_ld.sum 全局内存加载次数l1tex__t_requests_pipe_lsu_mem_global_op_ld.sum 全局内存加载事务smsp__sass_average_branch_targets_threads_uniform.pct 分支效率以RTX 3090为例:
如果实测只有50G floats/s,说明:
内存越界是常见错误:
bash复制cuda-memcheck --tool memcheck ./my_program
常见错误:
现代GPU(如Ampere架构)的内存子系统包含:
缓存行大小:
缓存策略选择:
cpp复制// 默认缓存配置
cudaDeviceSetCacheConfig(cudaFuncCachePreferNone);
// 偏好共享内存(减少共享内存等待)
cudaDeviceSetCacheConfig(cudaFuncCachePreferShared);
// 偏好L1缓存(优化全局内存访问)
cudaDeviceSetCacheConfig(cudaFuncCachePreferL1);
在深度学习中,推荐使用cudaFuncCachePreferL1,因为: