简介:本文从硬件结构出发,用通俗类比讲清CUDA编程的核心原理:SM、线程束调度器、SIMT执行模型、存储层次与矩阵计算单元。读者将理解GPU为何适合大规模并行计算,并掌握从线程索引到性能优化的完整入门路径,为后续编写高效内核打下基础。
很多初学者直接阅读官方编程指南,往往卡在“知道怎么写,但不知道为什么这样写”的阶段。CUDA 程序的性能差异极大,同一段逻辑,写法不同可能导致数十倍的性能差距。要写出高效的内核,必须先建立对 GPU 硬件结构的直觉理解。
本文以易懂性优先,在个别细节上牺牲严格准确性,帮助读者快速建立整体认知框架。如需精确语义,请以官方编程指南为准。
GPU 是一块多核并行处理器。以当前主流的数据中心 GPU 为例,其抽象布局可以这样理解:
这个结构带来第一个关键认知:GPU 的性能瓶颈通常不在计算,而在数据搬运。因此 CUDA 内核优化的一个核心目标,就是尽可能提高 L2 缓存命中率、减少对全局内存的重复访问。这一点与 CPU 优化思路相通,只是 CPU 程序员通常不需要手动操心,而 GPU 程序员必须主动设计数据复用。
每个 SM 内部被划分为四个子分区,每个子分区包含两类计算资源:
这种“专用 + 通用”的混合设计,是 GPU 在深度学习时代占据主导地位的硬件基础。
每个 SM 还有四个线程束调度器(Warp Scheduler)。这是理解 CUDA 执行模型的关键,但很多入门材料一笔带过。
先解释线程束(Warp):GPU 中线程数量极多,为了降低调度开销,硬件将 32 个线程绑定为一组,统一取指、统一分发。这 32 个线程构成一个线程束。
线程束调度器的职责,就是在每个周期从就绪的线程束中挑选一个,把指令发射给对应的计算单元。它的存在意义在于隐藏延迟:当某个线程束因等待内存数据而停顿时,调度器立刻切换到另一个就绪线程束继续执行,从而让计算单元保持忙碌。
这就是 SIMT(Single Instruction, Multiple Threads,单指令多线程)模型:一组线程在同一时刻执行同一条指令,但操作各自的数据。它介于 SIMD(单指令多数据,向量化)与多线程之间,兼具两者的特点。
SIMT 带来一个必须注意的约束:同一个线程束内的 32 个线程,如果遇到条件分支而走向不同路径,硬件只能串行执行各条路径,即先执行路径 A(路径 B 的线程被屏蔽),再执行路径 B。这称为分支发散。
发散会让有效并行度下降。因此编写内核时应尽量让同一线程束内的线程执行相同控制流。常见做法包括:
CUDA 的存储层次大致为:
| 层次 | 位置 | 特点 |
|---|---|---|
| 寄存器 | 片上 | 最快,私有于线程 |
| 共享内存 | 片上 | 快,块内线程共享,需手动管理 |
| L2 缓存 | 片上 | 全芯片共享,容量有限 |
| 全局内存 | 片外 | 容量大,延迟高,全线程可见 |
入门阶段最重要的三条实践原则:
下面是一个向量加法的内核,覆盖了线程索引计算、全局内存访问和边界检查三个基础要素:
__global__ void vectorAdd(const float* a, const float* b, float* c, int n) {int idx = blockIdx.x * blockDim.x + threadIdx.x;if (idx < n) {c[idx] = a[idx] + b[idx];}}// 主机端调用int threadsPerBlock = 256;int blocksPerGrid = (n + threadsPerBlock - 1) / threadsPerBlock;vectorAdd<<<blocksPerGrid, threadsPerBlock>>>(d_a, d_b, d_c, n);
关键点说明:
threadIdx.x 是块内线程编号,blockIdx.x 是块编号,两者结合得到全局唯一索引。if (idx < n) 做边界保护。CUDA 编程的本质,是在一个“多核 + 多线程 + SIMT”的执行模型上,合理安排数据搬运与计算调度。硬件结构决定了优化方向:提高缓存命中、减少发散、保证并行度、善用专用计算单元。理解这些原理之后,再回头看各种编程指南和优化技巧,就会从“记住写法”变成“理解为什么”。这正是从入门走向熟练的分水岭。