Files
dmlc--dgl/python/dgl/backend/pytorch/sparse.py
T
2026-07-13 13:35:51 +08:00

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)