Skip to content

Commit

Permalink
[TOPI] Sparse Add Op added (apache#7435)
Browse files Browse the repository at this point in the history
* [TOPI] Sparse Add Op added

* lint resolved

* TF frontend support added

* Test case added

* [1] Review comment handled

* [2] Review comment handled

* [3] Review comment handled

* [4] Review comment handled

* [5] Review comment handled
  • Loading branch information
ANSHUMAN TRIPATHY authored and Trevor Morris committed May 6, 2021
1 parent 6400d0c commit 88c2a22
Show file tree
Hide file tree
Showing 8 changed files with 296 additions and 0 deletions.
35 changes: 35 additions & 0 deletions python/tvm/relay/frontend/tensorflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,40 @@ def _impl(inputs, attr, params, mod):
return _impl


def _sparse_tensor_dense_add():
# Sparse utility from scipy
from scipy.sparse import csr_matrix

def _impl(inputs, attr, params, mod):
assert (
len(inputs) == 4
), "There should be 4 input tensors [sparse_indices, sparse_values, sparse_shape, dense]."

indices_tensor = _infer_value(inputs[0], params, mod).asnumpy()
values_tensor = _infer_value(inputs[1], params, mod).asnumpy()
dense_shape_tensor = _infer_value(inputs[2], params, mod).asnumpy()

data = inputs[3]

rows = [x[0] for x in indices_tensor]
cols = [x[1] for x in indices_tensor]

# Create scipy sparse Tensor(CSR)
weight_sp = csr_matrix(
(values_tensor, (rows, cols)), shape=tuple(dense_shape_tensor.tolist())
)

weight_data = _expr.const(weight_sp.data, weight_sp.data.dtype)
weight_indptrs = _expr.const(weight_sp.indptr, weight_sp.indptr.dtype)
weight_indices = _expr.const(weight_sp.indices, weight_sp.indices.dtype)

ret = _op.nn.sparse_add(data, [weight_data, weight_indices, weight_indptrs])

return ret

return _impl


def _identity():
def _impl(inputs, attr, params, mod):
return inputs[0]
Expand Down Expand Up @@ -2787,6 +2821,7 @@ def _impl(inputs, attr, params, mod):
"SparseSegmentSqrtNWithNumSegments": _sparse_segment_sum_sqrtn_with_num_segments(),
"SparseSegmentMean": _sparse_segment_mean(),
"SparseSegmentMeanWithNumSegments": _sparse_segment_mean_with_num_segments(),
"SparseTensorDenseAdd": _sparse_tensor_dense_add(),
"Split": _split(False),
"SplitV": _split(True),
"Sqrt": AttrCvt("sqrt"),
Expand Down
5 changes: 5 additions & 0 deletions python/tvm/relay/op/nn/_nn.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ def alter_op_layout_sparse_dense(attrs, inputs, tinfos, out_type):
return topi.nn.sparse_dense_alter_layout(attrs, inputs, tinfos, out_type)


# sparse_add
reg.register_strategy("nn.sparse_add", strategy.sparse_add_strategy)
reg.register_pattern("nn.sparse_add", reg.OpPattern.OPAQUE)


@reg.register_compute("nn.internal.sparse_dense_padded")
def compute_sparse_dense_padded(attrs, inputs, out_type):
"""Compute definition of sparse_dense_padded"""
Expand Down
47 changes: 47 additions & 0 deletions python/tvm/relay/op/nn/nn.py
Original file line number Diff line number Diff line change
Expand Up @@ -2148,6 +2148,53 @@ def sparse_transpose(x):
return expr.TupleWrapper(_make.sparse_transpose(x[0], x[1], x[2]), 3)


# pylint: disable=no-else-return,inconsistent-return-statements
def sparse_add(dense_mat, sparse_mat):
r"""
Computes the matrix addition of `dense_mat` and `sparse_mat`, where `dense_mat` is
a dense matrix and `sparse_mat` is a sparse (CSR) namedtuple with
fields `data`, `indices`, and `indptr`.
.. math::
\mbox{sparse_add}(dense_mat, sparse_mat)[m, n] = \mbox{add}(\mbox{as_dense}(S), (D))[m, n]
where `as_dense` returns dense equivalent of the given S(sparse matrix)
while performing addition with given D(dense matrix).
Parameters
----------
dense_mat : tvm.relay.Expr
The input dense matrix for the matrix addition
sparse_mat : Union[namedtuple, Tuple[ndarray, ndarray, ndarray]].
The input sparse matrix(CSR) for the matrix addition.
Returns
-------
result: tvm.relay.Expr
The computed result.
Examples
-------
.. code-block:: python
dense_data = [[ 3., 4., 4. ]
[ 4., 2., 5. ]]
sparse_data = [4., 8.]
sparse_indices =[0, 2]
sparse_indptr =[0, 1, 2]
output = relay.sparse_add(dense_data, sparse_data, sparse_indices, sparse_indptr)
output = [[ 7., 4., 4. ]
[ 4., 2., 13. ]]
"""
if hasattr(sparse_mat, "indices"):
return _make.sparse_add(dense_mat, sparse_mat.data, sparse_mat.indices, sparse_mat.indptr)
else:
return _make.sparse_add(dense_mat, sparse_mat[0], sparse_mat[1], sparse_mat[2])


def contrib_conv2d_winograd_without_weight_transform(
data,
weight,
Expand Down
23 changes: 23 additions & 0 deletions python/tvm/relay/op/strategy/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,29 @@ def sparse_dense_padded_strategy(attrs, inputs, out_type, target):
raise NotImplementedError("sparse_dense_padded is only implemented for cuda")


# sparse_add
def wrap_compute_sparse_add(topi_compute):
"""wrap sparse add topi compute"""

def _compute_sparse_add(attrs, inputs, out_type):
return [topi_compute(inputs[0], inputs[1], inputs[2], inputs[3])]

return _compute_sparse_add


@override_native_generic_func("sparse_add_strategy")
def sparse_add_strategy(attrs, inputs, out_type, target):
"""sparse add generic strategy"""
logger.warning("sparse add is not optimized for this platform.")
strategy = _op.OpStrategy()
strategy.add_implementation(
wrap_compute_sparse_add(topi.nn.sparse_add),
wrap_topi_schedule(topi.generic.schedule_extern),
name="sparse_add.generic",
)
return strategy


# sparse_transpose
@generic_func
def schedule_sparse_transpose(attrs, outs, target):
Expand Down
69 changes: 69 additions & 0 deletions python/tvm/topi/nn/sparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,3 +468,72 @@ def _traverse(t):
sparse_input_map[sparse_indptr] = sparse_prefix + "W_indptr"

return sparse_input_map


def sparse_add(dense_data, sparse_data, sparse_indices, sparse_indptr):
"""
Computes sparse-dense addition
Parameters
----------
dense_data : tvm.te.Tensor
2-D with shape [M, N]
sparse_data : tvm.te.Tensor
1-D with shape [nnz] (CSR)
sparse_indices : tvm.te.Tensor
1-D with shape [nnz] (CSR)
sparse_indptr : tvm.te.Tensor
1-D with shape [M + 1] (CSR)
Returns
-------
output : tvm.te.Tensor
2-D with shape [M, N]
"""
# TODO(ANSHUMAN87): support BSR format too
assert len(sparse_data.shape) == 1, "only CSR format is supported"
return _sparse_add_csr(dense_data, sparse_data, sparse_indices, sparse_indptr)


def _sparse_add_csr(dense_data_inp, sparse_data_inp, sparse_indices_inp, sparse_indptr_inp):
oshape = get_const_tuple(dense_data_inp.shape)

def _csr_add_ir(dense_data, sparse_data, sparse_indices, sparse_indptr, out_data):
irb = tvm.tir.ir_builder.create()
dense_data_ptr = irb.buffer_ptr(dense_data)
sparse_data_ptr = irb.buffer_ptr(sparse_data)
sparse_indices_ptr = irb.buffer_ptr(sparse_indices)
sparse_indptr_ptr = irb.buffer_ptr(sparse_indptr)

out_data_ptr = irb.buffer_ptr(out_data)

with irb.for_range(0, oshape[0], kind="vectorize", name="row") as row:
with irb.for_range(0, oshape[1], kind="parallel", name="col") as col:
out_data_ptr[row, col] = dense_data_ptr[row, col]

with irb.for_range(0, oshape[0], kind="parallel", name="row") as row:
offset = sparse_indptr_ptr[row]
diff = sparse_indptr_ptr[row + 1] - sparse_indptr_ptr[row]
with irb.for_range(0, diff, kind="serial", name="idx") as idx:
real_idx = offset + idx
col = sparse_indices_ptr[real_idx]
out_data_ptr[row, col] = sparse_data_ptr[real_idx] + out_data_ptr[row, col]

return irb.get()

return te.extern(
shape=oshape,
inputs=[dense_data_inp, sparse_data_inp, sparse_indices_inp, sparse_indptr_inp],
fcompute=lambda ins, outs: _csr_add_ir(ins[0], ins[1], ins[2], ins[3], outs[0]),
tag="sparse_add_csr",
dtype=[
dense_data_inp.dtype,
sparse_data_inp.dtype,
sparse_indices_inp.dtype,
sparse_indptr_inp.dtype,
],
name="sparse_add_csr_output",
)
41 changes: 41 additions & 0 deletions src/relay/op/nn/sparse.cc
Original file line number Diff line number Diff line change
Expand Up @@ -196,5 +196,46 @@ RELAY_REGISTER_OP("nn.sparse_transpose")
.set_support_level(1)
.add_type_rel("SparseTranspose", SparseTransposeRel);

// relay.nn.sparse_add
bool SparseAddRel(const Array<Type>& types, int num_inputs, const Attrs& attrs,
const TypeReporter& reporter) {
ICHECK_EQ(types.size(), 5) << "expecting 4 inputs and 1 output.";
const auto* dense_data = types[0].as<TensorTypeNode>();
const auto* sparse_data = types[1].as<TensorTypeNode>();
ICHECK(reporter->Assert(sparse_data->dtype == dense_data->dtype))
<< "sparse tensor and dense tensor datatype should match.";
ICHECK(reporter->Assert(sparse_data->shape.size() == 1)) << "sparse data tensor should be 1D.";
const auto* sparse_indices = types[2].as<TensorTypeNode>();
ICHECK(reporter->Assert(sparse_indices->shape.size() == 1))
<< "sparse indices tensor should be 1D.";

reporter->Assign(types[4], TensorType(dense_data->shape, dense_data->dtype));
return true;
}

Expr MakeSparseAdd(Expr dense_data, Expr sparse_data, Expr sparse_indices, Expr sparse_indptr) {
static const Op& op = Op::Get("nn.sparse_add");
return Call(op, {dense_data, sparse_data, sparse_indices, sparse_indptr}, Attrs(), {});
}

TVM_REGISTER_GLOBAL("relay.op.nn._make.sparse_add").set_body_typed(MakeSparseAdd);

RELAY_REGISTER_OP("nn.sparse_add")
.describe(R"code(Add a dense matrix X with sparse matrix Y.
- **dense**: `(M, N)`
- **sparse**: `(M, N)`
- **out**: `(M, N)`.
)code" TVM_ADD_FILELINE)
.set_num_inputs(4)
.add_argument("dense_data", "2D Tensor", "Dense data matrix.")
.add_argument("sparse_data", "1D Tensor", "Sparse data vector.")
.add_argument("sparse_indices", "1D Tensor", "Sparse indices vector.")
.add_argument("sparse_indptr", "1D Tensor", "Sparse index pointer vector.")
.set_support_level(1)
.add_type_rel("SparseAdd", SparseAddRel);

} // namespace relay
} // namespace tvm
48 changes: 48 additions & 0 deletions tests/python/frontend/tensorflow/test_forward.py
Original file line number Diff line number Diff line change
Expand Up @@ -2367,6 +2367,54 @@ def test_forward_sparse_to_dense_v2():
_test_sparse_to_dense_v2([[0, 0], [1, 3], [4, 3]], [3.0, 6.0, 9.0], [5, 5], "float32", 1.9)


