论文阅读-DepGraph

题目:DepGraph: A Dependency-Driven Accelerator for Efficient Iterative Graph Processing
摘要:
Dep是什么?结点的状态依赖于相邻结点的状态。
问题?首先,由于依赖导致串行,硬件资源的(有效)利用率比较低;其次,依赖链长会导致收敛速度降低。
我们propose了什么?一个加速器,好处是减少依赖、更快传递状态。
具体诀窍是什么?主线是dependency-dirven asynchronous execution。预取 on the fly;长且常用的依赖链转换成直接依赖at runtime(这些“短路”称为hub index);多条链的传递可以并行;
结论是什么?在迭代图算法上,64核处理器有5-22倍加速,0.6%面积开销;比现有的HATS、Minnow、PHI分别好3-14.2、2.2-5.8、2.4-10.1倍。

引言:
迭代图算法有软、硬件的加速。传统加速器虽然很强,但是可编程性差、进入市场的难度大(都是相比通用处理器)。
之后,一些集成进通用多核处理器的加速器出现了。如HATS、Minnow、PHI,(这些都达成了更快速收敛和更好的数据局部性。没有证据)
由于结点状态传播链的串行性,所以多核的并行度没有被利用起来。此外,稳定的状态可能又被邻居结点读到,造成邻居结点不必要的更新。
我们发现:(1)异步运行会使更新次数更少:本来,这一轮我更新了、邻居也更新了;下一轮,邻居根据我的新状态值要再一次更新,那它这一轮的更新可能就没必要了,就不如让它在我这一轮直接更新。(前人也有这个发现)(2)大多数结点的状态只在很少的一部分路径上传播,这些路径是大度结点之间的路径(原因是图的power-law,即一小部分大度结点会和大多数结点有连接)。
我们提出了DepGraph,在Core和L2之间。
DepGraph的Key idea是什么?异步、dependency-driven。具体地:从一条始于active vertex的依赖链上预取结点;不同依赖链可以由不同核并行处理(好处:数据局部性更好、状态传播更快?、同步开销更小(原来每轮都有同步开销、现在核之间,即依赖链之间的状态传播更少了(不是0,因为两个依赖链可能是首尾相连或者有其它交集)));远程依赖可以转换为直接依赖(at runtime, 短路,根据迭代图算法的性质转换,这种直接依赖存放在hub index结构里,好处:迭代次数减少、并行度增加)
实验结果:update减少了61.4%-82.2%,核利用率提高了(没说具体);在迭代图算法上,64核处理器有5-22倍加速,0.6%面积开销;比现有的HATS、Minnow、PHI分别好3-14.2、2.2-5.8、2.4-10.1倍。

研究背景:
在这里插入图片描述

图编程模型:GAS(Gather-Apply-Scatter):在每一轮:每个结点,(1)先调用Accum函数,收集邻居结点的Influence,并更新自己的状态;(2)调用EdgeCompute把自己的状态的Influence发给邻居,例如:
图数据结构:CSR结构。注意有vertex state array,存两轮之间的delta state和本轮的recent state这2种state
现有的问题:依赖链限制了有效的并行性(可以有无效并行,因为可能由于依赖链较长,源结点信息没有传过来,远程结点的状态的更新是无效的)。例如:
论文阅读-DepGraph_第1张图片

图中,v5-v20是一条依赖链。那么一个中间结点(e.g. v22)到v20的子链的更新有效 iff 该中间结点收到v5的新状态信息。我们还做了个实验来验证这个现象:对比的系统有Ligra、Mosiac, Wonderland, FBSGraph, Ligra-o, 串行执行(Ligra-o的单线程),测量了incremental pagerank。我们首先测量了串行执行下的vertex update次数(记为u_s,代表需要的更新次数)和每个系统下的vertex update次数(记为u_d,代表实际执行的更新次数)。然后我们估计有效更新利用率r_e=u_s/u_d U where U是总的利用率。结果如图:
论文阅读-DepGraph_第2张图片

