欢迎关注我的公众号 [极智视界],获取我的更多经验分享
大家好,我是极智视界,本文分享一下 CUDA线程全局索引计算方式。
邀您加入我的知识星球「极智视界」,星球内有超多好玩的项目实战源码和资源下载,链接:t.zsxq.com/0aiNxERDq
CUDA 线程全局索引的计算,是很容易混淆的概念,因为 CUDA 线程模型的矩阵排布和咱们通常理解的数学矩阵排布秩序不太一样。在谈论这个话题前,还是要先看下下面这张 CUDA 线程层次模型图,关键先要对 Grid、Block、Thread 有个清晰的概念,如下,(需要注意的是,下面是 CUDA12 中的线程模型,新引入了一个 Thread Block Cluster 结构),
基本关系是 Thread 在一起组成了 Block,Block 在一起组成了 Grid,所以是 Grid 包含 Block 再包含 Thread 的关系。为了便于讲解以及回归最经典的 CUDA 线程模型,下面还有张更加清晰的图可以表达,
可以看到在上面的图中,一个 Grid 中是包含了 6 个线程块,而每个线程块又包含了 9 个线程,这其中有两个重要的概念,
- grid ==> 网格 ==> grid 中包含若干 block ==> 最大允许网格大小为
2^31 -1
(针对一维网格情况); - block ==> 线程块 ==> block 中包含若干 thread ==> 最大允许线程块大小为
1024
;
线程是 CUDA 编程中的最小单位,实际上线程分块是逻辑上的划分,在物理上线程不分块。在调用 GPU 的时候,核函数中是允许开很多线程的,开的线程个数可以远高于 GPU 计算核心的数量。在设计总的线程数时,至少需要等于硬件的计算核心数,这样才可能充分发挥硬件的计算能力。
在实际 CUDA 编程中,代码中是使用 <<<grid_size, block_size>>>
来进行配置线程的,意思是说线程都是在这个 <<<grid_size, block_size>>>
的规模中,其中 grid_size
是用来配置 block 的规模,而 block_size
是用来配置 thread 的规模,这点比较绕不要弄错了。
- grid_size ==> 通过
gridDim.x
来配置;==> 可以是int
或dim3
的结构; - block_size ==> 通过
blockDim.x
来配置;==> 可以是int
或dim3
的结构;
然后又来一个概念叫 线程索引,如下,它们是 built-in
的内建变量,意思是代码中可以直接拿来用而不用提前声明,
- blockIdx.x ==> 用于指定线程在网格中的线程块索引值,取值范围
0 ~ gridDim.x-1
;==> 展平开来; - threadIdx.x ==> 用于指定线程在线程块中的线程索引值,取值范围
0 ~ blockDim.x-1
;==> 展平开来;
来举个例子,来定义一个 kernel_ful<<<2, 4>>>()
的线程模型,示意如下,
其中 gridDim.x
值为 2,blockDim.x
值为 4,blockIdx.x
的取值范围为 0 ~ 1
,threadIdx.x
的取值范围为 0 ~ 3
,计算标红色 Star 线程的唯一标识为:Idx = threadIdx.x + blockIdx.x * blockDim.x = 2 + 1 * 4 = 6
,所以标红色 Star 的全局索引为 thread6,这是符合预期的。 当然,这是一维网格一维线程块的情况,也是最简单的情况,接着来推广到多维。
CUDA 最多其实可以组织三维的网格和线程块,可以用 dim3
的数据结构来表达,其实一维的情况也可以用 dim3
来表达,只是其他维度为 1 而已。这样来看,其实 blockIdx
和 threadIdx
是一个 uint3
的结构体,都包括 x、y、z 三个维度。再回过头来看上面的例子,对于一维的情况只用到了 x 维度,其他维度默认为 1,这个 1 若是展示出来就是 dim3
,若是隐藏起来就是 int
,所以 <<< _ , _ >>>
中的传参可以是 dim3
也可以为 int
,奏是这么简单。
对于多维的情况,可以采用下面的定义方式:
scss
dim3 grid_size(Gx, Gy, Gz);
dim3 block_size(Bx, By, Bz);
kernel_ful<<<grid_size, block_size>>>();
具体来说,比如定义一个 2 x 3 x 1
的网格、6 x 2 x 2
的线程块,可以这么写,
scss
dim3 grid_size(2, 3); // or dim3 grid_size(2, 3, 1);
dim3 block_size(6, 2, 2);
kernel_ful<<<grid_size, block_size>>>();
然后继续来看这张图,
前面说到 CUDA 线程模型的矩阵排布和咱们通常理解的数学矩阵排布秩序不太一样,看上图中 Block(1, 1)
中的矩阵排布,第一排是 (0, 0)、(1, 0)、(2, 0)、(3, 0),是最先变化 x;第一列是 (0, 0)、(0, 1)、(0, 2),再变化 y,原点是在左上。整体的排序组织顺序是下面这样的,
下面介绍 CUDA 线程全局索引的计算方式,全部列举出来其实是有下面九种情况,
- 一维网格一维线程块;
- 一维网格两维线程块;
- 一维网格三维线程块;
- 两维网格一维线程块;
- 两维网格两维线程块;
- 两维网格三维线程块;
- 三维网格一维线程块;
- 三维网格两维线程块;
- 三维网格三维线程块;
前面已经介绍过一维网格一维线程块的线程全局索引的计算方式,接着来介绍二维网格二维线程块的计算方式,如下,
要计算上图中红色块的线程索引,调用核函数的线程配置代码如下,
scss
dim3 grid_size(3, 2);
dim3 block_size(4, 4);
kernel_ful<<<grid_size, block_size>>>();
然后开始计算,
ini
int blockId = blockIdx.x + blockId.y * gridDim.x;
int threadId = threadIdx.y * blockDim.x + threadIdx.x;
int id = blockId * (blockDim.x * blockDim.y) + threadId;
// 带入计算
int blockId = 1 + 1 * 3 = 4;
int threadId = 2 * 4 + 2 = 10;
int id = 4 * (4 * 4) + 10 = 74;
所以计算出来,红色块线程的全局索引就为 thread74,这也是符合预期的。
三维的图画画起来稍显复杂,我这里就不画画了。
其实仔细想想,上面的九种组织情况都可以视为是 三维网格三维线程块 的情况,只是比如一维或者二维的时候,其他维度为 1 而已。若是都把它们都看成三维格式,这样不管哪种线程组织方式,都可以套用 三维网格三维线程块 的计算方式,整理如下,
ini
// 线程块索引
int blockId = blockIdx.x + blockIdx.y * gridDim.x + gridDim.x * gridDim.y * blockIdx.z;
// 局部线程索引
int threadId = threadIdx.z * blockDim.x * blockDim.y + threadIdx.y * blockDim.x + threadIdx.x;
// 全局线程索引
int id = blockId * (blockDim.x * blockDim.y * blockDim.z) + threadId;
好了,以上分享了 CUDA线程全局索引计算方式,希望我的分享能对你的学习有一点帮助。
【公众号传送】
畅享人工智能的科技魅力,让好玩的AI项目不难玩。邀请您加入我的知识星球, 星球内我精心整备了大量好玩的AI项目,皆以工程源码形式开放使用,涵盖人脸、检测、分割、多模态、AIGC、自动驾驶、工业等。一定会对你学习有所帮助,也一定非常好玩,并持续更新更加有趣的项目。 t.zsxq.com/0aiNxERDq