dist_graph.py 25.9 KB
Newer Older
1
2
3
"""Define distributed graph."""

from collections.abc import MutableMapping
4
import os
5
import numpy as np
6

7
8
from ..heterograph import DGLHeteroGraph
from .. import heterograph_index
9
10
from .. import backend as F
from ..base import NID, EID
11
from .kvstore import KVServer, KVClient
12
from .standalone_kvstore import KVClient as SA_KVClient
13
14
15
from .._ffi.ndarray import empty_shared_mem
from ..frame import infer_scheme
from .partition import load_partition
Da Zheng's avatar
Da Zheng committed
16
from .graph_partition_book import PartitionPolicy, get_shared_mem_partition_book
17
from .graph_partition_book import NODE_PART_POLICY, EDGE_PART_POLICY
18
from .shared_mem_utils import _to_shared_mem, _get_ndata_path, _get_edata_path, DTYPE_DICT
19
from . import rpc
20
from .rpc_client import connect_to_server
21
22
from .server_state import ServerState
from .rpc_server import start_server
23
from .graph_services import find_edges as dist_find_edges
24
from .dist_tensor import DistTensor, _get_data_name
25
26

def _copy_graph_to_shared_mem(g, graph_name):
27
    new_g = g.shared_memory(graph_name, formats='csc')
28
29
    # We should share the node/edge data to the client explicitly instead of putting them
    # in the KVStore because some of the node/edge data may be duplicated.
Da Zheng's avatar
Da Zheng committed
30
31
32
33
    local_node_path = _get_ndata_path(graph_name, 'inner_node')
    new_g.ndata['inner_node'] = _to_shared_mem(g.ndata['inner_node'], local_node_path)
    local_edge_path = _get_edata_path(graph_name, 'inner_edge')
    new_g.edata['inner_edge'] = _to_shared_mem(g.edata['inner_edge'], local_edge_path)
34
35
    new_g.ndata[NID] = _to_shared_mem(g.ndata[NID], _get_ndata_path(graph_name, NID))
    new_g.edata[EID] = _to_shared_mem(g.edata[EID], _get_edata_path(graph_name, EID))
36
37
    return new_g

Da Zheng's avatar
Da Zheng committed
38
39
FIELD_DICT = {'inner_node': F.int64,
              'inner_edge': F.int64,
40
41
42
43
44
              NID: F.int64,
              EID: F.int64}

def _is_ndata_name(name):
    ''' Is this node data in the kvstore '''
45
    return name[:5] == NODE_PART_POLICY + ':'
46
47
48

def _is_edata_name(name):
    ''' Is this edge data in the kvstore '''
49
    return name[:5] == EDGE_PART_POLICY + ':'
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

def _get_shared_mem_ndata(g, graph_name, name):
    ''' Get shared-memory node data from DistGraph server.

    This is called by the DistGraph client to access the node data in the DistGraph server
    with shared memory.
    '''
    shape = (g.number_of_nodes(),)
    dtype = FIELD_DICT[name]
    dtype = DTYPE_DICT[dtype]
    data = empty_shared_mem(_get_ndata_path(graph_name, name), False, shape, dtype)
    dlpack = data.to_dlpack()
    return F.zerocopy_from_dlpack(dlpack)

def _get_shared_mem_edata(g, graph_name, name):
    ''' Get shared-memory edge data from DistGraph server.

    This is called by the DistGraph client to access the edge data in the DistGraph server
    with shared memory.
    '''
    shape = (g.number_of_edges(),)
    dtype = FIELD_DICT[name]
    dtype = DTYPE_DICT[dtype]
    data = empty_shared_mem(_get_edata_path(graph_name, name), False, shape, dtype)
    dlpack = data.to_dlpack()
    return F.zerocopy_from_dlpack(dlpack)

def _get_graph_from_shared_mem(graph_name):
    ''' Get the graph from the DistGraph server.

    The DistGraph server puts the graph structure of the local partition in the shared memory.
    The client can access the graph structure and some metadata on nodes and edges directly
    through shared memory to reduce the overhead of data access.
    '''
84
85
86
87
    g, ntypes, etypes = heterograph_index.create_heterograph_from_shared_memory(graph_name)
    if g is None:
        return None
    g = DGLHeteroGraph(g, ntypes, etypes)
