本 节 书 摘 来 自 华 章 出 版 社 《CUDA高性能并行计算》 一 书 中 的 第3章,第3.1节, 作 者 CUDA for Engineers: An Introduction to High-Performance Parallel Computing[美] 杜安·斯托尔蒂(Duane Storti)梅特·尤尔托卢(Mete Yurtoglu) 著,苏统华 项文成 李松泽 姚宇鹏 孙博文 译 , 更 多 章 节 内 容 可 以 访 问 云 栖 社 区 “华 章 计 算 机” 公 众 号 查 看。
第3章
从循环到网格
现在我们要使用第2章所学知识将第1章和附录C中的C语言代码进行并行化。回顾我们创建的两个版本的距离计算应用。在dist_v1
中我们使用一个for循环来计算一个数组的距离值。在dist_v2
中我们为输出值创建了一个数组然后调用distanceArray()
方法来计算全部距离值的数组(同样是使用一个for循环串行的计算)。本章会使用CUDA来并行化这个距离应用,并行化的方法是使用一个可以同时执行的线程网格来替换循环的串行计算。
我们将会创建、编译并执行新的应用,但是重点关注使用CUDA进行并行化。如果读者在编译并执行没有CUDA声明的C语言程序中存在问题,请参考附录C。
3.1 并行化 dist_v1
这是首个探索并行化的示例,在展示完整的并行代码之前,我们要一步步地讲述如何从dist_v1
的代码变化到并行化版本dist_v1_cuda
,完整的代码参见代码清单3.1。读者需要执行以下的基本步骤。
1.创建一个包含kernel.cu文件的dist_v1_cuda
工程。
a.在Linux下,创建一个dist_v1_cuda
文件夹并在其中新建一个kernel.cu文件。
b.在Visual Studio中,创建一个新的CUDA 7.5运行时项目,命名为dist_v1_cuda
,项目中包含一个kernel.cu文件。删除kernel.cu文件中的样例代码。
2.将dist_v1/main.cpp
中的代码复制并粘贴到kernel.cu文件中。按照如下过程修改kernel.cu
代码:
a.删除#include <math.h>
,因为CUDA内联文件已经包含了math.h。加入#include <stdio.h>
使程序能够在控制台输出。
b.在目前的#define字段下,加入另一个#define TPB 32
,来声明在执行我们的核函数中将作为核函数执行配置中线程块维度的数大小。
c.拷贝整个for循环并粘贴在main()
函数上方。我们将把这个循环编写到一个核函数的定义中。
d.使用distanceKernel<<<N/TPB, TPB>>>(d_out, ref, N)
核函数调用替换main()
函数中的for循环,调用中包含了三个要素:
i.函数名称distanceKernel
。
ii.三对<>符号,其中包含了执行的配置参数。此时,我们设定N/TPB个线程块(2个)其中每个线程块包含TPB个线程(32个)。
iii.代码清单中还包含了指向输出数组的指针d_out
,参考地址ref和数组长度N。
e.将main()
函数之前的for循环按照如下步骤转换成一个核函数:
i.使用由如下内容组成的核函数声明替换第一行的for循环({}中的所有内容):
1.标识符__global__
2.返回类型void
3.函数名distanceKernel
4.括号中包含了使用逗号分隔的带有类型的形参列表(float *d_out, float ref, int len)
ii.在{}的第一行中输入下面的计算公式,式中使用核函数的内建线程索引和维度变量计算索引i(用来代替循环中现在被删除了的同名索引)。
iii.将左边的数组out[i]修改为d_out[i]
。
iv.在{}的最后一行中输入下面的printf()
语句:
f.在scale()
和distance()
的定义前添加标识符__device__
.
g.管理如下的存储:
i.在main()
开始前,删除定义out
数组的语句(此处不需要的语句),使用下面的代码创建一个位于设备上的可以存储N个浮点数的数组d_out
。
ii.在main()
函数的return
语句上方,使用如下语句释放内存:
为了确保所有的语句段能够正确组合在一起,dist_v1_cua/kernel.cu
文件的完整代码如代码清单3.1所示。
在执行这个并行的应用之前,有几个需要依次给出的评论。希望通过我们的努力能够让读者在阅读完整的代码时对代码的所有组件的目的和意图有一个正确的理解。
我们也想要提醒读者一些核函数的特殊性,因为它是我们使用CUDA并行化的主要工具。一个需要注意的事实是核函数(比如distanceKernel
)在设备上执行,所以它不能向主机端返回变量,因此使用void类型作为返回值。核函数可以访问设备内存,但是不能访问主机内存,因此我们需要使用cudaMalloc()
函数(而不是malloc
函数)为设备上输出数组分配内存,同时使用d_out命名,d_作为一个便于记忆的标志(显然不是强制性的标志)来提醒读者d_out是一个设备上的数组。最后,注意核函数的定义就如同我们只在单线程编程一样进行书写。在之前串行代码的循环中发生的所有事情现在被每个线程所对应的i所接管,它根据CUDA为每个线程提供的索引和维度变量来获取所有计算网格中的数据索引。
执行dist_v1_cuda
当编码完成后,是时候来编译和执行这个应用了。由于代码中已经包含了核函数加载(不是C/C++的一部分),因此我们需要调用NVIDIA C编译器—nvcc。在Windows下,Visual Studio识别这种扩展并且自动调用nvcc编译器,直接按下F7来构建这个应用。在Linux下,使用代码清单3.2中的Makefile文件指示编译器使用nvcc编译器来构建这个程序。
注意在代码清单3.1中,我们在第1行里包括了标准的输入和输出,并且在第20行加入了一个printf()
语句来将结果打印在屏幕上,因此现在你可以执行这个应用然后在控制台中观察输出结果。检验输出结果和那些由串行应用dist_v1
和dist_v2
运算的结果是否一致。
可能你注意到的第一件事是输出不一定是以索引顺序从0到63依次输出的。这是串行和并行应用的一个根本区别。在串行应用中,计算在一个循环中按照一定先后顺序执行。在CUDA实现的并行中,我们放弃了对计算顺序的部分控制而获得由成百上千个处理器并行计算提供的计算加速。
第二件事(也是最重要的事)是检查利用并行计算得到的距离值是否与应用dist_v1
和dist_v2
计算得到的值一致。总而言之,如果产生了不正确的结果,那么加速再多也是毫无意义的。