dmlc--dgl
273 行
9.8 KiB
Python
273 行
9.8 KiB
Python
import torch as th
|
|
from ...base import is_all, ALL
|
|
from ...sparse import _gspmm, _gsddmm, _segment_reduce, _bwd_segment_cmp
|
|
|
|
__all__ = ['gspmm', 'gsddmm', 'edge_softmax', 'segment_reduce']
|
|
|
|
|
|
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))
|
|
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."""
|
|
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. / x if op == 'div' else x
|
|
|
|
|
|
def _addsub(op, x):
|
|
return -x if op == 'sub' else x
|
|
|
|
|
|
def _expand(x, shape):
|
|
return x.expand(-1, *shape)
|
|
|
|
|
|
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)
|
|
ctx.backward_cache = gidx, op, reduce_op
|
|
ctx.save_for_backward(X, Y, argX, argY)
|
|
return out
|
|
|
|
@staticmethod
|
|
def backward(ctx, dZ):
|
|
gidx, op, reduce_op = 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 in ['mul', 'div']:
|
|
dX = gspmm(g_rev, 'mul', 'sum', dZ, _muldiv(op, Y))
|
|
elif op in ['add', 'sub']:
|
|
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=X.dtype, device=X.device)
|
|
if op in ['mul', 'div']:
|
|
grad = _muldiv(op, _expand(Y, dZ.shape[1:]).gather(
|
|
0, argY.long())) * dZ
|
|
dX.scatter_add_(0, argX.long(), grad)
|
|
elif op in ['add', 'sub', '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 _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: # max/min
|
|
dY = th.zeros((Y.shape[0],) + dZ.shape[1:],
|
|
dtype=Y.dtype, device=Y.device)
|
|
if op in ['mul', 'div']:
|
|
grad = _expand(X, dZ.shape[1:]).gather(
|
|
0, argX.long()) * dZ
|
|
dY.scatter_add_(0, argY.long(), grad)
|
|
if op == 'div':
|
|
dY = -dY / (Y ** 2)
|
|
elif op in ['add', 'sub', 'copy_rhs']:
|
|
dY.scatter_add_(0, argY.long(), _addsub(op, dZ))
|
|
dY = _reduce_grad(dY, Y.shape)
|
|
else: # Y has no gradient
|
|
dY = None
|
|
return None, None, None, dX, dY
|
|
|
|
|
|
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)
|
|
ctx.backward_cache = gidx, op, lhs_target, rhs_target
|
|
ctx.save_for_backward(X, Y)
|
|
return out
|
|
|
|
@staticmethod
|
|
def backward(ctx, dZ):
|
|
gidx, op, lhs_target, rhs_target = 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', 'sub', 'copy_lhs']:
|
|
dX = gspmm(_gidx, 'copy_rhs', 'sum', None, dZ)
|
|
else: # mul, div, dot
|
|
if rhs_target == lhs_target:
|
|
dX = gspmm(_gidx, 'copy_rhs', 'sum', None, dZ) * _muldiv(op, Y)
|
|
elif rhs_target == 'e':
|
|
dX = gspmm(_gidx, 'copy_rhs', 'sum', None, dZ * _muldiv(op, Y))
|
|
else: # rhs_target = !lhs_target
|
|
dX = gspmm(_gidx, 'mul', 'sum', _muldiv(op, Y), dZ)
|
|
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 = 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', 'sub', 'copy_rhs']:
|
|
dY = gspmm(_gidx, 'copy_rhs', 'sum', None, _addsub(op, dZ))
|
|
else: # mul, div, 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)
|
|
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 = None
|
|
return None, None, dX, dY, None, None
|
|
|
|
|
|
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()
|
|
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')
|
|
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
|
|
accum = gspmm(gidx, 'copy_rhs', 'sum', None, sds)
|
|
grad_score = sds - gsddmm(gidx, 'mul', out, accum, 'e', 'v')
|
|
return None, grad_score, None, None
|
|
|
|
|
|
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:-1]
|
|
indices = th.zeros(
|
|
(m,), device=offsets.device, dtype=offsets.dtype)
|
|
indices.scatter_add_(0, offsets, th.ones_like(offsets))
|
|
indices = th.cumsum(indices, -1)
|
|
dx = dy[indices]
|
|
else:
|
|
dx = _bwd_segment_cmp(dy, arg, m)
|
|
return None, dx, None
|
|
|
|
|
|
def gspmm(gidx, op, reduce_op, lhs_data, rhs_data):
|
|
return GSpMM.apply(gidx, op, reduce_op, lhs_data, rhs_data)
|
|
|
|
|
|
def gsddmm(gidx, op, lhs_data, rhs_data, lhs_target='u', rhs_target='v'):
|
|
return GSDDMM.apply(gidx, op, lhs_data, rhs_data, lhs_target, rhs_target)
|
|
|
|
|
|
def edge_softmax(gidx, logits, eids=ALL, norm_by='dst'):
|
|
return EdgeSoftmax.apply(gidx, logits, eids, norm_by)
|
|
|
|
|
|
def segment_reduce(op, x, offsets):
|
|
return SegmentReduce.apply(op, x, offsets)
|