Da Zheng's avatar
Da Zheng committed
88
89
    g.ndata['inner_node'] = _get_shared_mem_ndata(g, graph_name, 'inner_node')
    g.edata['inner_edge'] = _get_shared_mem_edata(g, graph_name, 'inner_edge')
90
91
92
93
94
95
96
97
98
99
100
101
102
103
    g.ndata[NID] = _get_shared_mem_ndata(g, graph_name, NID)
    g.edata[EID] = _get_shared_mem_edata(g, graph_name, EID)
    return g

class NodeDataView(MutableMapping):
    """The data view class when dist_graph.ndata[...].data is called.
    """
    __slots__ = ['_graph', '_data']

    def __init__(self, g):
        self._graph = g
        # When this is created, the server may already load node data. We need to
        # initialize the node data in advance.
        names = g._get_all_ndata_names()
104
105
106
107
108
109
110
        policy = PartitionPolicy(NODE_PART_POLICY, g.get_partition_book())
        self._data = {}
        for name in names:
            name1 = _get_data_name(name, policy.policy_str)
            dtype, shape, _ = g._client.get_data_meta(name1)
            # We create a wrapper on the existing tensor in the kvstore.
            self._data[name] = DistTensor(g, shape, dtype, name, part_policy=policy)
111
112
113
114
115
116
117
118

    def _get_names(self):
        return list(self._data.keys())

    def __getitem__(self, key):
        return self._data[key]

    def __setitem__(self, key, val):
119
        self._data[key] = val
120
121

    def __delitem__(self, key):
122
        del self._data[key]
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149

    def __len__(self):
        # The number of node data may change. Let's count it every time we need them.
        # It's not called frequently. It should be fine.
        return len(self._data)

    def __iter__(self):
        return iter(self._data)

    def __repr__(self):
        reprs = {}
        for name in self._data:
            dtype = F.dtype(self._data[name])
            shape = F.shape(self._data[name])
            reprs[name] = 'DistTensor(shape={}, dtype={})'.format(str(shape), str(dtype))
        return repr(reprs)

class EdgeDataView(MutableMapping):
    """The data view class when G.edges[...].data is called.
    """
    __slots__ = ['_graph', '_data']

    def __init__(self, g):
        self._graph = g
        # When this is created, the server may already load edge data. We need to
        # initialize the edge data in advance.
        names = g._get_all_edata_names()
150
151
152
153
154
155
156
        policy = PartitionPolicy(EDGE_PART_POLICY, g.get_partition_book())
        self._data = {}
        for name in names:
            name1 = _get_data_name(name, policy.policy_str)
            dtype, shape, _ = g._client.get_data_meta(name1)
            # We create a wrapper on the existing tensor in the kvstore.
            self._data[name] = DistTensor(g, shape, dtype, name, part_policy=policy)
157
158
159
160
161
162
163
164

    def _get_names(self):
        return list(self._data.keys())

    def __getitem__(self, key):
        return self._data[key]

    def __setitem__(self, key, val):
165
        self._data[key] = val
166
167

    def __delitem__(self, key):
168
        del self._data[key]
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198

    def __len__(self):
        # The number of edge data may change. Let's count it every time we need them.
        # It's not called frequently. It should be fine.
        return len(self._data)

    def __iter__(self):
        return iter(self._data)

    def __repr__(self):
        reprs = {}
        for name in self._data:
            dtype = F.dtype(self._data[name])
            shape = F.shape(self._data[name])
            reprs[name] = 'DistTensor(shape={}, dtype={})'.format(str(shape), str(dtype))
        return repr(reprs)


class DistGraphServer(KVServer):
    ''' The DistGraph server.

    This DistGraph server loads the graph data and sets up a service so that clients can read data
    of a graph partition (graph structure, node data and edge data) from remote machines.
    A server is responsible for one graph partition.

    Currently, each machine runs only one main server with a set of backup servers to handle
    clients' requests. The main server and the backup servers all handle the requests for the same
    graph partition. They all share the partition data (graph structure and node/edge data) with
    shared memory.

199
200
201
    By default, the partition data is shared with the DistGraph clients that run on
    the same machine. However, a user can disable shared memory option. This is useful for the case
    that a user wants to run the server and the client on different machines.
202
203
204
205
206

    Parameters
    ----------
    server_id : int
        The server ID (start from 0).
207
208
209
    ip_config : str
        Path of IP configuration file.
    num_clients : int
210
        Total number of client nodes.
211
    part_config : string
212
        The path of the config file generated by the partition tool.
213
214
    disable_shared_mem : bool
        Disable shared memory.
215
    '''
