chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:35:51 +08:00
commit c36a561cd8
2172 changed files with 455595 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
from __future__ import absolute_import
import importlib
import json
import logging
import os
import sys
from . import backend
from .set_default_backend import set_default_backend
_enabled_apis = set()
logger = logging.getLogger("dgl-core")
def _gen_missing_api(api, mod_name):
def _missing_api(*args, **kwargs):
raise ImportError(
'API "%s" is not supported by backend "%s".'
" You can switch to other backends by setting"
" the DGLBACKEND environment." % (api, mod_name)
)
return _missing_api
def load_backend(mod_name):
# Load backend does four things:
# (1) Import backend framework (PyTorch, MXNet, Tensorflow, etc.)
# (2) Import DGL C library. DGL imports it *after* PyTorch/MXNet/Tensorflow. Otherwise
# DGL will crash with errors like `munmap_chunk(): invalid pointer`.
# (3) Sets up the tensoradapter library path.
# (4) Import the Python wrappers of the backend framework. DGL does this last because
# it already depends on both the backend framework and the DGL C library.
if mod_name == "pytorch":
import torch
mod = torch
elif mod_name == "mxnet":
import mxnet
mod = mxnet
elif mod_name == "tensorflow":
import tensorflow
mod = tensorflow
else:
raise NotImplementedError("Unsupported backend: %s" % mod_name)
from .._ffi.base import load_tensor_adapter # imports DGL C library
version = mod.__version__
load_tensor_adapter(mod_name, version)
logger.debug("Using backend: %s" % mod_name)
mod = importlib.import_module(".%s" % mod_name, __name__)
thismod = sys.modules[__name__]
for api in backend.__dict__.keys():
if api.startswith("__"):
# ignore python builtin attributes
continue
if api == "data_type_dict":
# load data type
if api not in mod.__dict__:
raise ImportError(
'API "data_type_dict" is required but missing for'
' backend "%s".' % (mod_name)
)
data_type_dict = mod.__dict__[api]()
for name, dtype in data_type_dict.items():
setattr(thismod, name, dtype)
# override data type dict function
setattr(thismod, "data_type_dict", data_type_dict)
# for data types with aliases, treat the first listed type as
# the true one
rev_data_type_dict = {}
for k, v in data_type_dict.items():
if not v in rev_data_type_dict.keys():
rev_data_type_dict[v] = k
setattr(thismod, "reverse_data_type_dict", rev_data_type_dict)
# log backend name
setattr(thismod, "backend_name", mod_name)
else:
# load functions
if api in mod.__dict__:
_enabled_apis.add(api)
setattr(thismod, api, mod.__dict__[api])
else:
setattr(thismod, api, _gen_missing_api(api, mod_name))
def get_preferred_backend():
default_dir = None
if "DGLDEFAULTDIR" in os.environ:
default_dir = os.getenv("DGLDEFAULTDIR")
else:
default_dir = os.path.join(os.path.expanduser("~"), ".dgl")
config_path = os.path.join(default_dir, "config.json")
backend_name = None
if "DGLBACKEND" in os.environ:
backend_name = os.getenv("DGLBACKEND")
elif os.path.exists(config_path):
with open(config_path, "r") as config_file:
config_dict = json.load(config_file)
backend_name = config_dict.get("backend", "").lower()
if backend_name in ["tensorflow", "mxnet", "pytorch"]:
return backend_name
else:
print(
"DGL backend not selected or invalid. "
"Assuming PyTorch for now.",
file=sys.stderr,
)
set_default_backend(default_dir, "pytorch")
return "pytorch"
load_backend(get_preferred_backend())
def is_enabled(api):
"""Return true if the api is enabled by the current backend.
Parameters
----------
api : str
The api name.
Returns
-------
bool
True if the API is enabled by the current backend.
"""
return api in _enabled_apis
def to_dgl_nd(data):
return zerocopy_to_dgl_ndarray(data)
def from_dgl_nd(data):
return zerocopy_from_dgl_ndarray(data)
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
from .sparse import *
from .tensor import *
+558
View File
@@ -0,0 +1,558 @@
import mxnet as mx
import numpy as np
from mxnet import nd
from ..._sparse_ops import (
_bwd_segment_cmp,
_csrmask,
_csrmm,
_csrsum,
_gsddmm,
_gspmm,
_scatter_add,
_segment_reduce,
)
from ...base import ALL, dgl_warning, is_all
from ...heterograph_index import create_unitgraph_from_csr
from .tensor import (
asnumpy,
context,
copy_to,
to_backend_ctx,
zerocopy_from_numpy,
)
__all__ = [
"gspmm",
"gsddmm",
"edge_softmax",
"segment_reduce",
"scatter_add",
"csrmm",
"csrsum",
"csrmask",
]
def _scatter_nd(index, src, n_rows):
"""Similar to PyTorch's scatter nd on first dimension."""
assert index.shape == src.shape
dgl_warning("MXNet do not support scatter_add, fallback to numpy.")
ctx = context(src)
index = asnumpy(index)
src = asnumpy(src)
shp = index.shape
ndim = src.ndim
offsets = []
stride = 1
for i in reversed(range(1, ndim)):
di = shp[i]
offset_i = np.arange(di, dtype=index.dtype)
offsets.append(
(stride * offset_i).reshape(
(1,) * i + (di,) + (1,) * (ndim - 1 - i)
)
)
stride *= di
if ndim > 1:
new_idx = index * stride + sum(offsets)
else:
new_idx = index
src = src.reshape(-1)
new_idx = new_idx.reshape(-1)
rst = np.zeros((stride * n_rows,), dtype=src.dtype)
np.add.at(rst, new_idx, src)
rst = rst.reshape(n_rows, *shp[1:])
rst = copy_to(zerocopy_from_numpy(rst), ctx)
return rst
def _gather_nd(index, src):
"""Similar to PyTorch's gather nd on first dimension."""
ctx = context(src)
shp = index.shape
ndim = src.ndim
offsets = []
stride = 1
for i in reversed(range(1, ndim)):
di = shp[i]
offset_i = nd.arange(di, dtype=index.dtype)
offsets.append(
(stride * offset_i).reshape(
(1,) * i + (di,) + (1,) * (ndim - 1 - i)
)
)
stride *= di
if ndim > 1:
new_idx = index * stride + copy_to(sum(offsets), ctx)
else:
new_idx = index
src = src.reshape(-1)
new_idx = new_idx.reshape(-1)
rst = nd.take(src, new_idx).reshape(shp)
return rst
def _reduce_grad(grad, shape):
"""Reduce gradient on the broadcast dimension
If there is broadcast in forward pass, gradients need to be reduced on
broadcast dimension. This function checks the input tensor shape and
gradient shape and perform the reduction.
Parameters
----------
grad: Tensor
Gradient tensor
shape: tuple
Shape of input tensor
Returns
-------
Tensor
"""
grad_shape = grad.shape[1:]
in_shape = shape[1:]
if in_shape == grad_shape:
# no need to reduce
return grad
num_to_squeeze = len(grad_shape) - len(in_shape)
# pad inshape
in_shape = (1,) * num_to_squeeze + in_shape
# pad in_shape
in_shape = (1,) * num_to_squeeze + in_shape
reduce_idx = np.nonzero(np.asarray(grad_shape) - np.asarray(in_shape))[0]
reduce_idx += 1 # skip batch dim
grad = grad.sum(axis=tuple(reduce_idx), keepdims=True)
return grad.reshape(shape)
def _need_reduce_last_dim(ufeat, efeat):
"""Indicates whether to reduce the last dimension on edges
in the backward pass of spmm,
if so, use dot instead of mul."""
ushp = ufeat.shape
eshp = efeat.shape
return ushp[1:-1] == eshp[1:-1] and eshp[-1] == 1 and ushp[-1] > 1
def _muldiv(op, x):
return 1.0 / x if op == "div" else x
def _addsub(op, x):
return -x if op == "sub" else x
def _expand(x, shape):
return x.broadcast_to((x.shape[0], *shape))
class GSpMM(mx.autograd.Function):
def __init__(self, gidx, op, reduce_op):
super(GSpMM, self).__init__()
self.gidx = gidx
self.op = op
self.reduce_op = reduce_op
def forward(self, X, Y):
out, (argX, argY) = _gspmm(self.gidx, self.op, self.reduce_op, X, Y)
self.save_for_backward(X, Y, argX, argY)
return out
def backward(self, dZ):
ctx = context(dZ)
X, Y, argX, argY = self.saved_tensors
gidx, op, reduce_op = self.gidx, self.op, self.reduce_op
if op != "copy_rhs":
g_rev = gidx.reverse()
if reduce_op == "sum":
if op in ["mul", "div"]:
dX = _gspmm(g_rev, "mul", "sum", dZ, _muldiv(op, Y))[0]
elif op in ["add", "sub"]:
dX = _gspmm(g_rev, "copy_lhs", "sum", dZ, Y)[0]
elif op == "copy_lhs":
dX = _gspmm(g_rev, "copy_lhs", "sum", dZ, None)[0]
else:
if op in ["mul", "div"]:
dX = _scatter_nd(
argX,
_muldiv(op, _gather_nd(argY, _expand(Y, dZ.shape[1:])))
* dZ,
X.shape[0],
)
elif op in ["add", "sub", "copy_lhs"]:
dX = _scatter_nd(argX, dZ, X.shape[0])
dX = _reduce_grad(dX, X.shape)
else:
dX = nd.zeros_like(X)
if op != "copy_lhs":
if reduce_op == "sum":
if op == "mul" and _need_reduce_last_dim(X, Y):
dY = _gsddmm(gidx, "dot", X, dZ)
elif op in ["mul", "div"]:
dY = _gsddmm(gidx, "mul", X, dZ)
if op == "div":
dY = -dY / (Y**2)
elif op in ["add", "sub", "copy_rhs"]:
dY = _gsddmm(gidx, "copy_rhs", X, _addsub(op, dZ))
else:
if op in ["mul", "div"]:
dY = _scatter_nd(
argY,
_gather_nd(argX, _expand(X, dZ.shape[1:])) * dZ,
Y.shape[0],
)
if op == "div":
dY = -dY / (Y**2)
elif op in ["add", "sub", "copy_rhs"]:
dY = _scatter_nd(argY, _addsub(op, dZ), Y.shape[0])
dY = _reduce_grad(dY, Y.shape)
else:
dY = nd.zeros_like(Y)
self.saved_tensors = None
return dX, dY
def gspmm(gidx, op, reduce_op, lhs_data, rhs_data):
func = GSpMM(gidx, op, reduce_op)
ctx = to_backend_ctx(gidx.ctx)
# XXX(minjie): There is a bug in MXNet's autograd system when one of the inputs
# does not require gradient. Although it still invokes the backward function,
# it does not set the gradient value to the correct buffer, resulting all the
# input gradients to be zero. Fix this by enforcing all the inputs to require
# gradients.
if lhs_data is None:
lhs_data = nd.zeros((1,), ctx=ctx)
lhs_data.attach_grad()
if rhs_data is None:
rhs_data = nd.zeros((1,), ctx=ctx)
rhs_data.attach_grad()
return func(lhs_data, rhs_data)
class GSDDMM(mx.autograd.Function):
def __init__(self, gidx, op, lhs_target, rhs_target):
super(GSDDMM, self).__init__()
self.gidx = gidx
self.op = op
self.lhs_target = lhs_target
self.rhs_target = rhs_target
def forward(self, X, Y):
out = _gsddmm(
self.gidx, self.op, X, Y, self.lhs_target, self.rhs_target
)
self.save_for_backward(X, Y)
return out
def backward(self, dZ):
ctx = context(dZ)
X, Y = self.saved_tensors
gidx, op = self.gidx, self.op
lhs_target, rhs_target = self.lhs_target, self.rhs_target
if op != "copy_rhs":
if lhs_target in ["u", "v"]:
_gidx = gidx if self.lhs_target == "v" else gidx.reverse()
if op in ["add", "sub", "copy_lhs"]:
dX = _gspmm(_gidx, "copy_rhs", "sum", None, dZ)[0]
else: # mul, div, dot
if rhs_target == lhs_target:
dX = _gspmm(_gidx, "copy_rhs", "sum", None, dZ)[
0
] * _muldiv(op, Y)
elif self.rhs_target == "e":
dX = _gspmm(
_gidx, "copy_rhs", "sum", None, dZ * _muldiv(op, Y)
)[0]
else: # rhs_target = !lhs_target
dX = _gspmm(_gidx, "mul", "sum", _muldiv(op, Y), dZ)[0]
else: # lhs_target == 'e'
if op in ["add", "sub", "copy_lhs"]:
dX = dZ
else: # mul, div, dot
dX = _gsddmm(
gidx, "mul", dZ, _muldiv(op, Y), "e", rhs_target
)
dX = _reduce_grad(dX, X.shape)
else:
dX = nd.zeros_like(X)
if op != "copy_lhs":
if self.rhs_target in ["u", "v"]:
_gidx = gidx if rhs_target == "v" else gidx.reverse()
if op in ["add", "sub", "copy_rhs"]:
dY = _gspmm(
_gidx, "copy_rhs", "sum", None, _addsub(op, dZ)
)[0]
else: # mul, div, dot
if lhs_target == rhs_target:
dY = _gspmm(_gidx, "copy_rhs", "sum", None, dZ)[0] * X
elif self.lhs_target == "e":
dY = _gspmm(_gidx, "copy_rhs", "sum", None, dZ * X)[0]
else: # rhs_target = !lhs_target
dY = _gspmm(_gidx, "mul", "sum", X, dZ)[0]
if op == "div":
dY = -dY / (Y**2)
else:
if op in ["add", "sub", "copy_rhs"]:
dY = _addsub(op, dZ)
else: # mul, div, dot
dY = _gsddmm(gidx, "mul", dZ, X, "e", lhs_target)
if op == "div":
dY = -dY / (Y**2)
dY = _reduce_grad(dY, Y.shape)
else:
dY = nd.zeros_like(Y)
self.saved_tensors = None
return dX, dY
def gsddmm(gidx, op, lhs_data, rhs_data, lhs_target="u", rhs_target="v"):
func = GSDDMM(gidx, op, lhs_target, rhs_target)
ctx = to_backend_ctx(gidx.ctx)
if lhs_data is None:
lhs_data = nd.zeros((1,), ctx=ctx)
if rhs_data is None:
rhs_data = nd.zeros((1,), ctx=ctx)
return func(lhs_data, rhs_data)
class EdgeSoftmax(mx.autograd.Function):
def __init__(self, gidx, eids, norm_by):
super(EdgeSoftmax, self).__init__()
if not is_all(eids):
gidx = gidx.edge_subgraph([eids], True).graph
if norm_by == "src":
gidx = gidx.reverse()
self.gidx = gidx
def forward(self, score):
"""Forward function.
Pseudo-code:
.. code:: python
score = dgl.EData(g, score)
score_max = score.dst_max() # of type dgl.NData
score = score - score_max # edge_sub_dst, ret dgl.EData
score_sum = score.dst_sum() # of type dgl.NData
out = score / score_sum # edge_div_dst, ret dgl.EData
return out.data
"""
gidx = self.gidx
score_max = _gspmm(gidx, "copy_rhs", "max", None, score)[0]
score = mx.nd.exp(_gsddmm(gidx, "sub", score, score_max, "e", "v"))
score_sum = _gspmm(gidx, "copy_rhs", "sum", None, score)[0]
out = _gsddmm(gidx, "div", score, score_sum, "e", "v")
self.save_for_backward(out)
return out
def backward(self, grad_out):
"""Backward function.
Pseudo-code:
.. code:: python
g, out = ctx.backward_cache
grad_out = dgl.EData(g, grad_out)
out = dgl.EData(g, out)
sds = out * grad_out # type dgl.EData
sds_sum = sds.dst_sum() # type dgl.NData
grad_score = sds - sds * sds_sum # multiple expressions
"""
(out,) = self.saved_tensors
gidx = self.gidx
sds = out * grad_out
accum = gspmm(gidx, "copy_rhs", "sum", None, sds)
grad_score = sds - gsddmm(gidx, "mul", out, accum, "e", "v")
self.save_tensors = None
return grad_score
def edge_softmax(gidx, logits, eids=ALL, norm_by="dst"):
softmax_op = EdgeSoftmax(gidx, eids, norm_by)
return softmax_op(logits)
class SegmentReduce(mx.autograd.Function):
def __init__(self, op, offsets):
super(SegmentReduce, self).__init__()
self.op = op
self.offsets = offsets
def forward(self, x):
y, arg = _segment_reduce(self.op, x, self.offsets)
self.save_for_backward(arg)
return y
def backward(self, dy):
(arg,) = self.saved_tensors
offsets = self.offsets
m = offsets[-1].asscalar()
if self.op == "sum":
offsets_np = asnumpy(offsets[1:])
indices_np = np.zeros((m + 1,), dtype=offsets_np.dtype)
np.add.at(indices_np, offsets_np, np.ones_like(offsets_np))
indices_np = np.cumsum(indices_np, -1)[:-1]
indices = zerocopy_from_numpy(indices_np)
dx = dy[indices]
else:
dx = _bwd_segment_cmp(dy, arg, m)
return dx
def segment_reduce(op, x, offsets):
segment_reduce_op = SegmentReduce(op, offsets)
return segment_reduce_op(x)
class ScatterAdd(mx.autograd.Function):
def __init__(self, idx, m):
super(ScatterAdd, self).__init__()
self.idx = idx
self.m = m
def forward(self, x):
y = _scatter_add(x, self.idx, self.m)
return y
def backward(self, dy):
return dy[self.idx]
def scatter_add(x, idx, m):
scatter_add_op = ScatterAdd(idx, m)
return scatter_add_op(x)
class CSRMM(mx.autograd.Function):
def __init__(self, gidxA, gidxB, num_vtypes):
super().__init__()
self.gidxA = gidxA
self.gidxB = gidxB
self.num_vtypes = num_vtypes
def forward(self, A_weights, B_weights):
gidxC, C_weights = _csrmm(
self.gidxA, A_weights, self.gidxB, B_weights, self.num_vtypes
)
(
nrows,
ncols,
C_indptr,
C_indices,
C_eids,
) = gidxC.adjacency_matrix_tensors(0, False, "csr")
# Note: the returned C_indptr, C_indices and C_eids tensors MUST be the same
# as the underlying tensors of the created graph gidxC.
self.backward_cache = gidxC
self.save_for_backward(A_weights, B_weights)
nrows = nd.array([nrows], dtype="int64")
ncols = nd.array([ncols], dtype="int64")
return nrows, ncols, C_indptr, C_indices, C_eids, C_weights
def backward(
self, dnrows, dncols, dC_indptr, dC_indices, dC_eids, dC_weights
):
# Only the last argument is meaningful.
gidxC = self.backward_cache
A_weights, B_weights = self.saved_tensors
dgidxA, dA_weights = _csrmm(
gidxC,
dC_weights,
self.gidxB.reverse(),
B_weights,
self.gidxA.number_of_ntypes(),
)
dgidxB, dB_weights = _csrmm(
self.gidxA.reverse(),
A_weights,
gidxC,
dC_weights,
self.gidxB.number_of_ntypes(),
)
dA_weights = _csrmask(dgidxA, dA_weights, self.gidxA)
dB_weights = _csrmask(dgidxB, dB_weights, self.gidxB)
return dA_weights, dB_weights
def csrmm(gidxA, A_weights, gidxB, B_weights, num_vtypes):
op = CSRMM(gidxA, gidxB, num_vtypes)
nrows, ncols, C_indptr, C_indices, C_eids, C_weights = op(
A_weights, B_weights
)
gidxC = create_unitgraph_from_csr(
num_vtypes,
nrows.asscalar(),
ncols.asscalar(),
C_indptr,
C_indices,
C_eids,
["coo", "csr", "csc"],
)
return gidxC, C_weights
class CSRSum(mx.autograd.Function):
def __init__(self, gidxs):
super().__init__()
self.gidxs = gidxs
def forward(self, *weights):
gidxC, C_weights = _csrsum(self.gidxs, weights)
(
nrows,
ncols,
C_indptr,
C_indices,
C_eids,
) = gidxC.adjacency_matrix_tensors(0, False, "csr")
# Note: the returned C_indptr, C_indices and C_eids tensors MUST be the same
# as the underlying tensors of the created graph gidxC.
self.backward_cache = gidxC
nrows = nd.array([nrows], dtype="int64")
ncols = nd.array([ncols], dtype="int64")
return nrows, ncols, C_indptr, C_indices, C_eids, C_weights
def backward(
self, dnrows, dncols, dC_indptr, dC_indices, dC_eids, dC_weights
):
# Only the last argument is meaningful.
gidxC = self.backward_cache
return tuple(csrmask(gidxC, dC_weights, gidx) for gidx in self.gidxs)
def csrsum(gidxs, weights):
op = CSRSum(gidxs)
nrows, ncols, C_indptr, C_indices, C_eids, C_weights = op(*weights)
num_vtypes = gidxs[0].number_of_ntypes()
gidxC = create_unitgraph_from_csr(
num_vtypes,
nrows.asscalar(),
ncols.asscalar(),
C_indptr,
C_indices,
C_eids,
["coo", "csr", "csc"],
)
return gidxC, C_weights
class CSRMask(mx.autograd.Function):
def __init__(self, gidxA, gidxB):
super().__init__()
self.gidxA = gidxA
self.gidxB = gidxB
def forward(self, A_weights):
return _csrmask(self.gidxA, A_weights, self.gidxB)
def backward(self, dB_weights):
return _csrmask(self.gidxB, dB_weights, self.gidxA)
def csrmask(gidxA, A_weights, gidxB):
op = CSRMask(gidxA, gidxB)
return op(A_weights)
+1
View File
@@ -0,0 +1 @@
"""Sparse optimizer is not supported for mxnet"""
+573
View File
@@ -0,0 +1,573 @@
from __future__ import absolute_import
import builtins
import numbers
import os
import mxnet as mx
import mxnet.ndarray as nd
import numpy as np
from ... import ndarray as dglnd
from ...function.base import TargetCode
from ...utils import version
if version.parse(mx.__version__) < version.parse("1.6.0"):
raise RuntimeError("DGL requires MXNet >= 1.6")
# After MXNet 1.5, empty tensors aren't supprted by default.
# After we turn on the numpy compatible flag, MXNet supports empty NDArray.
mx.set_np_shape(bool(os.environ.get("DGL_MXNET_SET_NP_SHAPE", True)))
def data_type_dict():
return {
"float16": np.float16,
"float32": np.float32,
"float64": np.float64,
"uint8": np.uint8,
"int8": np.int8,
"int16": np.int16,
"int32": np.int32,
"int64": np.int64,
"bool": np.bool_,
} # mxnet does not support bool
def cpu():
return mx.cpu()
def tensor(data, dtype=None):
if dtype == np.bool_:
# mxnet doesn't support bool
dtype = np.int32
if isinstance(data, nd.NDArray):
if dtype is None or data.dtype == dtype:
return data
else:
return data.astype(dtype)
else:
if isinstance(data, numbers.Number):
data = [data]
if dtype is None:
if isinstance(data, np.ndarray):
dtype = np.int32 if data.dtype == np.bool_ else data.dtype
elif len(data) == 0:
dtype = np.int64
else:
dtype = (
np.int64
if isinstance(data[0], numbers.Integral)
else np.float32
)
return nd.array(data, dtype=dtype)
def as_scalar(data):
if data.size != 1:
raise ValueError("The current array is not a scalar")
if data.shape != (1,):
data = data.expand_dims(axis=0)
return data.asscalar()
def get_preferred_sparse_format():
"""Get the preferred sparse matrix format supported by the backend.
Different backends have their preferred backend. This info is useful when
constructing a sparse matrix.
"""
return "csr"
def sparse_matrix(data, index, shape, force_format=False):
fmt = index[0]
if fmt == "coo":
if force_format:
raise TypeError(
"MXNet backend only supports CSR format,"
" but COO format is forced."
)
coord = index[1]
# generate convert idx
# FIXME: cannot use int64
tmp_data = nd.arange(
len(coord[0]), dtype=data.dtype, ctx=coord[0].context
)
tmp_spmat = nd.sparse.csr_matrix(
(tmp_data, (coord[0], coord[1])), tuple(shape), ctx=data.context
)
convert_idx = nd.cast(tmp_spmat.data, dtype="int64")
# shuffle the data
data = data[convert_idx]
spmat = nd.sparse.csr_matrix(
(data, tmp_spmat.indices, tmp_spmat.indptr),
tuple(shape),
ctx=data.context,
)
return spmat, convert_idx
elif fmt == "csr":
indices = index[1]
indptr = index[2]
spmat = nd.sparse.csr_matrix(
(data, indices, indptr), tuple(shape), ctx=data.context
)
# No conversion is required.
return spmat, None
else:
raise TypeError("Invalid format: %s." % fmt)
def sparse_matrix_indices(spmat):
return ("csr", spmat.indices, spmat.indptr)
def is_tensor(obj):
return isinstance(obj, nd.NDArray)
def shape(input):
# NOTE: the input cannot be a symbol
return input.shape
def dtype(input):
# NOTE: the input cannot be a symbol
return input.dtype
def ndim(input):
return input.ndim
def context(input):
return input.context
def device_type(ctx):
return ctx.device_type
def device_id(ctx):
return ctx.device_id
def to_backend_ctx(dglctx):
dev_type = dglctx.device_type
if dev_type == 1:
return mx.cpu()
elif dev_type == 2:
return mx.gpu(dglctx.device_id)
else:
raise ValueError("Unsupported DGL device context:", dglctx)
def astype(input, ty):
if ty == np.bool_:
ty = np.int32
return input.astype(ty)
def asnumpy(input):
return input.asnumpy()
def copy_to(input, ctx, **kwargs):
return input.as_in_context(ctx)
def is_pinned(input):
return input.context == mx.cpu_pinned()
def sum(input, dim, keepdims=False):
if len(input) == 0:
return nd.array([0.0], dtype=input.dtype, ctx=input.context)
return nd.sum(input, axis=dim, keepdims=keepdims)
def floor_div(in1, in2):
return in1 / in2
def reduce_sum(input):
return input.sum()
def cumsum(input, dim):
return nd.cumsum(input, axis=dim)
def mean(input, dim):
return nd.mean(input, axis=dim)
def reduce_mean(input):
return input.mean()
def max(input, dim):
return nd.max(input, axis=dim)
def reduce_max(input):
return input.max()
def min(input, dim):
return nd.min(input, axis=dim)
def reduce_min(input):
return input.min()
def topk(input, k, dim, descending=True):
return nd.topk(
input, axis=dim, k=k, ret_typ="value", is_ascend=not descending
)
def argtopk(input, k, dim, descending=True):
idx = nd.argsort(input, dim, is_ascend=not descending)
return nd.slice_axis(input, dim, 0, k)
def argsort(input, dim, descending):
idx = nd.argsort(input, dim, is_ascend=not descending)
idx = nd.cast(idx, dtype="int64")
return idx
def exp(input):
return nd.exp(input)
def inverse(input):
return nd.linalg_inverse(input)
def sqrt(input):
return nd.sqrt(input)
def softmax(input, dim=-1):
return nd.softmax(input, axis=dim)
def cat(seq, dim):
return nd.concat(*seq, dim=dim)
def stack(seq, dim):
return nd.stack(*seq, axis=dim)
def split(x, sizes_or_sections, dim):
if isinstance(sizes_or_sections, list) and len(sizes_or_sections) == 1:
assert len(x) == sizes_or_sections[0]
return [x]
if isinstance(sizes_or_sections, (np.ndarray, list)):
sizes_or_sections1 = tuple(np.cumsum(sizes_or_sections)[:-1])
return nd.split_v2(x, sizes_or_sections1, axis=dim)
def repeat(input, repeats, dim):
if isinstance(repeats, nd.NDArray):
return nd.array(
np.repeat(input.asnumpy(), repeats.asnumpy(), axis=dim),
ctx=input.context,
dtype=input.dtype,
)
else:
return nd.repeat(input, repeats, axis=dim)
def gather_row(data, row_index):
# MXNet workaround for empty row index
if len(row_index) == 0:
if data.shape[0] == 0:
return data
else:
return data[0:0]
if isinstance(row_index, nd.NDArray):
return nd.take(data, row_index)
else:
return data[
row_index,
]
def slice_axis(data, axis, begin, end):
dim = data.shape[axis]
if begin < 0:
begin += dim
if end <= 0:
end += dim
return nd.slice_axis(data, axis, begin, end)
def take(data, indices, dim):
return nd.take(data, indices, dim)
def narrow_row(data, start, stop):
return data[start:stop]
def index_add_inplace(data, row_idx, value):
raise NotImplementedError("MXNet doesn't support inplace index_add")
def scatter_row(data, row_index, value):
return mx.nd.contrib.index_copy(data, row_index, value)
def scatter_row_inplace(data, row_index, value):
data[row_index] = value
def squeeze(input, dim):
return nd.squeeze(input, axis=dim)
def unsqueeze(input, dim):
return nd.expand_dims(input, axis=dim)
def reshape(input, shape):
# NOTE: the input cannot be a symbol
return nd.reshape(input, shape)
def swapaxes(input, axis1, axis2):
return nd.swapaxes(input, axis1, axis2)
def empty(shape, dtype, ctx):
return nd.empty(shape, dtype=dtype, ctx=ctx)
def zeros(shape, dtype, ctx):
return nd.zeros(shape, dtype=dtype, ctx=ctx)
def zeros_like(input):
return nd.zeros_like(input)
def ones(shape, dtype, ctx):
return nd.ones(shape, dtype=dtype, ctx=ctx)
def uniform(shape, dtype, ctx, low, high):
return nd.random.uniform(low, high, ctx=ctx, dtype=dtype, shape=shape)
def randint(shape, dtype, ctx, low, high):
return nd.random.randint(low, high, ctx=ctx, dtype=dtype, shape=shape)
def pad_packed_tensor(input, lengths, value, l_min=None):
old_shape = input.shape
if isinstance(lengths, nd.NDArray):
lengths = list(lengths.asnumpy())
max_len = builtins.max(lengths)
if l_min is not None:
max_len = builtins.max(max_len, l_min)
batch_size = len(lengths)
ctx = input.context
dtype = input.dtype
x = nd.full(
(batch_size * max_len, *old_shape[1:]), value, ctx=ctx, dtype=dtype
)
index = []
for i, l in enumerate(lengths):
index.extend(range(i * max_len, i * max_len + l))
index = nd.array(index, ctx=ctx)
return scatter_row(x, index, input).reshape(
batch_size, max_len, *old_shape[1:]
)
def pack_padded_tensor(input, lengths):
batch_size, max_len = input.shape[:2]
ctx = input.context
index = []
for i, l in enumerate(lengths):
index.extend(range(i * max_len, i * max_len + l))
index = nd.array(index, ctx=ctx)
return gather_row(input.reshape(batch_size * max_len, -1), index)
def boolean_mask(input, mask):
return mx.contrib.nd.boolean_mask(input, mask)
def equal(x, y):
return x == y
def allclose(x, y, rtol=1e-4, atol=1e-4):
return np.allclose(x.asnumpy(), y.asnumpy(), rtol=rtol, atol=atol)
def logical_not(input):
return nd.logical_not(input)
def logical_and(input1, input2):
return nd.logical_and(input1, input2)
def clone(input):
return input.copy()
def clamp(data, min_val, max_val):
return nd.clip(data, min_val, max_val)
def replace_inf_with_zero(x):
return nd.where(nd.abs(x) == np.inf, nd.zeros_like(x), x)
def count_nonzero(input):
# TODO: fallback to numpy is unfortunate
tmp = input.asnumpy()
return np.count_nonzero(tmp)
def unique(input, return_inverse=False, return_counts=False):
# TODO: fallback to numpy is unfortunate
tmp = input.asnumpy()
if return_inverse and return_counts:
tmp, inv, count = np.unique(
tmp, return_inverse=True, return_counts=True
)
tmp = nd.array(tmp, ctx=input.context, dtype=input.dtype)
inv = nd.array(inv, ctx=input.context)
count = nd.array(count, ctx=input.context)
return tmp, inv, count
elif return_inverse or return_counts:
tmp, tmp2 = np.unique(
tmp, return_inverse=return_inverse, return_counts=return_counts
)
tmp = nd.array(tmp, ctx=input.context, dtype=input.dtype)
tmp2 = nd.array(tmp2, ctx=input.context)
return tmp, tmp2
else:
tmp = np.unique(tmp)
return nd.array(tmp, ctx=input.context, dtype=input.dtype)
def full_1d(length, fill_value, dtype, ctx):
return nd.full((length,), fill_value, dtype=dtype, ctx=ctx)
def nonzero_1d(input):
# TODO: fallback to numpy is unfortunate
tmp = input.asnumpy()
tmp = np.nonzero(tmp)[0]
r = nd.array(tmp, ctx=input.context, dtype=tmp.dtype)
return r
def sort_1d(input):
# TODO: this isn't an ideal implementation.
val = nd.sort(input, axis=None, is_ascend=True)
idx = nd.argsort(input, is_ascend=True)
idx = nd.cast(idx, dtype="int64")
return val, idx
def arange(start, stop, dtype=np.int64, ctx=None):
if start >= stop:
return nd.array([], dtype=dtype, ctx=ctx)
else:
return nd.arange(start, stop, dtype=dtype, ctx=ctx)
def rand_shuffle(arr):
return mx.nd.random.shuffle(arr)
def zerocopy_to_dlpack(arr):
return arr.to_dlpack_for_read()
def zerocopy_from_dlpack(dlpack_arr):
return nd.from_dlpack(dlpack_arr)
def zerocopy_to_numpy(arr):
# NOTE: not zerocopy
return arr.asnumpy()
def zerocopy_from_numpy(np_data):
np_data = np.asarray(np_data, order="C")
return mx.nd.from_numpy(np_data, zero_copy=True)
def zerocopy_to_dgl_ndarray(arr):
arr.to_dlpack_for_read()
return dglnd.from_dlpack(arr.to_dlpack_for_read())
def zerocopy_to_dgl_ndarray_for_write(arr):
return dglnd.from_dlpack(arr.to_dlpack_for_write())
def zerocopy_from_dgl_ndarray(arr):
return nd.from_dlpack(arr.to_dlpack())
def sync():
"""Synchronize computation.
In DL frameworks such as MXNet and TensorFlow, the computation in operators
are done asynchronously. This is to synchronize computation and makes sure
that all computation is complete after this function call.
"""
mx.nd.waitall()
def attach_grad(tensor):
tensor.attach_grad()
return tensor
def backward(x, head_gradient=None):
x.backward(head_gradient)
def grad(x):
return x.grad
def is_no_grad(x):
return (x != 0).sum() == 0
def is_recording():
return mx.autograd.is_recording()
record_grad = mx.autograd.record
class no_grad(object):
def __init__(self):
pass
def __enter__(self):
pass
def __exit__(self, exc_type, exc_value, exc_traceback):
pass
+2
View File
@@ -0,0 +1,2 @@
from .sparse import *
from .tensor import *
File diff suppressed because it is too large Load Diff
+535
View File
@@ -0,0 +1,535 @@
from __future__ import absolute_import
import builtins
import numbers
import numpy as np
import scipy # Weird bug in new pytorch when import scipy after import torch
import torch as th
from torch.utils import dlpack
from ... import ndarray as nd
from ...function.base import TargetCode
from ...utils import version
if version.parse(th.__version__) < version.parse("2.1.0"):
raise RuntimeError("DGL requires PyTorch >= 2.1.0")
def data_type_dict():
return {
"bfloat16": th.bfloat16,
"float16": th.float16,
"float32": th.float32,
"float64": th.float64,
"uint8": th.uint8,
"int8": th.int8,
"int16": th.int16,
"int32": th.int32,
"int64": th.int64,
"bool": th.bool,
}
def cpu():
return th.device("cpu")
def tensor(data, dtype=None):
if isinstance(data, numbers.Number):
data = [data]
if (
isinstance(data, list)
and len(data) > 0
and isinstance(data[0], th.Tensor)
):
# prevent GPU->CPU->GPU copies
if data[0].ndim == 0:
# zero dimenion scalar tensors
return th.stack(data)
if isinstance(data, th.Tensor):
return th.as_tensor(data, dtype=dtype, device=data.device)
else:
return th.as_tensor(data, dtype=dtype)
def as_scalar(data):
return data.item()
def get_preferred_sparse_format():
"""Get the preferred sparse matrix format supported by the backend.
Different backends have their preferred backend. This info is useful when
constructing a sparse matrix.
"""
return "coo"
def sparse_matrix(data, index, shape, force_format=False):
fmt = index[0]
if fmt != "coo":
raise TypeError(
"Pytorch backend only supports COO format. But got %s." % fmt
)
spmat = th.sparse_coo_tensor(index[1], data, shape)
return spmat, None
def sparse_matrix_indices(spmat):
return ("coo", spmat._indices())
def is_tensor(obj):
return isinstance(obj, th.Tensor)
def shape(input):
return input.shape
def dtype(input):
return input.dtype
def ndim(input):
return input.dim()
def context(input):
return input.device
def device_type(ctx):
return th.device(ctx).type
def device_id(ctx):
ctx = th.device(ctx)
if ctx.index is None:
return 0 if ctx.type == "cpu" else th.cuda.current_device()
else:
return ctx.index
def to_backend_ctx(dglctx):
dev_type = dglctx.device_type
if dev_type == 1:
return th.device("cpu")
elif dev_type == 2:
return th.device("cuda", dglctx.device_id)
else:
raise ValueError("Unsupported DGL device context:", dglctx)
def astype(input, ty):
return input.type(ty)
def asnumpy(input):
if isinstance(input, th.sparse.FloatTensor):
return input.to_dense().cpu().detach().numpy()
else:
return input.cpu().detach().numpy()
def copy_to(input, ctx, **kwargs):
ctx = th.device(ctx)
if ctx.type == "cpu":
return input.cpu()
elif ctx.type == "cuda":
if ctx.index is not None:
th.cuda.set_device(ctx.index)
return input.cuda(**kwargs)
else:
raise RuntimeError("Invalid context", ctx)
def is_pinned(input):
return input.is_pinned()
def sum(input, dim, keepdims=False):
return th.sum(input, dim=dim, keepdim=keepdims)
def floor_div(in1, in2):
return in1 // in2
def reduce_sum(input):
return input.sum()
def cumsum(input, dim):
return th.cumsum(input, dim=dim)
def mean(input, dim):
return th.mean(input, dim=dim)
def reduce_mean(input):
return input.mean()
def max(input, dim):
# NOTE: the second argmax array is not returned
return th.max(input, dim=dim)[0]
def reduce_max(input):
return input.max()
def min(input, dim):
# NOTE: the second argmin array is not returned
return th.min(input, dim=dim)[0]
def reduce_min(input):
return input.min()
def argsort(input, dim, descending):
return th.argsort(input, dim=dim, descending=descending)
def topk(input, k, dim, descending=True):
return th.topk(input, k, dim, largest=descending)[0]
def argtopk(input, k, dim, descending=True):
return th.topk(input, k, dim, largest=descending)[1]
def exp(input):
return th.exp(input)
def inverse(input):
return th.inverse(input)
def sqrt(input):
return th.sqrt(input)
def softmax(input, dim=-1):
return th.softmax(input, dim=dim)
def cat(seq, dim):
return th.cat(seq, dim=dim)
def stack(seq, dim):
return th.stack(seq, dim=dim)
def split(input, sizes_or_sections, dim):
return th.split(input, sizes_or_sections, dim)
def repeat(input, repeats, dim):
return th.repeat_interleave(input, repeats, dim) # PyTorch 1.1
def gather_row(data, row_index):
return th.index_select(data, 0, row_index.long())
def slice_axis(data, axis, begin, end):
return th.narrow(data, axis, begin, end - begin)
def take(data, indices, dim):
new_shape = data.shape[:dim] + indices.shape + data.shape[dim + 1 :]
return th.index_select(data, dim, indices.view(-1)).view(new_shape)
def narrow_row(x, start, stop):
return x[start:stop]
def index_add_inplace(data, row_idx, value):
data.index_add_(0, row_idx, value)
def scatter_row(data, row_index, value):
return data.index_copy(0, row_index.long(), value)
def scatter_row_inplace(data, row_index, value):
data[row_index.long()] = value
def squeeze(input, dim):
return th.squeeze(input, dim)
def unsqueeze(input, dim):
return th.unsqueeze(input, dim)
def reshape(input, shape):
return th.reshape(input, shape)
def swapaxes(input, axis1, axis2):
return th.transpose(input, axis1, axis2)
def empty(shape, dtype, ctx):
return th.empty(shape, dtype=dtype, device=ctx)
def zeros(shape, dtype, ctx):
return th.zeros(shape, dtype=dtype, device=ctx)
def zeros_like(input):
return th.zeros_like(input)
def ones(shape, dtype, ctx):
return th.ones(shape, dtype=dtype, device=ctx)
def uniform(shape, dtype, ctx, low, high):
return th.empty(shape, dtype=dtype, device=ctx).uniform_(low, high)
def randint(shape, dtype, ctx, low, high):
return th.randint(low, high, shape, dtype=dtype, device=ctx)
def pad_packed_tensor(input, lengths, value, l_min=None):
old_shape = input.shape
device = input.device
if not is_tensor(lengths):
lengths = th.tensor(lengths, dtype=th.int64, device=device)
else:
lengths = lengths.to(device)
max_len = as_scalar(lengths.max())
if l_min is not None:
max_len = builtins.max(max_len, l_min)
batch_size = len(lengths)
x = input.new(batch_size * max_len, *old_shape[1:])
x.fill_(value)
index = th.ones(len(input), dtype=th.int64, device=device)
cum_lengths = th.cumsum(lengths, 0)
index[cum_lengths[:-1]] += max_len - lengths[:-1]
index = th.cumsum(index, 0) - 1
x[index] = input
return x.view(batch_size, max_len, *old_shape[1:])
def pack_padded_tensor(input, lengths):
max_len = input.shape[1]
device = input.device
if not is_tensor(lengths):
lengths = th.tensor(lengths, dtype=th.int64, device=device)
else:
lengths = lengths.to(device)
input = input.view(-1, *input.shape[2:])
out_len = lengths.sum().item()
index = th.ones(out_len, dtype=th.int64, device=device)
cum_lengths = th.cumsum(lengths, 0)
index[cum_lengths[:-1]] += max_len - lengths[:-1]
index = th.cumsum(index, 0) - 1
return input[index]
def boolean_mask(input, mask):
if "bool" not in str(mask.dtype):
mask = th.as_tensor(mask, dtype=th.bool)
return input[mask]
def equal(x, y):
return x == y
def allclose(x, y, rtol=1e-4, atol=1e-4):
return th.allclose(x, y, rtol=rtol, atol=atol)
def logical_not(input):
return ~input
def logical_and(input1, input2):
return input1 & input2
def clone(input):
return input.clone()
def clamp(data, min_val, max_val):
return th.clamp(data, min_val, max_val)
def replace_inf_with_zero(x):
return th.masked_fill(x, th.isinf(x), 0)
def count_nonzero(input):
# TODO: fallback to numpy for backward compatibility
return np.count_nonzero(input)
def unique(input, return_inverse=False, return_counts=False):
if input.dtype == th.bool:
input = input.type(th.int8)
return th.unique(
input, return_inverse=return_inverse, return_counts=return_counts
)
def full_1d(length, fill_value, dtype, ctx):
return th.full((length,), fill_value, dtype=dtype, device=ctx)
def nonzero_1d(input):
x = th.nonzero(input, as_tuple=False).squeeze()
return x if x.dim() == 1 else x.view(-1)
def sort_1d(input):
return th.sort(input)
def arange(start, stop, dtype=th.int64, ctx=None):
return th.arange(start, stop, dtype=dtype, device=ctx)
def rand_shuffle(arr):
idx = th.randperm(len(arr))
return arr[idx]
def zerocopy_to_dlpack(input):
return dlpack.to_dlpack(input.contiguous())
def zerocopy_from_dlpack(dlpack_tensor):
return dlpack.from_dlpack(dlpack_tensor)
def zerocopy_to_numpy(input):
# NOTE: not zerocopy
return asnumpy(input)
def zerocopy_from_numpy(np_array):
return th.as_tensor(np_array)
def zerocopy_to_dgl_ndarray(data):
if data.dtype == th.bool:
data = data.byte()
return nd.from_dlpack(dlpack.to_dlpack(data.contiguous()))
# NGC PyTorch containers are shipping alpha version PyTorch.
if version.parse(th.__version__) >= version.parse("2.0.0a0"):
def check_is_view(input):
assert (
input.data_ptr() == input.untyped_storage().data_ptr()
), "Cannot convert view tensors to dgl ndarray for write."
else:
def check_is_view(input):
assert (
input.data_ptr() == input._storage().data_ptr()
), "Cannot convert view tensors to dgl ndarray for write."
def zerocopy_to_dgl_ndarray_for_write(input):
if input.numel() > 0:
# only check non-empty tensors
assert input.is_contiguous(), (
"Cannot convert non-contiguous tensors "
"to dgl ndarray for write. Call .to_contiguous() first."
)
check_is_view(input)
return zerocopy_to_dgl_ndarray(input)
def zerocopy_from_dgl_ndarray(data):
if data.shape == (0,):
# NOTE: PyTorch v1.5 does not accept DLPack object representing empty CUDA tensor.
# Related issue: https://github.com/pytorch/pytorch/issues/41182
# The issue will be fixed in v1.6 and later.
return th.tensor(
[], dtype=getattr(th, data.dtype), device=to_backend_ctx(data.ctx)
)
elif len(data.shape) == 0 or builtins.min(data.shape) == 0:
# Workaround the same issue as above, but preserve the shape of the
# empty tensor. This is needed by the sparse optimizer when one of
# processors may receive no gradients to update, but we want to keep
# the dimension of the embedding.
return th.empty(
data.shape,
dtype=getattr(th, data.dtype),
device=to_backend_ctx(data.ctx),
)
else:
return dlpack.from_dlpack(data.to_dlpack())
def sync():
# Pytorch performs computation synchronously, so no need for synchronization.
pass
def attach_grad(x):
if x.grad is not None:
x.grad.zero_()
return x
else:
return x.requires_grad_()
def backward(x, head_gradient=None):
if (
head_gradient is not None
and head_gradient.shape[0] == 1
and len(head_gradient.shape) == 1
):
# Fix for torch 1.3.1
head_gradient = th.tensor(head_gradient.item()).to(head_gradient.device)
x.backward(head_gradient)
def grad(x):
x.retain_grad()
return x.grad
def is_no_grad(x):
return x.grad is None or (x.grad == 0).all()
def is_recording():
return th.is_grad_enabled()
class record_grad(object):
def __init__(self):
pass
def __enter__(self):
pass
def __exit__(self, exc_type, exc_value, exc_traceback):
pass
no_grad = th.no_grad
+35
View File
@@ -0,0 +1,35 @@
import argparse
import json
import os
def set_default_backend(default_dir, backend_name):
os.makedirs(default_dir, exist_ok=True)
config_path = os.path.join(default_dir, "config.json")
with open(config_path, "w") as config_file:
json.dump({"backend": backend_name.lower()}, config_file)
print(
'Setting the default backend to "{}". You can change it in the '
"~/.dgl/config.json file or export the DGLBACKEND environment variable. "
"Valid options are: pytorch, mxnet, tensorflow (all lowercase)".format(
backend_name
)
)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"default_dir",
type=str,
default=os.path.join(os.path.expanduser("~"), ".dgl"),
)
parser.add_argument(
"backend",
nargs=1,
type=str,
choices=["pytorch", "tensorflow", "mxnet"],
help="Set default backend",
)
args = parser.parse_args()
set_default_backend(args.default_dir, args.backend[0])
@@ -0,0 +1,6 @@
import os
os.environ["TF_FORCE_GPU_ALLOW_GROWTH"] = "true"
from .sparse import *
from .tensor import *
+461
View File
@@ -0,0 +1,461 @@
import numpy as np
import tensorflow as tf
from ..._sparse_ops import (
_bwd_segment_cmp,
_csrmask,
_csrmm,
_csrsum,
_gsddmm,
_gspmm,
_scatter_add,
_segment_reduce,
)
from ...base import ALL, is_all
from ...heterograph_index import create_unitgraph_from_csr
from .tensor import asnumpy, context, copy_to, tensor, zerocopy_from_numpy
__all__ = [
"gspmm",
"gsddmm",
"edge_softmax",
"segment_reduce",
"scatter_add",
"csrmm",
"csrsum",
"csrmask",
]
def _scatter_nd(index, src, n_rows):
assert index.shape == src.shape
shp = index.shape
ctx = context(src)
ndim = index.ndim
offsets = []
stride = 1
for i in reversed(range(1, ndim)):
di = shp[i]
offset_i = tf.range(di, dtype=index.dtype)
offsets.append(
tf.reshape(
(stride * offset_i), (1,) * i + (di,) + (1,) * (ndim - 1 - i)
)
)
stride *= di
if ndim > 1:
new_idx = index * stride + copy_to(sum(offsets), ctx)
else:
new_idx = index
src = tf.reshape(src, (-1,))
new_idx = tf.reshape(new_idx, (-1, 1))
rst = tf.reshape(
tf.scatter_nd(new_idx, src, (stride * n_rows,)), (n_rows, *shp[1:])
)
return rst
def _gather_nd(index, src):
shp = index.shape
ctx = context(src)
ndim = index.ndim
offsets = []
stride = 1
for i in reversed(range(1, ndim)):
di = shp[i]
offset_i = tf.range(di, dtype=index.dtype)
offsets.append(
tf.reshape(
(stride * offset_i), (1,) * i + (di,) + (1,) * (ndim - 1 - i)
)
)
stride *= di
if ndim > 1:
new_idx = index * stride + copy_to(sum(offsets), ctx)
else:
new_idx = index
src = tf.reshape(src, (-1,))
new_idx = tf.reshape(new_idx, (-1))
rst = tf.reshape(tf.gather(src, new_idx), shp)
return rst
def _reduce_grad(grad, shape):
"""Reduce gradient on the broadcast dimension
If there is broadcast in forward pass, gradients need to be reduced on
broadcast dimension. This function checks the input tensor shape and
gradient shape and perform the reduction.
Parameters
----------
grad: Tensor
Gradient tensor
shape: tuple
Shape of input tensor
Returns
-------
Tensor
"""
grad_shape = grad.shape[1:]
in_shape = shape[1:]
if in_shape == grad_shape:
# no need to reduce
return grad
num_to_squeeze = len(grad_shape) - len(in_shape)
# pad inshape
in_shape = (1,) * num_to_squeeze + in_shape
reduce_idx = np.asarray(
np.nonzero(np.asarray(grad_shape) - np.asarray(in_shape))
)
reduce_idx += 1 # skip batch dim
reduce_idx_tensor = tf.constant(
tuple(reduce_idx.flatten().tolist()), dtype=tf.int32
)
grad = tf.reduce_sum(grad, axis=reduce_idx_tensor, keepdims=True)
return tf.reshape(grad, shape)
def _need_reduce_last_dim(ufeat, efeat):
"""Indicates whether to reduce the last dimension on edges
in the backward pass of spmm,
if so, use dot instead of mul."""
ushp = ufeat.shape
eshp = efeat.shape
return ushp[1:-1] == eshp[1:-1] and eshp[-1] == 1 and ushp[-1] > 1
def _muldiv(op, x):
return 1.0 / x if op == "div" else x
def _addsub(op, x):
return -x if op == "sub" else x
def _expand(x, shape):
return tf.broadcast_to(x, (x.shape[0], *shape))
def gspmm_real(gidx, op, reduce_op, X, Y):
out, (argX, argY) = _gspmm(gidx, op, reduce_op, X, Y)
def grad(dZ):
dZ = tensor(dZ)
if op != "copy_rhs":
g_rev = gidx.reverse()
if reduce_op == "sum":
if op in ["mul", "div"]:
dX = _gspmm(g_rev, "mul", "sum", dZ, _muldiv(op, Y))[0]
elif op in ["add", "sub"]:
dX = _gspmm(g_rev, "copy_lhs", "sum", dZ, Y)[0]
elif op == "copy_lhs":
dX = _gspmm(g_rev, "copy_lhs", "sum", dZ, None)[0]
else:
if op in ["mul", "div"]:
dX = _scatter_nd(
argX,
_muldiv(op, _gather_nd(argY, _expand(Y, dZ.shape[1:])))
* dZ,
X.shape[0],
)
elif op in ["add", "sub", "copy_lhs"]:
dX = _scatter_nd(argX, dZ, X.shape[0])
dX = _reduce_grad(dX, X.shape)
else:
dX = tf.zeros_like(X)
if op != "copy_lhs":
if reduce_op == "sum":
if op == "mul" and _need_reduce_last_dim(X, Y):
dY = _gsddmm(gidx, "dot", X, dZ)
elif op in ["mul", "div"]:
dY = _gsddmm(gidx, "mul", X, dZ)
if op == "div":
dY = -dY / (Y**2)
elif op in ["add", "sub", "copy_rhs"]:
dY = _gsddmm(gidx, "copy_rhs", X, _addsub(op, dZ))
else:
out_shp = (Y.shape[0],) + dZ.shape[1:]
if op in ["mul", "div"]:
dY = _scatter_nd(
argY,
_gather_nd(argX, _expand(X, dZ.shape[1:])) * dZ,
Y.shape[0],
)
if op == "div":
dY = -dY / (Y**2)
elif op in ["add", "sub", "copy_rhs"]:
dY = _scatter_nd(argY, _addsub(op, dZ), Y.shape[0])
dY = _reduce_grad(dY, Y.shape)
else:
dY = tf.zeros_like(Y)
return dX, dY
return out, grad
def gspmm(gidx, op, reduce_op, X, Y):
@tf.custom_gradient
def _lambda(X, Y):
return gspmm_real(gidx, op, reduce_op, X, Y)
if X is None:
X = tf.zeros(())
if Y is None:
Y = tf.zeros(())
return _lambda(X, Y)
def gsddmm_real(gidx, op, X, Y, lhs_target, rhs_target):
out = _gsddmm(gidx, op, X, Y, lhs_target, rhs_target)
def grad(dZ):
if op != "copy_rhs":
if lhs_target in ["u", "v"]:
_gidx = gidx if lhs_target == "v" else gidx.reverse()
if op in ["add", "sub", "copy_lhs"]:
dX = _gspmm(_gidx, "copy_rhs", "sum", None, dZ)[0]
else: # mul, div, dot
if rhs_target == lhs_target:
dX = _gspmm(_gidx, "copy_rhs", "sum", None, dZ)[
0
] * _muldiv(op, Y)
elif rhs_target == "e":
dX = _gspmm(
_gidx, "copy_rhs", "sum", None, dZ * _muldiv(op, Y)
)[0]
else: # rhs_target = !lhs_target
dX = _gspmm(_gidx, "mul", "sum", _muldiv(op, Y), dZ)[0]
else: # lhs_target == 'e'
if op in ["add", "sub", "copy_lhs"]:
dX = dZ
else: # mul, div, dot
dX = _gsddmm(
gidx, "mul", dZ, _muldiv(op, Y), "e", rhs_target
)
dX = _reduce_grad(dX, X.shape)
else:
dX = tf.zeros_like(X)
if op != "copy_lhs":
if rhs_target in ["u", "v"]:
_gidx = gidx if rhs_target == "v" else gidx.reverse()
if op in ["add", "sub", "copy_rhs"]:
dY = _gspmm(
_gidx, "copy_rhs", "sum", None, _addsub(op, dZ)
)[0]
else: # mul, div, dot
if lhs_target == rhs_target:
dY = _gspmm(_gidx, "copy_rhs", "sum", None, dZ)[0] * X
elif lhs_target == "e":
dY = _gspmm(_gidx, "copy_rhs", "sum", None, dZ * X)[0]
else: # rhs_target = !lhs_target
dY = _gspmm(_gidx, "mul", "sum", X, dZ)[0]
if op == "div":
dY = -dY / (Y**2)
else:
if op in ["add", "sub", "copy_rhs"]:
dY = _addsub(op, dZ)
else: # mul, div, dot
dY = _gsddmm(gidx, "mul", dZ, X, "e", lhs_target)
if op == "div":
dY = -dY / (Y**2)
dY = _reduce_grad(dY, Y.shape)
else:
dY = tf.zeros_like(Y)
return dX, dY
return out, grad
def gsddmm(gidx, op, X, Y, lhs_target="u", rhs_target="v"):
@tf.custom_gradient
def _lambda(X, Y):
return gsddmm_real(gidx, op, X, Y, lhs_target, rhs_target)
if X is None:
X = tf.zeros(())
if Y is None:
Y = tf.zeros(())
return _lambda(X, Y)
def edge_softmax_real(gidx, score, eids=ALL, norm_by="dst"):
if not is_all(eids):
gidx = gidx.edge_subgraph([eids], True).graph
if norm_by == "src":
gidx = gidx.reverse()
score_max = _gspmm(gidx, "copy_rhs", "max", None, score)[0]
score = tf.math.exp(_gsddmm(gidx, "sub", score, score_max, "e", "v"))
score_sum = _gspmm(gidx, "copy_rhs", "sum", None, score)[0]
out = _gsddmm(gidx, "div", score, score_sum, "e", "v")
def edge_softmax_backward(grad_out):
sds = out * grad_out
accum = gspmm(gidx, "copy_rhs", "sum", None, sds)
grad_score = sds - gsddmm(gidx, "mul", out, accum, "e", "v")
return grad_score
return out, edge_softmax_backward
def edge_softmax(gidx, logits, eids=ALL, norm_by="dst"):
@tf.custom_gradient
def _lambda(logits):
return edge_softmax_real(gidx, logits, eids, norm_by)
return _lambda(logits)
def segment_reduce_real(op, x, offsets):
y, arg = _segment_reduce(op, x, offsets)
def segment_reduce_backward(dy):
m = x.shape[0]
if op == "sum":
offsets_np = asnumpy(offsets[1:])
indices_np = np.zeros((m + 1,), dtype=offsets_np.dtype)
np.add.at(indices_np, offsets_np, np.ones_like(offsets_np))
indices_np = np.cumsum(indices_np, -1)[:-1]
indices = zerocopy_from_numpy(indices_np)
dx = tf.gather(dy, indices)
else:
dx = _bwd_segment_cmp(dy, arg, m)
return dx
return y, segment_reduce_backward
def segment_reduce(op, x, offsets):
@tf.custom_gradient
def _lambda(x):
return segment_reduce_real(op, x, offsets)
return _lambda(x)
def scatter_add_real(x, idx, m):
y = _scatter_add(x, idx, m)
def scatter_add_backward(dy):
return tf.gather(dy, idx)
return y, scatter_add_backward
def scatter_add(x, idx, m):
@tf.custom_gradient
def _lambda(x):
return scatter_add_real(x, idx, m)
return _lambda(x)
def csrmm_real(gidxA, A_weights, gidxB, B_weights, num_vtypes):
gidxC, C_weights = _csrmm(gidxA, A_weights, gidxB, B_weights, num_vtypes)
nrows, ncols, C_indptr, C_indices, C_eids = gidxC.adjacency_matrix_tensors(
0, False, "csr"
)
def grad(dnrows, dncols, dC_indptr, dC_indices, dC_eids, dC_weights):
# Only the last argument is meaningful.
dgidxA, dA_weights = _csrmm(
gidxC,
dC_weights,
gidxB.reverse(),
B_weights,
gidxA.number_of_ntypes(),
)
dgidxB, dB_weights = _csrmm(
gidxA.reverse(),
A_weights,
gidxC,
dC_weights,
gidxB.number_of_ntypes(),
)
dA_weights = _csrmask(dgidxA, dA_weights, gidxA)
dB_weights = _csrmask(dgidxB, dB_weights, gidxB)
return dA_weights, dB_weights
return (
tf.constant(nrows),
tf.constant(ncols),
C_indptr,
C_indices,
C_eids,
C_weights,
), grad
def csrmm(gidxA, A_weights, gidxB, B_weights, num_vtypes):
@tf.custom_gradient
def _lambda(A_weights, B_weights):
return csrmm_real(gidxA, A_weights, gidxB, B_weights, num_vtypes)
nrows, ncols, C_indptr, C_indices, C_eids, C_weights = _lambda(
A_weights, B_weights
)
gidxC = create_unitgraph_from_csr(
num_vtypes,
nrows.numpy(),
ncols.numpy(),
C_indptr,
C_indices,
C_eids,
["coo", "csr", "csc"],
)
return gidxC, C_weights
def csrsum_real(gidxs, weights):
gidxC, C_weights = _csrsum(gidxs, weights)
nrows, ncols, C_indptr, C_indices, C_eids = gidxC.adjacency_matrix_tensors(
0, False, "csr"
)
def grad(dnrows, dncols, dC_indptr, dC_indices, dC_eids, dC_weights):
# Only the last argument is meaningful.
return tuple(_csrmask(gidxC, dC_weights, gidx) for gidx in gidxs)
return (
tf.constant(nrows),
tf.constant(ncols),
C_indptr,
C_indices,
C_eids,
C_weights,
), grad
def csrsum(gidxs, weights):
@tf.custom_gradient
def _lambda(*weights):
return csrsum_real(gidxs, weights)
nrows, ncols, C_indptr, C_indices, C_eids, C_weights = _lambda(*weights)
num_vtypes = gidxs[0].number_of_ntypes()
gidxC = create_unitgraph_from_csr(
num_vtypes,
nrows.numpy(),
ncols.numpy(),
C_indptr,
C_indices,
C_eids,
["coo", "csr", "csc"],
)
return gidxC, C_weights
def csrmask_real(gidxA, A_weights, gidxB):
B_weights = _csrmask(gidxA, A_weights, gidxB)
def grad(dB_weights):
return _csrmask(gidxB, dB_weights, gidxA)
return B_weights, grad
def csrmask(gidxA, A_weights, gidxB):
@tf.custom_gradient
def _lambda(A_weights):
return csrmask_real(gidxA, A_weights, gidxB)
return _lambda(A_weights)
@@ -0,0 +1 @@
"""Sparse optimizer is not supported for tensorflow"""
+619
View File
@@ -0,0 +1,619 @@
"""Tensorflow backend implementation"""
from __future__ import absolute_import
import builtins
import numbers
import numpy as np
import tensorflow as tf
from ... import ndarray as nd
from ...function.base import TargetCode
from ...utils import version
if version.parse(tf.__version__) < version.parse("2.3.0"):
raise RuntimeError(
"DGL requires TensorFlow>=2.3.0 for the official DLPack support."
)
def zerocopy_to_dlpack(data):
return tf.experimental.dlpack.to_dlpack(data)
def zerocopy_from_dlpack(dlpack_tensor):
# TODO(Jinjing): Tensorflow requires memory to be 64-bytes aligned. We check the
# alignment and make a copy if needed. The functionality is better in TF's main repo.
aligned = nd.from_dlpack(dlpack_tensor).to_dlpack(64)
return tf.experimental.dlpack.from_dlpack(aligned)
def data_type_dict():
return {
"bfloat16": tf.bfloat16,
"float16": tf.float16,
"float32": tf.float32,
"float64": tf.float64,
"uint8": tf.uint8,
"int8": tf.int8,
"int16": tf.int16,
"int32": tf.int32,
"int64": tf.int64,
"bool": tf.bool,
}
def cpu():
return "/cpu:0"
def tensor(data, dtype=None):
if isinstance(data, tf.Tensor):
if dtype is None or data.dtype == dtype:
return data
else:
return tf.cast(data, dtype=dtype)
else:
if isinstance(data, numbers.Number):
data = [data]
return tf.convert_to_tensor(data, dtype=dtype)
def initialize_context():
tf.zeros(1)
def as_scalar(data):
data = data.numpy()
return data if np.isscalar(data) else data.item()
def get_preferred_sparse_format():
"""Get the preferred sparse matrix format supported by the backend.
Different backends have their preferred backend. This info is useful when
constructing a sparse matrix.
"""
return "coo"
def sparse_matrix(data, index, shape, force_format=False):
fmt = index[0]
if fmt != "coo":
raise TypeError(
"Tensorflow backend only supports COO format. But got %s." % fmt
)
# tf.SparseTensor only supports int64 indexing,
# therefore manually casting to int64 when input in int32
spmat = tf.SparseTensor(
indices=tf.cast(tf.transpose(index[1], (1, 0)), tf.int64),
values=data,
dense_shape=shape,
)
return spmat, None
def sparse_matrix_indices(spmat):
return ("coo", spmat.indices)
def is_tensor(obj):
return isinstance(obj, tf.Tensor)
def shape(input):
return input.shape
def dtype(input):
return input.dtype
def ndim(input):
return input.ndim
def context(input):
spec = tf.DeviceSpec.from_string(input.device)
return "/{}:{}".format(spec.device_type.lower(), spec.device_index)
def device_type(ctx):
return tf.DeviceSpec.from_string(ctx).device_type.lower()
def device_id(ctx):
return tf.DeviceSpec.from_string(ctx).device_index
def to_backend_ctx(dglctx):
dev_type = dglctx.device_type
if dev_type == 1:
return "/cpu:0"
elif dev_type == 2:
return "/gpu:%d" % (dglctx.device_id)
else:
raise ValueError("Unsupported DGL device context:", dglctx)
def astype(input, ty):
with tf.device(input.device):
return tf.cast(input, dtype=ty)
def asnumpy(input):
if isinstance(input, tf.SparseTensor):
# tf.sparse.to_dense assume sorted indices, need to turn off validate_indices in our cases
return tf.sparse.to_dense(input, validate_indices=False).numpy()
else:
return input.numpy()
def copy_to(input, ctx, **kwargs):
with tf.device(ctx):
new_tensor = tf.identity(input)
return new_tensor
def is_pinned(input):
return False # not sure how to do this
def sum(input, dim, keepdims=False):
if input.dtype == tf.bool:
input = tf.cast(input, tf.int32)
return tf.reduce_sum(input, axis=dim, keepdims=keepdims)
def floor_div(in1, in2):
return astype(in1 / in2, dtype(in1))
def reduce_sum(input):
if input.dtype == tf.bool:
input = tf.cast(input, tf.int32)
return tf.reduce_sum(input)
def cumsum(input, dim):
if input.dtype == tf.bool:
input = tf.cast(input, tf.int32)
return tf.cumsum(input, axis=dim)
def mean(input, dim):
return tf.reduce_mean(input, axis=dim)
def reduce_mean(input):
return tf.reduce_mean(input)
def max(input, dim):
return tf.reduce_max(input, axis=dim)
def reduce_max(input):
return tf.reduce_max(input)
def min(input, dim):
return tf.reduce_min(input, axis=dim)
def reduce_min(input):
return tf.reduce_min(input)
def argsort(input, dim, descending):
if descending:
return tf.cast(
tf.argsort(input, axis=dim, direction="DESCENDING"), dtype=tf.int64
)
else:
return tf.cast(
tf.argsort(input, axis=dim, direction="ASCENDING"), dtype=tf.int64
)
def topk(input, k, dim, descending=True):
if not descending:
input = -input
shape = np.arange(input.ndim)
shape[dim], shape[-1] = shape[-1], shape[dim]
out1 = tf.transpose(input, perm=shape)
out2 = tf.math.top_k(out1, k=k, sorted=True)
out = tf.transpose(out2[0], shape)
if not descending:
out = -out
return out
def argtopk(input, k, dim, descending=True):
if not descending:
input = -input
shape = np.arange(input.ndim)
shape[dim], shape[-1] = shape[-1], shape[dim]
out1 = tf.transpose(input, perm=shape)
out2 = tf.math.top_k(out1, k=k, sorted=True)
out = tf.transpose(out2[1], shape)
if not descending:
out = -out
return out
def exp(input):
return tf.exp(input)
def inverse(input):
return tf.linalg.inv(input)
def sqrt(input):
return tf.sqrt(input)
def softmax(input, dim=-1):
return tf.math.softmax(input, axis=dim)
def cat(seq, dim):
return tf.concat(seq, axis=dim)
def stack(seq, dim):
return tf.stack(seq, axis=dim)
def split(input, sizes_or_sections, dim):
return [
copy_to(_, input.device)
for _ in tf.split(input, sizes_or_sections, axis=dim)
]
def repeat(input, repeats, dim):
return tf.repeat(input, repeats, dim)
def gather_row(data, row_index):
return tf.gather(data, row_index)
def slice_axis(data, axis, begin, end):
# assert axis == 0
# tf doesn't behave well with negative
s = [slice(None) for i in range(data.ndim)]
if end == 0:
end = data.shape[axis]
s[axis] = slice(begin, end, None)
return data[tuple(s)]
def take(data, indices, dim):
return tf.gather_nd(data, indices, dim)
def narrow_row(x, start, stop):
return x[start:stop]
def scatter_row(data, row_index, value):
row_index = tf.expand_dims(row_index, 1)
# XXX(minjie): Normally, the copy_to here is unnecessary. However, TF has this
# notorious legacy issue that int32 type data is always on CPU, which will
# crash the program since DGL requires feature data to be on the same device
# as graph structure.
return copy_to(
tf.tensor_scatter_nd_update(data, row_index, value), data.device
)
def index_add_inplace(data, row_idx, value):
raise NotImplementedError("Tensorflow doesn't support inplace index_add")
def scatter_row_inplace(data, row_index, value):
raise NotImplementedError("Tensorflow doesn't support inplace update")
def squeeze(input, dim):
return tf.squeeze(input, axis=dim)
def unsqueeze(input, dim):
return tf.expand_dims(input, axis=dim)
def reshape(input, shape):
return tf.reshape(input, shape)
def swapaxes(input, axis1, axis2):
ndim = input.ndim
t = list(range(ndim))
t[axis1], t[axis2] = axis2 % ndim, axis1 % ndim
return tf.transpose(input, perm=t)
def empty(shape, dtype, ctx):
# tf doesn't have tf.empty(), use zeros() as a workaround
return zeros(shape, dtype, ctx)
def zeros(shape, dtype, ctx):
with tf.device(ctx):
t = tf.zeros(shape, dtype=dtype)
return t
def zeros_like(input):
return tf.zeros_like(input)
def ones(shape, dtype, ctx):
with tf.device(ctx):
t = tf.ones(shape, dtype=dtype)
return t
def uniform(shape, dtype, ctx, low, high):
with tf.device(ctx):
t = tf.random.uniform(shape, dtype=dtype, minval=low, maxval=high)
return t
def randint(shape, dtype, ctx, low, high):
with tf.device(ctx):
t = tf.random.uniform(shape, dtype=dtype, minval=low, maxval=high)
return t
def pad_packed_tensor(input, lengths, value, l_min=None):
old_shape = input.shape
if isinstance(lengths, tf.Tensor):
max_len = as_scalar(tf.reduce_max(lengths))
else:
max_len = builtins.max(lengths)
if l_min is not None:
max_len = builtins.max(max_len, l_min)
batch_size = len(lengths)
ndim = input.ndim
tensor_list = []
cum_row = 0
pad_nparray = np.zeros((ndim, 2), dtype=np.int32)
for l in lengths:
t = input[cum_row : cum_row + l]
pad_nparray[0, 1] = max_len - l
t = tf.pad(
t, tf.constant(pad_nparray), mode="CONSTANT", constant_values=value
)
tensor_list.append(t)
cum_row += l
return tf.stack(tensor_list, axis=0)
def pack_padded_tensor(input, lengths):
out_list = []
for i, l in enumerate(lengths):
t = input[i]
out = t[:l]
out_list.append(out)
return tf.concat(out_list, axis=0)
def boolean_mask(input, mask):
return tf.boolean_mask(input, mask)
def equal(x, y):
return x == y
def allclose(x, y, rtol=1e-4, atol=1e-4):
return np.allclose(
tf.convert_to_tensor(x).numpy(),
tf.convert_to_tensor(y).numpy(),
rtol=rtol,
atol=atol,
)
def logical_not(input):
return ~input
def logical_and(input1, input2):
return tf.math.logical_and(input1, input2)
def clone(input):
# TF tensor is always immutable so returning the input is safe.
return input
def clamp(data, min_val, max_val):
return tf.clip_by_value(data, min_val, max_val)
def replace_inf_with_zero(x):
return tf.where(tf.abs(x) == np.inf, 0, x)
def count_nonzero(input):
return int(tf.math.count_nonzero(input))
def unique(input, return_inverse=False, return_counts=False):
if return_inverse and return_counts:
return tf.unique_with_counts(input)
elif return_counts:
result = tf.unique_with_counts(input)
return result.y, result.count
elif return_inverse:
return tf.unique(input)
else:
return tf.unique(input).y
def full_1d(length, fill_value, dtype, ctx):
with tf.device(ctx):
t = tf.fill([length], value=fill_value)
t = tf.cast(t, dtype=dtype)
return t
def nonzero_1d(input):
nonzero_bool = tf.cast(input, tf.bool)
return tf.reshape(tf.where(nonzero_bool), (-1,))
def sort_1d(input):
return tf.sort(input), tf.cast(tf.argsort(input), dtype=tf.int64)
def arange(start, stop, dtype=tf.int64, ctx=None):
if not ctx:
ctx = "/cpu:0"
with tf.device(ctx):
t = tf.range(start, stop, dtype=dtype)
return t
def rand_shuffle(arr):
return tf.random.shuffle(arr)
def zerocopy_to_numpy(input):
return np.asarray(memoryview(input))
def zerocopy_from_numpy(np_array):
# NOTE: not zerocopy
# This assumes tensor should be on cpu
with tf.device("/cpu:0"):
t = tf.convert_to_tensor(np_array)
return t
def zerocopy_to_dgl_ndarray(data):
if device_type(data.device) == "gpu" and data.dtype in (tf.int32, tf.int64):
# NOTE: TF doesn't keep signed tensors on GPU due to legacy issues with
# shape inference. Convert it to unsigned and cast it back afterwards.
if data.dtype == tf.int32:
data = tf.cast(data, tf.uint32)
elif data.dtype == tf.int64:
data = tf.cast(data, tf.uint64)
return nd.cast_to_signed(nd.from_dlpack(zerocopy_to_dlpack(data)))
else:
return nd.from_dlpack(zerocopy_to_dlpack(data))
def zerocopy_to_dgl_ndarray_for_write(input):
return zerocopy_to_dgl_ndarray(input)
def zerocopy_from_dgl_ndarray(input):
return zerocopy_from_dlpack(input.to_dlpack())
def sync():
context = context().context()
context.async_wait()
class GradContext:
def __init__(self):
self.tensor_for_grad = []
self.grad_list = []
self.tape = None
def set_tape(self, tape):
self.tape = tape
def add_tensor(self, x):
idx_pop = []
for idx, ele in enumerate(self.tensor_for_grad):
if ele._id == x._id:
idx_pop.append(idx)
if len(idx_pop) > 0:
self.tensor_for_grad.pop(idx_pop[0])
if self.tape is not None:
self.tape.watch(x)
self.tensor_for_grad.append(x)
def backward(self, x, head_gradient=None):
if head_gradient is not None:
x = x * head_gradient
self.grad_list = self.tape.gradient(x, self.tensor_for_grad)
def is_no_grad(self, x):
idx_pop = []
for idx, ele in enumerate(self.tensor_for_grad):
if ele._id == x._id:
idx_pop.append(idx)
if len(idx_pop) == 0:
return True
else:
return self.grad_list[idx_pop[0]] is None
def grad(self, x):
idx_pop = []
for idx, ele in enumerate(self.tensor_for_grad):
if ele._id == x._id:
idx_pop.append(idx)
assert len(idx_pop) == 1
t = self.grad_list[idx_pop[0]]
return tf.convert_to_tensor(t)
cgrad = GradContext()
def get_cgrad():
return cgrad
class record_grad:
def __init__(self):
self.tape = tf.GradientTape()
def __enter__(self):
cgrad.set_tape(self.tape)
self.tape.__enter__()
for x in cgrad.tensor_for_grad:
self.tape.watch(x)
def __exit__(self, exc_type, exc_value, exc_traceback):
# pass
self.tape.__exit__(exc_type, exc_value, exc_traceback)
cgrad.tape = None
def attach_grad(x):
cgrad.add_tensor(x)
return x
def backward(x, head_gradient=None):
cgrad.backward(x, head_gradient)
def grad(x):
return cgrad.grad(x)
def is_no_grad(x):
return cgrad.is_no_grad(x)
def is_recording():
raise NotImplementedError("Tensorflow doesn't support is_recording")
no_grad = None
initialize_context()