首先,ligra-o比其它好,因为有效利用率高。但是并行度仍然没有被充分利用(?,没有证据),我们测量了GL AZ PK OK LJ FS数据集上平均依赖链长度,为4.2, 17.9, 7.7, 6.9, 8.5, and 16.1, 可见依赖链长,有效update利用率有减少的趋势。

现有问题的详细原因:为什么依赖链会导致有效更新利用率r_e很低呢?(1)现有方法中,一个结点的邻居结点都达到稳定值后,这个结点依然会更新一下(虽然更新后值不变),那么这种更新就是没有必要的(例如,在图3,在某一轮,v5的值被用于更新v7,v7的值被用于更新v9,所以v9的本次更新无效,下一轮更新才有效)(这个现象也是3.里说的)。In Figure 4(a), in Ligra, Ligra-o, Mosaic, Wonderland, and FBSGraph, only 7.4%−14.5%, 14.6%−21.9%, 7.7%−16.9%, 12.1%−20.2%, and 11.3%−17.2% of the total updates are useful, respectively. 另外,虽然ligra-o的无效更新次数少,但是当线程数增加,无效更新次数也会增加(因为所需的不变,总次数增加,导致无效的增加),如图: 。
论文阅读-DepGraph_第3张图片

(2)很多结点是inactive的,即更新它不会导致状态变化(之前说的是新状态传不过来,这里当新状态传不过来,状态不变,即便传过来了,也可能因为收敛而不变(例如Accum是Min/Max时))。Figure 4(a), the total utilization delivered by Ligra-o is only 25.9%−38.6% 但是论文里只提到了状态传不过来:我的理解应该是:Inactive的结点根本不会被计算,因此虽然有很多核,但是这些核由于inactive结点很少,所以很少工作。但是这个问题的起因还是依赖链长导致激活(make it active)缓慢。可见fig 4©
现有工作利用硬件加速。例如,GARSP优化了cache,减少cache thrashing;Hardware prefetch,HATS使用了硬件的traversal scheduler?来提高数据局部性,Minnow 给vertex设优先级来提高收敛速度,PHI提高了update效率(设计cache层级,efficient commutative scatter updates)。但是它们都没有解决长依赖链的慢传播导致的上述2个问题。
我们的2个发现:(1.1)只有源结点是active态的依赖链是需要load并处理的。(例如,当图3里只有v22是active的,那么只有v22-v26-v23-v20和v22-v25这两条链是需要处理的)。此外,由于有些结点是运行中变成inactive的,所以静态(编译时)方法不够用,因此,我们需要运行时找到起源于active结点的依赖链去处理。(1.2)异步处理会使update变少。按照链处理,而非按照结点处理,根据引言4.,update显然会变少。(2)大多数结点的状态只在很少的一部分路径上传播,这些路径是大度结点之间的路径。我们又做了一个实验。在实验中,首先,我们把结点按照度大小降序排列,然后找到前k%个结点之间的链(我理解是这些链的首尾是前k%结点),然后记录在这些链上传播的次数占总传播次数的比例。
论文阅读-DepGraph_第4张图片
结果是60%的传播都经过的是前0.5%的结点间的路径。

研究方法:
大概过程:(1)每个核有一个engine,它们并行load active的源结点(root),然后根据源结点和依赖链,On-the-fly得预取各自的依赖链上的结点。(2)此外,为了优化,at runtime得把大度结点之间的依赖链转换成直接依赖(短路),好处有2:一是路径短了传播更快了,二是更重要是:依赖链上的传播可以并行?
A. The Dependency-driven Execution Approach
定义1:Hub-vertex 和 hub-path。G= 一个path表示为:p_l=0,v_l1,…,v_l^|p_l | >, L是这个path的标识号,每个v的上标代表了结点在path的顺序,|p_l |代表了path的长度(节点数-1=边数)。由于结点v_h的度越大,在传播时越重要(第2个发现),那么我们将度>T的点称为 hub vertex(通常,是通过指定hub vertex的比例 λ 来隐式指定T,由于power law,λ 一般比较小)。我们的系统需要识别这种结点。为了减少排序开销,我们随机选取β比例的结点,然后找这β*n个点里的前λ部分,以此决定T。设 H={v∈V┤|v is a hub vertex},那么头、尾均在H(头尾都是hub vertex)的path就是hub path. (个人理解:值观得说,大度结点是hub vertex,以大度结点为头尾的path是hub path)

定义2:Core-subgraph(记为G_s): 所有hub-path的并集。讨论:虽然我们可以将hub-path短路,但是由于core-subgraph里多条hub path可能共享边,and many direct dependencies need to be generated to get this goal, undermining its efficiency.(分成多个短路径?短路数量增加->短路的生成和存储开销增加)。为了减少短路(direct dependency)的生成和存储开销,core-subgraph被进一步表示为不相交的core-path的集合(不相交是指除了头、尾外不共享结点)。 那,这些共享的头或尾就称为core-vertex. 例如,在图5(a)里: 假设v5 v13 v22是hub vertex,从v5出发,到v15有分支,所以v5-v15是core-path,然后v15-v13, v15-v22, v13-v5也是core-path。所以, v5, v15, v13是core-vertex。之后,我们可以对每个core-path进行短路(用直接依赖替换),从而让hub vertex之间信息更快得传播。
论文阅读-DepGraph_第5张图片
Dependency-driven Asynchronous Execution执行模型:(1)每个核读取active点,并行地处理没有依赖关系的core-path。例如,(a)中v22-v26-v23-v20,在一个round里v22传播到v26到v23到v20。一个核处理一个core-path,降低了memory开销,相比一个点同步一次,降低了同步开销(一条路多个点同步一次)。(2)对于core-path之间的依赖关系,我们为了提高并行度,可以把core-path短路,即存储头尾的直接依赖关系在hub index,hub index每一项存一个直接依赖关系,且动态产生。从而降低了等的时间,提高了并行度。例如(fig 5 (b))。用例子讲好处:1 v5-v15-v13-v5更快收敛. 2 提高不同路径的状态传播的并行度。例如, 在fig 5©,
论文阅读-DepGraph_第6张图片
core 2 根据短路值可以开始计算,使v18 v21 v22的计算核v7 v9 v12 v15的计算并行起来,提高不同路径的并行话,尽管路径之间有依赖。 Dependency Transformation(短路)的适用性。(适用于什么样的图算法?):图算法需要满足2个性质:(1)可以表达成GASmodel(2)EdgeCompute需要是一个线性表达式。满足上述2性质的迭代图算法还是不少的,如果不满足,就关闭dependency transformation功能即可。需要注意到,转换后的短路依赖依然是线性的:f_((v_j.v_i ) ) (s_j )=μ*s_j+ε 代表sj对vi的影响。这使得计算、存储的效率变高(III-B2) dependency Transformation(短路)的正确性。定理一:满足4.里2个性质的图算法,短路与否的结果一样。略。
B 体系结构
论文阅读-DepGraph_第7张图片

如果上面的方法完全用软件实现,则根据依赖链取结点、存储hub index这种用于存储短路的数据结构会造成很大的运行时开销,所以可以用硬件加速。 这个DepGraph会访问L2。DepGraph is a hardware-software co-design, which relies on the existing software graph processing system [47]. 这个软件系统会在执行前预处理图(e.g., dividing the graph into partitions and assigning them to the cores for parallel processing),然后调用depgraph的API来初始化DepGraph引擎,然后处理一下点、边,用work stealing平衡各个核的负载。在dividing the graph into partitions时,软件系统会对每个partition识别hub vertex和core vertex和初始active vertex。这些点的信息会传给DepGraph。DepGraph根据收到的初始active vertex去预取依赖链上的结点。这些结点传递给核上的graph processing system,来进行Accum和EdgeCompue的计算。在运行中,DepGraph会根据收到的hub vertex和core vertex来产生hub index结构,写入L2. 由于这个关系不会改变,所以不存在不一致问题。
Micro Arch:
论文阅读-DepGraph_第8张图片

