GPU编程入门:CUDA/ROCm环境搭建与第一个GPU程序
核心原理:GPU为何需要“异构编程”
CPU与GPU在设计哲学上存在本质差异。CPU拥有少量但强大的核心(如8~16核),擅长处理分支密集、依赖强的串行任务;GPU则集成数千个轻量级核心(如NVIDIA A100拥有6912个CUDA核心),专为大规模数据并行计算设计。CUDA(Compute Unified Device Architecture)和ROCm(Radeon Open Compute)分别是NVIDIA与AMD推出的异构编程平台,其核心思想是:由CPU(Host端)负责控制流与数据准备,将高度并行的计算任务卸载到GPU(Device端)执行。
一个典型的GPU程序执行流程遵循“五步法则”:
- 分配显存:在GPU上为输入输出数据开辟显存空间。
- 数据拷贝:将CPU内存中的数据通过PCIe总线传输至GPU显存。
- 启动内核:CPU发出指令,GPU按网格(Grid)和线程块(Block)的层级结构并行执行核函数(Kernel)。
- 结果回传:计算完成后,将结果从显存拷贝回CPU内存。
- 释放资源:清理显存与上下文,避免泄漏。
方法流程:CUDA/ROCm环境搭建与工程配置
环境搭建是GPU编程的第一道门槛。以Ubuntu 22.04 LTS为例,CUDA与ROCm的安装路径差异较大,但核心步骤都包含驱动安装 → 工具包安装 → 环境变量配置 → 编译验证四个阶段。
1. NVIDIA CUDA 环境搭建(以CUDA 12.x为例)
# 1) 检查GPU与驱动
lspci | grep -i nvidia
nvidia-smi # 确认驱动已识别GPU
# 2) 安装CUDA工具包(选择runfile方式,便于控制版本)
wget https://developer.download.nvidia.com/compute/cuda/12.3.0/local_installers/cuda_12.3.0_545.23.08_linux.run
sudo sh cuda_12.3.0_545.23.08_linux.run --toolkit --silent --override
# 3) 配置环境变量(写入 ~/.bashrc)
export PATH=/usr/local/cuda-12.3/bin:$PATH
export LD_LIBRARY_PATH=/usr/local/cuda-12.3/lib64:$LD_LIBRARY_PATH
source ~/.bashrc
# 4) 验证安装
nvcc --version # 显示nvcc编译器版本
2. AMD ROCm 环境搭建(以ROCm 6.x为例)
# 1) 添加AMD官方软件源
wget https://repo.radeon.com/amdgpu-install/6.0/ubuntu/jammy/amdgpu-install_6.0.60002-1_all.deb
sudo apt install ./amdgpu-install_6.0.60002-1_all.deb
# 2) 安装ROCm核心库
sudo amdgpu-install --usecase=rocm
# 3) 将用户加入render/video组(访问GPU设备)
sudo usermod -a -G render,video $USER
# 4) 验证安装
rocminfo | grep "Agent" # 显示GPU Agent信息
hipcc --version # HIP编译器版本
update-alternatives工具管理软链接。
案例实战:第一个GPU程序——向量加法
向量加法是GPU编程的“Hello World”,它清晰展示了数据并行分解方式。假设我们需要计算 C[i] = A[i] + B[i],其中向量长度为N=1024。在CPU上这是一个简单的for循环,而在GPU上我们需要将每个索引i映射到一个独立的线程。
CUDA实现(vector_add.cu)
#include <cstdio>
#include <cuda_runtime.h>
__global__ void vectorAdd(const float *A, const float *B, float *C, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x; // 全局线程ID
if (i < n) {
C[i] = A[i] + B[i];
}
}
int main() {
const int N = 1024;
size_t bytes = N * sizeof(float);
// 1. 分配主机内存并初始化
float *h_A = new float[N];
float *h_B = new float[N];
float *h_C = new float[N];
for (int i = 0; i < N; i++) { h_A[i] = i * 1.0f; h_B[i] = i * 2.0f; }
// 2. 分配设备显存
float *d_A, *d_B, *d_C;
cudaMalloc(&d_A, bytes);
cudaMalloc(&d_B, bytes);
cudaMalloc(&d_C, bytes);
// 3. 拷贝数据到GPU
cudaMemcpy(d_A, h_A, bytes, cudaMemcpyHostToDevice);
cudaMemcpy(d_B, h_B, bytes, cudaMemcpyHostToDevice);
// 4. 启动内核:1个Block,256个Thread
int threads = 256;
int blocks = (N + threads - 1) / threads; // 向上取整
vectorAdd<<<blocks, threads>>>(d_A, d_B, d_C, N);
// 5. 回传结果并验证
cudaMemcpy(h_C, d_C, bytes, cudaMemcpyDeviceToHost);
for (int i = 0; i < 5; i++) {
printf("C[%d] = %.1f (期望 %.1f)\n", i, h_C[i], h_A[i] + h_B[i]);
}
// 6. 释放资源
cudaFree(d_A); cudaFree(d_B); cudaFree(d_C);
delete[] h_A; delete[] h_B; delete[] h_C;
return 0;
}
编译与运行命令:
nvcc -o vector_add vector_add.cu
./vector_add
# 输出示例:
# C[0] = 0.0 (期望 0.0)
# C[1] = 3.0 (期望 3.0)
# C[2] = 6.0 (期望 6.0)
# C[3] = 9.0 (期望 9.0)
# C[4] = 12.0 (期望 12.0)
ROCm/HIP实现(vector_add.cpp)
ROCm使用HIP(Heterogeneous-Compute Interface for Portability)作为编程模型,其API设计与CUDA高度相似,仅需将cudaMalloc替换为hipMalloc,cudaMemcpy替换为hipMemcpy,核函数修饰符从__global__改为__global__(HIP中保持一致),启动语法从<<<blocks, threads>>>改为hipLaunchKernelGGL宏。核心逻辑完全一致,这使得CUDA代码迁移到AMD平台的成本极低。
# 编译HIP程序(使用hipcc编译器)
hipcc -o vector_add_hip vector_add.cpp
./vector_add_hip
误区与复盘:GPU编程的常见陷阱
| 误区/错误 | 现象 | 正确做法 |
|---|---|---|
| 忽略边界检查 | 当N不是线程数的整数倍时,越界访问导致cudaErrorIllegalAddress或数据错误 | 始终在核函数内添加 if (i < n) 边界判断 |
| 忘记同步 | 核函数启动是异步的,CPU代码可能先于GPU执行完毕,导致读取到未完成的数据 | 在拷贝结果前调用 cudaDeviceSynchronize() 或依赖 cudaMemcpy 的隐式同步 |
过度使用cudaMalloc | 频繁分配/释放显存造成严重性能损耗 | 复用显存池,或使用统一内存 cudaMallocManaged(适合小数据量) |
| 线程数设置不合理 | Block大小不是32的倍数(Warp大小),导致线程束分化 | Block大小建议设为128/256/512,且为32的倍数 |
| 忽略错误检查 | CUDA API调用失败后程序继续运行,产生难以排查的隐性bug | 封装错误检查宏,如 #define CHECK(call) { cudaError_t err = call; if (err != cudaSuccess) ... } |