7.4 高级图划分

本章涵盖图划分的一些高级主题。

METIS 划分算法

METIS 是一种最先进的图划分算法,可以生成跨分区的边数量最少的划分,这使得它适用于分布式消息传递,因为网络通信量与跨分区的边数量成比例。DGL 在其 dgl.distributed.partition_graph() API 中集成了 METIS 作为默认的划分算法。

输出格式

无论使用哪种划分算法,划分结果都存储在数据文件中,组织方式如下:

data_root_dir/
  |-- graph_name.json       # partition configuration file in JSON
  |-- part0/                # data for partition 0
  |  |-- node_feats.dgl     # node features stored in binary format
  |  |-- edge_feats.dgl     # edge features stored in binary format
  |  |-- graph.dgl          # graph structure of this partition stored in binary format
  |
  |-- part1/                # data for partition 1
  |  |-- node_feats.dgl
  |  |-- edge_feats.dgl
  |  |-- graph.dgl
  |
  |-- ...                   # data for other partitions

当分发到集群时,元数据 JSON 应该复制到所有机器,而 partX 文件夹应该相应地分派。

DGL 提供 dgl.distributed.load_partition() 函数来加载单个分区进行检查。

>>> import dgl
>>> # load partition 0
>>> part_data = dgl.distributed.load_partition('data_root_dir/graph_name.json', 0)
>>> g, nfeat, efeat, partition_book, graph_name, ntypes, etypes = part_data  # unpack
>>> print(g)
Graph(num_nodes=966043, num_edges=34270118,
      ndata_schemes={'orig_id': Scheme(shape=(), dtype=torch.int64),
                     'part_id': Scheme(shape=(), dtype=torch.int64),
                     '_ID': Scheme(shape=(), dtype=torch.int64),
                     'inner_node': Scheme(shape=(), dtype=torch.int32)}
      edata_schemes={'_ID': Scheme(shape=(), dtype=torch.int64),
                     'inner_edge': Scheme(shape=(), dtype=torch.int8),
                     'orig_id': Scheme(shape=(), dtype=torch.int64)})

`ID mapping`_部分所述,每个分区都包含作为 ndata 或 edata 保存的辅助信息,例如原始节点/边 ID、分区 ID 等。每个分区不仅保存其拥有的节点/边,还包括与分区相邻的节点/边(称为 HALO 节点/边)。inner_nodeinner_edge 指示节点/边是否真正属于该分区(值为 True)还是 HALO 节点/边(值为 False)。

通过 load_partition() 函数一次性加载所有数据。用户可以使用 dgl.distributed.load_partition_feats()dgl.distributed.load_partition_book() API 分别加载特征或分区簿。

并行 METIS 划分

对于需要并行预处理的大规模图,DGL 支持 ParMETIS 作为其中一种划分算法选择。

注意

因为 ParMETIS 不支持异构图,用户需要在运行 ParMETIS 前后进行 ID 转换。请查看 第 7.5 章 异构图深入解析 获取解释。

注意

请确保 ParMETIS 的输入图不包含重复边(或平行边)和自环边。

ParMETIS 安装

ParMETIS 需要 METIS 和 GKLib。请按照此处的说明编译和安装 GKLib。对于 METIS 的编译和安装,请按照下面的说明使用 GIT 克隆 METIS 并编译使其支持 int64。

git clone https://github.com/KarypisLab/METIS.git
make config shared=1 cc=gcc prefix=~/local i64=1
make install

目前,我们需要手动编译和安装 ParMETIS。我们克隆 ParMETIS 的 DGL 分支如下:

git clone --branch dgl https://github.com/KarypisLab/ParMETIS.git

然后编译并安装 ParMETIS。

make config cc=mpicc prefix=~/local
make install

在运行 ParMETIS 之前,我们需要设置两个环境变量:PATHLD_LIBRARY_PATH

export PATH=$PATH:$HOME/local/bin
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$HOME/local/lib/

输入格式

注意

作为先决条件,请阅读 guide-distributed-hetero 章以了解 DGL 如何组织异构图进行分布式训练。

ParMETIS 的输入图存储在三个文件中,名称如下:xxx_nodes.txtxxx_edges.txtxxx_stats.txt,其中 xxx 是图的名称。

xxx_nodes.txt 中的每一行存储一个节点的信息。行 ID 也是节点的齐次 ID,例如,第 0 行对应节点 0;第 1 行对应节点 1 等。每行的格式如下:

<node_type_id> <node_weight_list> <type_wise_node_id>