首先需要注意:每条边的处理是原子操作(为何这样?这是为了让这条边能够被任何线程处理、kernel不需要pin住、memory也可以page out)我们按照顺序讲述:(1)跑在core上的graph processing system配置DepGraph(传递hub vertex和core vertex)。在核运行中,每个核的相应线程会把active core放在一个内存空间的队列(cirecular queue)里。为了利用active vertex去预取,Hardware Dependency-aware Traveler Logic (HDTL)会读取active vertex from队列(一致性怎么保障?),然后从CSR格式里找信息去预取path上后续结点,这些结点取回来后存放在FIFO Edge Buffer里。核通过DEP_FETCH_EDGE指令来从FIFO Edge Buffer读取边信息,而该指令可以通过调用DEP fetch edge()库函数来使用。当没有且不再产生active vertex时,这个partition处理完成,开始处理下一个partition。当所有partition都完成且不需再处理,算法收敛。// 在Micro Arch里,DDMU来负责runtime地产生、维护hub index。当发现一个active vertex时,DDMU先检测它相关的直接依赖是不是都有,若没有,则产生。

细节:(1)初始化:核上的线程调用DEP configure() API来配置MM寄存器(DepGraph是被当作设备管理的)。传递的参数包括:base、size of offset array,edge array, vertex state array;该partition的开始、结束结点的ID;一个内存里的bitmap的地址和大小,这个Bitmap存的是H(m’’ )=Hm∪H(m^’ ),其中H^m是该partition里的hub vertex和core vertex,H(m’ )是该partition的boundary vertex(这些点连接到其它部分的hub和core vertex); local circular queue的地址和大小(回忆:queue里存的是初始的active vertex)。(2)根据依赖链预取边:HDTL通过DFS预取,所以一个有限深度的栈被用来记录信息,栈里每一项的信息如图7所示。当碰到H(m’’ )里的结点的时候,说明不能继续取了,则也说明遇到了一条core path.(3)产生hub index: 根据(2)中的描述,当我们发现一条core path后,若没短路,可短路之,即得到f_((v_j,v_i ) ) (s_j )=μs_j+E。然后,μ,E可以根据两组(si,sj)来确定(两点确定一条直线)。具体地:一开始没有,那一项的flag为N,当有了之后,那一项的flag设为I,并把(si,sj)存到E,μ; 如果那一项为N,那么设此时的状态为(s_j’,s_i’),那就通过μ=(s_i’-s_i)/(s_j’-s_j ),E=s_i’-μ*s_j;计算出系数,并把flag设为A(available)。(4)维护hub index: 因为两个core-path的首尾可能相同,所以需要存L(core path的id)作为区分,L可以是core path第二个结点的ID(因为core-path间不会共享边)。此外,hub index会被频繁访问,所以大概率会在L3上。(5)短路传播:给定root,DDMU会取查是否有hub index,在查的过程中,为了加速,使用一个hash table(in mem),. 查到offsets后去读取hub index,读到所有源于root的core path的参数,计算对尾结点的直接影响,然后把尾结点送入对应核的circular queue里,从而达成fig 5©的效果。但是有一个问题:在fig5©,核1短路计算了一次v15的值,核1也正常计算了一次v15的值,为了保障正确性,v15的值应该只被修改一次,具体地:如果Accum是min/max,那无妨,若Accum是sum,那么就有错,因此the thread then needs to reset v15(s_15←s_15-f_((v_5,v_15 ) ) (s_5),之前加了2遍,需要再减1遍),具体方法是:在每个core-path末尾加一个假想的结点<-1,v15,NULL, f_((v_5,v_15 ) ) (s_5)>放在FIFO EDGE BUFFER的core path的末尾。DEP fetch edge()每当取到这个假想结点,就把core path的tail结点的状态减去假想结点里的值,之后这个假想结点就被丢弃了。