#######################################################################
# tensorflow.sparse.add
# ----------------------------------


def _test_sparse_add(indices, values, A_shape, B_shape, dtype, flip=False):
""" One iteration of tf.sparse.add """

# TODO(ANSHUMAN87): support cuda
# TODO(ANSHUMAN87): support both sparse input case

with tf.Graph().as_default():
A_sp = tf.sparse.SparseTensor(
indices=indices, values=np.array(values).astype(dtype), dense_shape=A_shape
)
B = tf.placeholder(shape=B_shape, dtype=dtype, name="B")

# TODO(ANSHUMAN87): support user input threashold values
if flip:
result = tf.sparse.add(B, A_sp, threshold=0)
else:
result = tf.sparse.add(A_sp, B, threshold=0)

B_np = np.random.uniform(high=5.0, size=B_shape).astype(dtype)

compare_tf_with_tvm([B_np], [B.name], result.name, no_gpu=True)


def test_sparse_add():
""" sparse.add op test"""
###################################################################
#
# In order to create a SparseTensor, it requires 3 input as below:
# SparseTensor(indices=[[0, 0], [1, 2]], values=[1, 2], dense_shape=[3, 4])
#
# Above Sparse can be represented in Dense as below :
# [[1, 0, 0, 0]
# [0, 0, 2, 0]
# [0, 0, 0, 0]]
#
# ------------------------------------------------------------------
for dtype_inp in ["float32", "float64", "int32"]:
_test_sparse_add([[0, 0], [1, 2]], [4.0, 8.0], [3, 4], [3, 4], dtype_inp)
_test_sparse_add([[0, 0], [1, 2]], [4.0, 8.0], [3, 4], [3, 4], dtype_inp, True)
_test_sparse_add([[0, 0], [1, 3], [4, 3]], [3.0, 6.0, 9.0], [5, 5], [5, 5], dtype_inp)
_test_sparse_add([[0, 0], [1, 3], [4, 3]], [3.0, 6.0, 9.0], [5, 5], [5, 5], dtype_inp, True)


