配好SM,该block就会一直驻留在该SM中,直到执行结束。一个SM可以同时拥有多个blocks,但需要序列执行。下图显示了GPU内部的硬件架构:
CUDA中的内存模型分为以下几个层次:
线程访问这几类存储器的速度是register > local memory >shared memory > global memory
下面这幅图表示就是这些内存在计算机架构中的所在层次。
上面讲了这么多硬件相关的知识点,现在终于可以开始说说CUDA是怎么写程序的了。
我们先捋一捋常见的CUDA术语:
第一个要掌握的编程要点:我们怎么写一个能在GPU跑的程序或函数呢?
通过关键字就可以表示某个程序在CPU上跑还是在GPU上跑!如下表所示,比如我们用__global__定义一个kernel函数,就是CPU上调用,GPU上执行,注意__global__函数的返回值必须设置为void。
第二个编程要点:CPU和GPU间的数据传输怎么写?
首先介绍在GPU内存分配回收内存的函数接口:
CPU的数据和GPU端数据做数据传输的函数接口是一样的,他们通过传递的函数实参(枚举类型)来表示传输方向:
cudaMemcpy(void dst, void src, size_t nbytes,
enum cudaMemcpyKind direction)
enum cudaMemcpyKind:
第三个编程要点是:怎么用代码表示线程组织模型?
我们可以用dim3类来表示网格和线程块的组织方式,网格grid可以表示为一维和二维格式,线程块block可以表示为一维、二维和三维的数据格式。
接下来介绍一个非常重要又很难懂的一个知识点,我们怎么计算线程号呢?
此时的线程号的计算方式就是
其中threadId的取值范围为0到N-1。对于这种情况,我们可以将其看作是一个列向量,列向量中的每一行对应一个线程块。列向量中每一行只有1个元素,对应一个线程。
由于线程块是2维的,故可以看做是一个M*N的2维矩阵,其线程号有两个维度,即:
其中
这种情况一般用于处理2维数据结构,比如2维图像。每一个像素用一个线程来处理,此时需要线程号来映射图像像素的对应位置,如
此时线程号的计算方式为
其中threadId的范围是0到N-1,对于这种情况,可以看做是一个行向量,行向量中的每一个元素的每一个元素对应着一个线程。
这种情况,可以把它想象成二维矩阵,矩阵的行与线程块对应,矩阵的列与线程编号对应,那线程号的计算方式为
上面其实就是把二维的索引空间转换为一维索引空间的过程。
这种情况其实是我们遇到的最多情况,特别适用于处理具有二维数据结构的算法,比如图像处理领域。
其索引有两个维度
上述公式就是把线程和线程块的索引映射为图像像素坐标的计算方法。
我们已经掌握了CUDA编程的基本语法,现在我们开始以一些小例子来真正上手CUDA。
首先我们编写一个程序,查看我们GPU的一些硬件配置情况。
我们利用nvcc来编译程序。
输出结果:因为我的服务器是8个TITAN GPU,为了省略重复信息,下面只显示两个GPU结果
第一个计算任务:将两个元素数目为1024×1024的float数组相加。
首先我们思考一下如果只用CPU我们怎么串行完成这个任务。
CPU方式输出结果
如果我们使用GPU来做并行计算,速度将会如何呢?
编程要点:
GPU方式输出结果
由上面的例子看出,使用CUDA编程时我们看不到for循环了,因为CPU编程的循环已经被分散到各个thread上做了,所以我们也就看到不到for一类的语句。从结果上看,CPU的循环计算的速度比GPU计算快多了,原因就在于CUDA中有大量的内存拷贝操作(数据传输花费了大量时间,而计算时间却非常少),如果计算量比较小的话,CPU计算会更合适一些。
下面计算一个稍微复杂的例子,矩阵加法,即对两个矩阵对应坐标的元素相加后的结果存储在第三个的对应位置的元素上。
值得注意的是,这个计算任务我采用了二维数组的计算方式,注意一下二维数组在CUDA编程中的写法。
CPU版本
CPU方式输出
GPU版本
GPU输出
从结果看出,CPU计算时间还是比GPU的计算时间短。这里需要指出的是,这种二维数组的程序写法的效率并不高(虽然比较符合我们的思维方式),因为我们做了两次访存操作。所以一般而言,做高性能计算一般不会采取这种编程方式。
最后一个例子我们将计算一个更加复杂的任务,矩阵乘法
回顾一下矩阵乘法:两矩阵相乘,左矩阵第一行乘以右矩阵第一列(分别相乘,第一个数乘第一个数),乘完之后相加,即为结果的第一行第一列的数,依次往下算,直到计算完所有矩阵元素。
CPU版本
CPU输出
梳理一下CUDA求解矩阵乘法的思路:因为C=A×B,我们利用每个线程求解C矩阵每个(x, y)的元素,每个线程载入A的一行和B的一列,遍历各自行列元素,对A、B对应的元素做一次乘法和一次加法。
GPU版本
GPU输出
从这个矩阵乘法任务可以看出,我们通过GPU进行并行计算的方式仅花费了0.5秒,但是CPU串行计算方式却花费了7.6秒,计算速度提升了十多倍,可见并行计算的威力!