Files
2026-07-13 13:35:51 +08:00

354 lines
11 KiB
Python

r"""Libra partition functions.
Libra partition is a vertex-cut based partitioning algorithm from
`Distributed Power-law Graph Computing:
Theoretical and Empirical Analysis
<https://proceedings.neurips.cc/paper/2014/file/67d16d00201083a2b118dd5128dd6f59-Paper.pdf>`__
from Xie et al.
"""
# Copyright (c) 2021 Intel Corporation
# \file distgnn/partition/libra_partition.py
# \brief Libra - Vertex-cut based graph partitioner for distributed training
# \author Vasimuddin Md <vasimuddin.md@intel.com>,
# Guixiang Ma <guixiang.ma@intel.com>
# Sanchit Misra <sanchit.misra@intel.com>,
# Ramanarayan Mohanty <ramanarayan.mohanty@intel.com>,
# Sasikanth Avancha <sasikanth.avancha@intel.com>
# Nesreen K. Ahmed <nesreen.k.ahmed@intel.com>
# \cite Distributed Power-law Graph Computing: Theoretical and Empirical Analysis
import json
import os
import time
import torch as th
from dgl import DGLGraph
from dgl._sparse_ops import (
libra2dgl_build_adjlist,
libra2dgl_build_dict,
libra2dgl_set_lr,
libra_vertex_cut,
)
from dgl.base import DGLError
from dgl.data.utils import save_graphs, save_tensors
def libra_partition(num_community, G, resultdir):
"""
Performs vertex-cut based graph partitioning and converts the partitioning
output to DGL input format.
Parameters
----------
num_community : Number of partitions to create
G : Input graph to be partitioned
resultdir : Output location for storing the partitioned graphs
Output
------
1. Creates X partition folder as XCommunities (say, X=2, so, 2Communities)
XCommunities contains file name communityZ.txt per partition Z (Z <- 0 .. X-1);
each such file contains a list of edges assigned to that partition.
These files constitute the output of Libra graph partitioner
(An intermediate result of this function).
2. The folder also contains partZ folders, each of these folders stores
DGL/DistGNN graphs for the Z partitions;
these graph files are used as input to DistGNN.
3. The folder also contains a json file which contains partitions' information.
"""
num_nodes = G.num_nodes() # number of nodes
num_edges = G.num_edges() # number of edges
print("Number of nodes in the graph: ", num_nodes)
print("Number of edges in the graph: ", num_edges)
in_d = G.in_degrees()
out_d = G.out_degrees()
node_degree = in_d + out_d
edgenum_unassigned = node_degree.clone()
u_t, v_t = G.edges()
weight_ = th.ones(u_t.shape[0], dtype=th.int64)
community_weights = th.zeros(num_community, dtype=th.int64)
# self_loop = 0
# for p, q in zip(u_t, v_t):
# if p == q:
# self_loop += 1
# print("#self loops in the dataset: ", self_loop)
# del G
## call to C/C++ code
out = th.zeros(u_t.shape[0], dtype=th.int32)
libra_vertex_cut(
num_community,
node_degree,
edgenum_unassigned,
community_weights,
u_t,
v_t,
weight_,
out,
num_nodes,
num_edges,
resultdir,
)
print("Max partition size: ", int(community_weights.max()))
print(" ** Converting libra partitions to dgl graphs **")
fsize = int(community_weights.max()) + 1024 ## max edges in partition
# print("fsize: ", fsize, flush=True)
node_map = th.zeros(num_community, dtype=th.int64)
indices = th.zeros(num_nodes, dtype=th.int64)
lrtensor = th.zeros(num_nodes, dtype=th.int64)
gdt_key = th.zeros(num_nodes, dtype=th.int64)
gdt_value = th.zeros([num_nodes, num_community], dtype=th.int64)
offset = th.zeros(1, dtype=th.int64)
ldt_ar = []
gg_ar = [DGLGraph() for i in range(num_community)]
part_nodes = []
print(">>> ", "num_nodes ", " ", "num_edges")
## Iterator over number of partitions
for i in range(num_community):
g = gg_ar[i]
a_t = th.zeros(fsize, dtype=th.int64)
b_t = th.zeros(fsize, dtype=th.int64)
ldt_key = th.zeros(fsize, dtype=th.int64)
ldt_ar.append(ldt_key)
## building node, parition dictionary
## Assign local node ids and mapping to global node ids
ret = libra2dgl_build_dict(
a_t,
b_t,
indices,
ldt_key,
gdt_key,
gdt_value,
node_map,
offset,
num_community,
i,
fsize,
resultdir,
)
num_nodes_partition = int(ret[0])
num_edges_partition = int(ret[1])
part_nodes.append(num_nodes_partition)
print(">>> ", num_nodes_partition, " ", num_edges_partition)
g.add_edges(a_t[0:num_edges_partition], b_t[0:num_edges_partition])
########################################################
## fixing lr - 1-level tree for the split-nodes
libra2dgl_set_lr(gdt_key, gdt_value, lrtensor, num_community, num_nodes)
########################################################
# graph_name = dataset
graph_name = resultdir.split("_")[-1].split("/")[0]
part_method = "Libra"
num_parts = num_community ## number of paritions/communities
num_hops = 0
node_map_val = node_map.tolist()
edge_map_val = 0
out_path = resultdir
part_metadata = {
"graph_name": graph_name,
"num_nodes": G.num_nodes(),
"num_edges": G.num_edges(),
"part_method": part_method,
"num_parts": num_parts,
"halo_hops": num_hops,
"node_map": node_map_val,
"edge_map": edge_map_val,
}
############################################################
for i in range(num_community):
g = gg_ar[0]
num_nodes_partition = part_nodes[i]
adj = th.zeros([num_nodes_partition, num_community - 1], dtype=th.int64)
inner_node = th.zeros(num_nodes_partition, dtype=th.int32)
lr_t = th.zeros(num_nodes_partition, dtype=th.int64)
ldt = ldt_ar[0]
try:
feat = G.ndata["feat"]
except KeyError:
feat = G.ndata["features"]
try:
labels = G.ndata["label"]
except KeyError:
labels = G.ndata["labels"]
trainm = G.ndata["train_mask"].int()
testm = G.ndata["test_mask"].int()
valm = G.ndata["val_mask"].int()
feat_size = feat.shape[1]
gfeat = th.zeros([num_nodes_partition, feat_size], dtype=feat.dtype)
glabels = th.zeros(num_nodes_partition, dtype=labels.dtype)
gtrainm = th.zeros(num_nodes_partition, dtype=trainm.dtype)
gtestm = th.zeros(num_nodes_partition, dtype=testm.dtype)
gvalm = th.zeros(num_nodes_partition, dtype=valm.dtype)
## build remote node databse per local node
## gather feats, train, test, val, and labels for each partition
libra2dgl_build_adjlist(
feat,
gfeat,
adj,
inner_node,
ldt,
gdt_key,
gdt_value,
node_map,
lr_t,
lrtensor,
num_nodes_partition,
num_community,
i,
feat_size,
labels,
trainm,
testm,
valm,
glabels,
gtrainm,
gtestm,
gvalm,
feat.shape[0],
)
g.ndata["adj"] = adj ## database of remote clones
g.ndata["inner_node"] = inner_node ## split node '0' else '1'
g.ndata["feat"] = gfeat ## gathered features
g.ndata["lf"] = lr_t ## 1-level tree among split nodes
g.ndata["label"] = glabels
g.ndata["train_mask"] = gtrainm
g.ndata["test_mask"] = gtestm
g.ndata["val_mask"] = gvalm
# Validation code, run only small graphs
# for l in range(num_nodes_partition):
# index = int(ldt[l])
# assert glabels[l] == labels[index]
# assert gtrainm[l] == trainm[index]
# assert gtestm[l] == testm[index]
# for j in range(feat_size):
# assert gfeat[l][j] == feat[index][j]
print("Writing partition {} to file".format(i), flush=True)
part = g
part_id = i
part_dir = os.path.join(out_path, "part" + str(part_id))
node_feat_file = os.path.join(part_dir, "node_feat.dgl")
edge_feat_file = os.path.join(part_dir, "edge_feat.dgl")
part_graph_file = os.path.join(part_dir, "graph.dgl")
part_metadata["part-{}".format(part_id)] = {
"node_feats": node_feat_file,
"edge_feats": edge_feat_file,
"part_graph": part_graph_file,
}
os.makedirs(part_dir, mode=0o775, exist_ok=True)
save_tensors(node_feat_file, part.ndata)
save_graphs(part_graph_file, [part])
del g
del gg_ar[0]
del ldt
del ldt_ar[0]
with open("{}/{}.json".format(out_path, graph_name), "w") as outfile:
json.dump(part_metadata, outfile, sort_keys=True, indent=4)
print("Conversion libra2dgl completed !!!")
def partition_graph(num_community, G, resultdir):
"""
Performs vertex-cut based graph partitioning and converts the partitioning
output to DGL input format.
Given a graph, this function will create a folder named ``XCommunities`` where ``X``
stands for the number of communities. It will contain ``X`` files named
``communityZ.txt`` for each partition Z (from 0 to X-1);
each such file contains a list of edges assigned to that partition.
These files constitute the output of Libra graph partitioner.
The folder also contains X subfolders named ``partZ``, each of these folders stores
DGL/DistGNN graphs for partition Z; these graph files are used as input to
DistGNN.
The folder also contains a json file which contains partitions' information.
Currently we require the graph's node data to contain the following columns:
* ``features`` for node features.
* ``label`` for node labels.
* ``train_mask`` as a boolean mask of training node set.
* ``val_mask`` as a boolean mask of validation node set.
* ``test_mask`` as a boolean mask of test node set.
Parameters
----------
num_community : int
Number of partitions to create.
G : DGLGraph
Input graph to be partitioned.
resultdir : str
Output location for storing the partitioned graphs.
"""
print("num partitions: ", num_community)
print("output location: ", resultdir)
## create ouptut directory
try:
os.makedirs(resultdir, mode=0o775, exist_ok=True)
except:
raise DGLError("Error: Could not create directory: ", resultdir)
tic = time.time()
print(
"####################################################################"
)
print("Executing parititons: ", num_community)
ltic = time.time()
try:
resultdir = os.path.join(resultdir, str(num_community) + "Communities")
os.makedirs(resultdir, mode=0o775, exist_ok=True)
except:
raise DGLError("Error: Could not create sub-directory: ", resultdir)
## Libra partitioning
libra_partition(num_community, G, resultdir)
ltoc = time.time()
print(
"Time taken by {} partitions {:0.4f} sec".format(
num_community, ltoc - ltic
)
)
print()
toc = time.time()
print(
"Generated ",
num_community,
" partitions in {:0.4f} sec".format(toc - tic),
flush=True,
)
print("Partitioning completed successfully !!!")