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

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