4. 第一个CUDA程序:编写Hello World核函数、编译与运行、理解<<<>>>语法、错误处理

好,咱们直接动手写代码。

学任何编程语言,第一个程序都是 Hello World。CUDA 也不例外。不过,CUDA 的 Hello World 不是打印一句话那么简单——它要让你看到 GPU 在干活。

4.1 编写第一个核函数

先看代码。我习惯把核函数写在前面,这样逻辑清晰。

#include <stdio.h>

// 核函数定义
__global__ void helloFromGPU(void)
{
    printf("Hello World from GPU!\n");
}

int main(void)
{
    // 主机端打印
    printf("Hello World from CPU!\n");

    // 启动核函数
    helloFromGPU<<<1, 10>>>();

    // 同步,确保GPU执行完毕
    cudaDeviceSynchronize();

    return 0;
}

嗯,这里要注意几个关键点。

第一,__global__ 这个修饰符。 它告诉编译器:这个函数是在 GPU 上跑的,但要从 CPU 端调用。说白了,这就是 CPU 和 GPU 之间的桥梁。

第二,printf 能在核函数里用? 是的,从 CUDA 2.0 开始支持。我在项目中经常用它做快速调试,比用 cuda-gdb 快多了。不过正式代码里别这么干,性能会崩。

4.2 理解 <<<>>> 语法

这个三括号语法,是 CUDA 最标志性的东西。我第一次看到它时,心想:这什么鬼?

其实它很简单——就是告诉 GPU 要开多少个线程。

格式是:

kernel_name<<<gridDim, blockDim>>>(args...);
  • gridDim:线程块网格的维度。可以是一个整数,也可以是 dim3 类型。
  • blockDim:每个线程块里的线程数。同样可以是整数或 dim3

拿上面的例子来说:

helloFromGPU<<<1, 10>>>();

这表示:启动 1 个线程块,每个块里有 10 个线程。总共 10 个线程在跑。

核心概念: 每个线程都会执行一遍核函数里的代码。所以上面的程序会打印 10 次 "Hello World from GPU!"。

你想想看,如果改成 <<<2, 5>>>,结果一样是 10 个线程。但组织方式不同——2 个块,每块 5 个线程。这在后面讲线程协作时会很重要。

4.3 编译与运行

编译 CUDA 程序,用 nvcc 编译器。它是 NVIDIA 的 CUDA 编译器,会把核函数编译成 GPU 能执行的 PTX 或二进制代码。

nvcc -o hello hello.cu

然后运行:

./hello

输出应该是:

Hello World from CPU!
Hello World from GPU!
Hello World from GPU!
...(共10次)

小技巧: 我习惯在 Makefile 里加上 -arch=sm_75 这样的架构参数。不同显卡的架构不同,不加的话 nvcc 会用默认值,可能跑不满性能。你可以用 nvidia-smi 查看自己的显卡型号,然后查对应的计算能力。

4.4 错误处理——别偷懒

CUDA 编程里,错误处理是重中之重。我刚开始学的时候,经常核函数调用失败,程序直接崩了,连个错误信息都没有。

为什么会这样?因为核函数启动是异步的。CPU 不会等 GPU 执行完就继续往下跑。如果核函数出错了,CPU 根本不知道。

所以,我们需要显式检查错误。

最常用的方法是检查 cudaGetLastError() 的返回值:

cudaError_t err = cudaGetLastError();
if (err != cudaSuccess) {
    printf("CUDA error: %s\n", cudaGetErrorString(err));
    return -1;
}

我建议把它封装成一个宏,方便复用:

#define CHECK(call) \
{ \
    const cudaError_t err = call; \
    if (err != cudaSuccess) { \
        printf("Error: %s:%d, ", __FILE__, __LINE__); \
        printf("code: %d, reason: %s\n", err, cudaGetErrorString(err)); \
        exit(1); \
    } \
}

然后这样用:

CHECK(cudaDeviceSynchronize());

警告: 我曾经在一个项目里忘了检查 cudaMalloc 的返回值,结果显存不够时程序直接段错误,查了两天才找到原因。从那以后,每个 CUDA API 调用我都加 CHECK 宏。别嫌麻烦,这能救你命。

4.5 完整示例与执行流程

把上面的代码整合一下,加上错误处理:

#include <stdio.h>

#define CHECK(call) \
{ \
    const cudaError_t err = call; \
    if (err != cudaSuccess) { \
        printf("Error: %s:%d, ", __FILE__, __LINE__); \
        printf("code: %d, reason: %s\n", err, cudaGetErrorString(err)); \
        exit(1); \
    } \
}

__global__ void helloFromGPU(void)
{
    printf("Hello World from GPU! threadId: %d\n", threadIdx.x);
}

int main(void)
{
    printf("Hello World from CPU!\n");

    helloFromGPU<<<1, 10>>>();

    CHECK(cudaDeviceSynchronize());

    return 0;
}

这里我加上了 threadIdx.x,它是 CUDA 内置变量,表示当前线程在块内的索引。从 0 到 9,正好对应 10 个线程。

整个执行流程是这样的:

  1. CPU 执行 main 函数,打印 "Hello World from CPU!"
  2. 遇到 <<<1, 10>>>,CPU 把核函数发射到 GPU
  3. GPU 创建 10 个线程,每个线程独立执行 helloFromGPU
  4. 每个线程打印自己的 threadIdx.x
  5. cudaDeviceSynchronize() 等待所有 GPU 线程完成
  6. 程序退出

下面这张图展示了这个流程:

CPU (主机) main() 函数执行 printf("Hello World from CPU!") helloFromGPU<<<1,10>>>() 启动核函数 GPU (设备) 创建 10 个线程 每个线程执行核函数 printf("Hello World from GPU!") 线程块 (Block) 线程0 线程1 线程2 ... 线程9 blockDim.x = 10 cudaDeviceSynchronize() 同步完成,返回 CPU 程序结束

这张图把整个流程串起来了。你看,CPU 负责启动和同步,GPU 负责并行执行。分工明确。

个人经验: 刚开始写 CUDA 程序时,我总忘记加 cudaDeviceSynchronize()。结果程序跑完了,GPU 的输出还没打印出来。后来我养成了习惯:每个核函数调用后面都跟一个同步检查。调试阶段尤其重要。

好了,第一个 CUDA 程序就到这里。代码不多,但背后的概念很重要——线程、块、网格、同步、错误处理。这些是后续所有内容的基础。

本章要点回顾:

  • 核函数用 __global__ 修饰,从 CPU 调用,在 GPU 执行
  • <<<gridDim, blockDim>>> 指定线程组织方式
  • 每个线程独立执行核函数,通过 threadIdx.x 区分
  • 错误处理不能省,用 cudaGetLastError() 或封装宏
  • cudaDeviceSynchronize() 确保 GPU 执行完毕

公众号:蓝海资料掘金营,微信deep3321