前言

这一篇文章主要是作为我在看完《大规模并行处理器编程实战》这本书之后的一个学习记录。有些内容因为我在上一篇博客已经记录过了,这一篇就不做记录了。

第六章 性能优化

6.1 更多关于线程执行的问题

GPU调度的基本单位为warp,一般由32个thread组成。因为GPU的执行方式为SIMT(单指令多线程),也就是一条指令会被所有线程一起执行,等到这条指令被所有线程执行完,才执行下一条指令。所以如果warp中的thread存在很多条件分支,导致很多thread在等待某些thread执行分支内的指令,严重拖后程序的执行速度(举个例子,就好比有一半thread要执行then里的指令,有一半thread要执行else里的指令。cuda会让warp中的所有thread都执行then里的指令,那些不需要执行then里指令的thread的执行结果会被丢弃,但是它们还是要执行。也就是说,如果同一个warp中有很多分支的话,相当于warp中的thread要执行所有分支中的所有指令(不影响最后的正确结果),所以会执行比原本多很多的指令,效率很低)。因此同一个warp中,尽量不要有很多的分支,这样程序的执行效率最高。

gpu并行计算 多线程 gpu warp并行_并行计算

gpu并行计算 多线程 gpu warp并行_并行计算_02


以上是两种规约求和的方法,很明显下图的运算速度会更快。因为下图中每个warp存在很少的分支,而上图中的warp中存在大量的分支。

6.2 全局存储器的带宽

全局存储器通常采用DRAM来实现。根据DRAM的特性,地址连续的访存请求可以进行合并,合并为一个请求,相当于对全局存储器的一次访问可以取出多个连续的数据;而如果地址不连续的访存请求,无法进行合并,相当于多次对全局存储器提出访存请求,很明显这样子访存的效率会很低,因此尽量访问连续的内存地址。这是书本上说的。

当然,我认为跟cache块的机制也有一定的关系。当访问连续的内存单元时,一般都会在同一个cache块上,这样的访问效率会很高,只需要读一个cache块;当访问不连续的内存单元时,它们有可能分布在多个cache块中,这样就需要访问很多个cache块,这样的带宽明显就很低了。

而在共享存储器中,因为它是能实现高速的片上存储器,则不存在这种合并访存提高数据访问速度的特性,因此我们可以先将全局存储器中的数据按照连续地址单元的顺序读入共享存储器中,然后在共享存储器中可以随机访问,这不会影响访问效率。

gpu并行计算 多线程 gpu warp并行_cuda_03

gpu并行计算 多线程 gpu warp并行_数据_04

gpu并行计算 多线程 gpu warp并行_寄存器_05

6.3 SM资源的动态划分

SM中的执行资源包括寄存器、线程块槽和线程槽。每个SM上的执行资源数目是限定的,但实际的线程块和线程都是根据用户设定的参数来分配的。如果每个块中分配的线程多,则总的块数就会比较小;块中分配的线程少,则总的块数就会比较多,这是一个动态的过程。所有块的总线程数不能多于SM上限制的最大线程数。

此外,线程的数量还要受到寄存器数量的影响。SM上总的寄存器个数是一定的,它们会平均分给每一个线程。寄存器是线程私有的,每一个线程最少得对应一个寄存器,所以SM上总的线程数目不能够大于SM上寄存器的总数。线程中的私有变量都是存在寄存器中。

gpu并行计算 多线程 gpu warp并行_数据_06


gpu并行计算 多线程 gpu warp并行_gpu并行计算 多线程_07


gpu并行计算 多线程 gpu warp并行_cuda_08


gpu并行计算 多线程 gpu warp并行_数据_09


上述例子中所谓的零开销调度指的就是让SM一直保持运行的状态。因为实际上SM上的运行单位为warp,即在SM上每次只有一个warp处于运行的状态。SM上的调度都是以warp为单位进行调度。

6.4 数据预取

在GPU上,提高性能的关键在于避免计算核心(SM)空闲等待,保持计算核心的高效运算,也就是要让SM一直处于执行的状态。通过将一条需要好几百个时钟周期的访存指令拆分成两条,并在中间增加一些与访存无关的指令,可以使SM一直保持运行的状态。在存储器访问指令和已访问的数据使用指令之间增加多条独立指令,根据上一节的内容,这样子会使得SM一直处在一个执行的状态,使得吞吐量保持在一个较高的值。

gpu并行计算 多线程 gpu warp并行_并行计算_10


上面通过在读取global memory到shared memory中的这个过程中间,插入了独立的计算指令,有效地隐藏延迟

6.5 指令混合

所谓的指令混合,其实就是将循环展开,这样可以减掉一些分支指令、循环计数指令以及地址运算指令等,提高运行速度。

gpu并行计算 多线程 gpu warp并行_并行计算_11

6.6 线程粒度

通常情况下,尽可能多地把更多工作放在每个线程中并采用更少的线程是有优势的。其实就是让每一个线程做更多的事,从而减少线程的数量(相对地减少)。这样子有一个好处,可以减少访存的次数,提高程序运行的速度。

gpu并行计算 多线程 gpu warp并行_寄存器_12


gpu并行计算 多线程 gpu warp并行_cuda_13

6.7 小结

gpu并行计算 多线程 gpu warp并行_并行计算_14


上图中所说的块指的是tile而不是block。也就是每次预取的数据块的大小。(其实这个图我也看不太懂)

技术:分块,循环展开,数据预取,线程粒度

1:块(tile)的大小在性能中起主要作用。

2:块(title)的大小已经足够大的情况下,循环展开和数据预取将更重要。

3:对于线程粒度而言,如果数据预取一个16*16的tile使用的register超过了SM中寄存器总数,那么将会影响到其他优化的发挥。

4:各种优化的调整技术互相影响,要不断结合找到最好的优化方法。

第十章

10.1 并行编程的目标

  1. 较短时间内解决给定的问题
  2. 给定时间内解决一些较大的问题
  3. 给定时间内对给定问题取得更好的解决方案

并行编程通常可以分为4步:

  • 问题分解
  • 算法选择
  • 语言实现
  • 性能调整

10.2 问题分解

首先要对问题进行理解、分析,判断哪一些任务适合在主机上运行,哪一些任务适合在设备上运行。在设备上运行的任务,我们也需要考虑如何将它与CUDA的线程机制更好地结合起来,充分发挥CUDA的优势。

Amdahl加速比公式
加速比S = 1 / (1 - a + a / n)
其中, a为并行计算部分所占比例,n为并行处理结点个数

根据上面这个公式可以看出:并行计算受限于应用程序的串行部分,所以应用程序的加速有限。
在分解大型应用程序时,那些并不适合在CUDA设备上并行执行的小活动累加的执行时间,可能成为最终用户看到的限制加速的一个因素。

10.3 算法选择

算法必须具备3个基本特点:

  • 确定性(definiteness):每一步都是准确陈述的,要执行的步骤中不存在任何多义性。
  • 有效的可计算性(effective computability):计算机可以实现其中的一步。
  • 有限性(finiteness):算法必须保证有终点。
    对于涉及矩阵的应用程序,分块是取得高性能的重要算法策略之一。
    在计算过程中,可以通过稍微降低准确度,大幅度地提高网格算法的执行效率。

10.4 计算思想

gpu并行计算 多线程 gpu warp并行_并行计算_15


gpu并行计算 多线程 gpu warp并行_cuda_16


这四部分的知识都是计算思想的基础,学好了才能在设计并行计算算法方面有所建树。

第十二章 结论与展望

存储器带宽是限制大规模并行计算系统性能的主要因素。