216
    def __init__(self, server_id, ip_config, num_clients, part_config, disable_shared_mem=False):
217
218
219
220
        super(DistGraphServer, self).__init__(server_id=server_id, ip_config=ip_config,
                                              num_clients=num_clients)
        self.ip_config = ip_config
        # Load graph partition data.
221
        self.client_g, node_feats, edge_feats, self.gpb, graph_name = load_partition(part_config,
222
223
                                                                                     server_id)
        print('load ' + graph_name)
224
225
        if not disable_shared_mem:
            self.client_g = _copy_graph_to_shared_mem(self.client_g, graph_name)
226

227
        # Init kvstore.
228
229
        if not disable_shared_mem:
            self.gpb.shared_memory(graph_name)
230
        assert self.gpb.partid == server_id
231
232
        self.add_part_policy(PartitionPolicy(NODE_PART_POLICY, self.gpb))
        self.add_part_policy(PartitionPolicy(EDGE_PART_POLICY, self.gpb))
233
234

        if not self.is_backup_server():
235
            for name in node_feats:
236
237
                self.init_data(name=_get_data_name(name, NODE_PART_POLICY),
                               policy_str=NODE_PART_POLICY,
238
                               data_tensor=node_feats[name])
239
            for name in edge_feats:
240
241
                self.init_data(name=_get_data_name(name, EDGE_PART_POLICY),
                               policy_str=EDGE_PART_POLICY,
242
                               data_tensor=edge_feats[name])
243
244
        else:
            for name in node_feats:
245
246
                self.init_data(name=_get_data_name(name, NODE_PART_POLICY),
                               policy_str=NODE_PART_POLICY)
247
            for name in edge_feats:
248
249
                self.init_data(name=_get_data_name(name, EDGE_PART_POLICY),
                               policy_str=EDGE_PART_POLICY)
250
251
252
253
254

    def start(self):
        """ Start graph store server.
        """
        # start server
Jinjing Zhou's avatar
Jinjing Zhou committed
255
        server_state = ServerState(kv_store=self, local_g=self.client_g, partition_book=self.gpb)
256
        print('start graph service on server ' + str(self.server_id))
Jinjing Zhou's avatar
Jinjing Zhou committed
257
        start_server(server_id=self.server_id, ip_config=self.ip_config,
258
259
                     num_clients=self.num_clients, server_state=server_state)

260
261
262
263
class DistGraph:
    ''' The DistGraph client.

    This provides the graph interface to access the partitioned graph data for distributed GNN
264
265
    training. All data of partitions are loaded by the DistGraph server.

266
267
268
269
270
271
272
273
274
275
    DistGraph can run in two modes: the standalone mode and the distributed mode.

    * When a user runs the training script normally, DistGraph will be in the standalone mode.
    In this mode, the input graph has to be constructed with only one partition. This mode is
    used for testing and debugging purpose.
    * When a user runs the training script with the distributed launch script, DistGraph will
    be set into the distributed mode. This is used for actual distributed training.

    When running in the distributed mode, `DistGraph` uses shared-memory to access
    the partition data in the local machine.
276
277
278
279
280
    This gives the best performance for distributed training when we run `DistGraphServer`
    and `DistGraph` on the same machine. However, a user may want to run them in separate
    machines. In this case, a user may want to disable shared memory by passing
    `disable_shared_mem=False` when creating `DistGraphServer`. When shared-memory is disabled,
    a user has to pass a partition book.
281
282
283

    Parameters
    ----------
284
285
    ip_config : str
        Path of IP configuration file.
286
287
    graph_name : str
        The name of the graph. This name has to be the same as the one used in DistGraphServer.
288
289
    gpb : PartitionBook
        The partition book object
290
    part_config : str
291
        The partition config file. It's used in the standalone mode.
292
    '''
293
    def __init__(self, ip_config, graph_name, gpb=None, part_config=None):
294
        if os.environ.get('DGL_DIST_MODE', 'standalone') == 'standalone':
295
            assert part_config is not None, \
296
297
298
                    'When running in the standalone model, the partition config file is required'
            self._client = SA_KVClient()
            # Load graph partition data.
299
            g, node_feats, edge_feats, self._gpb, _ = load_partition(part_config, 0)
300
301
302
303
            assert self._gpb.num_partitions() == 1, \
                    'The standalone mode can only work with the graph data with one partition'
            if self._gpb is None:
                self._gpb = gpb
