Skip to content

Commit 9bb5d85

Browse files
Setup DistNeighborloader for graph store sampling (#432)
Co-authored-by: kmontemayor <kyle.e.montemayor@gmail.com>
1 parent 4e88d3d commit 9bb5d85

8 files changed

Lines changed: 458 additions & 80 deletions

File tree

python/gigl/distributed/distributed_neighborloader.py

Lines changed: 301 additions & 39 deletions
Large diffs are not rendered by default.

python/gigl/distributed/graph_store/compute.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,30 +36,34 @@ def init_compute_process(
3636
cluster_info.compute_node_rank * cluster_info.num_processes_per_compute
3737
+ local_rank
3838
)
39+
cluster_master_ip = cluster_info.storage_cluster_master_ip
3940
logger.info(
40-
f"Initializing compute process group {compute_cluster_rank} / {cluster_info.compute_cluster_world_size}. on {cluster_info.compute_cluster_master_ip}:{cluster_info.compute_cluster_master_port} with backend {compute_world_backend}."
41-
f" OS rank: {os.environ['RANK']}, local client rank: {local_rank}"
42-
)
43-
torch.distributed.init_process_group(
44-
backend=compute_world_backend,
45-
world_size=cluster_info.compute_cluster_world_size,
46-
rank=compute_cluster_rank,
47-
init_method=f"tcp://{cluster_info.compute_cluster_master_ip}:{cluster_info.compute_cluster_master_port}",
48-
)
49-
logger.info(
50-
f"Initializing RPC client for compute node {compute_cluster_rank} / {cluster_info.compute_cluster_world_size} on {cluster_info.cluster_master_ip}:{cluster_info.cluster_master_port}."
41+
f"Initializing RPC client for compute node {compute_cluster_rank} / {cluster_info.compute_cluster_world_size} on {cluster_master_ip}:{cluster_info.rpc_master_port}."
5142
f" OS rank: {os.environ['RANK']}, local compute rank: {local_rank}"
5243
f" num_servers: {cluster_info.num_storage_nodes}, num_clients: {cluster_info.compute_cluster_world_size}"
5344
)
45+
# Initialize the GLT client before starting the Torch Distributed process group.
46+
# Otherwise, we saw intermittent hangs when initializing the client.
5447
glt.distributed.init_client(
5548
num_servers=cluster_info.num_storage_nodes,
5649
num_clients=cluster_info.compute_cluster_world_size,
5750
client_rank=compute_cluster_rank,
58-
master_addr=cluster_info.cluster_master_ip,
59-
master_port=cluster_info.cluster_master_port,
51+
master_addr=cluster_master_ip,
52+
master_port=cluster_info.rpc_master_port,
6053
client_group_name="gigl_client_rpc",
6154
)
6255

56+
logger.info(
57+
f"Initializing compute process group {compute_cluster_rank} / {cluster_info.compute_cluster_world_size}. on {cluster_info.compute_cluster_master_ip}:{cluster_info.compute_cluster_master_port} with backend {compute_world_backend}."
58+
f" OS rank: {os.environ['RANK']}, local client rank: {local_rank}"
59+
)
60+
torch.distributed.init_process_group(
61+
backend=compute_world_backend,
62+
world_size=cluster_info.compute_cluster_world_size,
63+
rank=compute_cluster_rank,
64+
init_method=f"tcp://{cluster_info.compute_cluster_master_ip}:{cluster_info.compute_cluster_master_port}",
65+
)
66+
6367

6468
def shutdown_compute_proccess() -> None:
6569
"""

python/gigl/distributed/graph_store/storage_main.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,24 +30,32 @@ def _run_storage_process(
3030
storage_world_backend: Optional[str],
3131
) -> None:
3232
register_dataset(dataset)
33+
cluster_master_ip = cluster_info.storage_cluster_master_ip
3334
logger.info(
34-
f"Initializing storage node {storage_rank} / {cluster_info.num_storage_nodes} with backend {storage_world_backend} on {cluster_info.cluster_master_ip}:{torch_process_port}"
35-
)
36-
torch.distributed.init_process_group(
37-
backend=storage_world_backend,
38-
world_size=cluster_info.num_storage_nodes,
39-
rank=storage_rank,
40-
init_method=f"tcp://{cluster_info.cluster_master_ip}:{torch_process_port}",
35+
f"Initializing GLT server for storage node process group {storage_rank} / {cluster_info.num_storage_nodes} on {cluster_master_ip}:{cluster_info.rpc_master_port}"
4136
)
37+
# Initialize the GLT server before starting the Torch Distributed process group.
38+
# Otherwise, we saw intermittent hangs when initializing the server.
4239
glt.distributed.init_server(
4340
num_servers=cluster_info.num_storage_nodes,
4441
server_rank=storage_rank,
4542
dataset=dataset,
46-
master_addr=cluster_info.cluster_master_ip,
47-
master_port=cluster_info.cluster_master_port,
43+
master_addr=cluster_master_ip,
44+
master_port=cluster_info.rpc_master_port,
4845
num_clients=cluster_info.compute_cluster_world_size,
4946
)
5047

48+
init_method = f"tcp://{cluster_info.storage_cluster_master_ip}:{torch_process_port}"
49+
logger.info(
50+
f"Initializing storage node process group {storage_rank} / {cluster_info.num_storage_nodes} with backend {storage_world_backend} on {init_method}"
51+
)
52+
torch.distributed.init_process_group(
53+
backend=storage_world_backend,
54+
world_size=cluster_info.num_storage_nodes,
55+
rank=storage_rank,
56+
init_method=init_method,
57+
)
58+
5159
logger.info(
5260
f"Waiting for storage node {storage_rank} / {cluster_info.num_storage_nodes} to exit"
5361
)
@@ -59,7 +67,7 @@ def storage_node_process(
5967
storage_rank: int,
6068
cluster_info: GraphStoreInfo,
6169
task_config_uri: Uri,
62-
is_inference: bool,
70+
is_inference: bool = True,
6371
tf_record_uri_pattern: str = ".*-of-.*\.tfrecord(\.gz)?$",
6472
storage_world_backend: Optional[str] = None,
6573
) -> None:
@@ -71,7 +79,7 @@ def storage_node_process(
7179
storage_rank (int): The rank of the storage node.
7280
cluster_info (GraphStoreInfo): The cluster information.
7381
task_config_uri (Uri): The task config URI.
74-
is_inference (bool): Whether the process is an inference process.
82+
is_inference (bool): Whether the process is an inference process. Defaults to True.
7583
tf_record_uri_pattern (str): The TF Record URI pattern.
7684
storage_world_backend (Optional[str]): The backend for the storage Torch Distributed process group.
7785
"""
@@ -95,6 +103,7 @@ def storage_node_process(
95103
_tfrecord_uri_pattern=tf_record_uri_pattern,
96104
)
97105
torch_process_port = get_free_ports_from_master_node(num_ports=1)[0]
106+
torch.distributed.destroy_process_group()
98107
server_processes = []
99108
mp_context = torch.multiprocessing.get_context("spawn")
100109
# TODO(kmonte): Enable more than one server process per machine
@@ -120,18 +129,21 @@ def storage_node_process(
120129
parser = argparse.ArgumentParser()
121130
parser.add_argument("--task_config_uri", type=str, required=True)
122131
parser.add_argument("--resource_config_uri", type=str, required=True)
123-
parser.add_argument("--is_inference", action="store_true")
132+
parser.add_argument("--job_name", type=str, required=True)
124133
args = parser.parse_args()
125134
logger.info(f"Running storage node with arguments: {args}")
126135

127136
is_inference = args.is_inference
128-
torch.distributed.init_process_group()
137+
torch.distributed.init_process_group(backend="gloo")
129138
cluster_info = get_graph_store_info()
139+
logger.info(f"Cluster info: {cluster_info}")
140+
logger.info(
141+
f"World size: {torch.distributed.get_world_size()}, rank: {torch.distributed.get_rank()}, OS world size: {os.environ['WORLD_SIZE']}, OS rank: {os.environ['RANK']}"
142+
)
130143
# Tear down the """"global""" process group so we can have a server-specific process group.
131144
torch.distributed.destroy_process_group()
132145
storage_node_process(
133146
storage_rank=cluster_info.storage_node_rank,
134147
cluster_info=cluster_info,
135148
task_config_uri=UriFactory.create_uri(args.task_config_uri),
136-
is_inference=is_inference,
137149
)

python/gigl/distributed/utils/neighborloader.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""Utils for Neighbor loaders."""
22
from collections import abc
33
from copy import deepcopy
4-
from typing import Optional, TypeVar, Union
4+
from dataclasses import dataclass
5+
from enum import Enum
6+
from typing import Literal, Optional, TypeVar, Union
57

68
import torch
79
from torch_geometric.data import Data, HeteroData
@@ -15,6 +17,33 @@
1517
_GraphType = TypeVar("_GraphType", Data, HeteroData)
1618

1719

20+
class SamplingClusterSetup(Enum):
21+
"""
22+
The setup of the sampling cluster.
23+
"""
24+
25+
COLOCATED = "colocated"
26+
GRAPH_STORE = "graph_store"
27+
28+
29+
@dataclass(frozen=True)
30+
class DatasetSchema:
31+
"""
32+
Shared metadata between the local and remote datasets.
33+
"""
34+
35+
# If the dataset is labeled heterogeneous. E.g. one node type, one edge type, and "label" edges.
36+
is_labeled_heterogeneous: bool
37+
# List of all edge types in the graph.
38+
edge_types: Optional[list[EdgeType]]
39+
# Node feature info.
40+
node_feature_info: Optional[Union[FeatureInfo, dict[NodeType, FeatureInfo]]]
41+
# Edge feature info.
42+
edge_feature_info: Optional[Union[FeatureInfo, dict[EdgeType, FeatureInfo]]]
43+
# Edge direction.
44+
edge_dir: Union[str, Literal["in", "out"]]
45+
46+
1847
def patch_fanout_for_sampling(
1948
edge_types: Optional[list[EdgeType]],
2049
num_neighbors: Union[list[int], dict[EdgeType, list[int]]],

python/gigl/distributed/utils/networking.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ def get_internal_ip_from_master_node(
115115
) -> str:
116116
"""
117117
Get the internal IP address of the master node in a distributed setup.
118+
119+
Args:
120+
_global_rank_override (Optional[int]): Override for the global rank,
121+
useful for testing or if global rank is not accurately available.
122+
123+
Returns:
124+
str: The internal IP address of the master node.
118125
"""
119126
return get_internal_ip_from_node(
120127
node_rank=0, _global_rank_override=_global_rank_override
@@ -131,6 +138,12 @@ def get_internal_ip_from_node(
131138
132139
i.e. when using :py:obj:`gigl.distributed.dataset_factory`
133140
141+
Args:
142+
node_rank (int): Rank of the node, to fetch the internal IP address of.
143+
device (Optional[torch.device]): Device to use for communication. Defaults to None, which will use the default device.
144+
_global_rank_override (Optional[int]): Override for the global rank,
145+
useful for testing or if global rank is not accurately available.
146+
134147
Returns:
135148
str: The internal IP address of the node.
136149
"""
@@ -158,7 +171,9 @@ def get_internal_ip_from_node(
158171
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
159172
torch.distributed.broadcast_object_list(ip_list, src=node_rank, device=device)
160173
node_ip = ip_list[0]
161-
logger.info(f"Rank {rank} received master node's internal IP: {node_ip}")
174+
logger.info(
175+
f"Rank {rank} received master node's internal IP: {node_ip} on device {device}"
176+
)
162177
assert node_ip is not None, "Could not retrieve master node's internal IP"
163178
return node_ip
164179

@@ -230,12 +245,21 @@ def get_graph_store_info() -> GraphStoreInfo:
230245
compute_cluster_master_ip = cluster_master_ip
231246
storage_cluster_master_ip = get_internal_ip_from_node(node_rank=num_compute_nodes)
232247

233-
cluster_master_port, compute_cluster_master_port = get_free_ports_from_node(
234-
num_ports=2, node_rank=0
235-
)
236-
storage_cluster_master_port = get_free_ports_from_node(
237-
num_ports=1, node_rank=num_compute_nodes
238-
)[0]
248+
# Cluster master is by convention rank 0.
249+
cluster_master_rank = 0
250+
(
251+
cluster_master_port,
252+
compute_cluster_master_port,
253+
) = get_free_ports_from_node(num_ports=2, node_rank=cluster_master_rank)
254+
255+
# Since we structure the cluster as [compute0, ..., computeN, storage0, ..., storageN], the storage master is the first storage node.
256+
# And it's rank is the number of compute nodes.
257+
storage_master_rank = num_compute_nodes
258+
(
259+
storage_cluster_master_port,
260+
storage_rpc_port,
261+
storage_rpc_wait_port,
262+
) = get_free_ports_from_node(num_ports=3, node_rank=storage_master_rank)
239263

240264
num_processes_per_compute = int(
241265
os.environ.get(COMPUTE_CLUSTER_LOCAL_WORLD_SIZE_ENV_KEY, "1")
@@ -251,6 +275,8 @@ def get_graph_store_info() -> GraphStoreInfo:
251275
cluster_master_port=cluster_master_port,
252276
storage_cluster_master_port=storage_cluster_master_port,
253277
compute_cluster_master_port=compute_cluster_master_port,
278+
rpc_master_port=storage_rpc_port,
279+
rpc_wait_port=storage_rpc_wait_port,
254280
)
255281

256282

python/gigl/env/distributed.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ class GraphStoreInfo:
5555
# https://snapchat.github.io/GiGL/docs/api/snapchat/research/gbml/gigl_resource_config_pb2/index.html#snapchat.research.gbml.gigl_resource_config_pb2.VertexAiGraphStoreConfig
5656
num_processes_per_compute: int
5757

58+
# Port of the master node for the RPC communication.
59+
# NOTE: This should be on the *storage* master node, not the compute master node.
60+
rpc_master_port: int
61+
# Port of the master node for the RPC wait communication.
62+
# NOTE: This should be on the *storage* master node, not the compute master node.
63+
rpc_wait_port: int
64+
5865
@property
5966
def num_cluster_nodes(self) -> int:
6067
return self.num_storage_nodes + self.num_compute_nodes

python/tests/integration/distributed/graph_store/graph_store_integration_test.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import collections
22
import os
3+
import socket
34
import unittest
45
from typing import Optional
56
from unittest import mock
67

78
import torch
89
import torch.multiprocessing as mp
10+
from torch_geometric.data import Data
911

1012
from gigl.common import Uri
1113
from gigl.common.logger import Logger
14+
from gigl.distributed.distributed_neighborloader import DistNeighborLoader
1215
from gigl.distributed.graph_store.compute import (
1316
init_compute_process,
1417
shutdown_compute_proccess,
@@ -108,10 +111,37 @@ def _run_client_process(
108111
).get_node_ids()
109112
_assert_sampler_input(cluster_info, simple_sampler_input, expected_sampler_input)
110113

114+
# Check that the edge types are correct
111115
assert (
112116
remote_dist_dataset.get_edge_types() == expected_edge_types
113117
), f"Expected edge types {expected_edge_types}, got {remote_dist_dataset.get_edge_types()}"
114118

119+
torch.distributed.barrier()
120+
121+
# Test the DistNeighborLoader
122+
loader = DistNeighborLoader(
123+
dataset=remote_dist_dataset,
124+
num_neighbors=[2, 2],
125+
pin_memory_device=torch.device("cpu"),
126+
input_nodes=sampler_input,
127+
num_workers=2,
128+
worker_concurrency=2,
129+
)
130+
count = 0
131+
for datum in loader:
132+
assert isinstance(datum, Data)
133+
count += 1
134+
torch.distributed.barrier()
135+
logger.info(f"Rank {torch.distributed.get_rank()} loaded {count} batches")
136+
# Verify that we sampled all nodes.
137+
count_tensor = torch.tensor(count, dtype=torch.int64)
138+
all_node_count = 0
139+
for rank_expected_sampler_input in expected_sampler_input.values():
140+
all_node_count += sum(len(nodes) for nodes in rank_expected_sampler_input)
141+
torch.distributed.all_reduce(count_tensor, op=torch.distributed.ReduceOp.SUM)
142+
assert (
143+
count_tensor.item() == all_node_count
144+
), f"Expected {all_node_count} total nodes, got {count_tensor.item()}"
115145
shutdown_compute_proccess()
116146

117147

@@ -186,6 +216,7 @@ def _get_expected_input_nodes_by_rank(
186216
],
187217
}
188218
219+
189220
Args:
190221
num_nodes (int): The number of nodes in the graph.
191222
cluster_info (GraphStoreInfo): The cluster information.
@@ -220,17 +251,22 @@ def test_graph_store_locally(self):
220251
storage_cluster_master_port,
221252
compute_cluster_master_port,
222253
master_port,
223-
) = get_free_ports(num_ports=4)
254+
rpc_master_port,
255+
rpc_wait_port,
256+
) = get_free_ports(num_ports=6)
257+
host_ip = socket.gethostbyname(socket.gethostname())
224258
cluster_info = GraphStoreInfo(
225259
num_storage_nodes=2,
226260
num_compute_nodes=2,
227261
num_processes_per_compute=2,
228-
cluster_master_ip="localhost",
229-
storage_cluster_master_ip="localhost",
230-
compute_cluster_master_ip="localhost",
262+
cluster_master_ip=host_ip,
263+
storage_cluster_master_ip=host_ip,
264+
compute_cluster_master_ip=host_ip,
231265
cluster_master_port=cluster_master_port,
232266
storage_cluster_master_port=storage_cluster_master_port,
233267
compute_cluster_master_port=compute_cluster_master_port,
268+
rpc_master_port=rpc_master_port,
269+
rpc_wait_port=rpc_wait_port,
234270
)
235271

236272
num_cora_nodes = 2708
@@ -244,7 +280,7 @@ def test_graph_store_locally(self):
244280
with mock.patch.dict(
245281
os.environ,
246282
{
247-
"MASTER_ADDR": "localhost",
283+
"MASTER_ADDR": host_ip,
248284
"MASTER_PORT": str(master_port),
249285
"RANK": str(i),
250286
"WORLD_SIZE": str(cluster_info.compute_cluster_world_size),
@@ -271,7 +307,7 @@ def test_graph_store_locally(self):
271307
with mock.patch.dict(
272308
os.environ,
273309
{
274-
"MASTER_ADDR": "localhost",
310+
"MASTER_ADDR": host_ip,
275311
"MASTER_PORT": str(master_port),
276312
"RANK": str(i + cluster_info.num_compute_nodes),
277313
"WORLD_SIZE": str(cluster_info.compute_cluster_world_size),

python/tests/unit/env/distributed_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ def setUp(self) -> None:
2727
cluster_master_port=1234,
2828
storage_cluster_master_port=1235,
2929
compute_cluster_master_port=1236,
30+
rpc_master_port=1237,
31+
rpc_wait_port=1238,
3032
)
3133

3234
def test_num_cluster_nodes(self):

0 commit comments

Comments
 (0)