实验:
配置:ZSim 64 cores,只汇报运行时间,不含初始化时间。
系统:Ligra-o, DepGraph-S(只有软件实现), DepGraph-H(有硬件实现) λ=0.5%,β=0.001 (λ是大度结点比例,β是采样比例),stack深度=10,都开了SIMD,是不开的2.04-2.17倍,用McPAT计算能量估计,用Micron DDR3L datasheets来作为内存。
论文阅读-DepGraph_第9张图片

是运行时间的breakdown,other time包含内存访问时间等。首先,ligra-o需要更长的vertex processing time(因为update更多,依赖也更多);然后,DepGraph-S的vertex processing time是ligra-o的16.9%-37.0%;然而,other time在DepGraph-S占比较大(57.9%−95.0%)(因为软件有运行时开销,沿着依赖链取边占了36.5%-60.6%,hub index的维护与读取占了20.2%-32.7%);DepGraph-H降低了DepGraph-S的other time,DepGraph-H的other time只有DepGraph-S的4.5%-22.9%,占总时间的30.2%-78.2%,DepGraph-H存储hub index的内存占总所需内存的0.9%-2.8%;DepGraph-H最好,是ligra-o的5.0-22.7倍(原因就是之前说的减少不必的update,更高度的并行(利用短路),省去加载inactive结点的开销,还包括硬件加速-减少了runtime开销)。
论文阅读-DepGraph_第10张图片

这里还提到fig 11里,hub index based optimization会带来56.9%-71.5%的性能提升(其中DepGraph-H-w是关闭hub index的系统)。这说明不用短路也有一定的性能提升。
在这里插入图片描述

图10说明H比S的UPDATE略微多一点,因为在依赖链之间的传递中,H比S要多更新几次;此外,H比O减少了61.4%-82.2%的更新次数(因为H会沿着依赖链传播,而非以点为中心,这会减少计算、访存开销)。
论文阅读-DepGraph_第11张图片

在图12中,H比现有3种架构的有效更新比例明显增加,这也是性能提升的原因。之所以它们利用率低,有2个原因:(1)不必要的更新、Inactive结点。(2)Minnow PHI的数据局部性不好,As described in Figure 11, in different cases, DepGraph-H outperforms HATS, Minnow, and PHI by up to 3.0−14.2, 2.2−5.8, and 2.4−10.1 times, respectively
可扩展性:
论文阅读-DepGraph_第12张图片

核变多的时候,H的时间最短。因为前3个有很多不必要的操作。总之它因为之前的原因,可扩展性更好。
面积、能量开销:
论文阅读-DepGraph_第13张图片
论文阅读-DepGraph_第14张图片

14nm,verilog RTL, 同主频。面积开销小,因为它用Cache存hub index,其硬件开销只有逻辑部分(HDTL,DDMU)和6.1Kbits的stack和4.8Kbits的FIFO EDGE BUFFER。 然后在FS数据集上,测量了能量的breakdown,H的能量最少,因为利用率更高,算法收敛更快。能量是用22nm McPAT测的。
Sensitive:(1)对stack深度的敏感度:
10之后不太敏感:几乎是平的。(2)对L2、LLC的敏感度:
都比得过前3种。(3)对超参 lamda 和 beta的敏感度: 所有并不是单调的,都是U型的。当选出更多的大度结点时,访问hub index开销增加;当选出更少的大度结点时,core path就变少,更少的短路会错过一些直接影响的机会。(4)受图的特性的影响:
按照power-law(指少数结点能连接大多数的边)随机构造了图(固定1000万个结点,改变zipfian factor α)。 α 越小,vertex degree skewness越大。α 越小,H表现越好,因为更多短路会被构造出来。
论文阅读-DepGraph_第15张图片
论文阅读-DepGraph_第16张图片

论文阅读-DepGraph_第17张图片
论文阅读-DepGraph_第18张图片
在这里插入图片描述

你可能感兴趣的:(硬件)