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

161 lines
4.8 KiB
Python

import doctest
import operator
import sys
import backend as F
import dgl.sparse as dglsp
import pytest
import torch
dgl_op_map = {
"sum": "sum",
"amin": "smin",
"amax": "smax",
"mean": "smean",
"prod": "sprod",
}
default_entry = {
"sum": 0,
"amin": float("inf"),
"amax": float("-inf"),
"mean": 0,
"prod": 1,
}
binary_op_map = {
"sum": operator.add,
"amin": torch.min,
"amax": torch.max,
"mean": operator.add,
"prod": operator.mul,
}
NUM_ROWS = 10
NUM_COLS = 15
def _coalesce_dense(row, col, val, nrows, ncols, op):
# Sparse matrix coalescing on a dense matrix.
#
# It is done by stacking every non-zero entry on an individual slice
# of an (nrows x ncols x nnz), that is, construct a tensor A with
# shape (nrows, ncols, len(val)) where
#
# A[row[i], col[i], i] = val[i]
#
# and then reducing on the third "nnz" dimension.
#
# The mask matrix M has the same sparsity pattern as A with 1 being
# the non-zero entries. This is used for division if the reduce
# operator is mean.
M = torch.zeros(NUM_ROWS, NUM_COLS, device=F.ctx())
A = torch.full(
(NUM_ROWS, NUM_COLS, 20) + val.shape[1:],
default_entry[op],
device=F.ctx(),
dtype=val.dtype,
)
A = torch.index_put(A, (row, col, torch.arange(20)), val)
for i in range(20):
M[row[i], col[i]] += 1
if op == "mean":
A = A.sum(2)
else:
A = getattr(A, op)(2)
M = M.view(NUM_ROWS, NUM_COLS, *([1] * (val.dim() - 1)))
return A, M
# Add docstring tests of dglsp.reduction to unit tests
@pytest.mark.parametrize(
"func", ["reduce", "sum", "smin", "smax", "sprod", "smean"]
)
def test_docstring(func):
globs = {"torch": torch, "dglsp": dglsp}
runner = doctest.DebugRunner()
finder = doctest.DocTestFinder()
obj = getattr(dglsp, func)
for test in finder.find(obj, func, globs=globs):
runner.run(test)
@pytest.mark.parametrize("shape", [(20,), (20, 20)])
@pytest.mark.parametrize("op", ["sum", "amin", "amax", "mean", "prod"])
@pytest.mark.parametrize("use_reduce", [False, True])
def test_reduce_all(shape, op, use_reduce):
row = torch.randint(0, NUM_ROWS, (20,), device=F.ctx())
col = torch.randint(0, NUM_COLS, (20,), device=F.ctx())
val = torch.randn(*shape, device=F.ctx())
val2 = val.clone()
val = val.requires_grad_()
val2 = val2.requires_grad_()
A = dglsp.from_coo(row, col, val, shape=(NUM_ROWS, NUM_COLS))
A2, M = _coalesce_dense(row, col, val2, NUM_ROWS, NUM_COLS, op)
if not use_reduce:
output = getattr(A, dgl_op_map[op])()
else:
output = A.reduce(rtype=dgl_op_map[op])
if op == "mean":
output2 = A2.sum((0, 1)) / M.sum()
elif op == "prod":
output2 = A2.prod(0).prod(0) # prod() does not support tuple of dims
else:
output2 = getattr(A2, op)((0, 1))
assert (output - output2).abs().max() < 1e-4
head = torch.randn(*output.shape).to(val) if output.dim() > 0 else None
output.backward(head)
output2.backward(head)
assert (val.grad - val2.grad).abs().max() < 1e-4
@pytest.mark.parametrize("shape", [(20,), (20, 20)])
@pytest.mark.parametrize("dim", [0, 1])
@pytest.mark.parametrize("empty_nnz", [False, True])
@pytest.mark.parametrize("op", ["sum", "amin", "amax", "mean", "prod"])
@pytest.mark.parametrize("use_reduce", [False, True])
def test_reduce_along(shape, dim, empty_nnz, op, use_reduce):
row = torch.randint(0, NUM_ROWS, (20,), device=F.ctx())
col = torch.randint(0, NUM_COLS, (20,), device=F.ctx())
if dim == 0:
mask = torch.bincount(col, minlength=NUM_COLS) == 0
else:
mask = torch.bincount(row, minlength=NUM_ROWS) == 0
val = torch.randn(*shape, device=F.ctx())
val2 = val.clone()
val = val.requires_grad_()
val2 = val2.requires_grad_()
# empty_nnz controls whether at least one column or one row has no
# non-zero entry.
if empty_nnz:
row[row == 0] = 1
col[col == 0] = 1
A = dglsp.from_coo(row, col, val, shape=(NUM_ROWS, NUM_COLS))
A2, M = _coalesce_dense(row, col, val2, NUM_ROWS, NUM_COLS, op)
if not use_reduce:
output = getattr(A, dgl_op_map[op])(dim)
else:
output = A.reduce(dim=dim, rtype=dgl_op_map[op])
if op == "mean":
output2 = A2.sum(dim) / M.sum(dim)
else:
output2 = getattr(A2, op)(dim)
zero_entry_idx = (M.sum(dim) != 0).nonzero(as_tuple=True)[0]
output3 = torch.index_put(
torch.zeros_like(output2), (zero_entry_idx,), output2[zero_entry_idx]
)
assert (output - output3).abs().max() < 1e-4
head = torch.randn(*output.shape).to(val) if output.dim() > 0 else None
output.backward(head)
output3.backward(head)
assert (val.grad - val2.grad).abs().max() < 1e-4