4、线程层次结构:Grid、Block、Thread的三层模型,以及它们如何映射到硬件

好,咱们今天聊一个核心话题——线程层次结构。

说白了,就是CUDA里那三个概念:Grid、Block、Thread。很多初学者觉得这玩意儿就是个API调用规则,记下来就行了。但我跟你说,不理解这三层怎么映射到硬件,你写出来的核函数性能可能差十倍不止。

4.1 三层模型:从逻辑到物理的桥梁

先看逻辑模型。CUDA把线程组织成三层:

  • Thread(线程):最基础的计算单元,每个线程执行相同的核函数,但处理不同的数据。
  • Block(线程块):一组线程的集合,它们可以协作——通过共享内存通信,通过同步指令做同步。
  • Grid(网格):一组Block的集合,代表整个核函数启动的所有线程。

你想想看,这其实就是一个三维的嵌套结构。每个Thread有唯一的threadIdx,每个Block有唯一的blockIdx,再加上blockDim和gridDim,就能定位任意一个线程。

关键点:Block内的线程可以协作,但Block之间是独立的。Grid内的Block可以以任意顺序执行,甚至并行、串行都行。这个特性决定了你的算法设计思路。

4.2 硬件映射:SM才是真正的执行单元

好,逻辑模型讲完了。那它怎么映射到硬件上?

我个人习惯用一个比喻:Grid是工厂订单,Block是工作小组,Thread是工人

  • SM(Streaming Multiprocessor) 是车间。一个GPU有多个SM,每个SM可以同时处理多个Block。
  • Block被调度到SM上执行。一个SM可以同时驻留多个Block,但Block一旦被分配到一个SM,就不会迁移到其他SM。
  • Thread在SM内部进一步分组——这就是Warp。Warp是硬件调度的基本单位,32个线程一组。

嗯,这里要注意:Block内的线程是在同一个SM上执行的。这意味着它们可以访问共享内存,可以做同步。但Block之间的线程,可能在不同的SM上,甚至不同的时间片里执行,所以不能直接通信。

避坑指南:我曾经在一个项目里,试图让两个Block通过全局内存做同步,结果发现根本不可控。因为Block的执行顺序是不确定的。后来改成用单个Block处理,配合共享内存,性能反而上去了。

4.3 线程调度:Warp是真正的执行单位

你可能会问:Thread不是最小执行单位吗?为什么又说Warp是?

其实,GPU的硬件调度器不是一条一条地调度线程,而是以32个线程为一组,整组调度。这32个线程执行同一条指令,但处理不同的数据——这就是SIMT(单指令多线程)模型。

举个例子:

__global__ void vec_add(float *a, float *b, float *c, int n) {
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    if (idx < n) {
        c[idx] = a[idx] + b[idx];
    }
}

假设blockDim.x = 256,那么这256个线程会被分成8个Warp(256 / 32 = 8)。每个Warp内的32个线程,在同一时刻执行相同的指令——比如都执行 c[idx] = a[idx] + b[idx]

但这里有个坑:Warp内的线程如果走不同的分支(if-else),就会发生Warp divergence。比如32个线程里,16个走if,16个走else,那这两部分就得串行执行。性能直接砍半。

警告:我曾经调试过一个图像处理核函数,因为边界判断导致Warp内线程分支严重,性能从80%利用率掉到30%。后来我把边界处理单独拎出来,用掩码方式处理,才把性能拉回来。

4.4 资源限制:为什么Block不能开太大?

每个SM的资源是有限的。主要包括:

  • 寄存器数量:每个SM有固定数量的寄存器(比如65536个)。每个线程用的寄存器越多,SM能同时驻留的线程就越少。
  • 共享内存大小:每个SM有固定大小的共享内存(比如48KB或96KB)。每个Block用的共享内存越多,SM能同时驻留的Block就越少。
  • 最大线程数:每个SM能同时驻留的线程数有上限(比如1024或2048个)。

所以,你设计Block大小时,不能只看逻辑需求,还得考虑硬件限制。举个例子:

资源 SM上限 每个线程/Block占用 最大驻留数
寄存器 65536 32个/线程 2048线程
共享内存 48KB 16KB/Block 3个Block
线程数 2048 256线程/Block 8个Block

你看,这三个限制里,最严格的那个决定了你能驻留多少Block。我建议你写核函数时,先用cudaOccupancyMaxPotentialBlockSize算一下最优配置,别凭感觉设。

4.5 映射关系总结

好,咱们把整个映射关系捋一遍:

  • Thread → CUDA Core:每个线程最终在一个CUDA Core上执行。但注意,CUDA Core是物理单元,线程是逻辑概念,一个Core可以分时执行多个线程。
  • Block → SM:Block被调度到SM上执行。一个SM可以同时处理多个Block,但Block不会跨SM迁移。
  • Grid → GPU:整个Grid对应一次核函数启动,所有Block分布在所有SM上执行。
  • Warp → Warp Scheduler:Warp是硬件调度的基本单位,每个SM内有多个Warp Scheduler,每个Scheduler负责调度一组Warp。

核心思想:你写代码时,脑子里要有这张映射图。Block大小影响SM利用率,Warp大小影响指令效率,Grid大小影响整体并行度。这三层设计好了,性能就稳了。

我记得有一次给一个团队做代码审查,他们把一个很大的图像处理任务拆成了1024个Block,每个Block只有32个线程。结果SM利用率极低,因为每个SM只能驻留少量Block,大量SM空闲。后来改成256线程一个Block,Block数量减少到128个,性能直接翻倍。

所以,记住:Block不是越大越好,也不是越小越好,而是要匹配SM的资源。一般建议Block大小取128到512之间,Warp数量取4到16之间,这样资源利用率最高。

嗯,这一章就到这里。下一章咱们聊聊Warp调度和指令流水线,那才是真正决定性能的细节。