Green Marl 入门 Part1:语言相关

Green Marl是一种面向图的特定领域语言。用户可以在 Green-Marl 中使用高级的、图形特有的数据类型和运算符直观地编写自己的图形算法。

本系列将结合Green Marl的论文与文档,分析学习Green Marl语言。其中部分是对论文或文档的翻译。若有理解错误,请指出。

论文:Green-Marl: A DSL for Easy and Efficient Graph Analysis

Github Repo:Green-Marl

Green Marl 简介

  1. Green Marl的两个目标:Performance 和 Implementation
  2. 具体实现方法:设计一个DSL(Domain-Specific language),用户利用Green Marl描述算法,而无需考虑在具体机器上的实现,同时需要指出其中的秉性部分。提供一个Green Marl的编译器,能将Green Marl编译成优化、并行化后的目标语言(不是机器语言,提供的例子是C++的后端)

Green Marl 语言设计

Scope of the Language

从数学上来说,G = (N,E) 其中N是所有的点集,而E是所有的边集。与图中每一个节点相关的数据可以看成一个从点N或者边E到值域的一个映射。作者将其称为node property

对于 G=(V,E) 以及 Π={P1,,Pn} Green Marl主要用于完成以下的图形分析:

  • 根据 (G,Π) 计算一个标量(如计算Conductance)
  • 根据 (G,Π) 计算一个新的属性 Pn+1 (如PagerRank)
  • 从原来的图中选出一个子图,如强连接图

根据上面的描述,Green-Marl做出了两个假设:

  1. 图是固定不变的,不能够被修改
  2. 图中的元素都是没有别名的

总的来说,Green-Marl会将图看成一个静态的对象。

Parallelism in Green-Marl

Green-Marl不是一个完全独立的语言,而是基于一些现有语言的功能:

  1. 包含了能够暗示可并行数据的语言特性
  2. 可以明确的标出可并行的部分(如利用foreach)

对于第二种可并行部分,使用的并行方式是fork-join的风格,对于并行中的可并行部分fork一个新的进程,并在最后的一个join-point进行同步。

Green-Marl使用静态范围规则(在并行化中的变量作用域)

Green-Marl的内存一致性模型与OpenMP类似:

  1. 对于共享变量的写不能保证对其他并行线程可见。
  2. 对于共享变量的写在当前的遍历(并行)结束后才能保证可以被看见。
  3. 如果有多个并行的线程对同一个变量写,只有一个可以被看见。

Green-Marl保证所有的写都是原子的。

如下代码就会存在违背上面一致性的问题:

Foreach(s:G.Nodes)
    Foreach(t:s.OutNbrs)
        t.A = t.A + s.B

其中出现了写写冲突与读写冲突。

Language Constructs

数据类型

  • 五种基本类型: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 入门 Part1:语言相关_第1张图片

其中要注意的是:

  1. 对collection的赋值其实是创建了collection的一个副本,在并行操作中,对于共享的collection的赋值是不被允许的。
  2. 并行的向Order中push数据是被允许的。push的顺序不是确定的,且push的数据不能保证立马被其他并行线程看见。Pop是否支持与编译器相关。
  3. 每种collection都可以被串行或者并行的被遍历,但是并行的在Order或者Sequence中遍历会出现遗失collection的顺序。
  4. 在遍历collection的时候禁止修改collection。
  5. 在并行处理的时候,一个collection只能并行的扩展或者并行的收缩,不能同时扩展与收缩。

Iterations and Traversals

在Green-Marl中的遍历格式如下:

Foreach(iterator:source(-).range)(filter)
    body_statement

Foreach代表着下面的部分可以并行执行,而如果使用For关键字,则代表着下面的操作将串行执行。iterator与source如字面意思。filter是一个可选项,其是一个bool表达式,决定当前的遍历是否执行body_statement。

与图有关的可遍历的source的range如下图所示:

Green Marl 入门 Part1:语言相关_第2张图片

  1. 可以通过Graph.Nodes来遍历节点,也可以通过collection来遍历。
  2. 可以通过以下几种方式来遍历一个节点的临接节点:
    1. InNbrs与OutNbrs 用来通过入边或者出边连接的节点(有向图),在无向图中,InNbrs与OutNbrs = Nbrs。
    2. UpNbrs和Down只有在从特定的点进行BFS时才有意义。

上面的表中Access列则表示的遍历的本质。线性遍历表示每一个iterator都指向一个唯一的元素,所有的元素都只会被访问到一次。而Random则可能会出现混淆(aliasing,应该是指多次访问同一个元素)。

对于Order和Set串行的遍历才能体现出来其中的顺序,逆序遍历的例子如下:

Node_Order(G) O;
For(o:O-.Items) //reverse order iteration on O
    body_statement
  1. Green-Marl也提供了两种图的遍历模式:BFS和DFS。示例如下:

    InBFS (iter:src^.Nodes From root)[navigator](filter1)
    forward_body_statement
    InRBFS (filter2)
    backward_body_statement
    1. root defines the root node of BFS

    2. ^ means that we first create a transposed version of graph

      Green Marl 入门 Part1:语言相关_第3张图片

    3. 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中正序和逆序的遍历为InDFSInPost,同样也可以拥有两个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
    }

Deferred Assignment 延后操作

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的写在这里可见

Reductions 归约

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做出判断。

Green Marl 入门 Part1:语言相关_第4张图片

其中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

GreenMarl 本地编译环境(C++实现)搭建

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

Green-Marl 在macOS下搭建环境:

由于macOS原生的GCC版本太低为(4.2)版本,故使用port安装gcc5.0版本。

此版本执行文件为:gxx-mp-5。

现在可以在.bashrc中将gcc链接到此执行文件,也可以修改Makefile避免影响其他项目。这里修改了Makefile中的CC,可以正常在macOS下编译成功。

你可能感兴趣的:(算法,并行计算,C++,图计算,编译原理)