首页 > 图灵资讯 > 技术篇>正文

图计算引擎分析--GridGraph

2023-04-20 16:54:01

作者:京东科技 李永萍

GridGraph:Large-Scale Graph Processing on a Single Machine Using 2-Level Hierarchical Partitioning

图计算框架

根据计算方法,图形计算系统可分为:单机内存图处理系统、单机核外图处理系统、分布式内存图处理系统、分布式核外图处理系统。本文将详细介绍GridGraph。

GridGraph论文分析单机核外图处理系统

单机内存图处理系统受内存空间和单机计算能力的限制,可以解决的图规模有限。理论上,随着集群规模的增加,分布式内存图处理系统可以解决更大的图形规模,但网络带宽问题、负载不平衡、同步成本大、容错成本和图形分割挑战越来越明显。无论是单机还是分布式,内存式图处理系统能处理的图规模都是有限的。因此,如果您想使用更少的资源来解决更大的图形规模,您可以使用单机核外图处理系统。单机核外图处理系统采用磁盘顺序读写进行数据置换,可在有限的内存中计算较大的图纸。在选择调度和同步计算模式方面,单机核外图处理系统进行了重要探索,以最大限度地利用磁盘顺序读写。

GridGraph

GridGraph是一种单机核外图处理系统,在大规模图处理系统中充分利用磁盘读写,在有限的内存中高效地完成大规模图计算。

GridGraph充分利用磁盘大容量,解决单机内存有限时实现大规模图计算的问题。GridGraph通过Streaming-Apply减少计算中的IO 通过文件转移顺序减少不必要的io费用。 同时,GridGraph也利用顺序读写的特点,尽量少写硬盘。

主要贡献

GridGraph的主要贡献是:

1、基于边列表快速生成一种新的图形表示形式——网格划分。网格划分是一种不同于相邻矩阵和相邻链接表的表示形式。网格划分不需要对index进行排序。网格的边缘block可以从未排序的边缘列表转换,数据预处理成本小,可以应用于不同的算法和机器。

2、2-level hierarchical partitioning 采用两层分区划分模式,不仅适用于核外,还适用于内存。

3、为了提高IO,提出了streaming-aply模式。通过双滑动窗口(Dual sliding windows)保证顶点访问的局部性。

4、提供灵活的点边流接口函数,通过用户定制的过滤函数跳过非活动顶点(活动顶点:顶点index在bitmap中的状态为1)或非活动边计算。该方法显著提高了活动顶点集随收敛缩小的迭代算法的性能。

Grid Representation网格划分

为了在有限的内存中完成大规模的图纸计算,并严格控制内存消耗,需要对图纸进行网格划分。

1、顶点集分为P个均匀chunk。

2、边集分为P*Pblock,行表示源顶点,列表目标顶点。

图计算引擎分析--GridGraph_图计算

The Grid Format 网格格式

GridGraph partition预处理方法如下:

1、主线程从原始无序边集中读取边,读取一批边后,将这批边数据添加到队列中。(根据磁盘带宽,一般选择24M作为这批边的大小)

2、每个工作线程从队列中获取任务,计算边缘所属的block,并将边缘添加到边缘block文件中。为了增加I/O吞吐量,每个工作线程维护每个block的本地缓冲区,一旦缓冲区满,将刷新到文件。

GridGraph可在分区过程结束后进行计算。然而,由于现实世界图的不规则结构,一些边缘block可能太小,无法在HDD上实现大量的连续带宽。因此,由于频繁的磁盘搜索,有时无法实现顺序带宽。为了避免这种性能损失,GridGraph需要一个额外的合并阶段,以便更好地实现基于HDD的系统。在这个阶段,将边缘block文件逐一添加到大文件中,并记录元数据中每个块的起始偏移。

与Graphchi的shard分片模式不同,GridGraph不需要对边缘block进行排序,这减少了IO和计算成本。我们只需要在磁盘上读写一次边,而不是在Graphchi中多次遍历边。

