2006年当我第一次接触CUDA时,显卡还只是用来打游戏的设备。但当我看到用GPU加速的矩阵运算比CPU快100倍时,整个人都震惊了。这种性能差距在科学计算、深度学习等领域简直就是降维打击。
现代GPU通常拥有数千个计算核心(比如NVIDIA A100有6912个CUDA核心),而主流CPU通常只有几十个核心。GPU采用SIMT(单指令多线程)架构,特别适合处理高度并行的计算任务。比如在图像处理中,对每个像素的操作可以完全并行;在深度学习中,矩阵乘法的每个元素计算也可以并行。
注意:不是所有计算都适合GPU。对于串行依赖性强、分支复杂的算法,GPU可能反而比CPU慢。判断标准是计算密度(FLOPs/Byte),通常大于10才值得用GPU。
CUDA的编程模型建立在三个关键抽象上:
cpp复制// 典型CUDA核函数定义
__global__ void vectorAdd(float* A, float* B, float* C, int N) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < N) C[i] = A[i] + B[i];
}
GPU内存操作是最容易出错的地方之一。我总结的黄金法则是:
cudaMallocManaged分配统一内存简化编程__shared__内存cudaMalloc后都要cudaFreecpp复制float *d_A;
cudaMalloc(&d_A, N*sizeof(float));
// ... 使用d_A ...
cudaFree(d_A); // 必须释放!
Occupancy(占用率)指活跃warp与最大支持warp的比值。我常用这个公式估算:
code复制occupancy = (active_blocks_per_SM * threads_per_block) / max_threads_per_SM
提高occupancy的方法:
__launch_bounds__限制)实测案例:在矩阵乘法中,将block从16x16调整为32x32后,性能提升40%
现代CUDA(Compute Capability 7.0+)提供了更细粒度的warp操作:
__shfl_sync:warp内线程数据交换__reduce_add_sync:warp内归约运算__activemask:获取活跃线程掩码cpp复制// warp内归约求和
float val = ...;
for (int offset = 16; offset > 0; offset /= 2)
val += __shfl_down_sync(0xffffffff, val, offset);
CUDA调试一直是个痛点,这是我的调试工具箱:
cuda-memcheck:检查内存错误nsight systems:分析时间线printf大法:在kernel内加条件打印assert()和逐步注释代码cpp复制cudaMemcpy(host, device, size, cudaMemcpyDeviceToHost);
printf("%f", host[0]); // 可能读到旧数据!
修正:加cudaDeviceSynchronize()
cpp复制__global__ void kernel(float* data) {
data[threadIdx.x] = ...; // 可能越界!
}
修正:添加边界检查if(threadIdx.x < N)
CUDA 10+支持在device代码中使用lambda:
cpp复制auto square = [] __device__ (float x) { return x*x; };
__global__ void kernel(float* arr) {
arr[threadIdx.x] = square(arr[threadIdx.x]);
}
用模板实现通用核函数:
cpp复制template <typename T>
__global__ void scale(T* arr, T factor, int N) {
int i = blockIdx.x*blockDim.x + threadIdx.x;
if (i < N) arr[i] *= factor;
}
启用P2P访问:
cpp复制cudaDeviceEnablePeerAccess(peerDevice, 0);
// 然后可以直接拷贝
cudaMemcpyAsync(dst_ptr, src_ptr, size, cudaMemcpyDeviceToDevice, stream);
对于AllReduce等操作,NCCL比原生CUDA实现快得多:
cpp复制ncclAllReduce(sendbuff, recvbuff, count, ncclFloat, ncclSum, comm, stream);
我的性能分析工作流:
cpp复制cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
cudaEventRecord(start);
myKernel<<<...>>>();
cudaEventRecord(stop);
cudaEventSynchronize(stop);
float ms;
cudaEventElapsedTime(&ms, start, stop);
减少kernel启动开销:
cpp复制cudaGraph_t graph;
cudaGraphCreate(&graph, 0);
// 记录操作...
cudaGraphInstantiate(&instance, graph, NULL, NULL, 0);
cudaGraphLaunch(instance, stream);
提高GPU利用率:
bash复制nvidia-cuda-mps-control -d # 启动MPS守护进程
export CUDA_MPS_PIPE_DIRECTORY=/tmp/nvidia-mps
经过这些年实战,我认为CUDA编程最关键的思维转变是:从"如何实现算法"变为"如何组织并行计算"。每次优化前先用nvprof找准瓶颈,记住Amdahl定律——优化最耗时的部分才能获得最大收益。