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程序执行流程遵循“五步法则”:

  1. 分配显存:在GPU上为输入输出数据开辟显存空间。
  2. 数据拷贝:将CPU内存中的数据通过PCIe总线传输至GPU显存。
  3. 启动内核:CPU发出指令,GPU按网格(Grid)和线程块(Block)的层级结构并行执行核函数(Kernel)。
  4. 结果回传:计算完成后,将结果从显存拷贝回CPU内存。
  5. 释放资源:清理显存与上下文,避免泄漏。
关键认知:GPU并非“更快”的CPU,而是“更宽”的并行计算单元。向量加法这类元素间无依赖的任务,正是GPU的“主场”;而递归、链表遍历等强依赖任务,在GPU上反而可能因线程调度开销而变慢。
CPU (Host) 控制逻辑 + 缓存 Core 0-7 DRAM Ctrl 串行/分支任务 内存: 系统RAM GPU (Device) 大规模并行计算单元 SM 0 SM 1 SM 2 SM 3 SM 4 SM 5 SM 6 SM 7 显存: GDDR/HBM GPU 显存 (Global Memory) cudaMalloc / hipMalloc 分配 CPU 内存 (Host Memory) malloc / new 分配 ① cudaMemcpy (H2D) ② kernel<<<grid, block>>>() ③ cudaMemcpy (D2H) 结果回传 线程层级模型 Grid (网格) → Block (线程块) → Thread (线程) 向量加法: 1个Block, 256个Thread, 每个线程算1个元素 PCIe / NVLink 总线连接 CPU 与 GPU

方法流程: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编译器版本
版本选择建议:CUDA与ROCm均保持“驱动向后兼容”策略,即新驱动可运行旧版本工具包编译的程序。建议优先安装最新稳定版驱动,再根据项目依赖选择CUDA/ROCm工具包版本。若需在同一机器上切换多个CUDA版本,可使用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) ... }
性能复盘要点:向量加法是“内存带宽密集型”任务,其性能瓶颈在于PCIe传输与显存带宽,而非GPU计算能力。实测中,N=1024的向量加法在GPU上可能比CPU慢(因为数据拷贝开销远大于计算收益)。只有当数据规模达到百万级(N > 10^6)且计算密度足够高时,GPU的并行优势才会真正显现。因此,评估GPU加速收益时,务必计算“计算时间/数据传输时间”的比值(Arithmetic Intensity),而非盲目追求并行化。