所有字段都以空格分隔

  • <node_type_id> 是从 0 开始的整数。每个节点类型都被映射到一个整数。对于齐次图,其值始终为 0。

  • <node_weight_list> 是整数(以空格分隔),表示 ParMETIS 用于平衡图分区的节点权重。对于齐次图,此列表只有一个整数;而对于包含 \(T\) 个节点类型的异构图,此列表应包含 \(T\) 个整数。如果节点属于节点类型 \(t\),则除第 \(t\) 个整数外,所有整数都为零;第 \(t\) 个整数是该节点的权重。ParMETIS 将尝试平衡每个分区的总节点权重。对于异构图,它将尝试将相同类型的节点分布到所有分区。推荐的节点权重是 1(用于平衡每个分区中的节点数量)或节点度数(用于平衡每个分区中的边数量)。

  • <type_wise_node_id> 是一个整数,表示节点在其自身类型中的 ID。

下面展示了一个包含两种节点类型的异构图的节点文件示例。节点类型 0 有三个节点;节点类型 1 有四个节点。它使用两个节点权重来确保 ParMETIS 生成的分区中类型 0 和类型 1 的节点数量大致相同。

0 1 0 0
0 1 0 1
0 1 0 2
1 0 1 0
1 0 1 1
1 0 1 2
1 0 1 3

类似地,xxx_edges.txt 中的每一行存储一条边信息。行 ID 也是边的齐次 ID,例如,第 0 行对应边 0;第 1 行对应边 1 等。每行的格式如下:

<src_node_id> <dst_node_id> <type_wise_edge_id> <edge_type_id>

所有字段都以空格分隔

  • <src_node_id> 是源节点的齐次 ID。

  • <dst_node_id> 是目标节点的齐次 ID。

  • <type_wise_edge_id> 是该边类型中的边 ID。

  • <edge_type_id> 是从 0 开始的整数。每个边类型都被映射到一个整数。对于齐次图,其值始终为 0。

xxx_stats.txt 存储图的一些基本统计信息。它只有一行,包含三个字段,以空格分隔:

<num_nodes> <num_edges> <total_node_weights>
  • num_nodes 存储节点总数,不区分节点类型。

  • num_edges 存储边总数,不区分边类型。

  • total_node_weights 存储节点文件中的节点权重数量。

运行 ParMETIS 并输出格式

ParMETIS 包含一个名为 pm_dglpart 的命令,它从调用 pm_dglpart 的机器加载存储在三个文件中的图,将数据分发到集群中的所有机器,然后调用 ParMETIS 对图进行划分。完成后,它会为每个分区生成三个文件:p<part_id>-xxx_nodes.txtp<part_id>-xxx_edges.txtp<part_id>-xxx_stats.txt

注意

ParMETIS 在划分过程中会重新分配节点的 ID。ID 重新分配后,分区中的节点会被分配连续的 ID;此外,相同类型的节点也会被分配连续的 ID。

p<part_id>-xxx_nodes.txt 存储分区中的节点数据。每行代表一个节点,包含以下字段:

<node_id> <node_type_id> <node_weight_list> <type_wise_node_id>
  • <node_id> 是 ID 重新分配后的齐次节点 ID。

  • <node_type_id> 是节点类型 ID。

  • <node_weight_list> 是 ParMETIS 使用的节点权重(从输入文件复制)。

  • <type_wise_node_id> 是一个整数,表示节点在其自身类型中的 ID。

p<part_id>-xxx_edges.txt 存储分区中的边数据。每行代表一条边,包含以下字段:

<src_id> <dst_id> <orig_src_id> <orig_dst_id> <type_wise_edge_id> <edge_type_id>
  • <src_id> 是 ID 重新分配后源节点的齐次 ID。

  • <dst_id> 是 ID 重新分配后目标节点的齐次 ID。

  • <orig_src_id> 是输入图中的源节点的齐次 ID。

  • <orig_dst_id> 是输入图中的目标节点的齐次 ID。

  • <type_wise_edge_id> 是该边类型中的边 ID。

  • <edge_type_id> 是边类型 ID。

调用 pm_dglpart 时,三个输入文件:xxx_nodes.txtxxx_edges.txtxxx_stats.txt 应位于运行 pm_dglpart 的目录中。以下命令运行四个 ParMETIS 进程将名为 xxx 的图划分为八个分区(每个进程处理两个分区)。

mpirun -np 4 pm_dglpart xxx 2

ParMETIS 的输出文件需要转换为 划分分配格式,以便运行后续的预处理步骤。