dmlc--dgl
c341520d5b
* udp * add test * udp * fix mxnet
252 行
9.0 KiB
Python
252 行
9.0 KiB
Python
"""Module for sparse matrix operators."""
|
|
# pylint: disable= invalid-name
|
|
from __future__ import absolute_import
|
|
|
|
import dgl.ndarray as nd
|
|
from ._ffi.function import _init_api
|
|
from .base import DGLError
|
|
from . import backend as F
|
|
|
|
|
|
def infer_broadcast_shape(op, shp1, shp2):
|
|
r"""Check the shape validity, and infer the output shape given input shape and operator.
|
|
Note the both :attr:`shp1`, :attr:`shp2` and the returned shape are feature
|
|
shapes (i.e. we remove the first dimension, which correspond to graph statistics
|
|
such as number of nodes, number of edges, etc.).
|
|
|
|
We allow applying op on operands with different shapes, according to the
|
|
broadcasting semantics of Numpy/Scipy:
|
|
https://numpy.org/doc/stable/user/basics.broadcasting.html
|
|
|
|
Parameters
|
|
----------
|
|
op : str
|
|
The binary op's name, could be `add`, `sub`, `mul`, `div`, `dot`, `copy_lhs`, `copy_rhs`.
|
|
shp1 : tuple[int]
|
|
The shape of lhs operand.
|
|
shp2 : tuple[int]
|
|
The shape of rhs operand.
|
|
|
|
Returns
|
|
-------
|
|
tuple[int]
|
|
shape after broadcasting
|
|
"""
|
|
pad_shp1, pad_shp2 = shp1, shp2
|
|
if op == "dot":
|
|
if shp1[-1] != shp2[-1]:
|
|
raise DGLError("Dot operator is only available for arrays with the "
|
|
"same size on last dimension, but got {} and {}."
|
|
.format(shp1, shp2))
|
|
if op == "copy_lhs":
|
|
return shp1
|
|
if op == "copy_rhs":
|
|
return shp2
|
|
# operands are padded to have the same dimensionality with leading 1's.
|
|
if len(shp1) > len(shp2):
|
|
pad_shp2 = (1,) * (len(shp1) - len(shp2)) + shp2
|
|
elif len(shp1) < len(shp2):
|
|
pad_shp1 = (1,) * (len(shp2) - len(shp1)) + shp1
|
|
for d1, d2 in zip(pad_shp1, pad_shp2):
|
|
if d1 != d2 and d1 != 1 and d2 != 1:
|
|
raise DGLError("Feature shapes {} and {} are not valid for broadcasting."
|
|
.format(shp1, shp2))
|
|
rst = tuple(max(d1, d2) for d1, d2 in zip(pad_shp1, pad_shp2))
|
|
return rst[:-1] + (1,) if op == "dot" else rst
|
|
|
|
|
|
def to_dgl_nd(x):
|
|
"""Convert framework-specific tensor/None to dgl ndarray."""
|
|
return nd.NULL['int64'] if x is None else F.zerocopy_to_dgl_ndarray(x)
|
|
|
|
|
|
def to_dgl_nd_for_write(x):
|
|
"""Convert framework-specific tensor/None to dgl ndarray for write."""
|
|
return nd.NULL['int64'] if x is None else F.zerocopy_to_dgl_ndarray_for_write(x)
|
|
|
|
|
|
target_mapping = {
|
|
'u': 0,
|
|
'e': 1,
|
|
'v': 2,
|
|
'src': 0,
|
|
'edge': 1,
|
|
'dst': 2
|
|
}
|
|
|
|
|
|
def _gspmm(gidx, op, reduce_op, u, e):
|
|
r""" Generalized Sparse Matrix Multiplication interface. It takes the result of
|
|
:attr:`op` on source node feature and edge feature, leads to a message on edge.
|
|
Then aggregates the message by :attr:`reduce_op` on destination nodes.
|
|
|
|
.. math::
|
|
x_v = \psi_{(u, v, e)\in \mathcal{G}}(\rho(x_u, x_e))
|
|
|
|
where :math:`x_v` is the returned feature on destination nodes, and :math`x_u`,
|
|
:math:`x_e` refers to :attr:`u`, :attr:`e` respectively. :math:`\rho` means binary
|
|
operator :attr:`op` and :math:`\psi` means reduce operator :attr:`reduce_op`,
|
|
:math:`\mathcal{G}` is the graph we apply gspmm on: :attr:`g`.
|
|
|
|
Note that this function does not handle gradients.
|
|
|
|
Parameters
|
|
----------
|
|
gidx : HeteroGraphIndex
|
|
The input graph index.
|
|
op : str
|
|
The binary op's name, could be ``add``, ``sub``, ``mul``, ``div``, ``copy_lhs``,
|
|
``copy_rhs``.
|
|
reduce_op : str
|
|
Reduce operator, could be ``sum``, ``max``, ``min``.
|
|
u : tensor or None
|
|
The feature on source nodes, could be None if op is ``copy_rhs``.
|
|
e : tensor or None
|
|
The feature on edges, could be None if op is ``copy_lhs``.
|
|
|
|
Returns
|
|
-------
|
|
tuple
|
|
The returned tuple is composed of two elements:
|
|
- The first element refers to the result tensor.
|
|
- The second element refers to a tuple composed of arg_u and arg_e
|
|
(which is useful when reducer is `min`/`max`).
|
|
|
|
Notes
|
|
-----
|
|
This function does not handle gradients.
|
|
"""
|
|
if gidx.number_of_etypes() != 1:
|
|
raise DGLError("We only support gspmm on graph with one edge type")
|
|
use_u = op != 'copy_rhs'
|
|
use_e = op != 'copy_lhs'
|
|
# deal with scalar features.
|
|
expand_u, expand_e = False, False
|
|
if use_u:
|
|
if F.ndim(u) == 1:
|
|
u = F.unsqueeze(u, -1)
|
|
expand_u = True
|
|
if use_e:
|
|
if F.ndim(e) == 1:
|
|
e = F.unsqueeze(e, -1)
|
|
expand_e = True
|
|
ctx = F.context(u) if use_u else F.context(e)
|
|
dtype = F.dtype(u) if use_u else F.dtype(e)
|
|
u_shp = F.shape(u) if use_u else (0,)
|
|
e_shp = F.shape(e) if use_e else (0,)
|
|
_, dsttype = gidx.metagraph.find_edge(0)
|
|
v_shp = (gidx.number_of_nodes(dsttype), ) +\
|
|
infer_broadcast_shape(op, u_shp[1:], e_shp[1:])
|
|
v = F.zeros(v_shp, dtype, ctx)
|
|
use_cmp = reduce_op in ['max', 'min']
|
|
arg_u, arg_e = None, None
|
|
idtype = getattr(F, gidx.dtype)
|
|
if use_cmp:
|
|
if use_u:
|
|
arg_u = F.zeros(v_shp, idtype, ctx)
|
|
if use_e:
|
|
arg_e = F.zeros(v_shp, idtype, ctx)
|
|
arg_u_nd = to_dgl_nd_for_write(arg_u)
|
|
arg_e_nd = to_dgl_nd_for_write(arg_e)
|
|
if gidx.number_of_edges(0) > 0:
|
|
_CAPI_DGLKernelSpMM(gidx, op, reduce_op,
|
|
to_dgl_nd(u if use_u else None),
|
|
to_dgl_nd(e if use_e else None),
|
|
to_dgl_nd_for_write(v),
|
|
arg_u_nd,
|
|
arg_e_nd)
|
|
# NOTE(zihao): actually we can avoid the following step, because arg_*_nd
|
|
# refers to the data that stores arg_*. After we call _CAPI_DGLKernelSpMM,
|
|
# arg_* should have already been changed. But we found this doesn't work
|
|
# under Tensorflow when index type is int32. (arg_u and arg_e would be
|
|
# all zero).
|
|
# The workaround is proposed by Jinjing, and we still need to investigate
|
|
# where the problem is.
|
|
arg_u = None if arg_u is None else F.zerocopy_from_dgl_ndarray(arg_u_nd)
|
|
arg_e = None if arg_e is None else F.zerocopy_from_dgl_ndarray(arg_e_nd)
|
|
# To deal with scalar node/edge features.
|
|
if (expand_u or not use_u) and (expand_e or not use_e):
|
|
v = F.squeeze(v, -1)
|
|
if expand_u and use_cmp:
|
|
arg_u = F.squeeze(arg_u, -1)
|
|
if expand_e and use_cmp:
|
|
arg_e = F.squeeze(arg_e, -1)
|
|
return v, (arg_u, arg_e)
|
|
|
|
|
|
def _gsddmm(gidx, op, lhs, rhs, lhs_target='u', rhs_target='v'):
|
|
r""" Generalized Sampled-Dense-Dense Matrix Multiplication interface. It
|
|
takes the result of :attr:`op` on source node feature and destination node
|
|
feature, leads to a feature on edge.
|
|
|
|
.. math::
|
|
x_{e} = \phi(x_u, x_e, x_v), \forall (u,e,v)\in \mathcal{G}
|
|
|
|
where :math:`x_{e}` is the returned feature on edges and :math:`x_u`,
|
|
:math:`x_v` refers to :attr:`u`, :attr:`v` respectively. :math:`\phi`
|
|
is the binary operator :attr:`op`, and :math:`\mathcal{G}` is the graph
|
|
we apply gsddmm on: :attr:`g`.
|
|
|
|
Parameters
|
|
----------
|
|
gidx : HeteroGraphIndex
|
|
The input graph index.
|
|
op : str
|
|
Binary operator, could be ``add``, ``sub``, ``mul``, ``div``, ``dot``,
|
|
``copy_lhs``, ``copy_rhs``.
|
|
lhs : tensor or None
|
|
Left hand operand.
|
|
rhs : tensor or None
|
|
Right hand operand.
|
|
lhs_target : str
|
|
The target of left hand operand, could be ``src``, ``edge``, ``dst``
|
|
or their alias ``u``, ``e``, ``v``.
|
|
rhs_target : str
|
|
The target of right hand operand, could be ``src``, ``edge``, ``dst``
|
|
or their alias ``u``, ``e``, ``v``.
|
|
|
|
Returns
|
|
-------
|
|
tensor
|
|
The result tensor.
|
|
|
|
Notes
|
|
-----
|
|
This function does not handle gradients.
|
|
"""
|
|
if gidx.number_of_etypes() != 1:
|
|
raise DGLError("We only support gsddmm on graph with one edge type")
|
|
use_lhs = op != 'copy_rhs'
|
|
use_rhs = op != 'copy_lhs'
|
|
# deal with scalar features.
|
|
expand_lhs, expand_rhs = False, False
|
|
if use_lhs:
|
|
if F.ndim(lhs) == 1:
|
|
lhs = F.unsqueeze(lhs, -1)
|
|
expand_lhs = True
|
|
if use_rhs:
|
|
if F.ndim(rhs) == 1:
|
|
rhs = F.unsqueeze(rhs, -1)
|
|
expand_rhs = True
|
|
lhs_target = target_mapping[lhs_target]
|
|
rhs_target = target_mapping[rhs_target]
|
|
ctx = F.context(lhs) if use_lhs else F.context(rhs)
|
|
dtype = F.dtype(lhs) if use_lhs else F.dtype(rhs)
|
|
lhs_shp = F.shape(lhs) if use_lhs else (0,)
|
|
rhs_shp = F.shape(rhs) if use_rhs else (0,)
|
|
out_shp = (gidx.number_of_edges(0), ) +\
|
|
infer_broadcast_shape(op, lhs_shp[1:], rhs_shp[1:])
|
|
out = F.zeros(out_shp, dtype, ctx)
|
|
if gidx.number_of_edges(0) > 0:
|
|
_CAPI_DGLKernelSDDMM(gidx, op,
|
|
to_dgl_nd(lhs if use_lhs else None),
|
|
to_dgl_nd(rhs if use_rhs else None),
|
|
to_dgl_nd_for_write(out),
|
|
lhs_target, rhs_target)
|
|
if (expand_lhs or not use_lhs) and (expand_rhs or not use_rhs):
|
|
out = F.squeeze(out, -1)
|
|
return out
|
|
|
|
|
|
_init_api("dgl.sparse")
|