对X-Stream而言,X-Stream不需要显式预处理。根据流分区,几份文件被打乱。不需要排序,分区的数量很少。只需要一个流分区,就可以将许多顶点数据安装到内存中。然而,这种分割策略使其在选择调度时效率低下,这在很大程度上影响了它在许多迭代算法中的性能,因为它只使用了某些迭代中的部分顶点。(GraphChi和X-Stream都是单机核外图计算系统,这里就不赘述了。)

调度的选择是什么?选择调度是将图数据文件(通常是边缘文件)分成多个block,并按顺序编号,设置一个bitmap来记录所有block的访问状态。如果需要访问,将bitmap中index的block编号状态设置为1,在调度过程中跳过状态为0的block,选择状态为1的block从磁盘内存中计算。如果bitmap是空的,则默认所有block都需要参与计算,然后将block按顺序从磁盘输入内存。block的大小决定了选择调度的差异。block越大,包含的数据越多,block更换的概率越低,选择调度越好。相反,block越小,包含的数据越少,计算时更换block的概率越高,选择调度越差。

GridGraph完成预处理的时间很短。此外,生成的网格格式可用于在同一图中运行的所有算法。GridGraph可以通过分区进行选择性调度,以减少不必要的访问没有活动边缘的边缘。这对许多迭代算法(如BFS和WCC)做出了巨大的贡献,因为大多数顶点在许多迭代中都不活跃。

内存(In-memory)图计算系统将所有数据读取到Memory内存中,并在系统中使用Cache(缓存)和Memory(内存)来完成图计算过程(Out-of-core)计算系统将数据存储在Disk磁盘中,然后在计算时将所需的数据替换到Memory(内存)中,通常将数据存储到Cache缓存中,以缓解CPU和Memory之间的速度差异。磁盘存储空间>存储空间>缓存存储空间。

图计算引擎分析--GridGraph_磁盘_02

那么如何选择Partition呢?

粒度越细(即P值越大),预处理时间越长,P越大,每个chunk能表示的范围越广,每个block能存储的边数据越多,顶点数据访问的局部性越好,block更换概率越低,选择性调度潜力越大。因此,P越大越好。目前,我们暂时选择P的最大值,使顶点数据能够适应最后一级缓存。可以这样设置P的最小值:

(V/P)*U<=C<=>P>=C/UV

V是图中的顶点数,C是最后一级cache缓存的大小,U是每个顶点的大小。(V/P)在chunk中表示顶点范围,(V/P)*U表示每个chunk的大小。为了适应最后一级缓存,如果一个chunk的所有数据都可以一次放入最后一级缓存,那么chunk的大小应该小于或等于C,公式变换得P的最小值为C/UV.

这种分区方式不仅表现出良好的性能(尤其是在内存条件下),而且节省了大量的预处理成本。

The Streaming-Apply Processing Model

GridGraph采用流应用处理模型,在该模型中只需读取边缘一次,并且只需遍历一次顶点即可完成I/O总量。

GridGraph提供了两个流式处理函数,分别处理顶点(Algorithm1)和边缘(Algorithm2):

图计算引擎分析--GridGraph_数据_03

F是一个可选的用户自定义函数,它接受顶点作为输入(Streamvertices是当前的顶点,Streamedges是block中每个边缘的源顶点),并返回一个布尔值来指示流中是否需要顶点。当算法需要选择性的调度来跳过一些无用的流时,通常与位图一起使用,位图可以紧凑地表示活动的顶点集。

Fe和Fv是用户自定义的描述流处理函数。Fe接受一边作为输入,Fv接受顶点作为输入,返回R类型值,累积返回值,并作为最终结果提供给用户。该值通常用于获得活跃顶点的数量,但不限于此用法。例如,用户可以使用该函数来获得Pagerank中迭代之间的差异之和,以决定是否停止计算。