304
            self._g = g
305
            for name in node_feats:
306
                self._client.add_data(_get_data_name(name, NODE_PART_POLICY), node_feats[name])
307
            for name in edge_feats:
308
                self._client.add_data(_get_data_name(name, EDGE_PART_POLICY), edge_feats[name])
309
            rpc.set_num_client(1)
310
        else:
311
312
            connect_to_server(ip_config=ip_config)
            self._client = KVClient(ip_config)
313
            self._g = _get_graph_from_shared_mem(graph_name)
314
315
316
317
318
            self._gpb = get_shared_mem_partition_book(graph_name, self._g)
            if self._gpb is None:
                self._gpb = gpb
            self._client.barrier()
            self._client.map_shared_data(self._gpb)
319

320
321
322
        self._ndata = NodeDataView(self)
        self._edata = EdgeDataView(self)

Da Zheng's avatar
Da Zheng committed
323
324
325
326
327
328
        self._num_nodes = 0
        self._num_edges = 0
        for part_md in self._gpb.metadata():
            self._num_nodes += int(part_md['num_nodes'])
            self._num_edges += int(part_md['num_edges'])

329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
    @property
    def local_partition(self):
        ''' Return the local partition on the client

        DistGraph provides a global view of the distributed graph. Internally,
        it may contains a partition of the graph if it is co-located with
        the server. If there is no co-location, this returns None.

        Returns
        -------
        DGLHeterograph
            The local partition
        '''
        return self._g

344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
    @property
    def ndata(self):
        """Return the data view of all the nodes.

        Returns
        -------
        NodeDataView
            The data view in the distributed graph storage.
        """
        return self._ndata

    @property
    def edata(self):
        """Return the data view of all the edges.

        Returns
        -------
        EdgeDataView
            The data view in the distributed graph storage.
        """
        return self._edata

Da Zheng's avatar
Da Zheng committed
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
    @property
    def ntypes(self):
        """Return the list of node types of this graph.

        Returns
        -------
        list of str

        Examples
        --------

        >>> g = DistGraph("ip_config.txt", "test")
        >>> g.ntypes
        ['_U']
        """
        # Currently, we only support a graph with one node type.
        return ['_U']

    @property
    def etypes(self):
        """Return the list of edge types of this graph.

        Returns
        -------
        list of str

        Examples
        --------

        >>> g = DistGraph("ip_config.txt", "test")
        >>> g.etypes
        ['_E']
        """
        # Currently, we only support a graph with one edge type.
        return ['_E']

402
403
    def number_of_nodes(self):
        """Return the number of nodes"""
Da Zheng's avatar
Da Zheng committed
404
        return self._num_nodes
405
406
407

    def number_of_edges(self):
        """Return the number of edges"""
Da Zheng's avatar
Da Zheng committed
408
        return self._num_edges
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431

    def node_attr_schemes(self):
        """Return the node feature and embedding schemes."""
        schemes = {}
        for key in self.ndata:
            schemes[key] = infer_scheme(self.ndata[key])
        return schemes

    def edge_attr_schemes(self):
        """Return the edge feature and embedding schemes."""
        schemes = {}
        for key in self.edata:
            schemes[key] = infer_scheme(self.edata[key])
        return schemes

    def rank(self):
        ''' The rank of the distributed graph store.

        Returns
        -------
        int
            The rank of the current graph store.
        '''
432
433
434
        # If DistGraph doesn't have a local partition, it doesn't matter what rank
        # it returns. There is no data locality any way, as long as the returned rank
        # is unique in the system.
435
        if self._g is None:
436
            return rpc.get_rank()
437
        else:
438
439
440
441
442
443
444
445
            # If DistGraph has a local partition, we should be careful about the rank
            # we return. We need to return a rank that node_split or edge_split can split
            # the workload with respect to data locality.
            num_client = rpc.get_num_client()
            num_client_per_part = num_client // self._gpb.num_partitions()
            # all ranks of the clients in the same machine are in a contiguous range.
            client_id_in_part = rpc.get_rank() % num_client_per_part
            return int(self._gpb.partid * num_client_per_part + client_id_in_part)
446

447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
    def find_edges(self, edges):
        """ Given an edge ID array, return the source
        and destination node ID array ``s`` and ``d``.  ``s[i]`` and ``d[i]``
        are source and destination node ID for edge ``eid[i]``.

        Parameters
        ----------
        edges : tensor
            The edge ID array.

        Returns
        -------
        tensor
            The source node ID array.
        tensor
            The destination node ID array.
        """
        return dist_find_edges(self, edges)

