Green Marl是一种面向图的特定领域语言。用户可以在 Green-Marl 中使用高级的、图形特有的数据类型和运算符直观地编写自己的图形算法。
本系列将结合Green Marl的论文与文档,分析学习Green Marl语言。其中部分是对论文或文档的翻译。若有理解错误,请指出。
论文:Green-Marl: A DSL for Easy and Efficient Graph Analysis
Github Repo:Green-Marl
从数学上来说,G = (N,E) 其中N是所有的点集,而E是所有的边集。与图中每一个节点相关的数据可以看成一个从点N或者边E到值域的一个映射。作者将其称为node property
。
对于 G=(V,E) 以及 Π={P1,…,Pn} Green Marl主要用于完成以下的图形分析:
根据上面的描述,Green-Marl做出了两个假设:
总的来说,Green-Marl会将图看成一个静态的对象。
Green-Marl不是一个完全独立的语言,而是基于一些现有语言的功能:
对于第二种可并行部分,使用的并行方式是fork-join
的风格,对于并行中的可并行部分fork
一个新的进程,并在最后的一个join-point
进行同步。
Green-Marl使用静态范围规则(在并行化中的变量作用域)
Green-Marl的内存一致性模型与OpenMP类似:
Green-Marl保证所有的写都是原子的。
如下代码就会存在违背上面一致性的问题:
Foreach(s:G.Nodes)
Foreach(t:s.OutNbrs)
t.A = t.A + s.B
其中出现了写写冲突与读写冲突。
五种基本类型:Bool, Int, Long, Float, Double.
两种图类型:DGraph, UGRaph.
点与边:Node, Edge. 总是与图绑定,如下面的n1与n2
属性:属性也需要类型,如下面的Node_Prop
三种集合类型:Set, Order, Sequence.
Set:无序,不重复的
Order: 有序,不重复的
Sequence: 有序,不是唯一的(可能有重复)
这三种类型也需要绑定图,如下面的S。
Procedure foo(G1, G2:Graph, n1:Node(G1)){
Node(G2) n2; //a node of graph G2
n2 = n1; //type error? n1 n2 bound to different graphs
Node_Prop(G1) A; //integer node property for G1
n1.A = 0;
Node_Set(G1) S; // A node set of G1
S.Add(n1);
}
集合之间的操作支持如下表所示:
其中要注意的是:
在Green-Marl中的遍历格式如下:
Foreach(iterator:source(-).range)(filter)
body_statement
Foreach代表着下面的部分可以并行执行,而如果使用For关键字,则代表着下面的操作将串行执行。iterator与source如字面意思。filter是一个可选项,其是一个bool表达式,决定当前的遍历是否执行body_statement。
与图有关的可遍历的source的range如下图所示:
上面的表中Access列则表示的遍历的本质。线性遍历表示每一个iterator都指向一个唯一的元素,所有的元素都只会被访问到一次。而Random则可能会出现混淆(aliasing,应该是指多次访问同一个元素)。
对于Order和Set串行的遍历才能体现出来其中的顺序,逆序遍历的例子如下:
Node_Order(G) O;
For(o:O-.Items) //reverse order iteration on O
body_statement
Green-Marl也提供了两种图的遍历模式:BFS和DFS。示例如下:
InBFS (iter:src^.Nodes From root)[navigator](filter1)
forward_body_statement
InRBFS (filter2)
backward_body_statement
root defines the root node of BFS
^ means that we first create a transposed version of graph
navigator 表示哪些节点需要在遍历时被修剪
如果一个节点不满足navigator的条件,将不会在这个节点上进行扩展。
如果满足navigator但是不满足filter,依然会扩展,但是不会执行body_statement。
下面是一个实际利用BFS访问一个转置的图的例子,且只有在flag没有设置的节点才继续向后遍历:
Node_Prop(G) flag;
InBFS(s: G^.Nodes From r)[!s.flag]{
//...
}
BFS有两个body_statement块,其中第一块是在向前进行BFS遍历是执行(从近到远),第二个块是可选的,其是逆序BFS遍历时执行的,即从最远的到最近的节点。
对应的DFS中正序和逆序的遍历为InDFS
与InPost
,同样也可以拥有两个body_statement块。
对于DFS和BFS的具体实现,DFS是串行执行的,BFS是层次并行执行的,也就是说,离根节点的相同距离的节点会被并行的访问到,当访问完一个层次的所有节点后,会经历一次同步,然后再访问下一层的节点。所以以下代码没有数据冲突:
InBFS(s:G.Nodes From r) {
Foreach(t:s.UpNbrs)
s.A += t.A //s.A does not conflict with t.A
}
Green-Marl支持延后赋值,如下面代码中的s.X就是通过延后赋值。延后赋值类似于RCU,读者可能会读到旧数据,而对于数据的写操作,会在绑定的(@表示绑定)遍历结束后可见。
Foreach(s:G.Nodes){
s.X <= Sum(t:s.Nbrs) {t.X} @ s
// no conflict t.X gives 'old' value
}
// 所有对于X的写在这里可见
Int x,y;
// expression form (in-place form)
x = Sum(t:G.Nodes) {t.A};
// assignment form (need initialization)
y = 0;
Foreach(t:G.Nodes)
y += t.A
Green-Marl所支持的归约如下表,其中In-place
版本允许添加filter
,而在Assignment
版本则是在foreach
中的statement
做出判断。
其中Min,Max比较特别的一点是,其可以记录下来最大最小对应的节点的值,如下代码所示:
Int X = INF;
Node(G) from,to;
Foreach(t:G.Nodes)
Foreach(u:t.Nbrs)
X <from,to> min = (t.A + u.B);
同样的,归约也可以利用@
来进行绑定:x+=…@s
表示x
在进行s-iteration
的时候被+
归约。这就表明x
不能在s-iteration
之内的其他地方进行读写操作。
如果不手动指出,编译器会自己去找,若找不到会报错。
Int sum = 0;
Foreach(s:G.Nodes){
Foreach(t:s.Nbrs)
sum += t.A @s; //accumulate over s-iteration
if (s.A > THRESHOLD)
sum += s.B ; //@s is implied
s.C = sum; // this is an read-reduce conflict
}
在最后一句,由于之前对sum进行了写操作,而这里在同一个s-iteration
内直接进行读操作,会导致read-reduce conflict
。
Github Repo : https://github.com/stanford-ppl/Green-Marl
主要依赖有:
- gcc (version >= 4.2), which supports OpenMP
- g++
- GNU flex and bison
最新版本的源码commit 4c0d62e67d431d535ca27140df60b25c234a808b
在语法文件中有小问题,改一下就好了。
配置好环境跑示例程序如下:
# liu @ liu in ~/Desktop/Green-Marl/apps/output_cpp/bin on git:master x [11:38:00]
$ ./graph_gen 1000000 8000000 ../data/u1m_8m.bin 0
Creating Graph, N = 1000000, M = 8000000 , Type = 0
creation time (ms) = 1674.369000
saving to file = ../data/u1m_8m.bin
storing time (ms) = 201.063000
# liu @ liu in ~/Desktop/Green-Marl/apps/output_cpp/bin on git:master x [11:38:34]
$ ./conduct ../data/u1m_8m.bin 1
running with 1 threads
N = 1000000, M = 8000000
graph loading time=2303.032000
reverse edge creation time=0.000000
running time=145.662000
sum C = 3.000369
# liu @ liu in ~/Desktop/Green-Marl/apps/output_cpp/bin on git:master x [11:38:39]
$ ./conduct ../data/u1m_8m.bin 8
running with 8 threads
N = 1000000, M = 8000000
graph loading time=1254.962000
reverse edge creation time=0.000000
running time=103.377000
sum C = 3.000369
搭建好了编译环境,现在就写一个简单的程序来试试这个编译器。
书写程序foo.gm
如下:
Procedure foo(G:Graph,A:N_P) : Long
{
Long x;
x = Sum(t:G.Nodes) {t.A};
Return x;
}
对于图每一个节点的属性A,简单的求和。
书写Makefile
如下:
GM_COMP = /home/liu/Desktop/Green-Marl/bin/gm_comp
all: foo.gm
$(GM_COMP) foo.gm
编译后头文件如下:
#ifndef GM_GENERATED_CPP_FOO_H
#define GM_GENERATED_CPP_FOO_H
#include
#include
#include
#include
#include
#include
#include
#include
#include "gm.h"
int64_t foo(gm_graph& G, int32_t* G_A);
#endif
函数实现如下:
#include "foo.h"
int64_t foo(gm_graph& G, int32_t* G_A)
{
//Initializations
gm_rt_initialize();
G.freeze();
int32_t __S0 = 0 ;
__S0 = 0 ;
#pragma omp parallel
{
int32_t __S0_prv = 0 ;
__S0_prv = 0 ;
#pragma omp for nowait
for (node_t t = 0; t < G.num_nodes(); t ++)
{
__S0_prv = __S0_prv + G_A[t] ;
}
ATOMIC_ADD(&__S0, __S0_prv);
}
return __S0;
}
可以看出其并行部分是利用OpenMP
做的。
继承项目提供的main函数框架,书写主函数如下:
#include "common_main.h"
#include "foo.h"
class my_main: public main_t
{
public:
int* A;
long sum;
virtual ~my_main() {
delete[] A;
}
my_main() {
A = NULL;
sum = 0;
}
virtual bool prepare() {
A = new int[G.num_nodes()];
return true;
}
virtual bool run() {
printf("Graph has %d node\n",G.num_nodes());
for (int i = 0; i < G.num_nodes(); i++)
A[i] = i;
sum = foo(G, A);
return true;
}
virtual bool post_process() {
printf("Sum = %ld\n",sum);
return true;
}
};
int main(int argc, char** argv) {
my_main M;
M.main(argc, argv);
}
在双核虚拟机下跑的结果如下:
# liu @ liu in ~/Desktop/Green-Marl/apps on git:master x [13:14:53]
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 2
On-line CPU(s) list: 0,1
Thread(s) per core: 1
Core(s) per socket: 1
Socket(s): 2
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 61
Model name: Intel(R) Core(TM) i5-5250U CPU @ 1.60GHz
Stepping: 4
CPU MHz: 1599.289
BogoMIPS: 3198.57
Hypervisor vendor: VMware
Virtualization type: full
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 3072K
NUMA node0 CPU(s): 0,1
# liu @ liu in ~/Desktop/Green-Marl/apps on git:master x [13:14:45]
$ ./output_cpp/bin/foo ./output_cpp/data/u1m_8m.bin 2
running with 2 threads
N = 1000000, M = 8000000
graph loading time=1346.469000
reverse edge creation time=0.000000
Graph has 1000000 node
running time=1.045000
Sum = 1783293664
# liu @ liu in ~/Desktop/Green-Marl/apps on git:master x [13:14:49]
$ ./output_cpp/bin/foo ./output_cpp/data/u1m_8m.bin 1
running with 1 threads
N = 1000000, M = 8000000
graph loading time=1731.271000
reverse edge creation time=0.000000
Graph has 1000000 node
running time=1.814000
Sum = 1783293664
由于macOS原生的GCC版本太低为(4.2)版本,故使用port安装gcc5.0版本。
此版本执行文件为:gxx-mp-5。
现在可以在.bashrc中将gcc链接到此执行文件,也可以修改Makefile避免影响其他项目。这里修改了Makefile中的CC,可以正常在macOS下编译成功。