GridGraph将顶点数据存储在磁盘上。使用内存映射机制(通过mmap内存映射机制将顶点数据文件映射到内存)引用文件中的顶点数据,每个顶点数据文件对应一个顶点数据数组。因此,访问顶点数据文件就像访问内存中的数组一样,并简化了编程模型:开发人员可以将其视为普通数组,就像它们在内存中一样。

以Pagerank为例,我们来看看GridGraph是如何实现算法的。

Pagerank是一种链接分析算法(Algorithm3)点的数值权重,以测量其在顶点之间的相对重要性。最初所有顶点的公关值为1。在每次迭代中,每个顶点都向邻居发送自己的贡献,即当前的公关值除以其程度。每个顶点总结了邻居收集的贡献,并将其设置为一个新的公关值。当平均值差达到一定阈值时,算法收敛。

图计算引擎分析--GridGraph_图计算_04

Dual Sliding windows 双滑窗模式

GridGraph流式读取每个block的边缘。当block在第一行j列时,与block相关的顶点数据也落在第一行j列的chunk中。每个block包含两个顶点chunk,source chunk(源顶点chunk)和destinationn chunk(目的顶点chunk)。

通过P的设置,block足够小,可以将block放入最后一级缓存中,以确保在访问与block相关的顶点数据时具有良好的局部性。

根据更新模式,block的访问顺序可以是面向行或面向列。假设顶点状态从源顶点传播到目标顶点(这是许多应用程序中的典型模式),即读取源顶点数据,写入目标顶点数据。由于每个侧block列对应于目标顶点块,因此需要编写目标顶点块,因此在这种情况下,面向列的访问顺序优先考虑。当目的顶点的block缓存在内存中时,GridGraph从上到下流向同一列的block,因此昂贵的磁盘写作操作被聚合和最小化。特别是对于SSD系统来说,这是一个非常重要的性能,写入大量数据的性能会相应下降。另一方面,由于SSD有写入周期的上限,因此尽量减少磁盘随机写入以实现理想的持久性是非常重要的。

图计算引擎分析--GridGraph_图计算_05

以Pagerank为例,我们来看看GridGraph是如何用双滑动窗口更新顶点信息的。读取窗口(从源顶点数据中读取当前顶点的Pagerank值和出度)和编写窗口(累积目标顶点新Pagerank值的贡献)作为GridGraph沿block以面向列的顺序滑动。

1、初始化,每个顶点的初始PR值为1

图计算引擎分析--GridGraph_数据_06

2、Stream edge block(1,1)此时src.chunk 1和dest.chunk 一切都加载到内存中

读取窗口:读取src.chunk 1.PR和Deg

写dest:写dest.chunk NewPR1

IO总量:读取block中的两个边,读取srcc.chunk 读取desttt1中的顶点(1,2).chunk 1中的顶点(1,2)

图计算引擎分析--GridGraph_数据_07

3、Stream edge block (2,1)此时destt.chunk 1.将src放在内存中.chunk 2也加载到内存中

读取窗口:读取src.chunk 2.PR和Deg

写dest:写dest.chunk NewPR1

IO总量:读取block中的两个边,读取srcc.chunk 2中的顶点(3,4)

图计算引擎分析--GridGraph_GridGraph_08

4、Stream edge block (1,2),dest.chunk 1已全部更新,更新后的destt已经完成.将srccccunk1写回磁盘种,.chunk 1和dest.chunk 2将其加载到内存中

读取窗口:读取src.chunk 1.PR和Deg

写dest:写dest.chunk NewPR2

IO总量:读取block中的两个边,destt.chunk 1中的顶点(1,2)写入磁盘,读取SRC.chunk 读取desttt1中的顶点(1,2).chunk 2中的顶点(3,4)

图计算引擎分析--GridGraph_数据_09

5、Stream edge block (2,2)此时desttt.chunk 2在内存中,src.chunk 2也加载到内存中