466
467
468
469
470
471
472
473
474
    def get_partition_book(self):
        """Get the partition information.

        Returns
        -------
        GraphPartitionBook
            Object that stores all kinds of partition information.
        """
        return self._gpb
475

476
477
478
479
480
481
482
    def barrier(self):
        '''Barrier for all client nodes.

        This API will be blocked untill all the clients invoke this API.
        '''
        self._client.barrier()

483
484
485
    def _get_all_ndata_names(self):
        ''' Get the names of all node data.
        '''
486
        names = self._client.data_name_list()
487
488
489
490
491
492
493
494
495
496
        ndata_names = []
        for name in names:
            if _is_ndata_name(name):
                # Remove the prefix "node:"
                ndata_names.append(name[5:])
        return ndata_names

    def _get_all_edata_names(self):
        ''' Get the names of all edge data.
        '''
497
        names = self._client.data_name_list()
498
499
500
501
502
503
        edata_names = []
        for name in names:
            if _is_edata_name(name):
                # Remove the prefix "edge:"
                edata_names.append(name[5:])
        return edata_names
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527

def _get_overlap(mask_arr, ids):
    """ Select the Ids given a boolean mask array.

    The boolean mask array indicates all of the Ids to be selected. We want to
    find the overlap between the Ids selected by the boolean mask array and
    the Id array.

    Parameters
    ----------
    mask_arr : 1D tensor
        A boolean mask array.
    ids : 1D tensor
        A vector with Ids.

    Returns
    -------
    1D tensor
        The selected Ids.
    """
    if isinstance(mask_arr, DistTensor):
        masks = mask_arr[ids]
        return F.boolean_mask(ids, masks)
    else:
528
        masks = F.gather_row(F.tensor(mask_arr), ids)
529
530
        return F.boolean_mask(ids, masks)

531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
def _split_local(partition_book, rank, elements, local_eles):
    ''' Split the input element list with respect to data locality.
    '''
    num_clients = rpc.get_num_client()
    num_client_per_part = num_clients // partition_book.num_partitions()
    if rank is None:
        rank = rpc.get_rank()
    # all ranks of the clients in the same machine are in a contiguous range.
    client_id_in_part = rank  % num_client_per_part
    local_eles = _get_overlap(elements, local_eles)

    # get a subset for the local client.
    size = len(local_eles) // num_client_per_part
    # if this isn't the last client in the partition.
    if client_id_in_part + 1 < num_client_per_part:
        return local_eles[(size * client_id_in_part):(size * (client_id_in_part + 1))]
    else:
        return local_eles[(size * client_id_in_part):]

def _split_even(partition_book, rank, elements):
    ''' Split the input element list evenly.
    '''
    num_clients = rpc.get_num_client()
    num_client_per_part = num_clients // partition_book.num_partitions()
    if rank is None:
        rank = rpc.get_rank()
    # all ranks of the clients in the same machine are in a contiguous range.
    client_id_in_part = rank  % num_client_per_part
    rank = client_id_in_part + num_client_per_part * partition_book.partid

    if isinstance(elements, DistTensor):
        # Here we need to fetch all elements from the kvstore server.
        # I hope it's OK.
        eles = F.nonzero_1d(elements[0:len(elements)])
    else:
566
        eles = F.nonzero_1d(F.tensor(elements))
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592

    # here we divide the element list as evenly as possible. If we use range partitioning,
    # the split results also respect the data locality. Range partitioning is the default
    # strategy.
    # TODO(zhegnda) we need another way to divide the list for other partitioning strategy.

    # compute the offset of each split and ensure that the difference of each partition size
    # is 1.
    part_size = len(eles) // num_clients
    sizes = [part_size] * num_clients
    remain = len(eles) - part_size * num_clients
    if remain > 0:
        for i in range(num_clients):
            sizes[i] += 1
            remain -= 1
            if remain == 0:
                break
    offsets = np.cumsum(sizes)
    assert offsets[-1] == len(eles)

    if rank == 0:
        return eles[0:offsets[0]]
    else:
        return eles[offsets[rank-1]:offsets[rank]]


