1196 lines
39 KiB
Python
1196 lines
39 KiB
Python
import torch as th
|
|
|
|
from ..._sparse_ops import (
|
|
_bwd_segment_cmp,
|
|
_csrmask,
|
|
_csrmm,
|
|
_csrsum,
|
|
_edge_softmax_backward,
|
|
_edge_softmax_forward,
|
|
_gather_mm,
|
|
_gather_mm_scatter,
|
|
_gsddmm,
|
|
_gsddmm_hetero,
|
|
_gspmm,
|
|
_gspmm_hetero,
|
|
_scatter_add,
|
|
_segment_mm,
|
|
_segment_mm_backward_B,
|
|
_segment_reduce,
|
|
_update_grad_minmax_hetero,
|
|
)
|
|
|
|
from ...base import ALL, is_all
|
|
from ...heterograph_index import create_unitgraph_from_csr
|
|
|
|
__all__ = [
|
|
"gspmm",
|
|
"gsddmm",
|
|
"gspmm_hetero",
|
|
"gsddmm_hetero",
|
|
"edge_softmax",
|
|
"edge_softmax_hetero",
|
|
"segment_reduce",
|
|
"scatter_add",
|
|
"csrmm",
|
|
"csrsum",
|
|
"csrmask",
|
|
"gather_mm",
|
|
"segment_mm",
|
|
]
|
|
|
|
|
|
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 = th.nonzero(
|
|
th.tensor(grad_shape) - th.tensor(in_shape), as_tuple=False
|
|
)
|
|
reduce_idx += 1 # skip batch dim
|
|
if len(reduce_idx) > 0:
|
|
grad = grad.sum(dim=tuple(reduce_idx), keepdim=True)
|
|
return grad.view(-1, *shape[1:])
|
|
|
|
|
|
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."""
|
|
if ufeat is None or efeat is None:
|
|
return False
|
|
ushp = ufeat.shape
|
|
eshp = efeat.shape
|
|
return ushp[1:-1] == eshp[1:-1] and eshp[-1] == 1 and ushp[-1] > 1
|
|
|
|
|
|
def _expand(x, shape):
|
|
return x.expand(-1, *shape)
|
|
|
|
|
|
def spmm_cache_X(binary_op, reduce_op, req_grad_X, req_grad_Y):
|
|
"""Rules to identify whether to cache X in SpMM forward stage."""
|
|
if binary_op != "copy_lhs" and req_grad_Y:
|
|
if reduce_op == "sum":
|
|
return True
|
|
else:
|
|
if binary_op == "mul":
|
|
return True
|
|
return False
|
|
|
|
|
|
def spmm_cache_Y(binary_op, reduce_op, req_grad_X, req_grad_Y):
|
|
"""Rules to identify whether to cache Y in SpMM forward stage."""
|
|
if binary_op != "copy_rhs" and req_grad_X:
|
|
if reduce_op == "sum":
|
|
if binary_op in ["mul", "add"]:
|
|
return True
|
|
else:
|
|
if binary_op == "mul":
|
|
return True
|
|
return False
|
|
|
|
|
|
def spmm_cache_argX(binary_op, reduce_op, req_grad_X, req_grad_Y):
|
|
"""Rules to identify whether to cache argX in SpMM forward stage."""
|
|
if req_grad_X or req_grad_Y:
|
|
if reduce_op in ["min", "max"]:
|
|
return True
|
|
return False
|
|
|
|
|
|
def spmm_cache_argY(binary_op, reduce_op, req_grad_X, req_grad_Y):
|
|
"""Rules to identify whether to cache argY in SpMM forward stage."""
|
|
if req_grad_X or req_grad_Y:
|
|
if reduce_op in ["min", "max"]:
|
|
return True
|
|
return False
|
|
|
|
|
|
class empty_context:
|
|
"""Empty context that does nothing"""
|
|
|
|
def __init__(self, *args, **kargs):
|
|
return
|
|
|
|
def __enter__(self, *args, **kargs):
|
|
return self
|
|
|
|
def __exit__(self, *args, **kargs):
|
|
return
|
|
|
|
|
|
# Disable CUDA autocast since we have casted args manually,
|
|
# and do it only in a nested autocast context.
|
|
def _disable_autocast_if_enabled():
|
|
if th.is_autocast_enabled():
|
|
return th.cuda.amp.autocast(enabled=False)
|
|
else:
|
|
return empty_context()
|
|
|
|
|
|
def _cast_if_autocast_enabled(*args):
|
|
if not th.is_autocast_enabled():
|
|
return args
|
|
else:
|
|
return th.cuda.amp.autocast_mode._cast(
|
|
args, th.get_autocast_gpu_dtype()
|
|
)
|
|
|
|
|
|
class GSpMM(th.autograd.Function):
|
|
@staticmethod
|
|
def forward(ctx, gidx, op, reduce_op, X, Y):
|
|
out, (argX, argY) = _gspmm(gidx, op, reduce_op, X, Y)
|
|
reduce_last = _need_reduce_last_dim(X, Y)
|
|
X_shape = X.shape if X is not None else None
|
|
Y_shape = Y.shape if Y is not None else None
|
|
dtype = X.dtype if X is not None else Y.dtype
|
|
device = X.device if X is not None else Y.device
|
|
ctx.backward_cache = (
|
|
gidx,
|
|
op,
|
|
reduce_op,
|
|
X_shape,
|
|
Y_shape,
|
|
dtype,
|
|
device,
|
|
reduce_last,
|
|
)
|
|
req_grad_X = X.requires_grad if X is not None else False
|
|
req_grad_Y = Y.requires_grad if Y is not None else False
|
|
if not spmm_cache_X(op, reduce_op, req_grad_X, req_grad_Y):
|
|
X = None
|
|
if not spmm_cache_Y(op, reduce_op, req_grad_X, req_grad_Y):
|
|
Y = None
|
|
if not spmm_cache_argX(op, reduce_op, req_grad_X, req_grad_Y):
|
|
argX = None
|
|
if not spmm_cache_argY(op, reduce_op, req_grad_X, req_grad_Y):
|
|
argY = None
|
|
ctx.save_for_backward(X, Y, argX, argY)
|
|
return out
|
|
|
|
@staticmethod
|
|
def backward(ctx, dZ):
|
|
(
|
|
gidx,
|
|
op,
|
|
reduce_op,
|
|
X_shape,
|
|
Y_shape,
|
|
dtype,
|
|
device,
|
|
reduce_last,
|
|
) = ctx.backward_cache
|
|
X, Y, argX, argY = ctx.saved_tensors
|
|
if op != "copy_rhs" and ctx.needs_input_grad[3]:
|
|
g_rev = gidx.reverse()
|
|
if reduce_op == "sum":
|
|
if op == "mul":
|
|
dX = gspmm(g_rev, "mul", "sum", dZ, Y)
|
|
elif op == "add":
|
|
dX = gspmm(g_rev, "copy_lhs", "sum", dZ, Y)
|
|
elif op == "copy_lhs":
|
|
dX = gspmm(g_rev, "copy_lhs", "sum", dZ, None)
|
|
else: # max/min
|
|
dX = th.zeros(
|
|
(X_shape[0],) + dZ.shape[1:], dtype=dtype, device=device
|
|
)
|
|
if op == "mul":
|
|
grad = _expand(Y, dZ.shape[1:]).gather(0, argY.long()) * dZ
|
|
dX.scatter_add_(0, argX.long(), grad)
|
|
elif op in ["add", "copy_lhs"]:
|
|
dX.scatter_add_(0, argX.long(), dZ)
|
|
dX = _reduce_grad(dX, X_shape)
|
|
else: # X has not gradient
|
|
dX = None
|
|
if op != "copy_lhs" and ctx.needs_input_grad[4]:
|
|
if reduce_op == "sum":
|
|
if op == "mul" and reduce_last:
|
|
dY = gsddmm(gidx, "dot", X, dZ)
|
|
elif op == "mul":
|
|
dY = gsddmm(gidx, "mul", X, dZ)
|
|
elif op in ["add", "copy_rhs"]:
|
|
dY = gsddmm(gidx, "copy_rhs", X, dZ)
|
|
else: # max/min
|
|
dY = th.zeros(
|
|
(Y_shape[0],) + dZ.shape[1:], dtype=dtype, device=device
|
|
)
|
|
if op == "mul":
|
|
grad = _expand(X, dZ.shape[1:]).gather(0, argX.long()) * dZ
|
|
dY.scatter_add_(0, argY.long(), grad)
|
|
elif op in ["add", "copy_rhs"]:
|
|
dY.scatter_add_(0, argY.long(), dZ)
|
|
dY = _reduce_grad(dY, Y_shape)
|
|
else: # Y has no gradient
|
|
dY = None
|
|
return None, None, None, dX, dY
|
|
|
|
|
|
class GSpMM_hetero(th.autograd.Function):
|
|
@staticmethod
|
|
def forward(
|
|
ctx, gidx, op, reduce_op, X_len, *feats
|
|
): # feats = lhs_data + rhs_data
|
|
out, (argX, argY, argX_ntype, argY_etype) = _gspmm_hetero(
|
|
gidx, op, reduce_op, X_len, feats
|
|
)
|
|
X, Y = feats[:X_len], feats[X_len:]
|
|
# TODO (Israt): check target to decide src_id/dst_id?
|
|
src_id, dst_id = gidx.metagraph.find_edge(0)
|
|
reduce_last = _need_reduce_last_dim(X[src_id], Y[dst_id])
|
|
X_shape = tuple(
|
|
[X[i].shape if X[i] is not None else None for i in range(X_len)]
|
|
)
|
|
Y_shape = tuple(
|
|
[Y[i].shape if Y[i] is not None else None for i in range(len(Y))]
|
|
)
|
|
dtype = X[src_id].dtype if X[src_id] is not None else Y[dst_id].dtype
|
|
device = X[src_id].device if X[src_id] is not None else Y[dst_id].device
|
|
ctx.backward_cache = (
|
|
gidx,
|
|
op,
|
|
reduce_op,
|
|
X_shape,
|
|
Y_shape,
|
|
dtype,
|
|
device,
|
|
reduce_last,
|
|
X_len,
|
|
)
|
|
req_grad_X = tuple(
|
|
[
|
|
X[i].requires_grad if X[i] is not None else False
|
|
for i in range(X_len)
|
|
]
|
|
)
|
|
req_grad_Y = tuple(
|
|
[
|
|
Y[i].requires_grad if Y[i] is not None else False
|
|
for i in range(len(Y))
|
|
]
|
|
)
|
|
|
|
# checking the first relation to decide for all the relations
|
|
if not spmm_cache_argX(
|
|
op, reduce_op, req_grad_X[src_id], req_grad_Y[dst_id]
|
|
):
|
|
argX = tuple([None] * len(X))
|
|
if not spmm_cache_argY(
|
|
op, reduce_op, req_grad_X[src_id], req_grad_Y[dst_id]
|
|
):
|
|
argY = tuple([None] * len(X))
|
|
|
|
ctx.save_for_backward(*feats, *argX, *argX_ntype, *argY, *argY_etype)
|
|
return out
|
|
|
|
@staticmethod
|
|
def backward(ctx, *dZ):
|
|
(
|
|
gidx,
|
|
op,
|
|
reduce_op,
|
|
X_shape,
|
|
Y_shape,
|
|
dtype,
|
|
device,
|
|
reduce_last,
|
|
X_len,
|
|
) = ctx.backward_cache
|
|
num_ntypes = gidx.number_of_ntypes()
|
|
feats = ctx.saved_tensors[: -(4 * num_ntypes)]
|
|
argX = ctx.saved_tensors[-(4 * num_ntypes) : -(3 * num_ntypes)]
|
|
argX_ntype = ctx.saved_tensors[-(3 * num_ntypes) : -(2 * num_ntypes)]
|
|
argY = ctx.saved_tensors[-(2 * num_ntypes) : -num_ntypes]
|
|
argY_etype = ctx.saved_tensors[-num_ntypes:]
|
|
X, Y = feats[:X_len], feats[X_len:]
|
|
|
|
if op != "copy_rhs" and any([x is not None for x in X]):
|
|
g_rev = gidx.reverse()
|
|
if reduce_op == "sum":
|
|
if op == "mul":
|
|
dX = gspmm_hetero(
|
|
g_rev, "mul", "sum", len(X), *tuple(dZ + Y)
|
|
)
|
|
elif op == "add":
|
|
dX = gspmm_hetero(
|
|
g_rev, "copy_lhs", "sum", len(X), *tuple(dZ + Y)
|
|
)
|
|
elif op == "copy_lhs":
|
|
tpl_None = tuple([None] * len(Y))
|
|
dX = gspmm_hetero(
|
|
g_rev, "copy_lhs", "sum", len(X), *tuple(dZ + tpl_None)
|
|
)
|
|
else: # max/min
|
|
# Assuming that the features are of the same dimension (enforced by the forward function)
|
|
src_id, dst_id = gidx.metagraph.find_edge(0)
|
|
dX = tuple(
|
|
[
|
|
th.zeros(
|
|
(X_shape[i][0],) + dZ[dst_id].shape[1:],
|
|
dtype=dtype,
|
|
device=device,
|
|
)
|
|
if X[i] is not None
|
|
else None
|
|
for i in range(len(X))
|
|
]
|
|
)
|
|
if op == "mul":
|
|
grad = _expand(Y, dZ.shape[1:]).gather(0, argY.long()) * dZ
|
|
dX.scatter_add_(0, argX.long(), grad)
|
|
elif op in ["add", "copy_lhs"]:
|
|
dX = _update_grad_minmax_hetero(
|
|
g_rev, op, dZ, argX, argX_ntype, dX
|
|
)
|
|
dX = tuple(
|
|
[
|
|
_reduce_grad(dX[i], X_shape[i])
|
|
if X[i] is not None
|
|
else None
|
|
for i in range(len(X))
|
|
]
|
|
)
|
|
else: # X has not gradient
|
|
dX = tuple([None] * len(X))
|
|
if op != "copy_lhs" and any([y is not None for y in Y]):
|
|
# TODO(Israt): implement other combinations of reduce functions
|
|
if reduce_op == "sum":
|
|
tpl_dZ = tuple(
|
|
[
|
|
dZ[i] if dZ[i] is not None else None
|
|
for i in range(len(dZ))
|
|
]
|
|
)
|
|
tpl_X_dZ = tuple(X + tpl_dZ)
|
|
if op == "mul" and reduce_last:
|
|
dY = gsddmm_hetero(gidx, "dot", X_len, "u", "v", *tpl_X_dZ)
|
|
elif op == "mul":
|
|
dY = gsddmm_hetero(gidx, "mul", X_len, "u", "v", *tpl_X_dZ)
|
|
elif op in ["add", "copy_rhs"]:
|
|
dY = gsddmm_hetero(
|
|
gidx, "copy_rhs", X_len, "u", "v", *tpl_X_dZ
|
|
)
|
|
else: # max/min
|
|
src_id, dst_id = gidx.metagraph.find_edge(0)
|
|
dY = tuple(
|
|
[
|
|
th.zeros(
|
|
(Y_shape[i][0],) + dZ[dst_id].shape[1:],
|
|
dtype=dtype,
|
|
device=device,
|
|
)
|
|
if Y[i] is not None
|
|
else None
|
|
for i in range(len(Y))
|
|
]
|
|
)
|
|
if op == "mul":
|
|
grad = _expand(X, dZ.shape[1:]).gather(0, argX.long()) * dZ
|
|
dY.scatter_add_(0, argY.long(), grad)
|
|
elif op in ["add", "copy_rhs"]:
|
|
dY = _update_grad_minmax_hetero(
|
|
gidx.reverse(), op, dZ, argY, argY_etype, dY
|
|
)
|
|
dY = tuple(
|
|
[
|
|
_reduce_grad(dY[i], Y_shape[i])
|
|
if dY[i] is not None
|
|
else None
|
|
for i in range(len(dY))
|
|
]
|
|
)
|
|
else: # Y has no gradient
|
|
dY = tuple([None] * len(Y))
|
|
return (None, None, None, None) + dX + dY
|
|
|
|
|
|
def sddmm_cache_X(op, req_grad_X, req_grad_Y):
|
|
"""Rules to identify whether to cache X in SDDMM forward stage."""
|
|
if op in ["mul", "dot"] and req_grad_Y:
|
|
return True
|
|
return False
|
|
|
|
|
|
def sddmm_cache_Y(op, req_grad_X, req_grad_Y):
|
|
"""Rules to identify whether to cache Y in SDDMM forward stage."""
|
|
if op in ["mul", "dot"] and req_grad_X:
|
|
return True
|
|
return False
|
|
|
|
|
|
class GSDDMM(th.autograd.Function):
|
|
@staticmethod
|
|
def forward(ctx, gidx, op, X, Y, lhs_target, rhs_target):
|
|
out = _gsddmm(gidx, op, X, Y, lhs_target, rhs_target)
|
|
X_shape = X.shape if X is not None else None
|
|
Y_shape = Y.shape if Y is not None else None
|
|
ctx.backward_cache = gidx, op, lhs_target, rhs_target, X_shape, Y_shape
|
|
req_grad_X = X.requires_grad if X is not None else False
|
|
req_grad_Y = Y.requires_grad if Y is not None else False
|
|
if not sddmm_cache_X(op, req_grad_X, req_grad_Y):
|
|
X = None
|
|
if not sddmm_cache_Y(op, req_grad_X, req_grad_Y):
|
|
Y = None
|
|
ctx.save_for_backward(X, Y)
|
|
return out
|
|
|
|
@staticmethod
|
|
def backward(ctx, dZ):
|
|
gidx, op, lhs_target, rhs_target, X_shape, Y_shape = ctx.backward_cache
|
|
X, Y = ctx.saved_tensors
|
|
if op != "copy_rhs" and ctx.needs_input_grad[2]:
|
|
if lhs_target in ["u", "v"]:
|
|
_gidx = gidx if lhs_target == "v" else gidx.reverse()
|
|
if op in ["add", "copy_lhs"]:
|
|
dX = gspmm(_gidx, "copy_rhs", "sum", None, dZ)
|
|
else: # mul, dot
|
|
if rhs_target == lhs_target:
|
|
dX = gspmm(_gidx, "copy_rhs", "sum", None, dZ) * Y
|
|
elif rhs_target == "e":
|
|
dX = gspmm(_gidx, "copy_rhs", "sum", None, dZ * Y)
|
|
else: # rhs_target = !lhs_target
|
|
dX = gspmm(_gidx, "mul", "sum", Y, dZ)
|
|
else: # lhs_target == 'e'
|
|
if op in ["add", "copy_lhs"]:
|
|
dX = dZ
|
|
else: # mul, dot
|
|
dX = gsddmm(gidx, "mul", dZ, Y, "e", rhs_target)
|
|
dX = _reduce_grad(dX, X_shape)
|
|
else:
|
|
dX = None
|
|
if op != "copy_lhs" and ctx.needs_input_grad[3]:
|
|
if rhs_target in ["u", "v"]:
|
|
_gidx = gidx if rhs_target == "v" else gidx.reverse()
|
|
if op in ["add", "copy_rhs"]:
|
|
dY = gspmm(_gidx, "copy_rhs", "sum", None, dZ)
|
|
else: # mul, dot
|
|
if lhs_target == rhs_target:
|
|
dY = gspmm(_gidx, "copy_rhs", "sum", None, dZ) * X
|
|
elif lhs_target == "e":
|
|
dY = gspmm(_gidx, "copy_rhs", "sum", None, dZ * X)
|
|
else: # rhs_target = !lhs_target
|
|
dY = gspmm(_gidx, "mul", "sum", X, dZ)
|
|
else:
|
|
if op in ["add", "copy_rhs"]:
|
|
dY = dZ
|
|
else: # mul, dot
|
|
dY = gsddmm(gidx, "mul", dZ, X, "e", lhs_target)
|
|
dY = _reduce_grad(dY, Y_shape)
|
|
else:
|
|
dY = None
|
|
return None, None, dX, dY, None, None
|
|
|
|
|
|
class GSDDMM_hetero(th.autograd.Function):
|
|
@staticmethod
|
|
def forward(
|
|
ctx, gidx, op, X_len, lhs_target, rhs_target, *feats
|
|
): # feats = X+Y
|
|
out = _gsddmm_hetero(gidx, op, X_len, lhs_target, rhs_target, feats)
|
|
X, Y = feats[:X_len], feats[X_len:]
|
|
X_shape = tuple(
|
|
[X[i].shape if X[i] is not None else None for i in range(len(X))]
|
|
)
|
|
Y_shape = tuple(
|
|
[Y[i].shape if Y[i] is not None else None for i in range(len(Y))]
|
|
)
|
|
ctx.backward_cache = (
|
|
gidx,
|
|
op,
|
|
lhs_target,
|
|
rhs_target,
|
|
X_shape,
|
|
Y_shape,
|
|
X_len,
|
|
)
|
|
req_grad_X = tuple(
|
|
[
|
|
X[i].requires_grad if X[i] is not None else False
|
|
for i in range(len(X))
|
|
]
|
|
)
|
|
req_grad_Y = tuple(
|
|
[
|
|
Y[i].requires_grad if Y[i] is not None else False
|
|
for i in range(len(Y))
|
|
]
|
|
)
|
|
ctx.save_for_backward(*feats)
|
|
return out
|
|
|
|
@staticmethod
|
|
# TODO(Israt): Implement the complete backward operator
|
|
def backward(ctx, *dZ):
|
|
(
|
|
gidx,
|
|
op,
|
|
lhs_target,
|
|
rhs_target,
|
|
X_shape,
|
|
Y_shape,
|
|
X_len,
|
|
) = ctx.backward_cache
|
|
feats = ctx.saved_tensors
|
|
X, Y = feats[:X_len], feats[X_len:]
|
|
if op != "copy_rhs" and any([x is not None for x in X]):
|
|
if lhs_target in ["u", "v"]:
|
|
_gidx = gidx if lhs_target == "v" else gidx.reverse()
|
|
tpl_of_None = tuple([None] * len(X))
|
|
if op in ["add", "copy_lhs"]:
|
|
dX = gspmm_hetero(
|
|
_gidx,
|
|
"copy_rhs",
|
|
"sum",
|
|
len(X),
|
|
*(tuple(tpl_of_None + dZ))
|
|
)
|
|
else: # mul, dot
|
|
if rhs_target == lhs_target:
|
|
dX = (
|
|
gspmm_hetero(
|
|
_gidx,
|
|
"copy_rhs",
|
|
"sum",
|
|
len(X),
|
|
*(tuple(tpl_of_None + dZ))
|
|
)
|
|
* Y
|
|
)
|
|
elif rhs_target == "e":
|
|
dZ_mul_Y = tuple(
|
|
[
|
|
dZ[i] * Y[i] if dZ[i] is not None else None
|
|
for i in range(len(Y))
|
|
]
|
|
)
|
|
dX = gspmm_hetero(
|
|
_gidx,
|
|
"copy_rhs",
|
|
"sum",
|
|
len(X),
|
|
*(tuple(tpl_of_None + dZ_mul_Y))
|
|
)
|
|
else: # rhs_target = !lhs_target
|
|
dX = gspmm_hetero(
|
|
_gidx, "mul", "sum", len(X), *tuple(Y + dZ)
|
|
)
|
|
else: # lhs_target == 'e'
|
|
if op in ["add", "copy_lhs"]:
|
|
dX = dZ
|
|
else: # mul, dot
|
|
num_etype = gidx.number_of_etypes()
|
|
dX = gsddmm_hetero(
|
|
gidx, "mul", num_etype, "e", rhs_target, *tuple(dZ + Y)
|
|
)
|
|
dX = tuple(
|
|
[
|
|
_reduce_grad(dX[i], X_shape[i])
|
|
if X[i] is not None
|
|
else None
|
|
for i in range(len(X))
|
|
]
|
|
)
|
|
else:
|
|
dX = tuple([None] * len(X))
|
|
if op != "copy_lhs" and any([y is not None for y in Y]):
|
|
if rhs_target in ["u", "v"]:
|
|
_gidx = gidx if rhs_target == "v" else gidx.reverse()
|
|
tpl_of_None = tuple([None] * len(X))
|
|
if op in ["add", "copy_rhs"]:
|
|
dY = gspmm_hetero(
|
|
_gidx,
|
|
"copy_rhs",
|
|
"sum",
|
|
len(X),
|
|
*(tuple(tpl_of_None + dZ))
|
|
)
|
|
else: # mul, dot
|
|
if lhs_target == rhs_target:
|
|
dY = (
|
|
gspmm_hetero(
|
|
_gidx,
|
|
"copy_rhs",
|
|
"sum",
|
|
len(X),
|
|
*(tuple(tpl_of_None + dZ))
|
|
)
|
|
* X
|
|
)
|
|
elif lhs_target == "e":
|
|
dZ_mul_X = tuple(
|
|
[
|
|
dZ[i] * X[i] if dZ[i] is not None else None
|
|
for i in range(len(X))
|
|
]
|
|
)
|
|
dY = gspmm_hetero(
|
|
_gidx,
|
|
"copy_rhs",
|
|
"sum",
|
|
len(X),
|
|
*(tuple(tpl_of_None + dZ_mul_X))
|
|
)
|
|
else: # rhs_target = !lhs_target
|
|
dY = gspmm_hetero(
|
|
_gidx, "mul", "sum", len(X), *tuple(X + dZ)
|
|
)
|
|
else:
|
|
if op in ["add", "copy_rhs"]:
|
|
dY = tuple(
|
|
[
|
|
dZ[i] if dZ[i] is not None else None
|
|
for i in range(len(dZ))
|
|
]
|
|
)
|
|
else: # mul, dot
|
|
num_etype = gidx.number_of_etypes()
|
|
dY = gsddmm_hetero(
|
|
gidx, "mul", num_etype, "e", lhs_target, *tuple(dZ + X)
|
|
)
|
|
dY = tuple(
|
|
[
|
|
_reduce_grad(dY[i], Y_shape[i])
|
|
if Y[i] is not None
|
|
else None
|
|
for i in range(len(Y))
|
|
]
|
|
)
|
|
else:
|
|
dY = tuple([None] * len(Y))
|
|
return (None, None, None, None, None) + dX + dY
|
|
|
|
|
|
class EdgeSoftmax(th.autograd.Function):
|
|
@staticmethod
|
|
def forward(ctx, gidx, score, eids, norm_by):
|
|
"""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
|
|
"""
|
|
# remember to save the graph to backward cache before making it
|
|
# a local variable
|
|
if not is_all(eids):
|
|
gidx = gidx.edge_subgraph([eids], True).graph
|
|
if norm_by == "src":
|
|
gidx = gidx.reverse()
|
|
# Note: Now _edge_softmax_forward op only supports CPU
|
|
# TODO(Zhejiang): We will support GPU in the future
|
|
if score.is_cuda:
|
|
score_max = _gspmm(gidx, "copy_rhs", "max", None, score)[0]
|
|
score = th.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")
|
|
else:
|
|
out = _edge_softmax_forward(gidx, score, "copy_rhs")
|
|
ctx.backward_cache = gidx
|
|
ctx.save_for_backward(out)
|
|
return out
|
|
|
|
@staticmethod
|
|
def backward(ctx, 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 - out * sds_sum # multiple expressions
|
|
return grad_score.data
|
|
"""
|
|
gidx = ctx.backward_cache
|
|
(out,) = ctx.saved_tensors
|
|
sds = out * grad_out
|
|
# Note: Now _edge_softmax_backward op only supports CPU
|
|
# TODO(Zhejiang): We will support GPU in the future
|
|
if out.is_cuda:
|
|
accum = gspmm(gidx, "copy_rhs", "sum", None, sds)
|
|
|
|
grad_score = sds - gsddmm(gidx, "mul", out, accum, "e", "v")
|
|
else:
|
|
grad_score = _edge_softmax_backward(gidx, out, sds)
|
|
return None, grad_score, None, None
|
|
|
|
|
|
class EdgeSoftmax_hetero(th.autograd.Function):
|
|
@staticmethod
|
|
def forward(ctx, gidx, eids, norm_by, *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
|
|
"""
|
|
# remember to save the graph to backward cache before making it
|
|
# a local variable
|
|
if not is_all(eids):
|
|
gidx = gidx.edge_subgraph([eids], True).graph
|
|
if norm_by == "src":
|
|
gidx = gidx.reverse()
|
|
u_len = gidx.number_of_ntypes()
|
|
e_len = gidx.number_of_etypes()
|
|
lhs = [None] * u_len
|
|
feats = tuple(lhs + list(score))
|
|
score_max = _gspmm_hetero(gidx, "copy_rhs", "max", u_len, feats)[0]
|
|
out_tmp = _gsddmm_hetero(
|
|
gidx, "sub", e_len, "e", "v", tuple(list(score) + list(score_max))
|
|
)
|
|
score = tuple(
|
|
[
|
|
th.exp(out_tmp[i]) if out_tmp[i] is not None else None
|
|
for i in range(len(out_tmp))
|
|
]
|
|
)
|
|
score_sum = _gspmm_hetero(
|
|
gidx, "copy_rhs", "sum", u_len, tuple(lhs + list(score))
|
|
)[0]
|
|
out = _gsddmm_hetero(
|
|
gidx, "div", e_len, "e", "v", tuple(list(score) + list(score_sum))
|
|
)
|
|
ctx.backward_cache = gidx
|
|
ctx.save_for_backward(*out)
|
|
return out
|
|
|
|
@staticmethod
|
|
def backward(ctx, *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 - out * sds_sum # multiple expressions
|
|
return grad_score.data
|
|
"""
|
|
gidx = ctx.backward_cache
|
|
u_len = gidx.number_of_ntypes()
|
|
e_len = gidx.number_of_etypes()
|
|
lhs = [None] * u_len
|
|
out = ctx.saved_tensors
|
|
sds = tuple([out[i] * grad_out[i] for i in range(len(out))])
|
|
accum = _gspmm_hetero(
|
|
gidx, "copy_rhs", "sum", u_len, tuple(lhs + list(sds))
|
|
)[0]
|
|
out_sddmm = _gsddmm_hetero(
|
|
gidx, "mul", e_len, "e", "v", tuple(list(out) + list(accum))
|
|
)
|
|
grad_score = tuple([sds[i] - out_sddmm[i] for i in range(len(sds))])
|
|
return (None, None, None) + grad_score
|
|
|
|
|
|
class SegmentReduce(th.autograd.Function):
|
|
@staticmethod
|
|
def forward(ctx, op, x, offsets):
|
|
y, arg = _segment_reduce(op, x, offsets)
|
|
ctx.save_for_backward(arg, offsets)
|
|
ctx.backward_cache = op
|
|
return y
|
|
|
|
@staticmethod
|
|
def backward(ctx, dy):
|
|
op = ctx.backward_cache
|
|
arg, offsets = ctx.saved_tensors
|
|
m = offsets[-1].item()
|
|
if op == "sum":
|
|
offsets = offsets[1:]
|
|
# To address the issue of trailing zeros, related issue:
|
|
# https://github.com/dmlc/dgl/pull/2610
|
|
indices = th.zeros(
|
|
(m + 1,), device=offsets.device, dtype=offsets.dtype
|
|
)
|
|
indices.scatter_add_(0, offsets, th.ones_like(offsets))
|
|
indices = th.cumsum(indices, -1)[:-1]
|
|
dx = dy[indices]
|
|
else:
|
|
dx = _bwd_segment_cmp(dy, arg, m)
|
|
return None, dx, None
|
|
|
|
|
|
class ScatterAdd(th.autograd.Function):
|
|
@staticmethod
|
|
def forward(ctx, x, idx, m):
|
|
y = _scatter_add(x, idx, m)
|
|
ctx.save_for_backward(idx)
|
|
return y
|
|
|
|
@staticmethod
|
|
def backward(ctx, dy):
|
|
idx = ctx.saved_tensors
|
|
return dy[idx], None, None
|
|
|
|
|
|
class CSRMM(th.autograd.Function):
|
|
@staticmethod
|
|
def forward(ctx, 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")
|
|
# Note: the returned C_indptr, C_indices and C_eids tensors MUST be the same
|
|
# as the underlying tensors of the created graph gidxC.
|
|
ctx.backward_cache = gidxA, gidxB, gidxC
|
|
ctx.save_for_backward(A_weights, B_weights)
|
|
return (
|
|
th.tensor(nrows),
|
|
th.tensor(ncols),
|
|
C_indptr,
|
|
C_indices,
|
|
C_eids,
|
|
C_weights,
|
|
)
|
|
|
|
@staticmethod
|
|
def backward(
|
|
ctx, dnrows, dncols, dC_indptr, dC_indices, dC_eids, dC_weights
|
|
):
|
|
# Only the last argument is meaningful.
|
|
gidxA, gidxB, gidxC = ctx.backward_cache
|
|
A_weights, B_weights = ctx.saved_tensors
|
|
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 None, dA_weights, None, dB_weights, None
|
|
|
|
|
|
class CSRSum(th.autograd.Function):
|
|
@staticmethod
|
|
def forward(ctx, gidxs, *weights):
|
|
# PyTorch tensors must be explicit arguments of the forward function
|
|
gidxC, C_weights = _csrsum(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.
|
|
ctx.backward_cache = gidxs, gidxC
|
|
return (
|
|
th.tensor(nrows),
|
|
th.tensor(ncols),
|
|
C_indptr,
|
|
C_indices,
|
|
C_eids,
|
|
C_weights,
|
|
)
|
|
|
|
@staticmethod
|
|
def backward(
|
|
ctx, dnrows, dncols, dC_indptr, dC_indices, dC_eids, dC_weights
|
|
):
|
|
# Only the last argument is meaningful.
|
|
gidxs, gidxC = ctx.backward_cache
|
|
return (None,) + tuple(
|
|
csrmask(gidxC, dC_weights, gidx) for gidx in gidxs
|
|
)
|
|
|
|
|
|
class CSRMask(th.autograd.Function):
|
|
@staticmethod
|
|
def forward(ctx, gidxA, A_weights, gidxB):
|
|
ctx.backward_cache = gidxA, gidxB
|
|
return _csrmask(gidxA, A_weights, gidxB)
|
|
|
|
@staticmethod
|
|
def backward(ctx, dB_weights):
|
|
gidxA, gidxB = ctx.backward_cache
|
|
return None, csrmask(gidxB, dB_weights, gidxA), None
|
|
|
|
|
|
class SEGMENTMM(th.autograd.Function):
|
|
@staticmethod
|
|
def forward(ctx, A, B, seglen_A):
|
|
if B.dim() != 3:
|
|
raise ValueError("segment_mm expects B to be a 3D tensor.")
|
|
C = th.empty((A.shape[0], B.shape[2]), device=A.device, dtype=A.dtype)
|
|
C = _segment_mm(A, B, C, seglen_A)
|
|
ctx.backward_cache = A, B, seglen_A
|
|
return C
|
|
|
|
@staticmethod
|
|
def backward(ctx, dZ):
|
|
A, B, seglen_A = ctx.backward_cache
|
|
A_grad = B_grad = None
|
|
if ctx.needs_input_grad[0]:
|
|
# Compute A_grad = Out_grad * B^T
|
|
A_grad = th.empty(A.shape, device=A.device, dtype=A.dtype)
|
|
A_grad = _segment_mm(dZ, B, A_grad, seglen_A, b_trans=True)
|
|
if ctx.needs_input_grad[1]:
|
|
# Compute B_grad = A^T * Out_grad
|
|
B_grad = th.empty(B.shape, device=B.device, dtype=B.dtype)
|
|
B_grad = _segment_mm_backward_B(A, dZ, B_grad, seglen_A)
|
|
return A_grad, B_grad, None
|
|
|
|
|
|
class GATHERMM(th.autograd.Function):
|
|
@staticmethod
|
|
def forward(ctx, A, B, idx_a, idx_b):
|
|
if B.dim() != 3:
|
|
raise ValueError(
|
|
"Expected dimension of B is 3. Got " + str(B.dim())
|
|
)
|
|
N = len(idx_b) if idx_a is None else len(idx_a)
|
|
C = th.zeros((N, B.shape[2]), device=A.device, dtype=A.dtype)
|
|
C = _gather_mm(A, B, C, idx_a, idx_b)
|
|
ctx.backward_cache = A, B, idx_a, idx_b
|
|
return C
|
|
|
|
@staticmethod
|
|
def backward(ctx, dZ):
|
|
A, B, idx_a, idx_b = ctx.backward_cache
|
|
A_grad = B_grad = None
|
|
if ctx.needs_input_grad[0]:
|
|
# Compute A_grad = Out_grad * B^T
|
|
A_grad = th.zeros(A.shape, device=A.device, dtype=A.dtype)
|
|
A_grad = _gather_mm_scatter(
|
|
dZ, B.transpose(1, 2), A_grad, idx_b=idx_b, idx_c=idx_a
|
|
)
|
|
if ctx.needs_input_grad[1]:
|
|
# Compute B_grad = A^T * Out_grad
|
|
B_grad = th.zeros(B.shape, device=B.device, dtype=B.dtype)
|
|
B_grad = _gather_mm_scatter(A, dZ, B_grad, idx_a=idx_a, idx_c=idx_b)
|
|
return A_grad, B_grad, None, None
|
|
|
|
|
|
def gspmm(gidx, op, reduce_op, lhs_data, rhs_data):
|
|
if op == "sub":
|
|
op = "add"
|
|
rhs_data = -rhs_data
|
|
if op == "div":
|
|
op = "mul"
|
|
rhs_data = 1.0 / rhs_data
|
|
args = _cast_if_autocast_enabled(gidx, op, reduce_op, lhs_data, rhs_data)
|
|
with _disable_autocast_if_enabled():
|
|
return GSpMM.apply(*args)
|
|
|
|
|
|
def gsddmm(gidx, op, lhs_data, rhs_data, lhs_target="u", rhs_target="v"):
|
|
if op == "sub":
|
|
op = "add"
|
|
rhs_data = -rhs_data
|
|
if op == "div":
|
|
op = "mul"
|
|
rhs_data = 1.0 / rhs_data
|
|
args = _cast_if_autocast_enabled(
|
|
gidx, op, lhs_data, rhs_data, lhs_target, rhs_target
|
|
)
|
|
with _disable_autocast_if_enabled():
|
|
return GSDDMM.apply(*args)
|
|
|
|
|
|
def gspmm_hetero(g, op, reduce_op, lhs_len, *lhs_and_rhs_tuple):
|
|
lhs_tuple, rhs_tuple = (
|
|
lhs_and_rhs_tuple[:lhs_len],
|
|
lhs_and_rhs_tuple[lhs_len:],
|
|
)
|
|
if op == "sub":
|
|
op = "add"
|
|
rhs_tuple = tuple(
|
|
[
|
|
-rhs_tuple[i] if rhs_tuple[i] is not None else None
|
|
for i in range(len(rhs_tuple))
|
|
]
|
|
)
|
|
if op == "div":
|
|
op = "mul"
|
|
rhs_tuple = tuple(
|
|
[
|
|
(1.0 / rhs_tuple[i]) if rhs_tuple[i] is not None else None
|
|
for i in range(len(rhs_tuple))
|
|
]
|
|
)
|
|
if op in ["add", "mul"]:
|
|
lhs_and_rhs_tuple = tuple(list(lhs_tuple) + list(rhs_tuple))
|
|
|
|
args = _cast_if_autocast_enabled(
|
|
g, op, reduce_op, lhs_len, *lhs_and_rhs_tuple
|
|
)
|
|
with _disable_autocast_if_enabled():
|
|
return GSpMM_hetero.apply(*args)
|
|
|
|
|
|
def gsddmm_hetero(
|
|
g, op, lhs_len, lhs_target="u", rhs_target="v", *lhs_and_rhs_tuple
|
|
):
|
|
lhs_tuple, rhs_tuple = (
|
|
lhs_and_rhs_tuple[:lhs_len],
|
|
lhs_and_rhs_tuple[lhs_len:],
|
|
)
|
|
if op == "sub":
|
|
op = "add"
|
|
rhs_tuple = tuple(
|
|
[
|
|
-rhs_tuple[i] if rhs_tuple[i] is not None else None
|
|
for i in range(len(rhs_tuple))
|
|
]
|
|
)
|
|
if op == "div":
|
|
op = "mul"
|
|
rhs_tuple = tuple(
|
|
[
|
|
(1.0 / rhs_tuple[i]) if rhs_tuple[i] is not None else None
|
|
for i in range(len(rhs_tuple))
|
|
]
|
|
)
|
|
if op in ["add", "mul"]:
|
|
lhs_and_rhs_tuple = tuple(list(lhs_tuple) + list(rhs_tuple))
|
|
|
|
args = _cast_if_autocast_enabled(
|
|
g, op, lhs_len, lhs_target, rhs_target, *lhs_and_rhs_tuple
|
|
)
|
|
with _disable_autocast_if_enabled():
|
|
return GSDDMM_hetero.apply(*args)
|
|
|
|
|
|
def edge_softmax(gidx, logits, eids=ALL, norm_by="dst"):
|
|
args = _cast_if_autocast_enabled(gidx, logits, eids, norm_by)
|
|
with _disable_autocast_if_enabled():
|
|
return EdgeSoftmax.apply(*args)
|
|
|
|
|
|
def edge_softmax_hetero(gidx, eids=ALL, norm_by="dst", *logits):
|
|
args = _cast_if_autocast_enabled(gidx, eids, norm_by, *logits)
|
|
with _disable_autocast_if_enabled():
|
|
return EdgeSoftmax_hetero.apply(*args)
|
|
|
|
|
|
def segment_reduce(op, x, offsets):
|
|
args = _cast_if_autocast_enabled(op, x, offsets)
|
|
with _disable_autocast_if_enabled():
|
|
return SegmentReduce.apply(*args)
|
|
|
|
|
|
def scatter_add(x, idx, m):
|
|
args = _cast_if_autocast_enabled(x, idx, m)
|
|
with _disable_autocast_if_enabled():
|
|
return ScatterAdd.apply(*args)
|
|
|
|
|
|
def csrmm(gidxA, A_weights, gidxB, B_weights, num_vtypes):
|
|
nrows, ncols, C_indptr, C_indices, C_eids, C_weights = CSRMM.apply(
|
|
gidxA, A_weights, gidxB, B_weights, num_vtypes
|
|
)
|
|
gidxC = create_unitgraph_from_csr(
|
|
num_vtypes,
|
|
nrows.item(),
|
|
ncols.item(),
|
|
C_indptr,
|
|
C_indices,
|
|
C_eids,
|
|
["coo", "csr", "csc"],
|
|
)
|
|
return gidxC, C_weights
|
|
|
|
|
|
def csrsum(gidxs, weights):
|
|
nrows, ncols, C_indptr, C_indices, C_eids, C_weights = CSRSum.apply(
|
|
gidxs, *weights
|
|
)
|
|
gidxC = create_unitgraph_from_csr(
|
|
gidxs[0].number_of_ntypes(),
|
|
nrows.item(),
|
|
ncols.item(),
|
|
C_indptr,
|
|
C_indices,
|
|
C_eids,
|
|
["coo", "csr", "csc"],
|
|
)
|
|
return gidxC, C_weights
|
|
|
|
|
|
def csrmask(gidxA, A_weights, gidxB):
|
|
return CSRMask.apply(gidxA, A_weights, gidxB)
|
|
|
|
|
|
def segment_mm(A, B, seglen_A):
|
|
if A.device.type == "cpu":
|
|
C = []
|
|
off = 0
|
|
for i in range(B.shape[0]):
|
|
C.append(A[off : off + seglen_A[i]] @ B[i])
|
|
off += seglen_A[i]
|
|
return th.cat(C)
|
|
else:
|
|
args = _cast_if_autocast_enabled(A, B, seglen_A)
|
|
with _disable_autocast_if_enabled():
|
|
return SEGMENTMM.apply(*args)
|
|
|
|
|
|
def gather_mm(A, B, idx_A=None, idx_B=None):
|
|
if A.device.type == "cpu":
|
|
A = A[idx_A] if idx_A is not None else A
|
|
B = B[idx_B] if idx_B is not None else B
|
|
return th.bmm(A.unsqueeze(1), B).squeeze(1)
|
|
else:
|
|
args = _cast_if_autocast_enabled(A, B, idx_A, idx_B)
|
|
with _disable_autocast_if_enabled():
|
|
return GATHERMM.apply(*args)
|