读取窗口:读取src.chunk 2.PR和Deg

写dest:写dest.chunk NewPR2

IO总量:读取block中的一条边,读取srcc.chunk 2中的顶点(3,4)

图计算引擎分析--GridGraph_数据_10

6、完成dest所有chunk的遍历,并完成dest.chunk 更新后的结果写入磁盘。

IO总量:desttt:.chunk 2中顶点(3,4)的结果写入磁盘。

图计算引擎分析--GridGraph_GridGraph_11

网格图的I/O分析是在上面的一次流应用迭代中进行的,所有的边缘和顶点都被访问。以面向列的顺序访问边缘block为例:所有边缘访问一次,源顶点数据读取P次,目标顶点数据读写一次。IO用于一个完整的迭代和收敛:

E+(2+P)*V

E:读取所有边

2:读取和写入目标顶点的数据

P:读取每个P中源的顶点数据

GridGraph所需的内存非常紧凑。事实上,它只需要一个小的缓冲区来保存Stream的边缘blocl,这样页面缓存就可以使用其他空闲内存来保存更多的边缘block,当活跃的边缘block变得足够小以适合内存时,这是非常有用的。这种Streaming-Apply-Processing-Model流式应用模型的另一个优点是,它不仅支持经典的BSP模型,还允许异步更新。由于顶点更新是即时的,可以通过跟踪顶点的遍历来获得更新的效果,这使得许多迭代算法收敛得更快。可以看出,P应该是将顶点数据放入内存的最小值。因此,较小的P应该是最小化I/O量的首选,这似乎与我们上面提到的P越大越好,网格分区的原则相反。

Selective scheduling 选择调度

我们之前解释过什么是选择调度,也就是跳过不活跃的边block。在Stream函数中,F传入位图跳过不活跃的边block。

图计算引擎分析--GridGraph_数据_12

P越小,粒度越粗,访问顶点的次数越少,局部性越差,调度选择越差

P越大,粒度越细,局部性越好,调度越好,访问顶点的次数越多

为了解决这个问题,二级分区应用于边网格,以减少I/O访问的顶点。

2-level hierarchical partitioning

在P\*P网格中再进行一层网格划分,第二层网格有Q\*Q个边网格。在P\*P网格中应用Q\*Q分区。

应满足Q的选择:

(V/Q)*U <= M

M是给定的内存容量。

正如我们前面提到的,P的选择是将顶点数据放入容量远小于内存的上级缓存中,因此P应该远远大于Q。

图计算引擎分析--GridGraph_GridGraph_13

整个网格分为四个大块,每个大块包含四个小块。每个块中的数字表示访问顺序。在原始的4×精确的面向列访问顺序用于4分区。二级分区应用后,P:2×2 变成 Q:4×4分区后,我们以面向列的顺序访问粗粒度(大)块,在每个大块中,我们以列为导向的顺序访问细粒度块(小)块。这种二级分区不仅提供了灵活性,还提高了效率,因为高级分区(二级分区)是虚拟分区,GridGraph可以利用低级分区(一级分区)的结果,所以不会增加更多的实际开支。并且可以使用P网格划分的结果来选择调度。

总结

GridGraph定义了一种适应有限内存的新图表形式:网格划分;使用双窗口模式来减少IO访问的总量,特别是编写IO;选择调度来减少无用的IO;使用二级分区确保P尽可能大,减少IO访问。GridGraph在有限的内存中提高了IO效率,有效地完成了核外图的计算过程。

GridGraph源码分析

源码地址:https://github.com/thu-pacman/GridGraph

数据预处理模块

将原始二进制文件处理成grid格式的block文件

让我们来看看block文件是如何划分的:

