cuda 中核函数执行使用多线程并行(SIMD)的方式,同时计算多个数据,因此核函数的线程管理以及相应的任务分配就显得尤为重要。
首先说明一点,cuda 中使用 dim3 作为三维数据的表示方式,其表示的意义如下:
1 | dim3 blocks1D( 5 ); // 5*1*1 |
再来看看 cuda 中 kernel 函数的典型调用形式:
1 | kernel<<<Dg, Db, Ns, S>>>(params); |
- 参数
Dg是一个dim3类型,用于定义整个 grid 的维度,也就是一个 grid 中有多少个 block。dim3 Dg(Dg.x, Dg.y, 1)表示 grid 中每行有Dg.x个block,每列有Dg.y个block,第三维恒为 1。整个 grid 中共有Dg.x*Dg.y个 block,其中Dg.x和Dg.y最大值为 65535。- 对于一个 grid,其中包含了多个 block,使用
unit3类型的blockIdx来表示,通过blockIdx.x,blockIdx.y,blockIdx.z三个坐标可以定位 grid 中的一个 block。 - 注意:
dim3是手工定义的,主机端可见。uint3是设备端在执行的时候可见的,不可以在核函数运行时修改,初始化完成后uint3值就不变了。他们是有区别的,这一点必须要注意。
- 对于一个 grid,其中包含了多个 block,使用
- 参数
Db是一个dim3类型,用于定义一个 block 的维度,即一个 block 有多少个 thread。Dim3 Db(Db.x, Db.y, Db.z)表示整个 block 中每行有Db.x个thread,每列有Db.y个thread,高度为Db.z。Db.x和Db.y最大值为 512,Db.z最大值为 62。 一个 block中 共有Db.x*Db.y*Db.z个 thread。不同计算能力这个乘积的最大值不一样。- 和在 grid 中定位一个 block 类似,在一个 block 中定位一个 thread 也是用一个
unit3类型的threadIdx的三个坐标来表示的。
- 和在 grid 中定位一个 block 类似,在一个 block 中定位一个 thread 也是用一个
- 参数
Ns是一个可选参数,用于设置每个 block 除了静态分配的 shared memory 以外,最多能动态分配的 shared memory 大小,单位为 byte。不需要动态分配时该值为0或省略不写。 - 参数
S是一个cudaStream_t类型的可选参数,初始值为零,表示该核函数处在哪个流之中。
kernel 可以通过 grid 和 block 的设置实现了多线程并行计算,下面是 cuda 官方的一个向量相加的例子,其中的 kernel 函数就是实际计算程序:
1 |
|
上面的代码中可以通过 threadIdx.x + blockIdx.x * blockDim.x 定位当前执行线程的 index。但是我们实际操作的数据长度(33*1024) 大于设置的线程数量 (128*128)。因此一个线程可能会处理多个数据,因此使用 tid += blockDim.x * gridDim.x 来执行多个数据的处理。当然,需要判断 tid 是否越界。
因为我们都是通过多线程并行来实现 kernel 的高效执行,因此也可以说编写核函数的精髓就是如何利用线程的序号(索引值)来分配计算任务。这里有一个题外话,之所以在硬件上将线程抽象成三维数组来表示,就是为了方便图像处理里,利用三维的线程索引来对应图像数据索引,并行加速,其实对于底层硬件,不存在三维线程的概念。
至于对于一个任务应该分配多少线程,grid 和 block 应该设置为多大,这根据需求和硬件素质。通常选取 2 的倍数作为线程总数,合理地平均分配任务到各个线程。