Generator of tensor inputs with variable layout and structure (batch/non-batch, hybrid/non-hybrid, block/non-block) (#88914)

This PR introduces `TestCase.generate_simple_inputs` method that is an improved and generalized version of the `TestSparseCompressed._generate_small_inputs` method.

Pull Request resolved: https://github.com/pytorch/pytorch/pull/88914
Approved by: https://github.com/cpuhrsch
This commit is contained in:
Pearu Peterson 2022-11-30 00:35:45 +02:00 committed by PyTorch MergeBot
parent 275ade6371
commit 90bed8874f
11 changed files with 17816 additions and 30112 deletions

View file

@ -4049,6 +4049,52 @@ class TestSparseMeta(TestCase):
self.assertEqual(r.values(), torch.empty(0, 4, device='meta'))
class TestSparseAny(TestCase):
def test_generate_simple_inputs(self):
# Temporarily disable BSC and BSC layouts as these don't support select yet, see the next PR in the stack.
layouts = [torch.strided, torch.sparse_coo, torch.sparse_csr, torch.sparse_csc, torch.sparse_bsr, torch.sparse_bsc][:-2]
tested_combinations = set()
for tensors in zip(*map(self.generate_simple_inputs, layouts)):
for i, t in enumerate(tensors):
self.assertEqual(t.layout, layouts[i])
# all layouts must produce semantically the same tensors
self.assertEqual(t, tensors[0])
if t.layout is torch.strided:
is_hybrid = None
else:
is_hybrid = t.dense_dim() > 0
if t.layout in {torch.sparse_csr, torch.sparse_bsr}:
is_batch = t.crow_indices().ndim > 1
elif t.layout in {torch.sparse_csc, torch.sparse_bsc}:
is_batch = t.ccol_indices().ndim > 1
else:
is_batch = None
if t.layout in {torch.sparse_bsr, torch.sparse_bsc}:
blocksize = t.values().shape[1:3]
nontrivial_blocksize = 1 not in blocksize
else:
nontrivial_blocksize = None
tested_combinations.add((t.layout, is_hybrid, is_batch, nontrivial_blocksize))
# Ensure that the inputs generation covers all layout,
# non-hybrid/hybrid, and non-batch/batch combinations:
for layout in layouts:
for is_hybrid in [False, True]:
if layout is torch.strided:
is_hybrid = None
for is_batch in [False, True]:
if layout in {torch.sparse_coo, torch.strided}:
is_batch = None
for nontrivial_blocksize in [False, True]:
if layout not in {torch.sparse_bsr, torch.sparse_bsc}:
nontrivial_blocksize = None
key = (layout, is_hybrid, is_batch, nontrivial_blocksize)
assert key in tested_combinations, key
# e.g., TestSparseUnaryUfuncsCPU and TestSparseUnaryUfuncsCUDA
instantiate_device_type_tests(TestSparseUnaryUfuncs, globals(), except_for='meta')
@ -4058,5 +4104,7 @@ instantiate_device_type_tests(TestSparseMaskedReductions, globals(), except_for=
# e.g., TestSparseCPU and TestSparseCUDA
instantiate_device_type_tests(TestSparse, globals(), except_for='meta')
instantiate_device_type_tests(TestSparseAny, globals(), except_for='meta')
if __name__ == '__main__':
run_tests()

View file

@ -1,6 +1,5 @@
# Owner(s): ["module: sparse"]
import copy
import torch
import random
import itertools
@ -198,147 +197,6 @@ class TestSparseCompressed(TestCase):
device = self.device_type
return self.genSparseCompressedTensor(size, nnz, device=device, dtype=dtype, index_dtype=index_dtype, layout=layout)
def _generate_small_inputs_utils(self, layout, device=None, dtype=None):
def shape(shape, basedim=0, blocksize=(1, 1), dense_shape=()):
# Below, we define compressed and plain indices that
# correspond to row compressed tensors. In order to reuse
# the indices tensors for column compressed tensors, we
# swap the row and columns in shape dims (basedim and
# basedim + 1, respectively) to obtain the correct shape
# for column compressed tensors. Batch and dense
# dimensions remain as they are.
#
# Similarly, we reuse indices of non-block tensors for
# block tensors, that means, we'll need to multiply the
# base shape of the non-block tensor with blocksize to get
# the base shape of a block tensor.
if layout is torch.sparse_csc:
shape = shape[:basedim] + (shape[basedim + 1], shape[basedim]) + shape[basedim + 2:]
elif layout is torch.sparse_bsc:
shape = shape[:basedim] + (shape[basedim + 1] * blocksize[1], shape[basedim] * blocksize[0]) + shape[basedim + 2:]
elif layout is torch.sparse_bsr:
shape = shape[:basedim] + (shape[basedim] * blocksize[0], shape[basedim + 1] * blocksize[1]) + shape[basedim + 2:]
return shape
def values(lst, basedim=0, blocksize=(1, 1), densesize=(), device=device, dtype=dtype):
# Below, we define values for non-blocked and non-hybrid
# tensors. To reuse these for blocked tensors, we replace
# all values in lst with a double-list that "shape"
# corresponds to blocksize.
# To support hybrid tensors, the values in lst are further
# replaced with a N-list where N==len(densesize) and the
# shape corresponds to densesize.
max_val = torch.iinfo(dtype).max if dtype in [torch.int16, torch.int8, torch.uint8] else None
def list_add(lst, value):
# recursively add a value to lst items
if isinstance(lst, list):
return [list_add(item, value) for item in lst]
rc = lst + value
return rc if max_val is None else (rc % max_val)
def stretch_values(value, bdim, values_item_shape):
# replace a value with a new value that extends the
# dimensionality of the value by
# len(values_item_shape) from right. The left
# dimensions up to bdim are considered as batch
# dimensions.
if not values_item_shape:
return value
if isinstance(value, list) and bdim >= 0:
return [stretch_values(item, bdim - 1, values_item_shape) for item in value]
new_value = functools.reduce(lambda x, dims: [copy.deepcopy(x) for _ in range(dims)],
reversed(values_item_shape), None)
for p in itertools.product(*map(list, map(range, values_item_shape))):
row = functools.reduce(lambda x, i: x.__getitem__(i), p[:-1], new_value)
row[p[-1]] = list_add(value, sum([i * 10 ** d for d, i in enumerate(p)]))
return new_value
if layout is torch.sparse_bsr:
values_item_shape = blocksize + densesize
elif layout is torch.sparse_bsc:
values_item_shape = tuple(reversed(blocksize)) + densesize
else:
values_item_shape = densesize
if not lst:
return torch.tensor(lst, device=device, dtype=dtype).reshape(0, *values_item_shape)
lst = stretch_values(lst, basedim, values_item_shape)
return torch.tensor(lst, device=device, dtype=dtype)
return shape, values
def _generate_small_inputs(self, layout, device=None, dtype=None, index_dtype=None,
enable_batched=True, enable_hybrid=True):
"""Generator of inputs to sparse compressed tensor factory functions.
The input is defined as a 4-tuple:
compressed_indices, plain_indices, values, expected_size_from_shape_inference
"""
if index_dtype is None:
index_dtype = torch.int64
shape, values = self._generate_small_inputs_utils(layout, device, dtype)
# a regular tensor
yield (torch.tensor([0, 2, 4], device=device, dtype=index_dtype),
torch.tensor([0, 1, 0, 2], device=device, dtype=index_dtype),
values([1, 2, 3, 4], 0, (2, 1)),
shape((2, 3), 0, (2, 1)))
# a tensor with zero dimensions
yield (torch.tensor([0, ], device=device, dtype=index_dtype),
torch.tensor([], device=device, dtype=index_dtype),
values([], 0, (2, 1)),
shape((0, 0), 0, (2, 1)))
if enable_batched:
# a batched tensor with one batch dimension
yield (torch.tensor([[0, 2, 4], [0, 3, 4]], device=device, dtype=index_dtype),
torch.tensor([[0, 1, 0, 1], [0, 1, 2, 0]], device=device, dtype=index_dtype),
values([[1, 2, 3, 4], [5, 6, 7, 8]], 1, (1, 2)),
shape((2, 2, 3), 1, (1, 2)))
# a batched tensor with two batch dimensions
yield (torch.tensor([[[0, 2, 4], [0, 3, 4], [0, 1, 4]],
[[0, 1, 4], [0, 2, 4], [0, 3, 4]]],
device=device, dtype=index_dtype),
torch.tensor([[[0, 1, 0, 1], [0, 1, 2, 0], [0, 0, 1, 2]],
[[1, 0, 1, 2], [0, 2, 0, 1], [0, 1, 2, 1]]],
device=device, dtype=index_dtype),
values([[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]],
[[13, 14, 15, 16], [17, 18, 19, 20], [21, 22, 23, 24]]], 2, (2, 3)),
shape((2, 3, 2, 3), 2, (2, 3)))
if enable_hybrid:
# a tensor with one dense dimension
yield (torch.tensor([0, 2, 4], device=device, dtype=index_dtype),
torch.tensor([0, 1, 0, 2], device=device, dtype=index_dtype),
values([1, 2, 3, 4], 0, (3, 2), (2,)),
shape((2, 3, 2), 0, (3, 2)))
# a tensor with two dense dimensions
yield (torch.tensor([0, 2, 4], device=device, dtype=index_dtype),
torch.tensor([0, 1, 0, 2], device=device, dtype=index_dtype),
values([1, 2, 3, 4], 0, (2, 3), (4, 2)),
shape((2, 3, 4, 2), 0, (2, 3)))
if enable_batched and enable_hybrid:
# a batched tensor with two batch dimensions and two dense dimensions
yield (torch.tensor([[[0, 2, 4], [0, 3, 4], [0, 1, 4]],
[[0, 1, 4], [0, 2, 4], [0, 3, 4]]],
device=device, dtype=index_dtype),
torch.tensor([[[0, 1, 0, 1], [0, 1, 2, 0], [0, 0, 1, 2]],
[[1, 0, 1, 2], [0, 2, 0, 1], [0, 1, 2, 1]]],
device=device, dtype=index_dtype),
values([[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]],
[[13, 14, 15, 16], [17, 18, 19, 20], [21, 22, 23, 24]]], 2, (3, 2), (2, 1)),
shape((2, 3, 2, 3, 2, 1), 2, (3, 2)))
@all_sparse_compressed_layouts()
@onlyCPU
def test_layout(self, layout):
@ -352,11 +210,14 @@ class TestSparseCompressed(TestCase):
@dtypes(*all_types_and_complex_and(torch.half, torch.bool, torch.bfloat16))
def test_sparse_compressed_constructor(self, layout, device, dtype,
use_factory_function, shape_and_device_inference, input_kind):
if input_kind == 'list' and shape_and_device_inference and torch.device(device).type == 'cuda':
# list inputs to factory/constructor function without
# specifying device will result a sparse compressed tensor
# on CPU. So, skip testing against cuda device as unused.
self.skipTest("nothing to test")
if input_kind == 'list' and shape_and_device_inference:
if torch.device(device).type == 'cuda':
# list inputs to factory/constructor function without
# specifying device will result a sparse compressed tensor
# on CPU. So, skip testing against cuda device as unused.
self.skipTest("nothing to test")
if dtype not in {torch.float32, torch.complex64, torch.int64, torch.bool}:
self.skipTest("dtype not supported with list values")
expected_devices = [torch.device(device)]
if TEST_CUDA and torch.device(device).type == 'cuda' and torch.cuda.device_count() >= 2 and not shape_and_device_inference:
@ -369,29 +230,34 @@ class TestSparseCompressed(TestCase):
torch.sparse_bsc: torch.sparse_bsc_tensor,
}[layout]
compressed_indices_mth, plain_indices_mth = sparse_compressed_indices_methods[layout]
for index_dtype in [torch.int32, torch.int64]:
if input_kind == 'list':
index_dtypes = [torch.int64]
else:
index_dtypes = [torch.int32, torch.int64]
for index_dtype in index_dtypes:
for expected_device in expected_devices:
for compressed_indices, plain_indices, values, size in self._generate_small_inputs(
layout, expected_device, dtype, index_dtype):
for (compressed_indices, plain_indices, values), kwargs in self.generate_simple_inputs(
layout, device=expected_device, dtype=dtype, index_dtype=index_dtype,
# skip zero-sized tensors for list inputs:
enable_zero_sized=input_kind != 'list',
output_tensor=False):
size = kwargs['size']
if shape_and_device_inference and 0 in size:
# skip shape inference for zero-sized tensor
# inputs because (i) the shape determined from
# an empty list is ambiguous, and (ii) the
# size of the plain dimension defined as
# max(plain_indices) is undefined if
# plain_indices has no values
continue
compressed_indices_expect = compressed_indices
plain_indices_expect = plain_indices
values_expect = values
if input_kind == 'list':
if size == (0, 0):
# for this degenerate case, plain_indices must
# remain a tensor because
# tensor(plain_indices) results a float dtype
# when plain_indices is an empty list
if index_dtype == torch.int32:
# skip testing int32 case because
# tensor(compressed_indices) results a
# int64 dtype when compressed_indices is
# [0] (a list of single int zero).
continue
else:
plain_indices = plain_indices.tolist()
compressed_indices = compressed_indices.tolist()
plain_indices = plain_indices.tolist()
values = values.tolist()
if size == (0, 0) and layout in {torch.sparse_bsr, torch.sparse_bsc}:
# in the block sparse case, values of type list needs to represent a 3-D tensor
values = [[[]]]
if use_factory_function:
if shape_and_device_inference:
@ -407,9 +273,9 @@ class TestSparseCompressed(TestCase):
dtype=dtype, layout=layout, device=expected_device)
self.assertEqual(layout, sparse.layout)
self.assertEqual(size, sparse.shape)
self.assertEqual(compressed_indices, compressed_indices_mth(sparse))
self.assertEqual(plain_indices, plain_indices_mth(sparse))
self.assertEqual(values, sparse.values())
self.assertEqual(compressed_indices_expect, compressed_indices_mth(sparse))
self.assertEqual(plain_indices_expect, plain_indices_mth(sparse))
self.assertEqual(values_expect, sparse.values())
self.assertEqual(sparse.device, sparse.values().device)
self.assertEqual(sparse.device, expected_device)
@ -455,10 +321,10 @@ class TestSparseCompressed(TestCase):
@all_sparse_compressed_layouts()
@dtypes(*all_types_and_complex_and(torch.bool, torch.half, torch.bfloat16))
def test_clone(self, layout, device, dtype):
for compressed_indices, plain_indices, values, size in self._generate_small_inputs(
layout, device, dtype, index_dtype=torch.int32):
sparse = torch.sparse_compressed_tensor(compressed_indices, plain_indices, values, size,
dtype=dtype, layout=layout, device=device)
for sparse in self.generate_simple_inputs(
layout, device=device, dtype=dtype, index_dtype=torch.int32,
# Temporarily disable testing batch block tensors:
enable_batch=layout in {torch.sparse_csr, torch.sparse_csc}):
cloned_sparse = sparse.clone()
self.assertEqual(sparse, cloned_sparse)
@ -467,10 +333,37 @@ class TestSparseCompressed(TestCase):
compressed_indices_mth, plain_indices_mth = sparse_compressed_indices_methods[layout]
printed = []
for enable_hybrid in [False, True]:
# using local patterns for test_print stability
patterns = [
# 2 x 3 batch of 3 x 2 tensors, trivial blocksize, non-hybrid/hybrid:
([[[[1, 2, 0],
[1, 0, 3]],
[[1, 2, 3],
[1, 0, 0]],
[[1, 0, 0],
[1, 2, 3]]],
[[[0, 2, 0],
[1, 2, 3]],
[[1, 0, 3],
[1, 2, 0]],
[[1, 2, 3],
[0, 2, 0]]]], [(2, 1)], [(), (4,)] if enable_hybrid else [()]),
# tensor with non-trivial blocksize, non-hybrid/hybrid:
([[0, 1, 0, 2, 0, 2],
[0, 1, 0, 0, 2, 0],
[3, 3, 3, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 5, 0, 6, 6, 6],
[5, 0, 5, 6, 6, 6],
[0, 0, 0, 0, 8, 8],
[7, 7, 7, 0, 8, 8]], [(2, 3)], [(), (4, 2)] if enable_hybrid else [()]),
]
for index_dtype in [torch.int32, torch.int64]:
for dtype in [torch.float32, torch.float64]:
for compressed_indices, plain_indices, values, size in self._generate_small_inputs(
layout, device, dtype, index_dtype, enable_hybrid=enable_hybrid):
for (compressed_indices, plain_indices, values), kwargs in self.generate_simple_inputs(
layout, device=device, dtype=dtype, index_dtype=index_dtype, enable_hybrid=enable_hybrid,
enable_zero_sized=False, output_tensor=False, patterns=patterns):
size = tuple(kwargs['size'])
block_ndim = 2 if layout in {torch.sparse_bsr, torch.sparse_bsc} else 0
base_ndim = 2
batch_ndim = compressed_indices.dim() - 1
@ -647,9 +540,7 @@ class TestSparseCompressed(TestCase):
@all_sparse_compressed_layouts('layout2')
@dtypes(*all_types_and_complex_and(torch.bool, torch.half, torch.bfloat16))
def test_empty_like(self, layout, layout2, device, dtype):
for compressed_indices, plain_indices, values, size in self._generate_small_inputs(layout):
sparse = torch.sparse_compressed_tensor(compressed_indices, plain_indices, values, size,
dtype=dtype, layout=layout, device=device)
for sparse in self.generate_simple_inputs(layout):
if layout == layout2:
result = torch.empty_like(sparse, layout=layout2)
compressed_indices_mth, plain_indices_mth = sparse_compressed_indices_methods[result.layout]
@ -671,14 +562,28 @@ class TestSparseCompressed(TestCase):
@dtypes(*all_types_and_complex_and(torch.half, torch.bool, torch.bfloat16))
def test_validate(self, layout, device, dtype):
for index_dtype in [torch.int32, torch.int64]:
for compressed_indices, plain_indices, values, size in self._generate_small_inputs(
layout, device, dtype, index_dtype, enable_batched=True, enable_hybrid=True):
for (compressed_indices, plain_indices, values), kwargs in self.generate_simple_inputs(
layout, device=device, dtype=dtype, index_dtype=index_dtype, output_tensor=False):
size = kwargs['size']
torch._validate_sparse_compressed_tensor_args(compressed_indices, plain_indices, values, size, layout)
def _generate_invalid_input(self, layout, device):
from functools import partial
shape, values = self._generate_small_inputs_utils(layout, device=device)
def shape(shape, basedim=0):
blocksize = (1, 1)
if layout is torch.sparse_csc:
shape = shape[:basedim] + (shape[basedim + 1], shape[basedim]) + shape[basedim + 2:]
elif layout is torch.sparse_bsc:
shape = shape[:basedim] + (shape[basedim + 1] * blocksize[1], shape[basedim] * blocksize[0]) + shape[basedim + 2:]
elif layout is torch.sparse_bsr:
shape = shape[:basedim] + (shape[basedim] * blocksize[0], shape[basedim + 1] * blocksize[1]) + shape[basedim + 2:]
return shape
def values(lst, device=device):
if layout in {torch.sparse_bsr, torch.sparse_bsc}:
lst = [[[item]] for item in lst]
return torch.tensor(lst, device=device)
tensor = partial(torch.tensor, device=device)
values = partial(values, device=device)
@ -925,7 +830,8 @@ class TestSparseCompressed(TestCase):
@onlyCPU
@all_sparse_compressed_layouts()
def test_dim(self, layout):
for compressed_indices, plain_indices, values, size in self._generate_small_inputs(layout):
for (compressed_indices, plain_indices, values), kwargs in self.generate_simple_inputs(layout, output_tensor=False):
size = kwargs['size']
batch_dim = compressed_indices.dim() - 1
sparse_dim = 2
block_dim = 2 if layout in {torch.sparse_bsr, torch.sparse_bsc} else 0
@ -940,10 +846,7 @@ class TestSparseCompressed(TestCase):
@dtypes(*all_types_and_complex_and(torch.bool, torch.half, torch.bfloat16))
def test_to_dtype(self, layout, device, dtype):
# to_dense does not support hybrid inputs
input_gen = self._generate_small_inputs(layout, device=device, enable_hybrid=False)
for compressed_indices, plain_indices, values, size in input_gen:
sparse = torch.sparse_compressed_tensor(compressed_indices, plain_indices, values, size,
dtype=dtype, layout=layout, device=device)
for sparse in self.generate_simple_inputs(layout, dtype=dtype, device=device, enable_hybrid=False):
for to_dtype in all_types_and_complex_and(torch.bool, torch.half, torch.bfloat16):
sparse_to_dtype = sparse.to(to_dtype)
dense_to_dtype = sparse.to_dense().to(to_dtype)
@ -955,10 +858,7 @@ class TestSparseCompressed(TestCase):
def test_pickle(self, layout, dtype, device):
import pickle
input_gen = self._generate_small_inputs(layout)
for compressed_indices, plain_indices, values, size in input_gen:
sparse = torch.sparse_compressed_tensor(compressed_indices, plain_indices, values, size,
dtype=dtype, device=device, layout=layout)
for sparse in self.generate_simple_inputs(layout, device=device, dtype=dtype):
serialized = pickle.dumps(sparse)
sparse_loaded = pickle.loads(serialized)