将IOSIZE的数据放入buffers\[cursortasks记录了当前游标的字节数<cursor, bytes>,在tasks中获取cursor和bytes,根据cursor读取buffers中的数据,buffers[cursor根据src和dst所属的partition,将数据放入local\_buffer\[i\]\[j将local\_buffer\[i\]\[jblock\[i\]\[j在文件中。如下图所示:

图计算引擎分析--GridGraph_磁盘_14

代码位于:tools/preprocess.cpp

1、打开文件读取数据,并在task中添加数据。在这里,buffers的定义是全局的,tasks保存cursor和buffers的数据大小。

图计算引擎分析--GridGraph_GridGraph_15

2、让我们来看看tasks是什么,tasks是保存当前游标和数据大小的队列。grid\_buffer\_size = 12\*8\8,12表示<4 byte source, 4 byte destination, 4 byte float typed weight>,88表示每次读取64byte的数据时,都会写一个磁盘,这是一个magicc number。

图计算引擎分析--GridGraph_磁盘_16

3、真正的数据处理是threads的任务。每个thread处理一个buffers\[cursor数据。

图计算引擎分析--GridGraph_图计算_17

将local_buffer的数据写入相应的block文件

图计算引擎分析--GridGraph_磁盘_18

4、生成column文件,将所有block文件按列遍历保存到column文件中,并将每个block文件的大小保存到column_ofset文件中。

图计算引擎分析--GridGraph_GridGraph_19

5、同理生成row文件,按行遍历读取block文件,写入row文件,并记录offset。

图计算引擎分析--GridGraph_磁盘_20'

6、最后,将处理好的数据信息(是否包含权重、顶点数、边数、partition数)写入meta文件。

图计算引擎分析--GridGraph_磁盘_21

执行grid代码后,将生成P*Pblock文件、column文件、row文件column\_offset、row_offset和meta文件。

Graph实现

代码位于:core/graph.hpp

init

空间初始化,读取meta信息和column_offset、row\_offset数据,并记录每个block文件的大小

图计算引擎分析--GridGraph_数据_22

stream_vertices:

如果bitmap为空,且顶点数据字节总数(顶点数据字节总数初始化为0,则可在算法实现时设置,一般为顶点总数_顶点大小)大于0.8_内存字节数,首先获得partitionsbegin_vid和end\_vid,再次历历每一个partition,按照process执行每个partition中的每个vertex,并将返回值求和相加。最后,等待所有partition执行结束,得到begin\_vid和end\_vid。

图计算引擎分析--GridGraph_图计算_23

如果bitmap不是空的,或者顶点数据字节总数小于0.8*内存字节数,则通过每个partition获得每个partition的begin_vid和end\_vid。如果bitmap是空的,它将遍历partition中的所有顶点,并根据process执行,返回值相加。否则,从begin_vid开始,根据bitmap遍历,bitmap为1的vid执行process,返回值相加。

图计算引擎分析--GridGraph_磁盘_24

stream_edges:

根据bitmap决定需要遍历的partition,如果bitmap是空的,那么所有的partition都必须遍历,bitmap不是空的,根据partition是否包含bitmap中的vid,包括partition需要遍历。

图计算引擎分析--GridGraph_数据_25

统计所有需要遍历的partition文件的总大小

图计算引擎分析--GridGraph_GridGraph_26

update默认\_mode=若update_mode=0是行更新模式(行主序更新),update_mode=一是列更新模式(列主序)。数据准备阶段:

图计算引擎分析--GridGraph_磁盘_27

对于需要访问的遍历分区,分区的访问方式为:列不变,行从小到大遍历,行遍历后再向右移动。每次读取分区中IOSIZE大小的数据,最后读取PAGESIZE大小的数据,IOSIZE不够

图计算引擎分析--GridGraph_数据_28

按照process的方法执行每个侧面的操作

图计算引擎分析--GridGraph_磁盘_29

如果行主序,实现如下:按行遍历读取需要遍历的partition,每次处理IOSIZE大小的数据

图计算引擎分析--GridGraph_GridGraph_30

数据处理方法是读取row文件,从offset开始读取l将ength的数据放入buffer中,然后通过process执行每个边。

图计算引擎分析--GridGraph_GridGraph_31

让我们来看看Pagerank算法的实际使用,以Pagerank算法的实现为例,这里不再详细介绍Pagerank算法的原理。

实现Pagerank算法

代码位于:example/pagerank.cpp

degreeee首先初始化每个顶点:update_在这里mode=0、使用行主序更新。

图计算引擎分析--GridGraph_图计算_32

每个顶点的初始pr值为1:

图计算引擎分析--GridGraph_GridGraph_33

计算每个边的贡献值:

图计算引擎分析--GridGraph_数据_34

更新每个顶点的pr值,最后一轮迭代直接计算和更新sum:

图计算引擎分析--GridGraph_数据_35

总结

在grid文件处理中,有几个优化点:

1)、读取输入文件时,可根据文件数量并行读取文件,加快文件处理速度。

2)、grid空间的初始化,因为每个block在初始化时不会相互影响,所以可以使用omp并行初始化来提高效率。

3)、在thread线程中,由于每个线程处理不同的cursorbuffers数据,每个thread生成自己的local_buffer写入block文件,因为threads中没有数据交互,因此也可以并行化。

stream_vertices和stream\_edges我们都进行了分析,可以看出,无论是行主序还是列主序,折线式(Z型)边block遍历策略都是必然的,其优化点如下:

1、Z型边遍历可以更改为U型遍历。以列主序为例。当遍历到最后一行SRC时,SRC保持在内存中。此时,DST向右移动,SRC从下到上遍历,以此类推,可以节省P页面替换。

GridGraph提供了一个在有限内存中完成大规模图计算的系统。解决单机内存或分布式内存无法解决的大规模图计算问题。提供一种新的切图方法,将顶点和边缘分为1D chunk和2D block表示大规模地图的网格表示;使用新的streaming-apply模型来改进IO,流化阅读边缘block的方式,并对顶点进行局部友好;在不涉及I/O访问的情况下,GridGraph可以访问内存中的顶点数据,跳过边block,不需要遍历,提高算法执行效率。

GridGraph将顶点划分为P个顶点数量相等的chunk,并将边缘放置在P*P网格中的每个block中。边缘顶点的chunk决定了网格中的行,边缘目的顶点的chunk决定了网格中的列。它对Cache/RAM/Disk采用Streamm进行了两层网格划分 vertices and 图形编程模型edges。在计算过程中,双滑动窗口(Dual Sliding Windows)I/O费用也大大降低,尤其是写作费用。以block为单位选择调度,用原子操作更新顶点,确保线程安全。为了进一步降低所需的I/O带宽,提高效率,本文提到了边网格上的压缩技术。

参考文献:

1. Xiaowei Zhu, Wentao Han and Wenguang Chen. GridGraph: Large-Scale Graph Processing on a Single Machine Using 2-Level Hierarchical Partitioning. Proceedings of the 2015 USENIX Annual Technical Conference, pages 375-386.

2. ZHU Xiaowei — GridGraph: Large-‐Scale Graph Processing on a Single Machine. Using 2-‐Level Hierarchical Parffoning. Xiaowei ZHU, Wentao HAN, Wenguang CHEN.Presented at USENIX ATC '15

3. Amitabha Roy, Ivo Mihailovic, Willy Zwaenepoel. X-Stream: Edge-centric Graph Processing using Streaming Partitions

4. Aapo Kyrola Carnegie Mellon University akyrola@cs.cmu.edu, Guy Blelloch Carnegie Mellon University guyb@cs.cmu.edu,Carlos Guestrin University of Washington guestrin@cs.washington.edu. GraphChi: Large-Scale Graph Computation on Just a PC

上一篇 puTTY实现自动登录
下一篇 Java异常Exception详解

文章素材均来源于网络,如有侵权,请联系管理员删除。