4、第一个CUDA程序:编写Hello World核函数、编译与运行、nvcc编译器使用、常见编译错误排查

好,咱们现在正式开始动手写代码了。

前面聊了那么多GPU的架构、线程模型、内存层次,说实话,都是纸上谈兵。真正要理解CUDA,你得亲手写一个能跑起来的程序。就像学游泳,你看再多教学视频,不下水永远学不会。

这一章,我们就来写第一个CUDA程序——经典的Hello World。不过不是CPU上那种简单的printf,而是让GPU来打印这句话。你想想看,让成百上千个线程同时在GPU上喊出“Hello World”,那场面还是挺壮观的。

4.1 核函数是什么?

在CUDA里,在GPU上执行的函数叫核函数(Kernel)。它有几个特殊标记:

  • __global__ 修饰符声明
  • 返回类型必须是 void
  • 调用时用 <<<grid, block>>> 指定执行配置

说白了,__global__ 就是告诉编译器:“这个函数不是给CPU跑的,是给GPU跑的”。我个人习惯把核函数放在文件最前面,这样结构清晰。

核心概念:核函数由GPU上的多个线程并行执行。每个线程都有唯一的ID,通过内置变量 threadIdxblockIdxblockDimgridDim 来区分。

4.2 编写第一个核函数

来,直接上代码。这是最简单的Hello World核函数:

// hello.cu
#include <stdio.h>

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

int main()
{
    // 调用核函数:1个grid,1个block,每个block里1个线程
    helloFromGPU<<<1, 1>>>();

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

    printf("Hello World from CPU!\n");
    return 0;
}

嗯,这里要注意:printf 在核函数里是能用的,但只适用于计算能力2.0以上的GPU。我在项目中遇到过老旧的GTX 580,当时死活打不出东西,查了半天才发现是架构太老不支持。

小技巧:核函数里的printf输出是缓冲的,必须调用 cudaDeviceSynchronize()cudaDeviceReset() 才能刷新输出。不然你可能什么都看不到。

4.3 编译与运行:nvcc编译器

CUDA程序不能用普通的gcc编译,得用NVIDIA提供的nvcc编译器。它本质上是一个编译器驱动,会把核函数代码编译成GPU能执行的PTX或cubin,把主机代码交给gcc/clang处理。

编译命令很简单:

nvcc -o hello hello.cu

然后运行:

./hello

如果一切顺利,你会看到:

Hello World from GPU!
Hello World from CPU!

我第一次跑通这个程序时,说实话挺激动的。虽然只是打印一句话,但背后是GPU在干活,那种感觉不一样。

4.4 nvcc常用编译选项

实际项目中,你不会只用裸的 nvcc。这里列出我常用的几个选项:

选项 作用 示例
-o 指定输出文件名 nvcc -o myapp hello.cu
-arch 指定GPU架构(计算能力) -arch=sm_75(Turing架构)
-G 生成调试信息(device端) nvcc -G -o debug hello.cu
-lineinfo 生成行号信息,用于性能分析 nvcc -lineinfo -o prof hello.cu
-Xcompiler 传递参数给主机编译器 -Xcompiler -fopenmp

警告:-arch 参数一定要和你实际使用的GPU匹配。我曾经在项目里用 -arch=sm_86 编译,结果部署到一台只有GTX 1080(计算能力6.1)的机器上,直接报错说找不到兼容的二进制代码。血的教训。

4.5 常见编译错误排查

写CUDA程序,编译报错是家常便饭。我刚开始学的时候,几乎每写一行代码都要跟编译器斗智斗勇。下面列出最常见的几个错误,你遇到了别慌。

错误1:error: identifier "printf" is undefined

原因:核函数里用了printf,但没包含 <stdio.h>

解决:在文件开头加上 #include <stdio.h>。注意,这个头文件在device端也能用,因为CUDA对它做了特殊支持。

错误2:error: a __global__ function must have void return type

原因:核函数返回了非void类型。

解决:把返回类型改成 void。核函数不能有返回值,数据传递必须通过参数或全局内存。

错误3:error: no suitable execution configuration found

原因:调用核函数时没加 <<<...>>> 执行配置。

解决:检查调用语法,确保是 kernelName<<<grid, block>>>(args); 格式。

错误4:error: identifier "threadIdx" is undefined

原因:在主机函数里使用了 threadIdx 等内置变量。

解决:这些变量只在核函数内部可用。检查代码,确保没有在 main() 或其他主机函数里使用它们。

错误5:运行时出现 invalid configuration argument

原因:block尺寸设置过大。每个block的线程数不能超过1024(计算能力2.0+)。

解决:检查 <<<grid, block>>> 中的block参数,确保 block.x * block.y * block.z <= 1024

避坑指南:我曾经在项目里把block设成 <<<1, 2048>>>,结果程序直接崩溃,没有任何错误提示。后来用 cudaGetLastError() 才抓到错误。所以,建议每次调用核函数后都检查一下错误码:

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

4.6 让更多线程参与进来

只用一个线程打印Hello World,太浪费GPU了。我们来改一下,让多个线程一起喊:

__global__ void helloFromGPU()
{
    printf("Hello World from thread %d in block %d!\n",
           threadIdx.x, blockIdx.x);
}

int main()
{
    // 2个block,每个block 4个线程,总共8个线程
    helloFromGPU<<<2, 4>>>();
    cudaDeviceSynchronize();
    return 0;
}

运行结果:

Hello World from thread 0 in block 0!
Hello World from thread 1 in block 0!
Hello World from thread 2 in block 0!
Hello World from thread 3 in block 0!
Hello World from thread 0 in block 1!
Hello World from thread 1 in block 1!
Hello World from thread 2 in block 1!
Hello World from thread 3 in block 1!

你想想看,这8个线程是并行执行的。虽然打印顺序可能乱,但每个线程都拿到了自己的ID。这就是GPU并行计算的基础——每个线程知道自己是谁,该处理哪部分数据。

个人经验:刚开始学CUDA时,我建议你多试试不同的 <<<grid, block>>> 组合。比如 <<<4, 8>>><<<2, 16>>>,看看线程ID怎么变化。这能帮你快速理解线程层次结构。

4.7 本章小结

这一章我们做了三件事:

  • 写了第一个CUDA核函数,用 __global__ 声明,用 <<<...>>> 调用
  • 学会了用 nvcc 编译和运行CUDA程序
  • 掌握了几个常见编译错误的排查方法

说实话,Hello World虽然简单,但它验证了整个CUDA开发流程:编写、编译、运行、调试。后面的所有复杂程序,都是在这个基础上搭建起来的。

下一章,我们会深入线程层次结构,真正理解grid、block、thread是怎么协同工作的。到时候,你就可以写出真正高效的并行程序了。