#######################################################################
# StridedSlice
# ------------
Expand Down
28 changes: 28 additions & 0 deletions tests/python/topi/python/test_topi_sparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,33 @@ def test_sparse_dense_padded_alter_op():
x = relay.build(tvm.IRModule.from_expr(f), target=tvm.target.Target("cuda"))


def test_sparse_add_csr():
for indices_dtype in ["int32", "int64"]:
for data_dtype in ["float32", "float64"]:
M, K, density = 3, 49, 0.2
X_np = np.random.randn(M, K).astype(data_dtype)
Y_sp_np = sp.random(M, K, density=density, format="csr", dtype=data_dtype)
Y_np = Y_sp_np.todense()
Z_np = X_np + Y_np

Y_data = te.placeholder(shape=Y_sp_np.data.shape, dtype=data_dtype)
Y_indices = te.placeholder(shape=Y_sp_np.indices.shape, dtype=indices_dtype)
Y_indptr = te.placeholder(shape=Y_sp_np.indptr.shape, dtype=indices_dtype)
X = te.placeholder(shape=X_np.shape, dtype=data_dtype)
Z = topi.nn.sparse_add(X, Y_data, Y_indices, Y_indptr)
s = te.create_schedule(Z.op)
func = tvm.build(s, [X, Y_data, Y_indices, Y_indptr, Z])
Z_tvm = tvm.nd.array(np.zeros(Z_np.shape, dtype=Z_np.dtype))
func(
tvm.nd.array(X_np.astype(data_dtype)),
tvm.nd.array(Y_sp_np.data.astype(data_dtype)),
tvm.nd.array(Y_sp_np.indices.astype(indices_dtype)),
tvm.nd.array(Y_sp_np.indptr.astype(indices_dtype)),
Z_tvm,
)
tvm.testing.assert_allclose(Z_tvm.asnumpy(), Z_np, atol=1e-4, rtol=1e-4)


if __name__ == "__main__":
test_csrmv()
test_csrmm()
Expand All @@ -537,3 +564,4 @@ def test_sparse_dense_padded_alter_op():
test_sparse_dense_padded_alter_op()
test_sparse_dense_csr_reverse()
test_sparse_dense_bsr_reverse()
test_sparse_add_csr()

0 comments on commit 88c2a22

Please sign in to comment.