1. 课程导入与CUDA编程全景

在数值计算领域,CPU 长期承担着串行逻辑与复杂控制流的处理任务,而 GPU 凭借数千个流处理器核心,在大规模并行浮点运算上展现出数量级的吞吐优势。CUDA 正是 NVIDIA 提供的通用并行计算平台,它让开发者可以用接近 C/C++ 的语法,把计算任务映射到 GPU 的并行执行单元上。本课程的目标,是帮助大家从线程模型出发,逐步掌握编写、调试和优化数值核函数的能力。

要理解 CUDA 的定位,先要建立异构计算的基本图景。一个典型的 CUDA 程序运行在“主机 + 设备”的异构系统中:主机(Host)指 CPU 及其内存,设备(Device)指 GPU 及其显存。二者拥有各自独立的地址空间,通过 PCIe 总线连接。CPU 擅长分支判断、文件读写、任务调度;GPU 擅长对海量数据执行相同的算术操作。因此,一个 CUDA 程序的典型分工是:CPU 负责准备数据、分配显存、启动核函数、回收结果;GPU 负责在核函数中完成密集的并行计算。

主机 Host(CPU) CPU 内存:数组 h_A、h_B、h_C cudaMalloc 分配显存指针 cudaMemcpy 传输数据 核函数启动 <<<grid, block>>> 结果回拷并校验 设备 Device(GPU) 显存:d_A、d_B、d_C Grid(网格) Block(线程块) Thread(线程)执行核函数 每个线程处理一个元素 H2D 拷贝 D2H 拷贝 启动核函数

上图展示了主机与设备之间的数据流与执行分工。可以看到,数据必须先通过显存拷贝进入设备,核函数才能读取;计算完成后,结果再拷回主机。这一“分配—拷贝—启动—回拷”的骨架,是几乎所有 CUDA 程序的共同结构。

接下来是 CUDA 线程模型,这是本课程最核心的概念。CUDA 把并行执行体组织成三级层次:线程(Thread)是最小执行单元;若干线程组成一个线程块(Block);若干线程块组成一个网格(Grid)。每个线程通过内置变量 threadIdx、blockIdx、blockDim、gridDim 确定自己的全局编号。以最常见的向量加法为例,若数组长度为 N,每个线程处理一个元素,则全局索引可写为:

int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < N) {
    C[idx] = A[idx] + B[idx];
}

这里的 blockIdx.x * blockDim.x 定位到当前线程块在网格中的起始偏移,threadIdx.x 是线程在块内的偏移,两者相加得到全局下标。边界判断 idx < N 不可省略,因为线程总数通常向上取整,会略多于实际元素个数。理解这个索引公式,就理解了 CUDA 线程模型与数据元素之间的映射关系,后续的矩阵运算、归约、卷积都建立在这一映射之上。

开发环境方面,需要准备三样东西:支持 CUDA 的 NVIDIA 显卡、对应版本的显卡驱动,以及 CUDA Toolkit。安装完成后,用 nvcc --version 确认编译器可用,用 nvidia-smi 查看显卡型号与驱动版本。编写第一个程序时,源文件以 .cu 为扩展名,核函数用 __global__ 修饰,主机调用代码用 cudaMalloc 分配显存、cudaMemcpy 传输数据、cudaDeviceSynchronize 等待设备完成。编译命令形如:

nvcc -o vecadd vecadd.cu
./vecadd

一个值得强调的实践细节是错误检查。CUDA 的许多调用失败时不会中断程序,而是返回错误码。建议在每次显存分配、拷贝和核函数启动后,用 cudaGetLastError() 或封装宏检查返回值,否则很容易出现“结果全为零”却找不到原因的困境。例如核函数启动后立即调用 cudaGetLastError(),若返回非 cudaSuccess,说明网格或块维度配置有误。

建议在第一个程序中就养成三个习惯:每次 CUDA 调用后检查错误码;用 cudaMalloc 分配的显存必须用 cudaFree 释放;核函数启动后若需立即读取结果,先调用 cudaDeviceSynchronize 或依赖随后的阻塞式拷贝。

从学习路径看,本课程将沿着“线程模型 → 核函数编写 → 内存层次 → 性能分析 → 典型数值算法”的主线推进。第一阶段的重点不是追求极致性能,而是建立正确的映射思维:数据如何分布到线程、线程如何协作、主机与设备如何配合。当你能独立写出一个通过边界检查、结果正确的向量加法核函数,并能解释每个线程在做什么,就已经跨过了 CUDA 编程最重要的门槛。后续的共享内存、同步、归约与矩阵分块,都是在这一基础模型上的自然延伸。