CUDA系列笔记

CUDA学习笔记(LESSON1/2)——架构、通信模式与GPU硬件

CUDA学习笔记(LESSON3)——GPU基本算法(Part I)

CUDA学习笔记(LESSON4)——GPU基本算法(Part II)

CUDA学习笔记(LESSON5)——GPU优化

CUDA学习笔记(LESSON7)——常用优化策略&动态并行化


常用优化策略

下面让我们来看看一些常用的优化策略,这些策略我们之前已经谈过,现在只是对它进行一个总结。

GPU池化 容器方案 gpu资源池规划_GPU池化 容器方案

数据布局变换(Data layout transformation)

第一部分就是我们之前所说的coalescing存取模式,当相邻线程访问内存的相邻位置的时候我们能获得内存的最佳带宽。所以说数据布局的转换实质上是重新转换数据以获得更好的内存性能。我们来看一个关于这个技术的更加复杂的例子。也就是结构数组(AoS)与数组结构(SoA)的性能对比。

GPU池化 容器方案 gpu资源池规划_GPU_02

如果我们对这两段程序的内存读取性能进行对比的话我们会发现第二段程序的性能更加好,因为DRAM存取数据的时候是以一个chunk(或burst)为单位进行操作的,数组结构的形式可以很好地用到coalescing的技术,从而获得更好的性能。

GPU池化 容器方案 gpu资源池规划_GPU_03

发散-收集变换(Scatter-to-gather transformation)

我在之前的博客中写过scatter与gather两种模式的区别。如下图所示,我们知道gather操作会存在重叠读取的问题,但这并不会影响程序的效率;而scatter操作会导致相互冲突的写入操作,而我们不得不用线程同步的技术来保证多个线程不会同时访问同一个位置的内存。因此这一部分等待时间会降低GPU的效率。因此我们在写程序的时候应该尽量采用gather的模式,避免scatter的模式。

GPU池化 容器方案 gpu资源池规划_并行优化_04

平铺(Tiling)

很多时候我们需要频繁访问某一段内存,在CPU中,我们可以将这部分数据库做隐式拷贝,放入缓存中,来获得更高的访问速度。然而隐式拷贝不适合我们的GPU,因为我们拥有大量的线程,因此每个线程对应的缓存将非常少。因此我们在GPU中采用的是显示拷贝的技术,它让我们人工地去为每一个block中的线程分配一个缓存,这部分缓存中的数据能被一个block中的所有线程共用,这部分缓存也就是我们之前讲的shared memory。

GPU池化 容器方案 gpu资源池规划_GPU_05

下面让我们来看一个例子。

GPU池化 容器方案 gpu资源池规划_CUDA编程_06

对于以上两段代码都能;利用coalescing技术来获得很好的存取性能,但是第一段代码中A、B、C、D、E五个数组中每个数据只会访问一次,而第二段代码中in中的数据的数据却会被多次访问,因此我们可以将它放在shared memory中,从而获得更高的访问速度。

私有化(Privatization)

私有化是用来解决多个线程同时写入某一个位置的内存的冲突。每个线程将得到的结果私有化,放在自己的local memory中,然后再将多个线程得到的私有化结果合并来得到最终的结果。我们之前所讲的histogram无疑是一个非常好的例子,每个线程计算自己的local histogram,然后再合成最终的histogram。

GPU池化 容器方案 gpu资源池规划_GPU_07

进仓(Binning)/空间数据结构(Spatial data structure)

我们考虑之前gather的例子,我们发现如果我们要计算输出元素,我们不得不在每一个线程中去检查输入数组中的每个元素,来判断他们是否参与当前元素的计算。这样我们会进行大量不必要的检查,降低了效率。为了解决这个问题,我们将输入元素放在一个个bin中,下面我们来看一个例子。

GPU池化 容器方案 gpu资源池规划_GPU_08

如果我们要计算以美国每个城市为中心,300km以内城市的总人口有多少。若是不采用binnin的技巧,我们第一步是检查每一个城市,看是否离中心城市300km以内。在计算出来这些城市以后第二部是将在300km以内城市的人口数加起来。我们看到第一步的时候我们不仅对于离得近的城市需要进行检查,而对于那些离得非常远的,显然不在300km以内的城市也需要检查。因此我们将地图划分为300km的地图块,也就是我们之前提到的bin。这样对于每个中心城市,我们只需要检查与它相邻地图块中的城市即可,而大大提高了效率。这个思路实际上与我们之前histogram将输入元素划分进一个个bin中是一样的。