View file

@ -2446,6 +2446,296 @@ class TestCase(expecttest.TestCase):
x = x.detach().clone()._coalesced_(False)
return x, x._indices().clone(), x._values().clone()
def generate_simple_inputs(self, layout,
device=None,
dtype=None,
index_dtype=None,
enable_batch=True,
enable_hybrid=True,
enable_zero_sized=True,
enable_batch_variable_nse=False,
output_tensor=True,
patterns=None):
"""Generator of simple inputs for tensor constructors of the given layout.
The generated tensor inputs have the following properties:
- tensor shapes are minimal but not trivial
- tensor values are sorted sequences for COO and CSR formats, e.g. [1, 2, 3, 4]
- the generated tensors represent the same mathematical tensor for all layouts
- the generated tensors include regular, zero-sized, and optionally, batched or/and hybrid tensors.
If output_tensor is True, yield tensors with the given
layout. Otherwise, yield inputs to the corresponding tensor
constructors:
- sparse compressed input is defined as
(compressed_indices, plain_indices, values), dict(size=expected_size_from_shape_inference, device=device, dtype=dtype)
- sparse COO input is defined as
(indices, values), dict(size=expected_size_from_shape_inference, device=device, dtype=dtype)
- strided input is defined as
(values,), dict(device=device, dtype=dtype)
"""
if index_dtype is None:
index_dtype = torch.int64
is_compressed_sparse_layout = layout in {torch.sparse_csr, torch.sparse_csc, torch.sparse_bsr, torch.sparse_bsc}
if output_tensor:
for args, kwargs in self.generate_simple_inputs(layout, device=device, dtype=dtype, index_dtype=index_dtype,
enable_batch=enable_batch, enable_hybrid=enable_hybrid,
enable_zero_sized=enable_zero_sized,
enable_batch_variable_nse=enable_batch_variable_nse,
output_tensor=False):
if layout is torch.strided:
assert len(args) == 1
size = kwargs.pop('size', None) # to ensure that a zero-sized tensor has the desired shape
assert size is not None
yield args[0].reshape(size)
elif layout is torch.sparse_coo:
yield torch.sparse_coo_tensor(*args, **kwargs)
elif is_compressed_sparse_layout:
kwargs.update(layout=layout)
yield torch.sparse_compressed_tensor(*args, **kwargs)
else:
assert 0 # unreachable
return
def get_blockpattern(pattern, blocksize):
basesize = pattern.shape
assert basesize[0] % blocksize[0] == 0, (basesize, blocksize)
assert basesize[1] % blocksize[1] == 0, (basesize, blocksize)
blockpattern = pattern.reshape(-1,
blocksize[0],
basesize[1] // blocksize[1],
blocksize[1]).transpose(-3, -2).any(-1).any(-1)
block_ids = torch.arange(1, blockpattern.numel() + 1).reshape(blockpattern.shape)
return (blockpattern != 0) * block_ids
def get_sparse_data(pattern):
basesize = pattern.shape
assert len(basesize) == 2, basesize # pattern is expected to be a matrix
# We cannot use `torch.sparse_xyz_tensor(pattern)` to
# compute the sparse layout indices and values because
# generate_simple_inputs is used to generate the inputs to
# test `torch.sparse_xyz_tensor` factory functions, so
# we'll compute the indices and values independently of
# the factory functions.
indices = torch.where(pattern != 0)
coo_indices = torch.stack(indices)
crow_indices = torch.zeros(basesize[0] + 1, dtype=torch.int64)
crow_indices[1:] = torch.cumsum(coo_indices[0].bincount(minlength=basesize[0]), 0)
col_indices = coo_indices[1]
strided_values = torch.zeros(basesize, dtype=torch.int64)
# the property of `values == range(1, 1+nnz)` is used in
# get_sparse_data_with_block to relate BSR and BSC values,
# so, don't change the following line:
values = torch.arange(1, 1 + len(indices[0]), dtype=torch.int64)
strided_values[indices] = values
indices_T = torch.where(pattern.transpose(0, 1) != 0)
coo_indices_T = torch.stack(indices_T)
ccol_indices = torch.zeros(basesize[1] + 1, dtype=torch.int64)
ccol_indices[1:] = torch.cumsum(coo_indices_T[0].bincount(minlength=basesize[1]), 0)
row_indices = coo_indices_T[1]
csc_values = strided_values.transpose(0, 1)[indices_T]
return {torch.sparse_coo: (coo_indices, values),
torch.sparse_csr: (crow_indices, col_indices, values),
torch.sparse_csc: (ccol_indices, row_indices, csc_values),
torch.strided: (strided_values,)}
def get_sparse_data_with_block(pattern, blocksize):
nonblock_data = get_sparse_data(pattern)
blockpattern = get_blockpattern(pattern, blocksize)
block_data = get_sparse_data(blockpattern)
strided_values = nonblock_data[torch.strided][0]
block_indices = block_data[torch.sparse_coo][0]
bsr_values = torch.stack([strided_values[bi * blocksize[0]:(bi + 1) * blocksize[0],
bj * blocksize[1]:(bj + 1) * blocksize[1]]
for bi, bj in block_indices.transpose(0, 1)])
# here we use the property `values == range(1, 1+nnz)` and
# `values` relation to `csc_values` (see get_sparse_data)
# to get BSC blocks via reordering the BSR blocks:
bsc_values = bsr_values[block_data[torch.sparse_csc][2] - 1]
return {torch.sparse_bsr: (*block_data[torch.sparse_csr][:2], bsr_values),
torch.sparse_bsc: (*block_data[torch.sparse_csc][:2], bsc_values),
**nonblock_data}
def get_batch_sparse_data(pattern, blocksize):
size = pattern.shape
if len(size) <= 2: # non-batch
return get_sparse_data_with_block(pattern, blocksize)
# batch data is created recursively:
batch_data = {}
for i, item in enumerate(pattern):
for layout, d in get_batch_sparse_data(item, blocksize).items():
target = batch_data.get(layout)
if layout is torch.sparse_coo:
# a "batch COO" means a COO with the leading
# sparse dimensions interpreted as batch
# dimensions
ext_coo_indices1 = torch.cat((torch.full((1, len(d[1])), i, dtype=torch.int64), d[0]))
if target is None:
target = batch_data[layout] = (ext_coo_indices1, d[1])
else:
target[0].set_(torch.cat((target[0], ext_coo_indices1), 1))
target[1].set_(torch.cat((target[1], d[1])))
else:
if target is None:
target = batch_data[layout] = tuple(d[j].unsqueeze(0) for j in range(len(d)))
else:
for j in range(len(d)):
target[j].set_(torch.cat((target[j], d[j].unsqueeze(0))))
return batch_data
def generate_values(base, densesize):
"""Generates a tensor of shape densesize with values equal to
base + i_1 * 10^0 + ... + i_d * 10^{d - 1}
at indices i_1, ..., i_d (with 0 <= i_j < densesize[j] for any 1 <= j <=
len(densesize))
This mapping produces unique values as long as
densesize[i] < 10 for all i in range(len(densesize)).
"""
if not densesize:
return base
if not isinstance(base, int) and base.ndim > 0:
return torch.stack([generate_values(b, densesize) for b in base])
if base == 0:
return torch.zeros(densesize, dtype=torch.int64)
r = torch.arange(densesize[0], dtype=torch.int64)
for i, d in enumerate(densesize[1:]):
y = torch.arange(d, dtype=torch.int64) * (10 ** (i + 1))
r = r[..., None] + y[None, ...]
r.add_(base)
return r
if patterns is None:
# A pattern is a 3-tuple with the following items:
#
# - a list of integers with the depth of two or more. The
# integers define the sparsity patterns of the generated
# inputs: zero values correspond to unspecified
# elements/blocks, and non-zero values to the specified
# elements.
#
# For debugging convenience, the elements with the same
# value typically belong to the same block. However, it
# is not a hard requirement: as long as the shape of a
# pattern divides with block sizes, the pattern will be
# a valid one.
#
# If the depth of the list is larger than two, inputs
# with batch dimensions will be generated.
#
# - a list of 2-tuples of block sizes, used to generate
# BSR/BSC tensors with various block size parameters
#
# - a list of tuples of dense dimensions, used to generate
# hybrid tensors with various dense dimensions
#
patterns = [
# a simple 3 x 2 tensor: non-hybrid, hybrid with 1 and 2 dense dimensions
([[1, 2, 0],
[1, 0, 3]], [(2, 1), (1, 3)], [(), (2,), (4, 5)]),
# 2 x 3 batch of 3 x 2 tensors: non-hybrid and hybrid with 2 dense dimensions
([[[[1, 2, 0],
[1, 0, 3]],
[[1, 2, 3],
[1, 0, 0]],
[[1, 0, 0],
[1, 2, 3]]],
[[[0, 2, 0],
[1, 2, 3]],
[[1, 0, 3],
[1, 2, 0]],
[[1, 2, 3],
[0, 2, 0]]]], [(2, 1), (2, 3)], [(), (2,)]),
# tensor with non-trivial blocksize
([[0, 1, 0, 2, 0, 2],
[0, 1, 0, 0, 2, 0],
[3, 3, 3, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 5, 0, 6, 6, 6],
[5, 0, 5, 6, 6, 6],
[0, 0, 0, 0, 8, 8],
[7, 7, 7, 0, 8, 8]], [(2, 3)], [(), (4, 5)]),
# batch tensor with variable NSE
# Requires https://github.com/pytorch/pytorch/pull/84843 or similar.
([[[1, 2],
[3, 4]],
[[1, 0],
[0, 0]]], [(1, 1)], ([()] if enable_batch_variable_nse else []))]
# the main loop of the method:
for pattern, blocksizes, densesizes in patterns:
if not enable_hybrid:
densesizes = [s for s in densesizes if not s]
if not (densesizes and blocksizes):
continue
pattern = torch.tensor(pattern, dtype=torch.int64)
if not enable_batch and pattern.ndim > 2:
continue
for blocksize in blocksizes:
data = get_batch_sparse_data(pattern, blocksize)[layout]
for densesize in densesizes:
indices = [a.to(device=device, dtype=index_dtype) for a in data[:-1]]
values = generate_values(data[-1], densesize).to(device=device, dtype=dtype)
yield (*indices, values), dict(device=device, dtype=dtype,
size=pattern.shape + densesize)
# zero-sized tensor inputs, non-batch, non-hybrid/hybrid
if enable_zero_sized:
for basesize, blocksizes, densesizes in [
((2, 0), [(1, 2)], [(), (2,), (2, 3)] if enable_hybrid else [()]),
((0, 2), [(1, 2), (2, 1), (3, 2)], [()]),
((0, 0), [(1, 2)], [()]),
]:
for blocksize in blocksizes:
for densesize in densesizes:
if layout == torch.strided:
indices = ()
values = torch.empty((basesize + densesize), device=device, dtype=dtype)
elif layout == torch.sparse_coo:
indices = (torch.empty(len(basesize), 0, device=device, dtype=index_dtype),)
values = torch.empty((0, *densesize), device=device, dtype=dtype)
elif layout == torch.sparse_csr:
crow_indices = torch.tensor([0] * (basesize[0] + 1), device=device, dtype=index_dtype)
col_indices = torch.empty(0, device=device, dtype=index_dtype)
indices = (crow_indices, col_indices)
values = torch.empty((0, *densesize), device=device, dtype=dtype)
elif layout == torch.sparse_csc:
ccol_indices = torch.tensor([0] * (basesize[1] + 1), device=device, dtype=index_dtype)
row_indices = torch.empty(0, device=device, dtype=index_dtype)
indices = (ccol_indices, row_indices)
values = torch.empty((0, *densesize), device=device, dtype=dtype)
elif layout == torch.sparse_bsr:
crow_indices = torch.tensor([0] * (basesize[0] // blocksize[0] + 1), device=device, dtype=index_dtype)
col_indices = torch.empty(0, device=device, dtype=index_dtype)
indices = (crow_indices, col_indices)
values = torch.empty((0, *blocksize, *densesize), device=device, dtype=dtype)
elif layout == torch.sparse_bsc:
ccol_indices = torch.tensor([0] * (basesize[1] // blocksize[1] + 1), device=device, dtype=index_dtype)
row_indices = torch.empty(0, device=device, dtype=index_dtype)
indices = (ccol_indices, row_indices)
values = torch.empty((0, *blocksize, *densesize), device=device, dtype=dtype)
else:
assert 0 # unreachable
yield (*indices, values), dict(device=device, dtype=dtype, size=basesize + densesize)
def safeToDense(self, t):
# coalesce is only implemented for COO
if t.layout == torch.sparse_coo: