"git@developer.sourcefind.cn:OpenDAS/dgl.git" did not exist on "f0213d2163245cd0f0a90fc8aa8e66e94fd3724c"
Unverified Commit 04d4680d authored by Da Zheng's avatar Da Zheng Committed by GitHub
Browse files

[Distributed] add constraints for metis partitioning. (#1708)

* add constraint.

* fix bugs.

* temp fix.

* fix lint

* add balance_ntypes and balance_edges

* add comments.

* fix.

* fix
parent 7ee72b66
...@@ -172,9 +172,10 @@ class GraphOp { ...@@ -172,9 +172,10 @@ class GraphOp {
* The partitioning algorithm assigns each vertex to a partition. * The partitioning algorithm assigns each vertex to a partition.
* \param graph The input graph * \param graph The input graph
* \param k The number of partitions. * \param k The number of partitions.
* \param vwgt the vertex weight array.
* \return The partition assignments of all vertices. * \return The partition assignments of all vertices.
*/ */
static IdArray MetisPartition(GraphPtr graph, int32_t k); static IdArray MetisPartition(GraphPtr graph, int32_t k, NDArray vwgt);
}; };
} // namespace dgl } // namespace dgl
......
...@@ -178,7 +178,7 @@ def load_partition_book(conf_file, part_id, graph=None): ...@@ -178,7 +178,7 @@ def load_partition_book(conf_file, part_id, graph=None):
return GraphPartitionBook(part_id, num_parts, node_map, edge_map, graph) return GraphPartitionBook(part_id, num_parts, node_map, edge_map, graph)
def partition_graph(g, graph_name, num_parts, out_path, num_hops=1, part_method="metis", def partition_graph(g, graph_name, num_parts, out_path, num_hops=1, part_method="metis",
reshuffle=True): reshuffle=True, balance_ntypes=None, balance_edges=False):
''' Partition a graph for distributed training and store the partitions on files. ''' Partition a graph for distributed training and store the partitions on files.
The partitioning occurs in three steps: 1) run a partition algorithm (e.g., Metis) to The partitioning occurs in three steps: 1) run a partition algorithm (e.g., Metis) to
...@@ -213,6 +213,16 @@ def partition_graph(g, graph_name, num_parts, out_path, num_hops=1, part_method= ...@@ -213,6 +213,16 @@ def partition_graph(g, graph_name, num_parts, out_path, num_hops=1, part_method=
* "orig_id" exists when reshuffle=True. It indicates the original node Ids in the original * "orig_id" exists when reshuffle=True. It indicates the original node Ids in the original
graph before reshuffling. graph before reshuffling.
When performing Metis partitioning, we can put some constraint on the partitioning.
Current, it supports two constrants to balance the partitioning. By default, Metis
always tries to balance the number of nodes in each partition.
* `balance_ntypes` balances the number of nodes of different types in each partition.
* `balance_edges` balances the number of edges in each partition.
To balance the node types, a user needs to pass a vector of N elements to indicate
the type of each node. N is the number of nodes in the input graph.
Parameters Parameters
---------- ----------
g : DGLGraph g : DGLGraph
...@@ -230,6 +240,10 @@ def partition_graph(g, graph_name, num_parts, out_path, num_hops=1, part_method= ...@@ -230,6 +240,10 @@ def partition_graph(g, graph_name, num_parts, out_path, num_hops=1, part_method=
reshuffle : bool reshuffle : bool
Reshuffle nodes and edges so that nodes and edges in a partition are in Reshuffle nodes and edges so that nodes and edges in a partition are in
contiguous Id range. contiguous Id range.
balance_ntypes : tensor
Node type of each node
balance_edges : bool
Indicate whether to balance the edges.
''' '''
if num_parts == 1: if num_parts == 1:
client_parts = {0: g} client_parts = {0: g}
...@@ -242,7 +256,8 @@ def partition_graph(g, graph_name, num_parts, out_path, num_hops=1, part_method= ...@@ -242,7 +256,8 @@ def partition_graph(g, graph_name, num_parts, out_path, num_hops=1, part_method=
g.ndata['orig_id'] = F.arange(0, g.number_of_nodes()) g.ndata['orig_id'] = F.arange(0, g.number_of_nodes())
g.edata['orig_id'] = F.arange(0, g.number_of_edges()) g.edata['orig_id'] = F.arange(0, g.number_of_edges())
elif part_method == 'metis': elif part_method == 'metis':
node_parts = metis_partition_assignment(g, num_parts) node_parts = metis_partition_assignment(g, num_parts, balance_ntypes=balance_ntypes,
balance_edges=balance_edges)
client_parts = partition_graph_with_halo(g, node_parts, num_hops, reshuffle=reshuffle) client_parts = partition_graph_with_halo(g, node_parts, num_hops, reshuffle=reshuffle)
elif part_method == 'random': elif part_method == 'random':
node_parts = random_choice(num_parts, g.number_of_nodes()) node_parts = random_choice(num_parts, g.number_of_nodes())
......
...@@ -597,15 +597,12 @@ def partition_graph_with_halo(g, node_part, extra_cached_hops, reshuffle=False): ...@@ -597,15 +597,12 @@ def partition_graph_with_halo(g, node_part, extra_cached_hops, reshuffle=False):
------------ ------------
g: DGLGraph g: DGLGraph
The graph to be partitioned The graph to be partitioned
node_part: 1D tensor node_part: 1D tensor
Specify which partition a node is assigned to. The length of this tensor Specify which partition a node is assigned to. The length of this tensor
needs to be the same as the number of nodes of the graph. Each element needs to be the same as the number of nodes of the graph. Each element
indicates the partition Id of a node. indicates the partition Id of a node.
extra_cached_hops: int extra_cached_hops: int
The number of hops a HALO node can be accessed. The number of hops a HALO node can be accessed.
reshuffle : bool reshuffle : bool
Resuffle nodes so that nodes in the same partition are in the same Id range. Resuffle nodes so that nodes in the same partition are in the same Id range.
...@@ -656,9 +653,19 @@ def partition_graph_with_halo(g, node_part, extra_cached_hops, reshuffle=False): ...@@ -656,9 +653,19 @@ def partition_graph_with_halo(g, node_part, extra_cached_hops, reshuffle=False):
subg_dict[i] = subg subg_dict[i] = subg
return subg_dict return subg_dict
def metis_partition_assignment(g, k): def metis_partition_assignment(g, k, balance_ntypes=None, balance_edges=False):
''' This assigns nodes to different partitions with Metis partitioning algorithm. ''' This assigns nodes to different partitions with Metis partitioning algorithm.
When performing Metis partitioning, we can put some constraint on the partitioning.
Current, it supports two constrants to balance the partitioning. By default, Metis
always tries to balance the number of nodes in each partition.
* `balance_ntypes` balances the number of nodes of different types in each partition.
* `balance_edges` balances the number of edges in each partition.
To balance the node types, a user needs to pass a vector of N elements to indicate
the type of each node. N is the number of nodes in the input graph.
After the partition assignment, we construct partitions. After the partition assignment, we construct partitions.
Parameters Parameters
...@@ -667,6 +674,10 @@ def metis_partition_assignment(g, k): ...@@ -667,6 +674,10 @@ def metis_partition_assignment(g, k):
The graph to be partitioned The graph to be partitioned
k : int k : int
The number of partitions. The number of partitions.
balance_ntypes : tensor
Node type of each node
balance_edges : bool
Indicate whether to balance the edges.
Returns Returns
------- -------
...@@ -676,14 +687,49 @@ def metis_partition_assignment(g, k): ...@@ -676,14 +687,49 @@ def metis_partition_assignment(g, k):
# METIS works only on symmetric graphs. # METIS works only on symmetric graphs.
# The METIS runs on the symmetric graph to generate the node assignment to partitions. # The METIS runs on the symmetric graph to generate the node assignment to partitions.
sym_g = to_bidirected(g, readonly=True) sym_g = to_bidirected(g, readonly=True)
node_part = _CAPI_DGLMetisPartition(sym_g._graph, k) vwgt = []
# To balance the node types in each partition, we can take advantage of the vertex weights
# in Metis. When vertex weights are provided, Metis will tries to generate partitions with
# balanced vertex weights. A vertex can be assigned with multiple weights. The vertex weights
# are stored in a vector of N * w elements, where N is the number of vertices and w
# is the number of weights per vertex. Metis tries to balance the first weight, and then
# the second weight, and so on.
# When balancing node types, we use the first weight to indicate the first node type.
# if a node belongs to the first node type, its weight is set to 1; otherwise, 0.
# Similary, we set the second weight for the second node type and so on. The number
# of weights is the same as the number of node types.
if balance_ntypes is not None:
assert len(balance_ntypes) == g.number_of_nodes(), \
"The length of balance_ntypes should be equal to #nodes in the graph"
balance_ntypes = utils.toindex(balance_ntypes)
balance_ntypes = balance_ntypes.tousertensor()
uniq_ntypes = F.unique(balance_ntypes)
for ntype in uniq_ntypes:
vwgt.append(F.astype(balance_ntypes == ntype, F.int64))
# When balancing edges in partitions, we use in-degree as one of the weights.
if balance_edges:
vwgt.append(F.astype(g.in_degrees(), F.int64))
# The vertex weights have to be stored in a vector.
if len(vwgt) > 0:
vwgt = F.stack(vwgt, 1)
shape = (np.prod(F.shape(vwgt),),)
vwgt = F.reshape(vwgt, shape)
vwgt = F.zerocopy_to_dgl_ndarray(vwgt)
else:
vwgt = F.zeros((0,), F.int64, F.cpu())
vwgt = F.zerocopy_to_dgl_ndarray(vwgt)
node_part = _CAPI_DGLMetisPartition(sym_g._graph, k, vwgt)
if len(node_part) == 0: if len(node_part) == 0:
return None return None
else: else:
node_part = utils.toindex(node_part) node_part = utils.toindex(node_part)
return node_part.tousertensor() return node_part.tousertensor()
def metis_partition(g, k, extra_cached_hops=0, reshuffle=False): def metis_partition(g, k, extra_cached_hops=0, reshuffle=False,
balance_ntypes=None, balance_edges=False):
''' This is to partition a graph with Metis partitioning. ''' This is to partition a graph with Metis partitioning.
Metis assigns vertices to partitions. This API constructs subgraphs with the vertices assigned Metis assigns vertices to partitions. This API constructs subgraphs with the vertices assigned
...@@ -691,6 +737,16 @@ def metis_partition(g, k, extra_cached_hops=0, reshuffle=False): ...@@ -691,6 +737,16 @@ def metis_partition(g, k, extra_cached_hops=0, reshuffle=False):
not belong to the partition of a subgraph but are connected to the nodes not belong to the partition of a subgraph but are connected to the nodes
in the partition within a fixed number of hops. in the partition within a fixed number of hops.
When performing Metis partitioning, we can put some constraint on the partitioning.
Current, it supports two constrants to balance the partitioning. By default, Metis
always tries to balance the number of nodes in each partition.
* `balance_ntypes` balances the number of nodes of different types in each partition.
* `balance_edges` balances the number of edges in each partition.
To balance the node types, a user needs to pass a vector of N elements to indicate
the type of each node. N is the number of nodes in the input graph.
If `reshuffle` is turned on, the function reshuffles node Ids and edge Ids If `reshuffle` is turned on, the function reshuffles node Ids and edge Ids
of the input graph before partitioning. After reshuffling, all nodes and edges of the input graph before partitioning. After reshuffling, all nodes and edges
in a partition fall in a contiguous Id range in the input graph. in a partition fall in a contiguous Id range in the input graph.
...@@ -705,26 +761,24 @@ def metis_partition(g, k, extra_cached_hops=0, reshuffle=False): ...@@ -705,26 +761,24 @@ def metis_partition(g, k, extra_cached_hops=0, reshuffle=False):
------------ ------------
g: DGLGraph g: DGLGraph
The graph to be partitioned The graph to be partitioned
k: int k: int
The number of partitions. The number of partitions.
extra_cached_hops: int extra_cached_hops: int
The number of hops a HALO node can be accessed. The number of hops a HALO node can be accessed.
reshuffle : bool reshuffle : bool
Resuffle nodes so that nodes in the same partition are in the same Id range. Resuffle nodes so that nodes in the same partition are in the same Id range.
balance_ntypes : tensor
Node type of each node
balance_edges : bool
Indicate whether to balance the edges.
Returns Returns
-------- --------
a dict of DGLGraphs a dict of DGLGraphs
The key is the partition Id and the value is the DGLGraph of the partition. The key is the partition Id and the value is the DGLGraph of the partition.
''' '''
# METIS works only on symmetric graphs. node_part = metis_partition_assignment(g, k, balance_ntypes, balance_edges)
# The METIS runs on the symmetric graph to generate the node assignment to partitions. if node_part is None:
sym_g = to_bidirected(g, readonly=True)
node_part = _CAPI_DGLMetisPartition(sym_g._graph, k)
if len(node_part) == 0:
return None return None
# Then we split the original graph into parts based on the METIS partitioning results. # Then we split the original graph into parts based on the METIS partitioning results.
......
...@@ -16,7 +16,7 @@ using namespace dgl::runtime; ...@@ -16,7 +16,7 @@ using namespace dgl::runtime;
namespace dgl { namespace dgl {
IdArray GraphOp::MetisPartition(GraphPtr g, int k) { IdArray GraphOp::MetisPartition(GraphPtr g, int k, NDArray vwgt_arr) {
// The index type of Metis needs to be compatible with DGL index type. // The index type of Metis needs to be compatible with DGL index type.
CHECK_EQ(sizeof(idx_t), sizeof(dgl_id_t)); CHECK_EQ(sizeof(idx_t), sizeof(dgl_id_t));
ImmutableGraphPtr ig = std::dynamic_pointer_cast<ImmutableGraph>(g); ImmutableGraphPtr ig = std::dynamic_pointer_cast<ImmutableGraph>(g);
...@@ -32,11 +32,23 @@ IdArray GraphOp::MetisPartition(GraphPtr g, int k) { ...@@ -32,11 +32,23 @@ IdArray GraphOp::MetisPartition(GraphPtr g, int k) {
IdArray part_arr = aten::NewIdArray(nvtxs); IdArray part_arr = aten::NewIdArray(nvtxs);
idx_t objval = 0; idx_t objval = 0;
idx_t *part = static_cast<idx_t*>(part_arr->data); idx_t *part = static_cast<idx_t*>(part_arr->data);
int64_t vwgt_len = vwgt_arr->shape[0];
CHECK_EQ(sizeof(idx_t), vwgt_arr->dtype.bits / 8)
<< "The vertex weight array doesn't have right type";
CHECK(vwgt_len % g->NumVertices() == 0)
<< "The vertex weight array doesn't have right number of elements";
idx_t *vwgt = NULL;
if (vwgt_len > 0) {
ncon = vwgt_len / g->NumVertices();
vwgt = static_cast<idx_t*>(vwgt_arr->data);
}
int ret = METIS_PartGraphKway(&nvtxs, // The number of vertices int ret = METIS_PartGraphKway(&nvtxs, // The number of vertices
&ncon, // The number of balancing constraints. &ncon, // The number of balancing constraints.
xadj, // indptr xadj, // indptr
adjncy, // indices adjncy, // indices
NULL, // the weights of the vertices vwgt, // the weights of the vertices
NULL, // The size of the vertices for computing NULL, // The size of the vertices for computing
// the total communication volume // the total communication volume
NULL, // The weights of the edges NULL, // The weights of the edges
...@@ -69,7 +81,8 @@ DGL_REGISTER_GLOBAL("transform._CAPI_DGLMetisPartition") ...@@ -69,7 +81,8 @@ DGL_REGISTER_GLOBAL("transform._CAPI_DGLMetisPartition")
.set_body([] (DGLArgs args, DGLRetValue* rv) { .set_body([] (DGLArgs args, DGLRetValue* rv) {
GraphRef g = args[0]; GraphRef g = args[0];
int k = args[1]; int k = args[1];
*rv = GraphOp::MetisPartition(g.sptr(), k); NDArray vwgt = args[2];
*rv = GraphOp::MetisPartition(g.sptr(), k, vwgt);
}); });
} // namespace dgl } // namespace dgl
......
...@@ -255,9 +255,33 @@ def test_metis_partition(): ...@@ -255,9 +255,33 @@ def test_metis_partition():
check_metis_partition(g, 0) check_metis_partition(g, 0)
check_metis_partition(g, 1) check_metis_partition(g, 1)
check_metis_partition(g, 2) check_metis_partition(g, 2)
check_metis_partition_with_constraint(g)
def check_metis_partition_with_constraint(g):
ntypes = np.zeros((g.number_of_nodes(),), dtype=np.int32)
ntypes[0:int(g.number_of_nodes()/4)] = 1
ntypes[int(g.number_of_nodes()*3/4):] = 2
subgs = dgl.transform.metis_partition(g, 4, extra_cached_hops=1, balance_ntypes=ntypes)
if subgs is not None:
for i in subgs:
subg = subgs[i]
parent_nids = F.asnumpy(subg.ndata[dgl.NID])
sub_ntypes = ntypes[parent_nids]
print('type0:', np.sum(sub_ntypes == 0))
print('type1:', np.sum(sub_ntypes == 1))
print('type2:', np.sum(sub_ntypes == 2))
subgs = dgl.transform.metis_partition(g, 4, extra_cached_hops=1,
balance_ntypes=ntypes, balance_edges=True)
if subgs is not None:
for i in subgs:
subg = subgs[i]
parent_nids = F.asnumpy(subg.ndata[dgl.NID])
sub_ntypes = ntypes[parent_nids]
print('type0:', np.sum(sub_ntypes == 0))
print('type1:', np.sum(sub_ntypes == 1))
print('type2:', np.sum(sub_ntypes == 2))
def check_metis_partition(g, extra_hops): def check_metis_partition(g, extra_hops):
# partitions with 1-hop HALO nodes
subgs = dgl.transform.metis_partition(g, 4, extra_cached_hops=extra_hops) subgs = dgl.transform.metis_partition(g, 4, extra_cached_hops=extra_hops)
num_inner_nodes = 0 num_inner_nodes = 0
num_inner_edges = 0 num_inner_edges = 0
...@@ -274,7 +298,7 @@ def check_metis_partition(g, extra_hops): ...@@ -274,7 +298,7 @@ def check_metis_partition(g, extra_hops):
if extra_hops == 0: if extra_hops == 0:
return return
# partitions with 1-hop HALO nodes and reshuffling nodes # partitions with node reshuffling
subgs = dgl.transform.metis_partition(g, 4, extra_cached_hops=extra_hops, reshuffle=True) subgs = dgl.transform.metis_partition(g, 4, extra_cached_hops=extra_hops, reshuffle=True)
num_inner_nodes = 0 num_inner_nodes = 0
num_inner_edges = 0 num_inner_edges = 0
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment