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
+6
View File
@@ -0,0 +1,6 @@
"""dgl operator module."""
from .edge_softmax import *
from .gather_mm import *
from .sddmm import *
from .segment import *
from .spmm import *
+153
View File
@@ -0,0 +1,153 @@
"""dgl edge_softmax operator module."""
from ..backend import (
astype,
edge_softmax as edge_softmax_internal,
edge_softmax_hetero as edge_softmax_hetero_internal,
)
from ..base import ALL, is_all
__all__ = ["edge_softmax"]
def edge_softmax(graph, logits, eids=ALL, norm_by="dst"):
r"""Compute softmax over weights of incoming edges for every node.
For a node :math:`i`, edge softmax is an operation that computes
.. math::
a_{ij} = \frac{\exp(z_{ij})}{\sum_{j\in\mathcal{N}(i)}\exp(z_{ij})}
where :math:`z_{ij}` is a signal of edge :math:`j\rightarrow i`, also
called logits in the context of softmax. :math:`\mathcal{N}(i)` is
the set of nodes that have an edge to :math:`i`.
By default edge softmax is normalized by destination nodes(i.e. :math:`ij`
are incoming edges of `i` in the formula above). We also support edge
softmax normalized by source nodes(i.e. :math:`ij` are outgoing edges of
`i` in the formula). The former case corresponds to softmax in GAT and
Transformer, and the latter case corresponds to softmax in Capsule network.
An example of using edge softmax is in
`Graph Attention Network <https://arxiv.org/pdf/1710.10903.pdf>`__ where
the attention weights are computed with this operation.
Other non-GNN examples using this are
`Transformer <https://papers.nips.cc/paper/7181-attention-is-all-you-need.pdf>`__,
`Capsule <https://arxiv.org/pdf/1710.09829.pdf>`__, etc.
Parameters
----------
graph : DGLGraph
The graph over which edge softmax will be performed.
logits : torch.Tensor or dict of torch.Tensor
The input edge feature. Heterogeneous graphs can have dict of tensors where
each tensor stores the edge features of the corresponding relation type.
eids : torch.Tensor or ALL, optional
The IDs of the edges to apply edge softmax. If ALL, it will apply edge
softmax to all edges in the graph. Default: ALL.
norm_by : str, could be `src` or `dst`
Normalized by source nodes or destination nodes. Default: `dst`.
Returns
-------
Tensor or tuple of tensors
Softmax value.
Notes
-----
* Input shape: :math:`(E, *, 1)` where * means any number of
additional dimensions, :math:`E` equals the length of eids.
If the `eids` is ALL, :math:`E` equals the number of edges in
the graph.
* Return shape: :math:`(E, *, 1)`
Examples on a homogeneous graph
-------------------------------
The following example uses PyTorch backend.
>>> from dgl.nn.functional import edge_softmax
>>> import dgl
>>> import torch as th
Create a :code:`DGLGraph` object and initialize its edge features.
>>> g = dgl.graph((th.tensor([0, 0, 0, 1, 1, 2]), th.tensor([0, 1, 2, 1, 2, 2])))
>>> edata = th.ones(6, 1).float()
>>> edata
tensor([[1.],
[1.],
[1.],
[1.],
[1.],
[1.]])
Apply edge softmax over g:
>>> edge_softmax(g, edata)
tensor([[1.0000],
[0.5000],
[0.3333],
[0.5000],
[0.3333],
[0.3333]])
Apply edge softmax over g normalized by source nodes:
>>> edge_softmax(g, edata, norm_by='src')
tensor([[0.3333],
[0.3333],
[0.3333],
[0.5000],
[0.5000],
[1.0000]])
Apply edge softmax to first 4 edges of g:
>>> edge_softmax(g, edata[:4], th.Tensor([0,1,2,3]))
tensor([[1.0000],
[0.5000],
[1.0000],
[0.5000]])
Examples on a heterogeneous graph
---------------------------------
Create a heterogeneous graph and initialize its edge features.
>>> hg = dgl.heterograph({
... ('user', 'follows', 'user'): ([0, 0, 1], [0, 1, 2]),
... ('developer', 'develops', 'game'): ([0, 1], [0, 1])
... })
>>> edata_follows = th.ones(3, 1).float()
>>> edata_develops = th.ones(2, 1).float()
>>> edata_dict = {('user', 'follows', 'user'): edata_follows,
... ('developer','develops', 'game'): edata_develops}
Apply edge softmax over hg normalized by source nodes:
>>> edge_softmax(hg, edata_dict, norm_by='src')
{('developer', 'develops', 'game'): tensor([[1.],
[1.]]), ('user', 'follows', 'user'): tensor([[0.5000],
[0.5000],
[1.0000]])}
"""
if not is_all(eids):
eids = astype(eids, graph.idtype)
if graph._graph.number_of_etypes() == 1:
return edge_softmax_internal(
graph._graph, logits, eids=eids, norm_by=norm_by
)
else:
logits_list = [None] * graph._graph.number_of_etypes()
logits = {graph.to_canonical_etype(k): v for k, v in logits.items()}
for rel in graph.canonical_etypes:
etid = graph.get_etype_id(rel)
logits_list[etid] = logits[rel]
logits_tuple = tuple(logits_list)
score_tuple = edge_softmax_hetero_internal(
graph._graph, eids, norm_by, *logits_tuple
)
score = {}
for rel in graph.canonical_etypes:
etid = graph.get_etype_id(rel)
score[rel] = score_tuple[etid]
return score
+50
View File
@@ -0,0 +1,50 @@
"""dgl gather_mm operator module."""
from .. import backend as F
__all__ = ["gather_mm"]
def gather_mm(a, b, *, idx_b):
r"""Gather data according to the given indices and perform matrix multiplication.
Let the result tensor be ``c``, the operator conducts the following computation:
c[i] = a[i] @ b[idx_b[i]]
, where len(c) == len(idx_b)
Parameters
----------
a : Tensor
A 2-D tensor of shape ``(N, D1)``
b : Tensor
A 3-D tensor of shape ``(R, D1, D2)``
idx_b : Tensor, optional
An 1-D integer tensor of shape ``(N,)``.
Returns
-------
Tensor
The output dense matrix of shape ``(N, D2)``
"""
N, D1 = F.shape(a)
R, _, D2 = F.shape(b)
if N > 1000000 or D1 > 8 or D2 > 8:
# Use segment_mm for large workload
import torch
sorted_idx_b, perm = torch.sort(idx_b)
_, rev_perm = torch.sort(perm)
sorted_a = torch.index_select(a, 0, perm)
pos_l = torch.searchsorted(
sorted_idx_b, torch.arange(R, device=a.device)
)
pos_r = torch.cat(
[pos_l[1:], torch.tensor([len(idx_b)], device=a.device)]
)
seglen = (pos_r - pos_l).cpu() # XXX(minjie): cause device synchronize
return torch.index_select(
F.segment_mm(sorted_a, b, seglen), 0, rev_perm
)
else:
return F.gather_mm(a, b, None, idx_b)
+207
View File
@@ -0,0 +1,207 @@
"""dgl sddmm operator module."""
import sys
from itertools import product
from .. import backend as F
from ..backend import (
gsddmm as gsddmm_internal,
gsddmm_hetero as gsddmm_internal_hetero,
)
__all__ = ["gsddmm", "copy_u", "copy_v", "copy_e"]
def reshape_lhs_rhs(lhs_data, rhs_data):
r"""Expand dims so that there will be no broadcasting issues with different
number of dimensions. For example, given two shapes (N, 3, 1), (E, 5, 3, 4)
that are valid broadcastable shapes, change them to (N, 1, 3, 1) and
(E, 5, 3, 4)
Parameters
----------
lhs_data : tensor or None
The left operand, could be None if it's not required by op.
rhs_data : tensor or None
The right operand, could be None if it's not required by op.
"""
lhs_shape = F.shape(lhs_data)
rhs_shape = F.shape(rhs_data)
if len(lhs_shape) != len(rhs_shape):
max_ndims = max(len(lhs_shape), len(rhs_shape))
lhs_pad_ndims = max_ndims - len(lhs_shape)
rhs_pad_ndims = max_ndims - len(rhs_shape)
new_lhs_shape = (lhs_shape[0],) + (1,) * lhs_pad_ndims + lhs_shape[1:]
new_rhs_shape = (rhs_shape[0],) + (1,) * rhs_pad_ndims + rhs_shape[1:]
lhs_data = F.reshape(lhs_data, new_lhs_shape)
rhs_data = F.reshape(rhs_data, new_rhs_shape)
return lhs_data, rhs_data
def gsddmm(g, op, lhs_data, rhs_data, lhs_target="u", rhs_target="v"):
r"""Generalized Sampled-Dense-Dense Matrix Multiplication interface.
It computes edge features by :attr:`op` lhs features and rhs features.
.. math::
x_{e} = \phi(x_{lhs}, x_{rhs}), \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`. :math:`lhs` and :math:`rhs` are one of
:math:`u,v,e`'s.
Parameters
----------
g : DGLGraph
The input graph.
op : str
Binary operator, could be ``add``, ``sub``, ``mul``, ``div``, ``dot``,
``copy_lhs``, ``copy_rhs``.
lhs_data : tensor or None
The left operand, could be None if it's not required by op.
rhs_data : tensor or None
The right operand, could be None if it's not required by op.
lhs_target: str
Choice of ``u``(source), ``e``(edge) or ``v``(destination) for left operand.
rhs_target: str
Choice of ``u``(source), ``e``(edge) or ``v``(destination) for right operand.
Returns
-------
tensor
The result tensor.
"""
if g._graph.number_of_etypes() == 1:
if op not in ["copy_lhs", "copy_rhs"]:
lhs_data, rhs_data = reshape_lhs_rhs(lhs_data, rhs_data)
return gsddmm_internal(
g._graph, op, lhs_data, rhs_data, lhs_target, rhs_target
)
else:
if op == "copy_lhs":
rhs_data = [None] * g._graph.number_of_etypes()
elif op == "copy_rhs":
lhs_data = [None] * g._graph.number_of_ntypes()
# TODO (Israt): Call reshape_lhs_rhs() on lhs and rhs data to match their dimension
# and avoid broadcasting issue. Handle the case where different nodes have
# different dimensions, and different etypes may need different broadcasting
# dims for the same node.
lhs_and_rhs_tuple = tuple(list(lhs_data) + list(rhs_data))
return gsddmm_internal_hetero(
g._graph,
op,
len(lhs_data),
lhs_target,
rhs_target,
*lhs_and_rhs_tuple
)
def _gen_sddmm_func(lhs_target, rhs_target, binary_op):
name = "{}_{}_{}".format(lhs_target, binary_op, rhs_target)
target_dict = {"u": "source node", "e": "edge", "v": "destination node"}
lhs_str = target_dict[lhs_target]
rhs_str = target_dict[rhs_target]
docstring = r"""Generalized SDDMM function.
It computes edge features by {op} {lhs} features and {rhs} features.
Parameters
----------
g : DGLGraph
The input graph
x : tensor
The {lhs} features.
y : tensor
The {rhs} features.
Returns
-------
tensor
The result tensor.
Notes
-----
This function supports autograd (computing input gradients given the output gradient). If the
feature shape of two input operands do not match, we first broadcasts the features to a unified
shape (note that the memory usage will not increase accordingly) and then performs the operation.
Broadcasting follows NumPy semantics. Please see
https://docs.scipy.org/doc/numpy/user/basics.broadcasting.html
for more details about the NumPy broadcasting semantics.
""".format(
op=binary_op, lhs=lhs_str, rhs=rhs_str
)
def func(g, x, y):
return gsddmm(
g, binary_op, x, y, lhs_target=lhs_target, rhs_target=rhs_target
)
func.__name__ = name
func.__doc__ = docstring
return func
def _register_sddmm_func():
"""Register sddmm functions"""
target = ["u", "v", "e"]
for lhs, rhs in product(target, target):
if lhs != rhs:
for binary_op in ["add", "sub", "mul", "div", "dot"]:
func = _gen_sddmm_func(lhs, rhs, binary_op)
setattr(sys.modules[__name__], func.__name__, func)
__all__.append(func.__name__)
def copy_u(g, x):
r"""Generalized SDDMM function that copies source node features to edges.
Parameters
----------
g : DGLGraph
The input graph.
x : tensor
The source node features.
Returns
-------
tensor
The result tensor.
Notes
-----
This function supports autograd (computing input gradients given the output gradient).
"""
return gsddmm(g, "copy_lhs", x, None)
def copy_v(g, x):
r"""Generalized SDDMM function that copies destination node features to edges.
Parameters
----------
g : DGLGraph
The input graph.
x : tensor
The destination node features.
Returns
-------
tensor
The result tensor.
Notes
-----
This function supports autograd (computing input gradients given the output gradient).
"""
return gsddmm(g, "copy_rhs", None, x)
# pylint: disable=unused-argument
def copy_e(g, x):
r"""Generalized SDDMM function that copies destination node features to edges."""
return x
_register_sddmm_func()
+130
View File
@@ -0,0 +1,130 @@
"""Segment aggregation operators implemented using DGL graph."""
from .. import backend as F
from ..base import DGLError
__all__ = ["segment_reduce", "segment_softmax", "segment_mm"]
def segment_reduce(seglen, value, reducer="sum"):
"""Segment reduction operator.
It aggregates the value tensor along the first dimension by segments.
The first argument ``seglen`` stores the length of each segment. Its
summation must be equal to the first dimension of the ``value`` tensor.
Zero-length segments are allowed.
Parameters
----------
seglen : Tensor
Segment lengths.
value : Tensor
Value to aggregate.
reducer : str, optional
Aggregation method. Can be 'sum', 'max', 'min', 'mean'.
Returns
-------
Tensor
Aggregated tensor of shape ``(len(seglen), value.shape[1:])``.
Examples
--------
>>> import dgl
>>> import torch as th
>>> val = th.ones(10, 3)
>>> seg = th.tensor([1, 0, 5, 4]) # 4 segments
>>> dgl.segment_reduce(seg, val)
tensor([[1., 1., 1.],
[0., 0., 0.],
[5., 5., 5.],
[4., 4., 4.]])
"""
offsets = F.cumsum(
F.cat([F.zeros((1,), F.dtype(seglen), F.context(seglen)), seglen], 0), 0
)
if reducer == "mean":
rst = F.segment_reduce("sum", value, offsets)
rst_shape = F.shape(rst)
z = F.astype(F.clamp(seglen, 1, len(value)), F.dtype(rst))
z_shape = (rst_shape[0],) + (1,) * (len(rst_shape) - 1)
return rst / F.reshape(z, z_shape)
elif reducer in ["min", "sum", "max"]:
rst = F.segment_reduce(reducer, value, offsets)
if reducer in ["min", "max"]:
rst = F.replace_inf_with_zero(rst)
return rst
else:
raise DGLError("reducer {} not recognized.".format(reducer))
def segment_softmax(seglen, value):
"""Performa softmax on each segment.
The first argument ``seglen`` stores the length of each segment. Its
summation must be equal to the first dimension of the ``value`` tensor.
Zero-length segments are allowed.
Parameters
----------
seglen : Tensor
Segment lengths.
value : Tensor
Value to aggregate.
Returns
-------
Tensor
Result tensor of the same shape as the ``value`` tensor.
Examples
--------
>>> import dgl
>>> import torch as th
>>> val = th.ones(10, 3)
>>> seg = th.tensor([1, 0, 5, 4]) # 4 segments
>>> dgl.segment_softmax(seg, val)
tensor([[1.0000, 1.0000, 1.0000],
[0.2000, 0.2000, 0.2000],
[0.2000, 0.2000, 0.2000],
[0.2000, 0.2000, 0.2000],
[0.2000, 0.2000, 0.2000],
[0.2000, 0.2000, 0.2000],
[0.2500, 0.2500, 0.2500],
[0.2500, 0.2500, 0.2500],
[0.2500, 0.2500, 0.2500],
[0.2500, 0.2500, 0.2500]])
"""
value_max = segment_reduce(seglen, value, reducer="max")
value = F.exp(value - F.repeat(value_max, seglen, dim=0))
value_sum = segment_reduce(seglen, value, reducer="sum")
return value / F.repeat(value_sum, seglen, dim=0)
def segment_mm(a, b, seglen_a):
r"""Performs matrix multiplication according to segments.
Suppose ``seglen_a == [10, 5, 0, 3]``, the operator will perform
four matrix multiplications::
a[0:10] @ b[0], a[10:15] @ b[1],
a[15:15] @ b[2], a[15:18] @ b[3]
Parameters
----------
a : Tensor
The left operand, 2-D tensor of shape ``(N, D1)``
b : Tensor
The right operand, 3-D tensor of shape ``(R, D1, D2)``
seglen_a : Tensor
An integer tensor of shape ``(R,)``. Each element is the length of segments
of input ``a``. The summation of all elements must be equal to ``N``.
Returns
-------
Tensor
The output dense matrix of shape ``(N, D2)``
"""
return F.segment_mm(a, b, seglen_a)
+241
View File
@@ -0,0 +1,241 @@
"""Internal module for general spmm operators."""
import sys
from .. import backend as F
from ..backend import (
gspmm as gspmm_internal,
gspmm_hetero as gspmm_internal_hetero,
)
__all__ = ["gspmm"]
def reshape_lhs_rhs(lhs_data, rhs_data):
r"""Expand dims so that there will be no broadcasting issues with different
number of dimensions. For example, given two shapes (N, 3, 1), (E, 5, 3, 4)
that are valid broadcastable shapes, change them to (N, 1, 3, 1) and
(E, 5, 3, 4)
Parameters
----------
lhs_data : tensor or None
The left operand, could be None if it's not required by op.
rhs_data : tensor or None
The right operand, could be None if it's not required by op.
"""
lhs_shape = F.shape(lhs_data)
rhs_shape = F.shape(rhs_data)
if len(lhs_shape) != len(rhs_shape):
max_ndims = max(len(lhs_shape), len(rhs_shape))
lhs_pad_ndims = max_ndims - len(lhs_shape)
rhs_pad_ndims = max_ndims - len(rhs_shape)
new_lhs_shape = (lhs_shape[0],) + (1,) * lhs_pad_ndims + lhs_shape[1:]
new_rhs_shape = (rhs_shape[0],) + (1,) * rhs_pad_ndims + rhs_shape[1:]
lhs_data = F.reshape(lhs_data, new_lhs_shape)
rhs_data = F.reshape(rhs_data, new_rhs_shape)
return lhs_data, rhs_data
def gspmm(g, op, reduce_op, lhs_data, rhs_data):
r"""Generalized Sparse Matrix Multiplication interface.
It fuses two steps into one kernel.
1. Computes messages by :attr:`op` source node and edge features.
2. Aggregate the messages by :attr:`reduce_op` as the features 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
----------
g : DGLGraph
The input graph.
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``, ``mean``.
lhs_data : tensor or None
The left operand, could be None if it's not required by the op.
rhs_data : tensor or None
The right operand, could be None if it's not required by the op.
Returns
-------
tensor
The result tensor.
"""
if g._graph.number_of_etypes() == 1:
if op not in ["copy_lhs", "copy_rhs"]:
lhs_data, rhs_data = reshape_lhs_rhs(lhs_data, rhs_data)
# With max and min reducers infinity will be returned for zero degree nodes
ret = gspmm_internal(
g._graph,
op,
"sum" if reduce_op == "mean" else reduce_op,
lhs_data,
rhs_data,
)
else:
# lhs_data or rhs_data is None only in unary functions like ``copy-u`` or ``copy_e``
lhs_data = (
[None] * g._graph.number_of_ntypes()
if lhs_data is None
else lhs_data
)
rhs_data = (
[None] * g._graph.number_of_etypes()
if rhs_data is None
else rhs_data
)
# TODO (Israt): Call reshape func
lhs_and_rhs_tuple = tuple(list(lhs_data) + list(rhs_data))
ret = gspmm_internal_hetero(
g._graph,
op,
"sum" if reduce_op == "mean" else reduce_op,
len(lhs_data),
*lhs_and_rhs_tuple
)
# TODO (Israt): Add support for 'mean' in heterograph
# divide in degrees for mean reducer.
if reduce_op == "mean":
ret_shape = F.shape(ret)
deg = g.in_degrees()
deg = F.astype(F.clamp(deg, 1, max(g.num_edges(), 1)), F.dtype(ret))
deg_shape = (ret_shape[0],) + (1,) * (len(ret_shape) - 1)
return ret / F.reshape(deg, deg_shape)
else:
return ret
def _attach_zerodeg_note(docstring, reducer):
note1 = """
The {} function will return zero for nodes with no incoming messages.""".format(
reducer
)
note2 = """
This is implemented by replacing all {} values to zero.
""".format(
"infinity" if reducer == "min" else "negative infinity"
)
docstring = docstring + note1
if reducer in ("min", "max"):
docstring = docstring + note2
return docstring
def _gen_spmm_func(binary_op, reduce_op):
name = "u_{}_e_{}".format(binary_op, reduce_op)
docstring = """Generalized SpMM function.
It fuses two steps into one kernel.
1. Computes messages by {} source node and edge features.
2. Aggregate the messages by {} as the features on destination nodes.
Parameters
----------
g : DGLGraph
The input graph
x : tensor
The source node features.
y : tensor
The edge features.
Returns
-------
tensor
The result tensor.
Notes
-----
This function supports autograd (computing input gradients given the output gradient). If the
feature shape of two input operands do not match, we first broadcasts the features to a unified
shape (note that the memory usage will not increase accordingly) and then performs the operation.
Broadcasting follows NumPy semantics. Please see
https://docs.scipy.org/doc/numpy/user/basics.broadcasting.html
for more details about the NumPy broadcasting semantics.
""".format(
binary_op, reduce_op
)
docstring = _attach_zerodeg_note(docstring, reduce_op)
def func(g, x, y):
return gspmm(g, binary_op, reduce_op, x, y)
func.__name__ = name
func.__doc__ = docstring
return func
def _gen_copy_reduce_func(binary_op, reduce_op):
name = "{}_{}".format(binary_op, reduce_op)
binary_str = {
"copy_u": "It copies node feature to edge as the message.",
"copy_e": "It regards edge feature as message.",
}
x_str = {"copy_u": "source node", "copy_e": "edge"}
docstring = lambda binary_op: _attach_zerodeg_note(
"""Generalized SpMM function. {}
Then aggregates the message by {} on destination nodes.
Parameters
----------
g : DGLGraph
The input graph
x : tensor
The {} features.
Returns
-------
tensor
The result tensor.
Notes
-----
This function supports autograd (computing input gradients given the output gradient).
""".format(
binary_str[binary_op], reduce_op, x_str[binary_op]
),
reduce_op,
)
def func(g, x):
if binary_op == "copy_u":
return gspmm(g, "copy_lhs", reduce_op, x, None)
else:
return gspmm(g, "copy_rhs", reduce_op, None, x)
func.__name__ = name
func.__doc__ = docstring(binary_op)
return func
def _register_spmm_func():
"""Register spmm functions
- Binary operation plus reduction between u and e: u_[]_e_[]
- Copy u plus reduction: copy_u_[]
- Copy e plus reduction: copy_e_[]
"""
for binary_op in ["add", "sub", "mul", "div", "copy_u", "copy_e"]:
for reduce_op in ["sum", "max", "min", "mean"]:
if binary_op.startswith("copy"):
func = _gen_copy_reduce_func(binary_op, reduce_op)
else:
func = _gen_spmm_func(binary_op, reduce_op)
setattr(sys.modules[__name__], func.__name__, func)
__all__.append(func.__name__)
_register_spmm_func()