背景介绍

上图是一个GPU上的图架构的简单表示。硬件层来说GPU上的全局内存与主机内存相通,可以进行数据传输;GPU上有许多流多处理器(SM),每个流多处理器上有自己若干个核和自己的片上缓存,是相对独立的单元。软件层次上,程序员启动核函数(grid),其中包括多个block,每个block上又有若干warp。Block是依赖于流多处理器的,每个block只会运行于一个流多处理器。值得一提的是,每个warp中包含32个线程,这32个线程是按照严格单指令多线程的形式运行的,是严格的同步关系,因而warp内部线程在运行代码遇到分支或者循环结构时的线程发散会导致warp内部分线程的空闲,并且warp内线程执行的任务量的不均衡会导致很严重的线程发散,因为所有线程都在等待运行时间最长的线程结束。
线程发散是我们试图寻找更好的任务分配方式的初衷,更好的任务分配方式能够保证更少的线程发散,从而提升整体性能。
上图是GPU上的存储访问示意图,图(a)中,相邻的线程访问的是相邻并且对齐的地址,这样多个线程所需要的数据可以在较少的访存请求中一起取回来,而(b)图中线程访问的地址是随机的,无序的,就需要更多的访存次数。我们称(a)图中的模式是合并访存模式,是一种更加高效的访存方式。例子:
首先我们会以三角形计数为例子,讲解什么是更好的任务分配方式。

论文1: GPU-based Graph Traversal on Compressed Graphs
SIGMOD 2019
本文设计了一个GCGT的图处理系统,针对的是大规模图中的并行算法,并且算法中包含“扩张—过滤—压缩”的过程。




上图算法一中给出了一个直观的算法,每个线程负责处理一个点的邻接表,直接扩展这个邻接表中的所有节点。跟例子中介绍的直观三角形计数算法影响,由于图的power-law的性质,线程之间会存在比较明显的工作量负载不均衡的情况。
算法二中给出了GCGT的实现:所有的线程会先处理intervals然后处理residual,并且一个warp中的所有线程会同时进入处理residuals的阶段。并且处理interval也是分成两步,先处理长度大于32(一个warp的大小)的interval,再处理小于32的。

上图中表示的是GCGT框架下的处理流程图。这里主要是针对interval做了改进。所有的线程先协同处理最长的interval(图中t2获得的邻接表),剩下的Interval拼接起来,也由全部的线程协作完成。这里整体的处理周期由25缩减成11,有了非常明显的提高。 对于residual部分,也有一些改进,如下图所示。

论文2: High Performance and Scalable GPU Graph Traversal
TOPC 2015
本文中使用到任务动态分配的部分,也仅仅是从邻接表中收集邻居的部分。详细来说,对于一个BFS过程,这个算法仅仅从一系列邻接表中读进来所有的邻居,并且加载进本地存储或寄存器中。接下来我们比较几种实现。
1. 顺序读取
每个线程获取一个邻接表的起始和终止范围,然后负责记载这个邻接表中的所有元素。很显然,这是一个非常naïve的想法,线程的负载均衡会很差。
2. 粗粒度的,warp协同的方法
下图算法5中展示了warp协同的方法,warp中的线程竞争warp的控制权,竞争的胜利者将自己的任务广播给warp中所有的线程,大家一起完成该任务,然后重复“竞争-广播-协作”的过程。
此方法中,warp内每个线程的工作都是由整个warp内所有的线程协同完成的,比较好地均衡分配了任务。

3. 细粒度的,基于scan的方法
下图展示了基于scan的方法的算法流程。这个方法中,将一个block中所有的线程获得的邻接表都拼接到一起,然后所有block中的线程协作处理这个拼接完成后的表。这个算法比上一个算法更好的地方在于协调了整个block内线程的工作负载,但是也有额外的拼接代价。

4. Scan+warp 协调+block协调的方式。
1) 邻接表长度非常大的线程首先竞争整个block的控制权,整个block的运算资源都一起处理这个邻接表。
2) 剩下的线程中,邻接表长度适中的,线程竞争warp控制权,整个warp协作处理。
3) 最后剩下工作量比较小的线程,所有的任务拼接到一起,由整个block协作完成。
这个算法很好地协调了整个block内的工作负载的均衡性,又减少了大量拼接的代价,是最优的解决方案。从下面的三个性能指标中也能看出来,上面几种算法的性能,以及跟最优性能之间的差异。
论文3:Update on Triangle Counting on GPU
HPEC 2019
这篇文章研究的是GPU上的三角形计数算法,其中naïve的三角形计数的任务分配在前面已经介绍过了,使用一个线程处理一条边。下面的算法1就是这种实现的伪代码。
上文中我们还介绍了优化的三角形计数的任务分配,即根据任务量的预先估计,给每个边分配合适的线程,使得线程的任务大致相当。但是这种算法也有它的局限性:预先估计需要消耗额外的时间;并且同一组中的边获得相同数目的线程,但是实际上它们的任务量也有区别。本文中提出了动态分配的三角形计数算法,算法伪码如下算法2所示

总结
动态任务分配是GPU上进行图计算任务中一种比较好的任务分配方式,可以使线程之间的工作量更加均衡,很好解决图的不规则形带来的问题,并且能够减少线程的发散,实际上也有更好的合并访存效果。这种方法也不是完美的,动态计算需要额外的计算开销,以便得到线程各自的任务。当然这种负面影响完全可以被所带来的性能提升抵消。综合而言,这种方法还是非常值得我们学习和借鉴。参考文献
[1] J. Fox, O. Green, K. Gabert, X. An, and D. A. Bader. Fast and adaptive list intersections on the gpu. In 2018 IEEE High Performance extreme Computing Conference (HPEC), pages 1–7. IEEE, 2018. [2] O. Green, J. Fox, A. Watkins, A. Tripathy, K. Gabert, E. Kim, X. An, K. Aatish, and D. A. Bader. Logarithmic radix binning and vectorized triangle counting. In 2018 IEEE High Performance extreme Computing Conference (HPEC), pages 1–7. IEEE, 2018.[3] Sha M, Li Y, Tan K, et al. GPU-based Graph Traversal on Compressed Graphs[C]. international conference on management of data, 2019: 775-792.[4] Duane Merrill, Michael Garland, and Andrew Grimshaw. 2015. High-Performance and Scalable GPU Graph Traversal. ACM Trans. Parallel Comput. 1, 2, Article 14 (January 2015), 30 pages.[5] C. Pearson et al., "Update on Triangle Counting on GPU," 2019 IEEE High Performance Extreme Computing Conference (HPEC), Waltham, MA, USA, 2019, pp. 1-7, doi: 10.1109/HPEC.2019.8916547.