1605 lines
49 KiB
Python
1605 lines
49 KiB
Python
"""Define distributed kvstore"""
|
|
|
|
import os
|
|
|
|
import numpy as np
|
|
|
|
from .. import backend as F, utils
|
|
from .._ffi.ndarray import empty_shared_mem
|
|
|
|
from . import rpc
|
|
from .graph_partition_book import EdgePartitionPolicy, NodePartitionPolicy
|
|
from .standalone_kvstore import KVClient as SA_KVClient
|
|
|
|
############################ Register KVStore Requsts and Responses ###############################
|
|
|
|
KVSTORE_PULL = 901231
|
|
|
|
|
|
class PullResponse(rpc.Response):
|
|
"""Send the sliced data tensor back to the client.
|
|
|
|
Parameters
|
|
----------
|
|
server_id : int
|
|
ID of current server
|
|
data_tensor : tensor
|
|
sliced data tensor
|
|
"""
|
|
|
|
def __init__(self, server_id, data_tensor):
|
|
self.server_id = server_id
|
|
self.data_tensor = data_tensor
|
|
|
|
def __getstate__(self):
|
|
return self.server_id, self.data_tensor
|
|
|
|
def __setstate__(self, state):
|
|
self.server_id, self.data_tensor = state
|
|
|
|
|
|
class PullRequest(rpc.Request):
|
|
"""Send ID tensor to server and get target data tensor as response.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
data name
|
|
id_tensor : tensor
|
|
a vector storing the data ID
|
|
"""
|
|
|
|
def __init__(self, name, id_tensor):
|
|
self.name = name
|
|
self.id_tensor = id_tensor
|
|
|
|
def __getstate__(self):
|
|
return self.name, self.id_tensor
|
|
|
|
def __setstate__(self, state):
|
|
self.name, self.id_tensor = state
|
|
|
|
def process_request(self, server_state):
|
|
kv_store = server_state.kv_store
|
|
if self.name not in kv_store.part_policy:
|
|
raise RuntimeError(
|
|
"KVServer cannot find partition policy with name: %s"
|
|
% self.name
|
|
)
|
|
if self.name not in kv_store.data_store:
|
|
raise RuntimeError(
|
|
"KVServer Cannot find data tensor with name: %s" % self.name
|
|
)
|
|
local_id = kv_store.part_policy[self.name].to_local(self.id_tensor)
|
|
data = kv_store.pull_handlers[self.name](
|
|
kv_store.data_store, self.name, local_id
|
|
)
|
|
res = PullResponse(kv_store.server_id, data)
|
|
return res
|
|
|
|
|
|
KVSTORE_PUSH = 901232
|
|
|
|
|
|
class PushRequest(rpc.Request):
|
|
"""Send ID tensor and data tensor to server and update kvstore's data.
|
|
|
|
This request has no response.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
data name
|
|
id_tensor : tensor
|
|
a vector storing the data ID
|
|
data_tensor : tensor
|
|
a tensor with the same row size of data ID
|
|
"""
|
|
|
|
def __init__(self, name, id_tensor, data_tensor):
|
|
self.name = name
|
|
self.id_tensor = id_tensor
|
|
self.data_tensor = data_tensor
|
|
|
|
def __getstate__(self):
|
|
return self.name, self.id_tensor, self.data_tensor
|
|
|
|
def __setstate__(self, state):
|
|
self.name, self.id_tensor, self.data_tensor = state
|
|
|
|
def process_request(self, server_state):
|
|
kv_store = server_state.kv_store
|
|
if self.name not in kv_store.part_policy:
|
|
raise RuntimeError(
|
|
"KVServer cannot find partition policy with name: %s"
|
|
% self.name
|
|
)
|
|
if self.name not in kv_store.data_store:
|
|
raise RuntimeError(
|
|
"KVServer Cannot find data tensor with name: %s" % self.name
|
|
)
|
|
local_id = kv_store.part_policy[self.name].to_local(self.id_tensor)
|
|
kv_store.push_handlers[self.name](
|
|
kv_store.data_store, self.name, local_id, self.data_tensor
|
|
)
|
|
|
|
|
|
INIT_DATA = 901233
|
|
INIT_MSG = "Init"
|
|
|
|
|
|
class InitDataResponse(rpc.Response):
|
|
"""Send a confirmation response (just a short string message) of
|
|
InitDataRequest to client.
|
|
|
|
Parameters
|
|
----------
|
|
msg : string
|
|
string message
|
|
"""
|
|
|
|
def __init__(self, msg):
|
|
self.msg = msg
|
|
|
|
def __getstate__(self):
|
|
return self.msg
|
|
|
|
def __setstate__(self, state):
|
|
self.msg = state
|
|
|
|
|
|
class InitDataRequest(rpc.Request):
|
|
"""Send meta data to server and init data tensor
|
|
on server using UDF init function.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
data name
|
|
shape : tuple
|
|
data shape
|
|
dtype : str
|
|
data type string, e.g., 'int64', 'float32', etc.
|
|
policy_str : str
|
|
partition-policy string, e.g., 'edge' or 'node'.
|
|
init_func : function
|
|
UDF init function.
|
|
"""
|
|
|
|
def __init__(self, name, shape, dtype, policy_str, init_func):
|
|
self.name = name
|
|
self.shape = shape
|
|
self.dtype = dtype
|
|
self.policy_str = policy_str
|
|
self.init_func = init_func
|
|
|
|
def __getstate__(self):
|
|
return (
|
|
self.name,
|
|
self.shape,
|
|
self.dtype,
|
|
self.policy_str,
|
|
self.init_func,
|
|
)
|
|
|
|
def __setstate__(self, state):
|
|
(
|
|
self.name,
|
|
self.shape,
|
|
self.dtype,
|
|
self.policy_str,
|
|
self.init_func,
|
|
) = state
|
|
|
|
def process_request(self, server_state):
|
|
kv_store = server_state.kv_store
|
|
dtype = F.data_type_dict[self.dtype]
|
|
|
|
# We should see requests from multiple clients. We need to ignore the duplicated
|
|
# reqeusts.
|
|
if self.name in kv_store.data_store:
|
|
assert tuple(F.shape(kv_store.data_store[self.name])) == tuple(
|
|
self.shape
|
|
)
|
|
assert (
|
|
F.reverse_data_type_dict[
|
|
F.dtype(kv_store.data_store[self.name])
|
|
]
|
|
== self.dtype
|
|
)
|
|
assert kv_store.part_policy[self.name].policy_str == self.policy_str
|
|
else:
|
|
if not kv_store.is_backup_server():
|
|
data_tensor = self.init_func(self.shape, dtype)
|
|
kv_store.init_data(
|
|
name=self.name,
|
|
policy_str=self.policy_str,
|
|
data_tensor=data_tensor,
|
|
)
|
|
else:
|
|
kv_store.init_data(name=self.name, policy_str=self.policy_str)
|
|
res = InitDataResponse(INIT_MSG)
|
|
return res
|
|
|
|
|
|
BARRIER = 901234
|
|
BARRIER_MSG = "Barrier"
|
|
|
|
|
|
class BarrierResponse(rpc.Response):
|
|
"""Send an confimation signal (just a short string message) of
|
|
BarrierRequest to client.
|
|
|
|
Parameters
|
|
----------
|
|
msg : string
|
|
string msg
|
|
"""
|
|
|
|
def __init__(self, msg):
|
|
self.msg = msg
|
|
|
|
def __getstate__(self):
|
|
return self.msg
|
|
|
|
def __setstate__(self, state):
|
|
self.msg = state
|
|
|
|
|
|
class BarrierRequest(rpc.Request):
|
|
"""Send a barrier signal (just a short string message) to server.
|
|
|
|
Parameters
|
|
----------
|
|
role : string
|
|
client role
|
|
"""
|
|
|
|
def __init__(self, role):
|
|
self.role = role
|
|
self.group_id = rpc.get_group_id()
|
|
|
|
def __getstate__(self):
|
|
return self.role, self.group_id
|
|
|
|
def __setstate__(self, state):
|
|
self.role, self.group_id = state
|
|
|
|
def process_request(self, server_state):
|
|
kv_store = server_state.kv_store
|
|
roles = server_state.roles
|
|
role = roles[self.group_id]
|
|
barrier_count = kv_store.barrier_count[self.group_id]
|
|
count = barrier_count[self.role]
|
|
barrier_count[self.role] = count + 1
|
|
if barrier_count[self.role] == len(role[self.role]):
|
|
barrier_count[self.role] = 0
|
|
res_list = []
|
|
for client_id, _ in role[self.role]:
|
|
res_list.append((client_id, BarrierResponse(BARRIER_MSG)))
|
|
return res_list
|
|
return None
|
|
|
|
|
|
REGISTER_PULL = 901235
|
|
REGISTER_PULL_MSG = "Register_Pull"
|
|
|
|
|
|
class RegisterPullHandlerResponse(rpc.Response):
|
|
"""Send a confirmation signal (just a short string message) of
|
|
RegisterPullHandler to client.
|
|
|
|
Parameters
|
|
----------
|
|
msg : string
|
|
string message
|
|
"""
|
|
|
|
def __init__(self, msg):
|
|
self.msg = msg
|
|
|
|
def __getstate__(self):
|
|
return self.msg
|
|
|
|
def __setstate__(self, state):
|
|
self.msg = state
|
|
|
|
|
|
class RegisterPullHandlerRequest(rpc.Request):
|
|
"""Send an UDF and register Pull handler on server.
|
|
|
|
Parameters
|
|
----------
|
|
pull_func : func
|
|
UDF pull handler
|
|
"""
|
|
|
|
def __init__(self, name, pull_func):
|
|
self.name = name
|
|
self.pull_func = pull_func
|
|
|
|
def __getstate__(self):
|
|
return self.name, self.pull_func
|
|
|
|
def __setstate__(self, state):
|
|
self.name, self.pull_func = state
|
|
|
|
def process_request(self, server_state):
|
|
kv_store = server_state.kv_store
|
|
kv_store.pull_handlers[self.name] = self.pull_func
|
|
res = RegisterPullHandlerResponse(REGISTER_PULL_MSG)
|
|
return res
|
|
|
|
|
|
REGISTER_PUSH = 901236
|
|
REGISTER_PUSH_MSG = "Register_Push"
|
|
|
|
|
|
class RegisterPushHandlerResponse(rpc.Response):
|
|
"""Send a confirmation signal (just a short string message) of
|
|
RegisterPushHandler to client.
|
|
|
|
Parameters
|
|
----------
|
|
msg : string
|
|
string message
|
|
"""
|
|
|
|
def __init__(self, msg):
|
|
self.msg = msg
|
|
|
|
def __getstate__(self):
|
|
return self.msg
|
|
|
|
def __setstate__(self, state):
|
|
self.msg = state
|
|
|
|
|
|
class RegisterPushHandlerRequest(rpc.Request):
|
|
"""Send an UDF to register Push handler on server.
|
|
|
|
Parameters
|
|
----------
|
|
push_func : func
|
|
UDF push handler
|
|
"""
|
|
|
|
def __init__(self, name, push_func):
|
|
self.name = name
|
|
self.push_func = push_func
|
|
|
|
def __getstate__(self):
|
|
return self.name, self.push_func
|
|
|
|
def __setstate__(self, state):
|
|
self.name, self.push_func = state
|
|
|
|
def process_request(self, server_state):
|
|
kv_store = server_state.kv_store
|
|
kv_store.push_handlers[self.name] = self.push_func
|
|
res = RegisterPushHandlerResponse(REGISTER_PUSH_MSG)
|
|
return res
|
|
|
|
|
|
GET_SHARED = 901237
|
|
GET_SHARED_MSG = "Get_Shared"
|
|
|
|
|
|
class GetSharedDataResponse(rpc.Response):
|
|
"""Send meta data of shared-memory tensor to client.
|
|
|
|
Parameters
|
|
----------
|
|
meta : dict
|
|
a dict of meta, e.g.,
|
|
|
|
{'data_0' : (shape, dtype, policy_str),
|
|
'data_1' : (shape, dtype, policy_str)}
|
|
"""
|
|
|
|
def __init__(self, meta):
|
|
self.meta = meta
|
|
|
|
def __getstate__(self):
|
|
return self.meta
|
|
|
|
def __setstate__(self, state):
|
|
self.meta = state
|
|
|
|
|
|
class GetSharedDataRequest(rpc.Request):
|
|
"""Send a signal (just a short string message) to get the
|
|
meta data of shared-tensor from server.
|
|
|
|
Parameters
|
|
----------
|
|
msg : string
|
|
string message
|
|
"""
|
|
|
|
def __init__(self, msg):
|
|
self.msg = msg
|
|
|
|
def __getstate__(self):
|
|
return self.msg
|
|
|
|
def __setstate__(self, state):
|
|
self.msg = state
|
|
|
|
def process_request(self, server_state):
|
|
assert self.msg == GET_SHARED_MSG
|
|
meta = {}
|
|
kv_store = server_state.kv_store
|
|
for name, data in kv_store.data_store.items():
|
|
meta[name] = (
|
|
F.shape(data),
|
|
F.reverse_data_type_dict[F.dtype(data)],
|
|
kv_store.part_policy[name].policy_str,
|
|
)
|
|
res = GetSharedDataResponse(meta)
|
|
return res
|
|
|
|
|
|
GET_PART_SHAPE = 901238
|
|
|
|
|
|
class GetPartShapeResponse(rpc.Response):
|
|
"""Send the partitioned data shape back to client.
|
|
|
|
Parameters
|
|
----------
|
|
shape : tuple
|
|
shape of tensor
|
|
"""
|
|
|
|
def __init__(self, shape):
|
|
self.shape = shape
|
|
|
|
def __getstate__(self):
|
|
return self.shape
|
|
|
|
def __setstate__(self, state):
|
|
# When the shape has only one dimension, state is an integer.
|
|
if isinstance(state, int):
|
|
self.shape = (state,)
|
|
else:
|
|
self.shape = state
|
|
|
|
|
|
class GetPartShapeRequest(rpc.Request):
|
|
"""Send data name to get the partitioned data shape from server.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
data name
|
|
"""
|
|
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
def __getstate__(self):
|
|
return self.name
|
|
|
|
def __setstate__(self, state):
|
|
self.name = state
|
|
|
|
def process_request(self, server_state):
|
|
kv_store = server_state.kv_store
|
|
if self.name not in kv_store.data_store:
|
|
raise RuntimeError(
|
|
"KVServer Cannot find data tensor with name: %s" % self.name
|
|
)
|
|
data_shape = F.shape(kv_store.data_store[self.name])
|
|
res = GetPartShapeResponse(data_shape)
|
|
return res
|
|
|
|
|
|
SEND_META_TO_BACKUP = 901239
|
|
SEND_META_TO_BACKUP_MSG = "Send_Meta_TO_Backup"
|
|
|
|
|
|
class SendMetaToBackupResponse(rpc.Response):
|
|
"""Send a confirmation signal (just a short string message)
|
|
of SendMetaToBackupRequest to client.
|
|
"""
|
|
|
|
def __init__(self, msg):
|
|
self.msg = msg
|
|
|
|
def __getstate__(self):
|
|
return self.msg
|
|
|
|
def __setstate__(self, state):
|
|
self.msg = state
|
|
|
|
|
|
class SendMetaToBackupRequest(rpc.Request):
|
|
"""Send meta data to backup server and backup server
|
|
will use this meta data to read shared-memory tensor.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
data name
|
|
dtype : str
|
|
data type string
|
|
shape : tuple of int
|
|
data shape
|
|
policy_str : str
|
|
partition-policy string, e.g., 'edge' or 'node'.
|
|
pull_handler : callable
|
|
The callback function when data is pulled from kvstore.
|
|
push_handler : callable
|
|
The callback function when data is pushed to kvstore.
|
|
"""
|
|
|
|
def __init__(
|
|
self, name, dtype, shape, policy_str, pull_handler, push_handler
|
|
):
|
|
self.name = name
|
|
self.dtype = dtype
|
|
self.shape = shape
|
|
self.policy_str = policy_str
|
|
self.pull_handler = pull_handler
|
|
self.push_handler = push_handler
|
|
|
|
def __getstate__(self):
|
|
return (
|
|
self.name,
|
|
self.dtype,
|
|
self.shape,
|
|
self.policy_str,
|
|
self.pull_handler,
|
|
self.push_handler,
|
|
)
|
|
|
|
def __setstate__(self, state):
|
|
(
|
|
self.name,
|
|
self.dtype,
|
|
self.shape,
|
|
self.policy_str,
|
|
self.pull_handler,
|
|
self.push_handler,
|
|
) = state
|
|
|
|
def process_request(self, server_state):
|
|
kv_store = server_state.kv_store
|
|
assert kv_store.is_backup_server()
|
|
if self.name not in kv_store.data_store:
|
|
shared_data = empty_shared_mem(
|
|
self.name + "-kvdata-", False, self.shape, self.dtype
|
|
)
|
|
dlpack = shared_data.to_dlpack()
|
|
kv_store.data_store[self.name] = F.zerocopy_from_dlpack(dlpack)
|
|
kv_store.part_policy[self.name] = kv_store.find_policy(
|
|
self.policy_str
|
|
)
|
|
kv_store.pull_handlers[self.name] = self.pull_handler
|
|
kv_store.push_handlers[self.name] = self.push_handler
|
|
else:
|
|
assert tuple(F.shape(kv_store.data_store[self.name])) == tuple(
|
|
self.shape
|
|
)
|
|
assert (
|
|
F.reverse_data_type_dict[
|
|
F.dtype(kv_store.data_store[self.name])
|
|
]
|
|
== self.dtype
|
|
)
|
|
assert kv_store.part_policy[self.name].policy_str == self.policy_str
|
|
assert kv_store.pull_handlers[self.name] == self.pull_handler
|
|
assert kv_store.push_handlers[self.name] == self.push_handler
|
|
res = SendMetaToBackupResponse(SEND_META_TO_BACKUP_MSG)
|
|
return res
|
|
|
|
|
|
DELETE_DATA = 901240
|
|
DELETE_MSG = "Delete_Data"
|
|
|
|
|
|
class DeleteDataResponse(rpc.Response):
|
|
"""Send a confirmation signal (just a short string message)
|
|
of DeleteDataRequest to client.
|
|
"""
|
|
|
|
def __init__(self, msg):
|
|
self.msg = msg
|
|
|
|
def __getstate__(self):
|
|
return self.msg
|
|
|
|
def __setstate__(self, state):
|
|
self.msg = state
|
|
|
|
|
|
class DeleteDataRequest(rpc.Request):
|
|
"""Send message to server to delete data tensor
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
data name
|
|
"""
|
|
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
def __getstate__(self):
|
|
return self.name
|
|
|
|
def __setstate__(self, state):
|
|
self.name = state
|
|
|
|
def process_request(self, server_state):
|
|
kv_store = server_state.kv_store
|
|
if self.name in kv_store.data_store:
|
|
del kv_store.data_store[self.name]
|
|
del kv_store.part_policy[self.name]
|
|
del kv_store.push_handlers[self.name]
|
|
del kv_store.pull_handlers[self.name]
|
|
res = DeleteDataResponse(DELETE_MSG)
|
|
return res
|
|
|
|
|
|
COUNT_LOCAL_NONZERO = 901241
|
|
|
|
|
|
class CountLocalNonzeroResponse(rpc.Response):
|
|
"""Send the number of nonzero value in local data"""
|
|
|
|
def __init__(self, num_local_nonzero):
|
|
self.num_local_nonzero = num_local_nonzero
|
|
|
|
def __getstate__(self):
|
|
return self.num_local_nonzero
|
|
|
|
def __setstate__(self, state):
|
|
self.num_local_nonzero = state
|
|
|
|
|
|
class CountLocalNonzeroRequest(rpc.Request):
|
|
"""Send data name to server to count local nonzero value
|
|
Parameters
|
|
----------
|
|
name : str
|
|
data name
|
|
"""
|
|
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
def __getstate__(self):
|
|
return self.name
|
|
|
|
def __setstate__(self, state):
|
|
self.name = state
|
|
|
|
def process_request(self, server_state):
|
|
kv_store = server_state.kv_store
|
|
num_local_nonzero = kv_store.count_local_nonzero(self.name)
|
|
res = CountLocalNonzeroResponse(num_local_nonzero)
|
|
return res
|
|
|
|
|
|
############################ KVServer ###############################
|
|
|
|
|
|
def default_push_handler(target, name, id_tensor, data_tensor):
|
|
"""Default handler for PUSH message.
|
|
|
|
On default, _push_handler perform scatter_row() operation for the tensor.
|
|
|
|
Parameters
|
|
----------
|
|
target : tensor
|
|
target tensor
|
|
name : str
|
|
data name
|
|
id_tensor : tensor
|
|
a vector storing the ID list.
|
|
data_tensor : tensor
|
|
a tensor with the same row size of id
|
|
"""
|
|
# TODO(chao): support Tensorflow backend
|
|
target[name][id_tensor] = data_tensor
|
|
|
|
|
|
def default_pull_handler(target, name, id_tensor):
|
|
"""Default handler for PULL operation.
|
|
|
|
On default, _pull_handler perform gather_row() operation for the tensor.
|
|
|
|
Parameters
|
|
----------
|
|
target : tensor
|
|
target tensor
|
|
name : str
|
|
data name
|
|
id_tensor : tensor
|
|
a vector storing the ID list.
|
|
|
|
Return
|
|
------
|
|
tensor
|
|
a tensor with the same row size of ID.
|
|
"""
|
|
# TODO(chao): support Tensorflow backend
|
|
return target[name][id_tensor]
|
|
|
|
|
|
class KVServer(object):
|
|
"""KVServer is a lightweight key-value store service for DGL distributed training.
|
|
|
|
In practice, developers can use KVServer to hold large-scale graph features or
|
|
graph embeddings across machines in a distributed setting. KVServer depends on DGL rpc
|
|
infrastructure thats support backup servers, which means we can lunach many KVServers
|
|
on the same machine for load-balancing.
|
|
|
|
DO NOT use KVServer in mult-threads because this behavior is not defined. For now, KVServer
|
|
can only support CPU-to-CPU communication. We may support GPU-communication in the future.
|
|
|
|
Parameters
|
|
----------
|
|
server_id : int
|
|
ID of current server (starts from 0).
|
|
ip_config : str
|
|
Path of IP configuration file.
|
|
num_servers : int
|
|
Server count on each machine.
|
|
num_clients : int
|
|
Total number of KVClients that will be connected to the KVServer.
|
|
"""
|
|
|
|
def __init__(self, server_id, ip_config, num_servers, num_clients):
|
|
assert server_id >= 0, (
|
|
"server_id (%d) cannot be a negative number." % server_id
|
|
)
|
|
assert num_servers > 0, (
|
|
"num_servers (%d) must be a positive number." % num_servers
|
|
)
|
|
assert os.path.exists(ip_config), "Cannot open file: %s" % ip_config
|
|
assert num_clients >= 0, (
|
|
"num_clients (%d) cannot be a negative number." % num_clients
|
|
)
|
|
# Register services on server
|
|
rpc.register_service(KVSTORE_PULL, PullRequest, PullResponse)
|
|
rpc.register_service(KVSTORE_PUSH, PushRequest, None)
|
|
rpc.register_service(INIT_DATA, InitDataRequest, InitDataResponse)
|
|
rpc.register_service(BARRIER, BarrierRequest, BarrierResponse)
|
|
rpc.register_service(
|
|
REGISTER_PUSH,
|
|
RegisterPushHandlerRequest,
|
|
RegisterPushHandlerResponse,
|
|
)
|
|
rpc.register_service(
|
|
REGISTER_PULL,
|
|
RegisterPullHandlerRequest,
|
|
RegisterPullHandlerResponse,
|
|
)
|
|
rpc.register_service(
|
|
GET_SHARED, GetSharedDataRequest, GetSharedDataResponse
|
|
)
|
|
rpc.register_service(
|
|
GET_PART_SHAPE, GetPartShapeRequest, GetPartShapeResponse
|
|
)
|
|
rpc.register_service(
|
|
SEND_META_TO_BACKUP,
|
|
SendMetaToBackupRequest,
|
|
SendMetaToBackupResponse,
|
|
)
|
|
rpc.register_service(DELETE_DATA, DeleteDataRequest, DeleteDataResponse)
|
|
rpc.register_service(
|
|
COUNT_LOCAL_NONZERO,
|
|
CountLocalNonzeroRequest,
|
|
CountLocalNonzeroResponse,
|
|
)
|
|
# Store the tensor data with specified data name
|
|
self._data_store = {}
|
|
# Store original tensor data names when instantiating DistGraphServer
|
|
self._orig_data = set()
|
|
# Store the partition information with specified data name
|
|
self._policy_set = set()
|
|
self._part_policy = {}
|
|
# Basic information
|
|
self._server_id = server_id
|
|
self._server_namebook = rpc.read_ip_config(ip_config, num_servers)
|
|
assert (
|
|
server_id in self._server_namebook
|
|
), "Trying to start server {}, but there are {} servers in the config file".format(
|
|
server_id, len(self._server_namebook)
|
|
)
|
|
self._machine_id = self._server_namebook[server_id][0]
|
|
self._group_count = self._server_namebook[server_id][3]
|
|
# We assume partition_id is equal to machine_id
|
|
self._part_id = self._machine_id
|
|
self._num_clients = num_clients
|
|
self._barrier_count = {}
|
|
# push and pull handler
|
|
self._push_handlers = {}
|
|
self._pull_handlers = {}
|
|
|
|
@property
|
|
def server_id(self):
|
|
"""Get server ID"""
|
|
return self._server_id
|
|
|
|
@property
|
|
def barrier_count(self):
|
|
"""Get barrier count"""
|
|
return self._barrier_count
|
|
|
|
@barrier_count.setter
|
|
def barrier_count(self, count):
|
|
"""Set barrier count"""
|
|
self._barrier_count = count
|
|
|
|
@property
|
|
def num_clients(self):
|
|
"""Get number of clients"""
|
|
return self._num_clients
|
|
|
|
@property
|
|
def data_store(self):
|
|
"""Get data store"""
|
|
return self._data_store
|
|
|
|
@property
|
|
def orig_data(self):
|
|
"""Get original data"""
|
|
return self._orig_data
|
|
|
|
@property
|
|
def part_policy(self):
|
|
"""Get part policy"""
|
|
return self._part_policy
|
|
|
|
@property
|
|
def part_id(self):
|
|
"""Get part ID"""
|
|
return self._part_id
|
|
|
|
@property
|
|
def push_handlers(self):
|
|
"""Get push handler"""
|
|
return self._push_handlers
|
|
|
|
@property
|
|
def pull_handlers(self):
|
|
"""Get pull handler"""
|
|
return self._pull_handlers
|
|
|
|
def is_backup_server(self):
|
|
"""Return True if current server is a backup server."""
|
|
if self._server_id % self._group_count == 0:
|
|
return False
|
|
return True
|
|
|
|
def add_part_policy(self, policy):
|
|
"""Add partition policy to kvserver.
|
|
|
|
Parameters
|
|
----------
|
|
policy : PartitionPolicy
|
|
Store the partition information
|
|
"""
|
|
self._policy_set.add(policy)
|
|
|
|
def init_data(self, name, policy_str, data_tensor=None):
|
|
"""Init data tensor on kvserver.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
data name
|
|
policy_str : str
|
|
partition-policy string, e.g., 'edge' or 'node'.
|
|
data_tensor : tensor
|
|
If the data_tensor is None, KVServer will
|
|
read shared-memory when client invoking get_shared_data().
|
|
"""
|
|
assert len(name) > 0, "name cannot be empty."
|
|
if name in self._data_store:
|
|
raise RuntimeError("Data %s has already exists!" % name)
|
|
self._part_policy[name] = self.find_policy(policy_str)
|
|
if data_tensor is not None: # Create shared-tensor
|
|
data_type = F.reverse_data_type_dict[F.dtype(data_tensor)]
|
|
shared_data = empty_shared_mem(
|
|
name + "-kvdata-", True, data_tensor.shape, data_type
|
|
)
|
|
dlpack = shared_data.to_dlpack()
|
|
self._data_store[name] = F.zerocopy_from_dlpack(dlpack)
|
|
rpc.copy_data_to_shared_memory(self._data_store[name], data_tensor)
|
|
assert (
|
|
self._part_policy[name].get_part_size() == data_tensor.shape[0]
|
|
), "kvserver expect partition {} for {} has {} rows, but gets {} rows".format(
|
|
self._part_policy[name].part_id,
|
|
policy_str,
|
|
self._part_policy[name].get_part_size(),
|
|
data_tensor.shape[0],
|
|
)
|
|
self._pull_handlers[name] = default_pull_handler
|
|
self._push_handlers[name] = default_push_handler
|
|
|
|
def find_policy(self, policy_str):
|
|
"""Find a partition policy from existing policy set
|
|
|
|
Parameters
|
|
----------
|
|
policy_str : str
|
|
partition-policy string, e.g., 'edge' or 'node'.
|
|
"""
|
|
for policy in self._policy_set:
|
|
if policy_str == policy.policy_str:
|
|
return policy
|
|
raise RuntimeError(
|
|
"Cannot find policy_str: %s from kvserver." % policy_str
|
|
)
|
|
|
|
def count_local_nonzero(self, name):
|
|
"""Count nonzero in local data
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
data name.
|
|
|
|
Returns
|
|
-------
|
|
int
|
|
the number of nonzero in local data.
|
|
"""
|
|
assert len(name) > 0, "name cannot be empty."
|
|
if name not in self._data_store:
|
|
raise RuntimeError("Data %s has not be created!" % name)
|
|
return F.count_nonzero(self._data_store[name])
|
|
|
|
|
|
############################ KVClient ###############################
|
|
|
|
|
|
class KVClient(object):
|
|
"""KVClient is used to push/pull data to/from KVServer. If the
|
|
target kvclient and kvserver are in the same machine, they can
|
|
communicate with each other using local shared-memory
|
|
automatically, instead of going through the tcp/ip RPC.
|
|
|
|
DO NOT use KVClient in multi-threads because this behavior is
|
|
not defined. For now, KVClient can only support CPU-to-CPU communication.
|
|
We may support GPU-communication in the future.
|
|
|
|
Parameters
|
|
----------
|
|
ip_config : str
|
|
Path of IP configuration file.
|
|
num_servers : int
|
|
Server count on each machine.
|
|
role : str
|
|
We can set different role for kvstore.
|
|
"""
|
|
|
|
def __init__(self, ip_config, num_servers, role="default"):
|
|
assert (
|
|
rpc.get_rank() != -1
|
|
), "Please invoke rpc.connect_to_server() before creating KVClient."
|
|
assert os.path.exists(ip_config), "Cannot open file: %s" % ip_config
|
|
assert num_servers > 0, (
|
|
"num_servers (%d) must be a positive number." % num_servers
|
|
)
|
|
# Register services on client
|
|
rpc.register_service(KVSTORE_PULL, PullRequest, PullResponse)
|
|
rpc.register_service(KVSTORE_PUSH, PushRequest, None)
|
|
rpc.register_service(INIT_DATA, InitDataRequest, InitDataResponse)
|
|
rpc.register_service(BARRIER, BarrierRequest, BarrierResponse)
|
|
rpc.register_service(
|
|
REGISTER_PUSH,
|
|
RegisterPushHandlerRequest,
|
|
RegisterPushHandlerResponse,
|
|
)
|
|
rpc.register_service(
|
|
REGISTER_PULL,
|
|
RegisterPullHandlerRequest,
|
|
RegisterPullHandlerResponse,
|
|
)
|
|
rpc.register_service(
|
|
GET_SHARED, GetSharedDataRequest, GetSharedDataResponse
|
|
)
|
|
rpc.register_service(
|
|
GET_PART_SHAPE, GetPartShapeRequest, GetPartShapeResponse
|
|
)
|
|
rpc.register_service(
|
|
SEND_META_TO_BACKUP,
|
|
SendMetaToBackupRequest,
|
|
SendMetaToBackupResponse,
|
|
)
|
|
rpc.register_service(DELETE_DATA, DeleteDataRequest, DeleteDataResponse)
|
|
rpc.register_service(
|
|
COUNT_LOCAL_NONZERO,
|
|
CountLocalNonzeroRequest,
|
|
CountLocalNonzeroResponse,
|
|
)
|
|
# Store the tensor data with specified data name
|
|
self._data_store = {}
|
|
# Store the partition information with specified data name
|
|
self._part_policy = {}
|
|
# This stores all unique partition policies in the kvstore. The key is the policy name.
|
|
self._all_possible_part_policy = {}
|
|
# Store the full data shape across kvserver
|
|
self._full_data_shape = {}
|
|
# Store all the data name
|
|
self._data_name_list = set()
|
|
# Store all graph data name
|
|
self._gdata_name_list = set()
|
|
# Basic information
|
|
self._server_namebook = rpc.read_ip_config(ip_config, num_servers)
|
|
self._server_count = len(self._server_namebook)
|
|
self._group_count = self._server_namebook[0][3]
|
|
self._machine_count = int(self._server_count / self._group_count)
|
|
self._client_id = rpc.get_rank()
|
|
self._machine_id = rpc.get_machine_id()
|
|
self._part_id = self._machine_id
|
|
self._main_server_id = self._machine_id * self._group_count
|
|
|
|
# push and pull handler
|
|
self._pull_handlers = {}
|
|
self._push_handlers = {}
|
|
# register role on server-0
|
|
self._role = role
|
|
|
|
@property
|
|
def all_possible_part_policy(self):
|
|
"""Get all possible partition policies"""
|
|
return self._all_possible_part_policy
|
|
|
|
@property
|
|
def client_id(self):
|
|
"""Get client ID"""
|
|
return self._client_id
|
|
|
|
@property
|
|
def role(self):
|
|
"""Get client role"""
|
|
return self._role
|
|
|
|
@property
|
|
def machine_id(self):
|
|
"""Get machine ID"""
|
|
return self._machine_id
|
|
|
|
@property
|
|
def num_servers(self):
|
|
"""Get the number of servers"""
|
|
return self._server_count
|
|
|
|
@property
|
|
def group_count(self):
|
|
"""Get the number of groups --num_servers"""
|
|
return self._group_count
|
|
|
|
def barrier(self):
|
|
"""Barrier for all client nodes.
|
|
|
|
This API will be blocked untill all the clients invoke this API.
|
|
"""
|
|
request = BarrierRequest(self._role)
|
|
rpc.send_request(0, request)
|
|
response = rpc.recv_response()
|
|
assert response.msg == BARRIER_MSG
|
|
|
|
def register_push_handler(self, name, func):
|
|
"""Register UDF push function.
|
|
|
|
This UDF is triggered for every push. The signature of the UDF is
|
|
|
|
```
|
|
def push_handler(data_store, name, local_offset, data)
|
|
```
|
|
|
|
``data_store`` is a dict that contains all tensors in the kvstore. ``name`` is the name
|
|
of the tensor where new data is pushed to. ``local_offset`` is the offset where new
|
|
data should be written in the tensor in the local partition. ``data`` is the new data
|
|
to be written.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
The name of the tensor
|
|
func : callable
|
|
The function to be called.
|
|
"""
|
|
self.barrier()
|
|
request = RegisterPushHandlerRequest(name, func)
|
|
# send request to all the server nodes
|
|
for server_id in range(self._server_count):
|
|
rpc.send_request(server_id, request)
|
|
# recv response from all the server nodes
|
|
for _ in range(self._server_count):
|
|
response = rpc.recv_response()
|
|
assert response.msg == REGISTER_PUSH_MSG
|
|
self._push_handlers[name] = func
|
|
self.barrier()
|
|
|
|
def register_pull_handler(self, name, func):
|
|
"""Register UDF pull function.
|
|
|
|
This UDF is triggered for every pull. The signature of the UDF is
|
|
|
|
```
|
|
def pull_handler(data_store, name, local_offset)
|
|
```
|
|
|
|
``data_store`` is a dict that contains all tensors in the kvstore. ``name`` is the name
|
|
of the tensor where new data is pushed to. ``local_offset`` is the offset where new
|
|
data should be written in the tensor in the local partition.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
The name of the tensor
|
|
func : callable
|
|
The function to be called.
|
|
"""
|
|
self.barrier()
|
|
request = RegisterPullHandlerRequest(name, func)
|
|
# send request to all the server nodes
|
|
for server_id in range(self._server_count):
|
|
rpc.send_request(server_id, request)
|
|
# recv response from all the server nodes
|
|
for _ in range(self._server_count):
|
|
response = rpc.recv_response()
|
|
assert response.msg == REGISTER_PULL_MSG
|
|
self._pull_handlers[name] = func
|
|
self.barrier()
|
|
|
|
def init_data(
|
|
self, name, shape, dtype, part_policy, init_func, is_gdata=True
|
|
):
|
|
"""Send message to kvserver to initialize new data tensor and mapping this
|
|
data from server side to client side.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
data name
|
|
shape : list or tuple of int
|
|
data shape
|
|
dtype : dtype
|
|
data type
|
|
part_policy : PartitionPolicy
|
|
partition policy.
|
|
init_func : func
|
|
UDF init function
|
|
is_gdata : bool
|
|
Whether the created tensor is a ndata/edata or not.
|
|
"""
|
|
assert len(name) > 0, "name cannot be empty."
|
|
assert len(shape) > 0, "shape cannot be empty"
|
|
assert name not in self._data_name_list, (
|
|
"data name: %s already exists." % name
|
|
)
|
|
self.barrier()
|
|
shape = list(shape)
|
|
|
|
# Send request to the servers to initialize data.
|
|
# The servers may handle the duplicated initializations.
|
|
part_shape = shape.copy()
|
|
part_shape[0] = part_policy.get_part_size()
|
|
request = InitDataRequest(
|
|
name,
|
|
tuple(part_shape),
|
|
F.reverse_data_type_dict[dtype],
|
|
part_policy.policy_str,
|
|
init_func,
|
|
)
|
|
# The request is sent to the servers in one group, which are on the same machine.
|
|
for n in range(self._group_count):
|
|
server_id = part_policy.part_id * self._group_count + n
|
|
rpc.send_request(server_id, request)
|
|
for _ in range(self._group_count):
|
|
response = rpc.recv_response()
|
|
assert response.msg == INIT_MSG
|
|
|
|
self.barrier()
|
|
# Create local shared-data
|
|
local_shape = shape.copy()
|
|
local_shape[0] = part_policy.get_part_size()
|
|
if name in self._part_policy:
|
|
raise RuntimeError("Policy %s has already exists!" % name)
|
|
if name in self._data_store:
|
|
raise RuntimeError("Data %s has already exists!" % name)
|
|
if name in self._full_data_shape:
|
|
raise RuntimeError("Data shape %s has already exists!" % name)
|
|
self._part_policy[name] = part_policy
|
|
self._all_possible_part_policy[part_policy.policy_str] = part_policy
|
|
shared_data = empty_shared_mem(
|
|
name + "-kvdata-",
|
|
False,
|
|
local_shape,
|
|
F.reverse_data_type_dict[dtype],
|
|
)
|
|
dlpack = shared_data.to_dlpack()
|
|
self._data_store[name] = F.zerocopy_from_dlpack(dlpack)
|
|
self._data_name_list.add(name)
|
|
if is_gdata:
|
|
self._gdata_name_list.add(name)
|
|
self._full_data_shape[name] = tuple(shape)
|
|
self._pull_handlers[name] = default_pull_handler
|
|
self._push_handlers[name] = default_push_handler
|
|
|
|
# Now we need to tell the backup server the new tensor.
|
|
request = SendMetaToBackupRequest(
|
|
name,
|
|
F.reverse_data_type_dict[dtype],
|
|
part_shape,
|
|
part_policy.policy_str,
|
|
self._pull_handlers[name],
|
|
self._push_handlers[name],
|
|
)
|
|
# send request to all the backup server nodes
|
|
for i in range(self._group_count - 1):
|
|
server_id = self._machine_id * self._group_count + i + 1
|
|
rpc.send_request(server_id, request)
|
|
# recv response from all the backup server nodes
|
|
for _ in range(self._group_count - 1):
|
|
response = rpc.recv_response()
|
|
assert response.msg == SEND_META_TO_BACKUP_MSG
|
|
self.barrier()
|
|
|
|
def delete_data(self, name):
|
|
"""Send message to kvserver to delete tensor and clear the meta data
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
data name
|
|
"""
|
|
assert len(name) > 0, "name cannot be empty."
|
|
assert name in self._data_name_list, "data name: %s not exists." % name
|
|
self.barrier()
|
|
part_policy = self._part_policy[name]
|
|
|
|
# send request to every server nodes
|
|
request = DeleteDataRequest(name)
|
|
for n in range(self._group_count):
|
|
server_id = part_policy.part_id * self._group_count + n
|
|
rpc.send_request(server_id, request)
|
|
for _ in range(self._group_count):
|
|
response = rpc.recv_response()
|
|
assert response.msg == DELETE_MSG
|
|
|
|
self.barrier()
|
|
self._data_name_list.remove(name)
|
|
if name in self._gdata_name_list:
|
|
self._gdata_name_list.remove(name)
|
|
# TODO(chao) : remove the delete log print
|
|
del self._data_store[name]
|
|
del self._full_data_shape[name]
|
|
del self._part_policy[name]
|
|
del self._pull_handlers[name]
|
|
del self._push_handlers[name]
|
|
self.barrier()
|
|
|
|
def map_shared_data(self, partition_book):
|
|
"""Mapping shared-memory tensor from server to client.
|
|
|
|
Parameters
|
|
----------
|
|
partition_book : GraphPartitionBook
|
|
Store the partition information
|
|
"""
|
|
# Get all partition policies
|
|
for ntype in partition_book.ntypes:
|
|
policy = NodePartitionPolicy(partition_book, ntype)
|
|
self._all_possible_part_policy[policy.policy_str] = policy
|
|
for etype in partition_book.canonical_etypes:
|
|
policy = EdgePartitionPolicy(partition_book, etype)
|
|
self._all_possible_part_policy[policy.policy_str] = policy
|
|
|
|
# Get shared data from server side
|
|
self.barrier()
|
|
request = GetSharedDataRequest(GET_SHARED_MSG)
|
|
rpc.send_request(self._main_server_id, request)
|
|
response = rpc.recv_response()
|
|
for name, meta in response.meta.items():
|
|
if name not in self._data_name_list:
|
|
shape, dtype, policy_str = meta
|
|
assert policy_str in self._all_possible_part_policy
|
|
shared_data = empty_shared_mem(
|
|
name + "-kvdata-", False, shape, dtype
|
|
)
|
|
dlpack = shared_data.to_dlpack()
|
|
self._data_store[name] = F.zerocopy_from_dlpack(dlpack)
|
|
self._part_policy[name] = self._all_possible_part_policy[
|
|
policy_str
|
|
]
|
|
self._pull_handlers[name] = default_pull_handler
|
|
self._push_handlers[name] = default_push_handler
|
|
# Get full data shape across servers
|
|
for name, meta in response.meta.items():
|
|
if name not in self._data_name_list:
|
|
shape, _, _ = meta
|
|
data_shape = list(shape)
|
|
data_shape[0] = 0
|
|
request = GetPartShapeRequest(name)
|
|
# send request to all main server nodes
|
|
for machine_id in range(self._machine_count):
|
|
server_id = machine_id * self._group_count
|
|
rpc.send_request(server_id, request)
|
|
# recv response from all the main server nodes
|
|
for _ in range(self._machine_count):
|
|
res = rpc.recv_response()
|
|
data_shape[0] += res.shape[0]
|
|
self._full_data_shape[name] = tuple(data_shape)
|
|
# Send meta data to backup servers
|
|
for name, meta in response.meta.items():
|
|
shape, dtype, policy_str = meta
|
|
request = SendMetaToBackupRequest(
|
|
name,
|
|
dtype,
|
|
shape,
|
|
policy_str,
|
|
self._pull_handlers[name],
|
|
self._push_handlers[name],
|
|
)
|
|
# send request to all the backup server nodes
|
|
for i in range(self._group_count - 1):
|
|
server_id = self._machine_id * self._group_count + i + 1
|
|
rpc.send_request(server_id, request)
|
|
# recv response from all the backup server nodes
|
|
for _ in range(self._group_count - 1):
|
|
response = rpc.recv_response()
|
|
assert response.msg == SEND_META_TO_BACKUP_MSG
|
|
self._data_name_list.add(name)
|
|
# map_shared_data happens only at DistGraph initialization
|
|
# TODO(xiangsx): We assume there is no non-graph data initialized at this time
|
|
self._gdata_name_list.add(name)
|
|
self.barrier()
|
|
|
|
def gdata_name_list(self):
|
|
"""Get all the graph data name"""
|
|
return list(self._gdata_name_list)
|
|
|
|
def data_name_list(self):
|
|
"""Get all the data name"""
|
|
return list(self._data_name_list)
|
|
|
|
def get_data_meta(self, name):
|
|
"""Get meta data (data_type, data_shape, partition_policy)"""
|
|
assert len(name) > 0, "name cannot be empty."
|
|
data_type = F.dtype(self._data_store[name])
|
|
data_shape = self._full_data_shape[name]
|
|
part_policy = self._part_policy[name]
|
|
return (data_type, data_shape, part_policy)
|
|
|
|
def get_partid(self, name, id_tensor):
|
|
"""
|
|
Parameters
|
|
----------
|
|
name : str
|
|
data name
|
|
id_tensor : tensor
|
|
a vector storing the global data ID
|
|
"""
|
|
assert len(name) > 0, "name cannot be empty."
|
|
id_tensor = utils.toindex(id_tensor)
|
|
id_tensor = id_tensor.tousertensor()
|
|
assert F.ndim(id_tensor) == 1, "ID must be a vector."
|
|
# partition data
|
|
machine_id = self._part_policy[name].to_partid(id_tensor)
|
|
|
|
return machine_id
|
|
|
|
def push(self, name, id_tensor, data_tensor):
|
|
"""Push data to KVServer.
|
|
|
|
Note that, the push() is an non-blocking operation that will return immediately.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
data name
|
|
id_tensor : tensor
|
|
a vector storing the global data ID
|
|
data_tensor : tensor
|
|
a tensor with the same row size of data ID
|
|
"""
|
|
assert len(name) > 0, "name cannot be empty."
|
|
id_tensor = utils.toindex(id_tensor)
|
|
id_tensor = id_tensor.tousertensor()
|
|
assert F.ndim(id_tensor) == 1, "ID must be a vector."
|
|
assert (
|
|
F.shape(id_tensor)[0] == F.shape(data_tensor)[0]
|
|
), "The data must has the same row size with ID."
|
|
# partition data
|
|
machine_id = self._part_policy[name].to_partid(id_tensor)
|
|
# sort index by machine id
|
|
sorted_id = F.tensor(np.argsort(F.asnumpy(machine_id)))
|
|
id_tensor = id_tensor[sorted_id]
|
|
data_tensor = data_tensor[sorted_id]
|
|
machine, count = np.unique(F.asnumpy(machine_id), return_counts=True)
|
|
# push data to server by order
|
|
start = 0
|
|
local_id = None
|
|
local_data = None
|
|
for idx, machine_idx in enumerate(machine):
|
|
end = start + count[idx]
|
|
if start == end: # No data for target machine
|
|
continue
|
|
partial_id = id_tensor[start:end]
|
|
partial_data = data_tensor[start:end]
|
|
if machine_idx == self._machine_id: # local push
|
|
# Note that DO NOT push local data right now because we can overlap
|
|
# communication-local_push here
|
|
local_id = self._part_policy[name].to_local(partial_id)
|
|
local_data = partial_data
|
|
else: # push data to remote server
|
|
request = PushRequest(name, partial_id, partial_data)
|
|
rpc.send_request_to_machine(machine_idx, request)
|
|
start += count[idx]
|
|
if local_id is not None: # local push
|
|
self._push_handlers[name](
|
|
self._data_store, name, local_id, local_data
|
|
)
|
|
|
|
def pull(self, name, id_tensor):
|
|
"""Pull message from KVServer.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
data name
|
|
id_tensor : tensor
|
|
a vector storing the ID list
|
|
|
|
Returns
|
|
-------
|
|
tensor
|
|
a data tensor with the same row size of id_tensor.
|
|
"""
|
|
assert len(name) > 0, "name cannot be empty."
|
|
id_tensor = utils.toindex(id_tensor)
|
|
id_tensor = id_tensor.tousertensor()
|
|
assert F.ndim(id_tensor) == 1, "ID must be a vector."
|
|
if self._pull_handlers[name] is default_pull_handler: # Use fast-pull
|
|
part_id = self._part_policy[name].to_partid(id_tensor)
|
|
return rpc.fast_pull(
|
|
name,
|
|
id_tensor,
|
|
part_id,
|
|
KVSTORE_PULL,
|
|
self._machine_count,
|
|
self._group_count,
|
|
self._machine_id,
|
|
self._client_id,
|
|
self._data_store[name],
|
|
self._part_policy[name],
|
|
)
|
|
else:
|
|
# partition data
|
|
machine_id = self._part_policy[name].to_partid(id_tensor)
|
|
# sort index by machine id
|
|
sorted_id = F.tensor(np.argsort(F.asnumpy(machine_id)))
|
|
back_sorted_id = F.tensor(np.argsort(F.asnumpy(sorted_id)))
|
|
id_tensor = id_tensor[sorted_id]
|
|
machine, count = np.unique(
|
|
F.asnumpy(machine_id), return_counts=True
|
|
)
|
|
# pull data from server by order
|
|
start = 0
|
|
pull_count = 0
|
|
local_id = None
|
|
for idx, machine_idx in enumerate(machine):
|
|
end = start + count[idx]
|
|
if start == end: # No data for target machine
|
|
continue
|
|
partial_id = id_tensor[start:end]
|
|
if machine_idx == self._machine_id: # local pull
|
|
# Note that DO NOT pull local data right now because we can overlap
|
|
# communication-local_pull here
|
|
local_id = self._part_policy[name].to_local(partial_id)
|
|
else: # pull data from remote server
|
|
request = PullRequest(name, partial_id)
|
|
rpc.send_request_to_machine(machine_idx, request)
|
|
pull_count += 1
|
|
start += count[idx]
|
|
# recv response
|
|
response_list = []
|
|
if local_id is not None: # local pull
|
|
local_data = self._pull_handlers[name](
|
|
self._data_store, name, local_id
|
|
)
|
|
server_id = self._main_server_id
|
|
local_response = PullResponse(server_id, local_data)
|
|
response_list.append(local_response)
|
|
# wait response from remote server nodes
|
|
for _ in range(pull_count):
|
|
remote_response = rpc.recv_response()
|
|
response_list.append(remote_response)
|
|
# sort response by server_id and concat tensor
|
|
response_list.sort(key=self._take_id)
|
|
data_tensor = F.cat(
|
|
seq=[response.data_tensor for response in response_list], dim=0
|
|
)
|
|
return data_tensor[
|
|
back_sorted_id
|
|
] # return data with original index order
|
|
|
|
def union(self, operand1_name, operand2_name, output_name):
|
|
"""Compute the union of two mask arrays in the KVStore."""
|
|
# Each trainer computes its own result from its local storage.
|
|
self._data_store[output_name][:] = (
|
|
self._data_store[operand1_name] | self._data_store[operand2_name]
|
|
)
|
|
|
|
def _take_id(self, elem):
|
|
"""Used by sort response list"""
|
|
return elem.server_id
|
|
|
|
def count_nonzero(self, name):
|
|
"""Count nonzero value by pull request from KVServers.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
data name
|
|
|
|
Returns
|
|
-------
|
|
int
|
|
the number of nonzero in this data.
|
|
"""
|
|
total = 0
|
|
pull_count = 0
|
|
for machine_id in range(self._machine_count):
|
|
if machine_id == self._machine_id:
|
|
local_id = F.tensor(
|
|
np.arange(
|
|
self._part_policy[name].get_part_size(), dtype=np.int64
|
|
)
|
|
)
|
|
total += F.count_nonzero(self._data_store[name][local_id])
|
|
else:
|
|
request = CountLocalNonzeroRequest(name)
|
|
rpc.send_request_to_machine(machine_id, request)
|
|
pull_count += 1
|
|
for _ in range(pull_count):
|
|
res = rpc.recv_response()
|
|
total += res.num_local_nonzero
|
|
return total
|
|
|
|
@property
|
|
def data_store(self):
|
|
"""Return the local partition of the data storage.
|
|
|
|
Returns
|
|
-------
|
|
dict[str, Tensor]
|
|
The tensor storages of the local partition.
|
|
"""
|
|
return self._data_store
|
|
|
|
|
|
KVCLIENT = None
|
|
|
|
|
|
def init_kvstore(ip_config, num_servers, role):
|
|
"""initialize KVStore"""
|
|
global KVCLIENT
|
|
if KVCLIENT is None:
|
|
if os.environ.get("DGL_DIST_MODE", "standalone") == "standalone":
|
|
KVCLIENT = SA_KVClient()
|
|
else:
|
|
KVCLIENT = KVClient(ip_config, num_servers, role)
|
|
|
|
|
|
def close_kvstore():
|
|
"""Close the current KVClient"""
|
|
global KVCLIENT
|
|
KVCLIENT = None
|
|
|
|
|
|
def get_kvstore():
|
|
"""get the KVClient"""
|
|
return KVCLIENT
|