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个。

我曾经踩过的坑: 有一次我设了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图,帮你理清关系:

Grid(网格) Block (0,0) Block (1,0) Block (0,1) Block (1,1) Block (0,0) 内部 T0 T1 T2 ... threadIdx.x = 0,1,2,... 共享内存 __syncthreads() ← Grid 包含多个 Block ← Block 包含多个 Thread

4.6 实际项目中的选择策略

我刚开始学的时候,最头疼的就是:到底设多少个Block?每个Block多少个线程?

这里给个经验法则:

参数 推荐值 原因
每个Block的线程数 128 或 256 能充分利用SM的warp调度器,避免资源浪费
Block总数 至少是SM数量的2倍 保证所有SM都忙起来,隐藏访存延迟
Grid总线程数 ≥ 数据量 每个数据元素至少对应一个线程
我的习惯: 先固定blockDim为256,然后根据数据量N算出gridDim = (N + 255) / 256。这样既简单又高效。除非有特殊需求(比如需要共享内存),我才调整blockDim。

4.7 避坑指南

最后,分享几个我踩过的坑:

  • Block内线程数不是越多越好:我曾经设了1024个线程,结果每个线程用的寄存器太多,导致占用率暴跌。后来查了occupancy才明白。
  • 别忘了处理边界:数据量不一定能被线程数整除。一定要加if (tid < N)判断,不然会访问越界。
  • 二维Grid的索引计算:我见过有人把blockIdx.x和blockIdx.y搞反,结果数据全乱了。建议先画个图再写代码。

嗯,线程层次结构就这些。说白了就是三层嵌套:Grid管Block,Block管Thread。每个线程通过blockIdx * blockDim + threadIdx找到自己的位置。这个公式,你写一百次核函数,就会用一百次。

可可精品资料库