Da Zheng's avatar
Da Zheng committed
593
def node_split(nodes, partition_book=None, rank=None, force_even=True):
594
595
596
597
598
599
600
601
602
603
    ''' Split nodes and return a subset for the local rank.

    This function splits the input nodes based on the partition book and
    returns a subset of nodes for the local rank. This method is used for
    dividing workloads for distributed training.

    The input nodes can be stored as a vector of masks. The length of the vector is
    the same as the number of nodes in a graph; 1 indicates that the vertex in
    the corresponding location exists.

604
605
606
607
608
609
610
611
    There are two strategies to split the nodes. By default, it splits the nodes
    in a way to maximize data locality. That is, all nodes that belong to a process
    are returned. If `force_even` is set to true, the nodes are split evenly so
    that each process gets almost the same number of nodes. The current implementation
    can still enable data locality when a graph is partitioned with range partitioning.
    In this case, majority of the nodes returned for a process are the ones that
    belong to the process. If range partitioning is not used, data locality isn't guaranteed.

612
613
614
615
616
617
618
    Parameters
    ----------
    nodes : 1D tensor or DistTensor
        A boolean mask vector that indicates input nodes.
    partition_book : GraphPartitionBook
        The graph partition book
    rank : int
619
620
621
        The rank of a process. If not given, the rank of the current process is used.
    force_even : bool
        Force the nodes are split evenly.
622
623
624
625
626
627
628

    Returns
    -------
    1D-tensor
        The vector of node Ids that belong to the rank.
    '''
    num_nodes = 0
Da Zheng's avatar
Da Zheng committed
629
630
631
632
    if not isinstance(nodes, DistTensor):
        assert partition_book is not None, 'Regular tensor requires a partition book.'
    elif partition_book is None:
        partition_book = nodes.part_policy.partition_book
633
634
635
636
    for part in partition_book.metadata():
        num_nodes += part['num_nodes']
    assert len(nodes) == num_nodes, \
            'The length of boolean mask vector should be the number of nodes in the graph.'
637
638
639
640
641
642
    if force_even:
        return _split_even(partition_book, rank, nodes)
    else:
        # Get all nodes that belong to the rank.
        local_nids = partition_book.partid2nids(partition_book.partid)
        return _split_local(partition_book, rank, nodes, local_nids)
643

Da Zheng's avatar
Da Zheng committed
644
def edge_split(edges, partition_book=None, rank=None, force_even=True):
645
646
647
648
649
650
651
652
653
654
    ''' Split edges and return a subset for the local rank.

    This function splits the input edges based on the partition book and
    returns a subset of edges for the local rank. This method is used for
    dividing workloads for distributed training.

    The input edges can be stored as a vector of masks. The length of the vector is
    the same as the number of edges in a graph; 1 indicates that the edge in
    the corresponding location exists.

655
656
657
658
659
660
661
662
    There are two strategies to split the edges. By default, it splits the edges
    in a way to maximize data locality. That is, all edges that belong to a process
    are returned. If `force_even` is set to true, the edges are split evenly so
    that each process gets almost the same number of edges. The current implementation
    can still enable data locality when a graph is partitioned with range partitioning.
    In this case, majority of the edges returned for a process are the ones that
    belong to the process. If range partitioning is not used, data locality isn't guaranteed.

663
664
665
    Parameters
    ----------
    edges : 1D tensor or DistTensor
666
        A boolean mask vector that indicates input edges.
667
668
669
    partition_book : GraphPartitionBook
        The graph partition book
    rank : int
670
671
672
        The rank of a process. If not given, the rank of the current process is used.
    force_even : bool
        Force the edges are split evenly.
673
674
675
676
677
678
679

    Returns
    -------
    1D-tensor
        The vector of edge Ids that belong to the rank.
    '''
    num_edges = 0
Da Zheng's avatar
Da Zheng committed
680
681
682
683
    if not isinstance(edges, DistTensor):
        assert partition_book is not None, 'Regular tensor requires a partition book.'
    elif partition_book is None:
        partition_book = edges.part_policy.partition_book
684
685
686
687
    for part in partition_book.metadata():
        num_edges += part['num_edges']
    assert len(edges) == num_edges, \
            'The length of boolean mask vector should be the number of edges in the graph.'
688
689
690
691
692
693
694

    if force_even:
        return _split_even(partition_book, rank, edges)
    else:
        # Get all edges that belong to the rank.
        local_eids = partition_book.partid2eids(partition_book.partid)
        return _split_local(partition_book, rank, edges, local_eids)