GPU池化 容器方案 gpu资源池规划_CUDA编程_09

压缩(Compact)

压缩我们之前已经讲得非常详细了,如果我们需要进行处理的元素太过分散,那么如果为数组中每个元素都开启一个线程,大部分线程将处在空闲状态,浪费了GPU资源。因此我们通过压缩的方法将需要处理的元素集中存放。

GPU池化 容器方案 gpu资源池规划_并行优化_10

正则化(Regularization)

正则化是为了解决不同线程负载不平衡的问题。我们还是考虑之前计算人口的例子,我们会发现有的地方城市数密集,有的城市数稀疏,那么密集处线程的工作量显然大,稀疏的地方小。因此会存在大量闲置的线程等待那些还处在工作中的线程。为了这个问题我们采用了正则化的方案。我们可以规定一个bin中的城市平均个数,例如5个/bin,那么我们的线程将会循环五次来处理每个bin中的城市,对于那些城市不满5个的bin,线程也不需要等待太久,对于那些城市数超过5个的bin,我们可以对超出部分采用新的算法计算或者将它放在CPU中计算。这样我们在某种程度上就实现了负载均衡。正则化对于那些具有平均情况的例子有很好的优化效果,而对于那些不存在平均情况的例子则影响不大。

GPU池化 容器方案 gpu资源池规划_并行计算_11

到这常用的优化方案就基本上总结完了。课程的后半部分还介绍了一些库的使用,以及一些支持CUDA的其他平台或其他语言,在此就不再赘述了。下面就是本门课的最后一部分内容:动态并行化,这部分内容是比较新的内容,CUDA5.0以后才具有动态并行化的特性,这个技术对我们来说非常有用。

 

动态并行化(Dynamic Parallelism)

听起来这个名词有点牜牜,但实际上这就是一个很简单的技术,我们之前讲CPU作为host,GPU作为device,也只有CPU能够开启GPU的线程,GPU无法在线程内部来开启新的线程。这就带来了极大的不方便,也有悖于我们的直观理解。比如说我们在一个大的任务中有一个小的任务,比如我们需要做一个傅里叶变换,那么我们不得不保存当前的工作,返回CPU中开启新的线程来处理傅里叶变换,得到结果以后再返回CPU,重新加载我们刚才保存的东西。这加大的编程的复杂度,也使得数据要在CPU与GPU之间来回传输,造成不必要的开销。为了解决这个方法,我们引入了动态并行化,它让GPU能在其内部开启新的线程,这样就使得GPU无需再返回CPU来开启子任务,使得程序的整体性更强了。也可以轻易地实现没有动态并行化之前我们无法实现的递归调用。

并行可分为四大类:批量并行(bulk parallelisim)、

批量并行意思是每个线程都同时操作,而互相之间没有关联关系。这个是最基本的并行模式

GPU池化 容器方案 gpu资源池规划_CUDA编程_12

在有了动态并行化以后,我们又具有了以下三种并行模式。

嵌套并行(nested parallelism)指的是在一个并行程序中调用另一个并行程序

GPU池化 容器方案 gpu资源池规划_CUDA编程_13

任务并行(task parallelism)指的是我们运行多个独立的任务,多个任务只处理自己的数据。每一个任务内都可包含复杂的运算。

GPU池化 容器方案 gpu资源池规划_GPU_14

递归并行(recursive parallelism)就是在一个并行程序中不断的能开启新的线程调用自己自身直到任务解决。正是这项技术让我们可以在GPU上运行递归算法了。

GPU池化 容器方案 gpu资源池规划_并行优化_15

我们在运行动态并行化的时候还需要注意一些事情,比如:由于所有线程运行的程序都是一样的,我们要注意防止重复开启子任务,我们只需要开启一个子任务即可。

GPU池化 容器方案 gpu资源池规划_GPU池化 容器方案_16

我们还需要注意的是一个block内的数据,例如在shared memory中的数据是私有的,不能被其他blcok所用,因此当我们开启新的子线程以后,我们需要将shared memory中的数据写入global memory中后才能传入子线程。(注意:如果我们在kernel中用malloc()分配数据的话数据会被分到堆上,堆是global的)

 

到这这门课就基本结束了。从学习这门课到现在大约也就一个多月的时间,课程里的任务自己也都没有做,因此掌握的知识肯定不扎实,自己也只是把知识点稍微整理了一下,希望自己能够在实践的过程中不断加深完善对CUDA的理解吧~撒花✿✿ヽ(°▽°)ノ✿