4. 线程层次结构:Grid、Block、Thread的概念与关系
好,咱们今天聊一个核心中的核心——线程层次结构。
说白了,就是CUDA怎么组织那成千上万个线程的。你想想看,GPU里动辄几千个核心,如果每个线程都乱跑,那不乱成一锅粥?所以NVIDIA设计了一套层级结构:Thread → Block → Grid。
我个人习惯把这比作一个军队:
Thread是士兵,Block是连队,Grid是整个军团。
4.1 最底层:Thread(线程)
Thread就是执行你写的那个核函数的最小单位。每个线程执行相同的代码,但处理不同的数据。
怎么区分?靠threadIdx。
int tid = threadIdx.x; // 当前线程在Block内的编号
嗯,这里要注意:threadIdx是个三维向量 dim3。虽然我们常用一维,但CUDA支持三维索引。为什么?因为有些问题天然就是三维的,比如流体模拟、3D图像处理。
4.2 中间层:Block(线程块)
Block是一组线程的集合。这些线程在同一个块里,可以干两件大事:
- 共享内存通信:块内线程可以通过共享内存交换数据
- 同步:用
__syncthreads()让所有线程等齐了再往下走
每个Block也有自己的ID:blockIdx。同样是一维、二维或三维。
int blockId = blockIdx.x; // 当前Block在Grid内的编号
关键限制来了:一个Block里的线程数是有上限的。我做过测试,老一点的架构(比如Fermi)最多512个线程,现在的架构(比如Turing、Ampere)最多1024个。
cudaGetDeviceProperties查一下maxThreadsPerBlock,再动态分配。
4.3 最顶层:Grid(网格)
Grid就是所有Block的集合。你启动一个核函数,其实就是启动了一个Grid。
Grid的大小由你指定:
dim3 gridDim(16, 16); // 16x16 = 256个Block
dim3 blockDim(32, 32); // 每个Block有32x32 = 1024个线程
kernel<<<gridDim, blockDim>>>(...);
这里gridDim和blockDim都是dim3类型。你想想看,这样组合起来,一个Grid能有多少线程?
256个Block × 1024个线程 = 262,144个线程。嗯,这才只是一个小规模的启动。
4.4 它们怎么配合?——全局索引公式
这是面试常考题,也是写代码必须掌握的。怎么从threadIdx和blockIdx算出全局唯一的线程ID?
一维情况:
int globalId = blockIdx.x * blockDim.x + threadIdx.x;
二维情况(我项目中常用):
int globalIdx = blockIdx.x * blockDim.x + threadIdx.x;
int globalIdy = blockIdx.y * blockDim.y + threadIdx.y;
int globalId = globalIdy * width + globalIdx; // 线性化
blockIdx * blockDim + threadIdx 算出自己在全局数据中的位置。这样,所有线程各司其职,不会打架。
4.5 一张图看懂层次结构
我画了个SVG图,帮你理清关系:
4.6 实际项目中的选择策略
我刚开始学的时候,最头疼的就是:到底设多少个Block?每个Block多少个线程?
这里给个经验法则:
| 参数 | 推荐值 | 原因 |
|---|---|---|
| 每个Block的线程数 | 128 或 256 | 能充分利用SM的warp调度器,避免资源浪费 |
| Block总数 | 至少是SM数量的2倍 | 保证所有SM都忙起来,隐藏访存延迟 |
| Grid总线程数 | ≥ 数据量 | 每个数据元素至少对应一个线程 |
4.7 避坑指南
最后,分享几个我踩过的坑:
- Block内线程数不是越多越好:我曾经设了1024个线程,结果每个线程用的寄存器太多,导致占用率暴跌。后来查了
occupancy才明白。 - 别忘了处理边界:数据量不一定能被线程数整除。一定要加
if (tid < N)判断,不然会访问越界。 - 二维Grid的索引计算:我见过有人把
blockIdx.x和blockIdx.y搞反,结果数据全乱了。建议先画个图再写代码。
嗯,线程层次结构就这些。说白了就是三层嵌套:Grid管Block,Block管Thread。每个线程通过blockIdx * blockDim + threadIdx找到自己的位置。这个公式,你写一百次核函数,就会用一百次。