Repository: cupy/cupy

Issue:
Sparse min/max does not support `axis=None`
Rel #3474.

Sparse min/max does not support `axis=None`, which is the default value for the parameter.

```Python
>>> import cupy
>>> import cupyx
>>> m = cupy.random.rand(100).reshape(10, 10)
>>> m[m < 0.95] = 0
>>> m = cupyx.scipy.sparse.csr_matrix(m)
>>> m.min()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/ext-mtakagi/cupy/cupyx/scipy/sparse/data.py", line 284, in min
    return self._min_or_max(axis, out, cupy.min, sum_duplicates, nonzero)
  File "/home/ext-mtakagi/cupy/cupyx/scipy/sparse/data.py", line 163, in _min_or_max
    axis, min_or_max, sum_duplicates, non_zero)
  File "/home/ext-mtakagi/cupy/cupyx/scipy/sparse/data.py", line 139, in _min_or_max_axis
    N = self.shape[axis]
TypeError: tuple indices must be integers or slices, not NoneType
```


Discussion:
Hi @takagi, If you're gonna fix this, could you create a fast path for `axis=None` utilizing the existing reduction kernel? I think we could just do `m.data.min()` and friends. Then, the CUB reduction kernels or any other reduction backends (like cuTENSOR, see #2939), when enabled, will automatically kick in. (For any other `axis != None`, I think we will need to rely on the existing implementation.)
Sure!

Resolving patch:
diff --git a/cupyx/scipy/sparse/data.py b/cupyx/scipy/sparse/data.py
--- a/cupyx/scipy/sparse/data.py
+++ b/cupyx/scipy/sparse/data.py
@@ -1,4 +1,5 @@
 import cupy
+from cupy.core import internal
 from cupyx.scipy.sparse import base
 from cupyx.scipy.sparse import util
 
@@ -156,6 +157,27 @@ def _min_or_max(self, axis, out, min_or_max, sum_duplicates, non_zero):
 
         util.validateaxis(axis)
 
+        if axis is None:
+            if 0 in self.shape:
+                raise ValueError("zero-size array to reduction operation")
+
+            zero = cupy.zeros((), dtype=self.dtype)
+            if self.nnz == 0:
+                return zero
+            if sum_duplicates:
+                self.sum_duplicates()
+            m = min_or_max(self.data)
+            if non_zero:
+                return m
+            if self.nnz != internal.prod(self.shape):
+                if min_or_max is cupy.min:
+                    m = cupy.minimum(zero, m)
+                elif min_or_max is cupy.max:
+                    m = cupy.maximum(zero, m)
+                else:
+                    assert False
+            return m
+
         if axis == 0 or axis == 1:
             return self._min_or_max_axis(axis, min_or_max, sum_duplicates,
                                          non_zero)


Test patch:
diff --git a/tests/cupyx_tests/scipy_tests/sparse_tests/test_csc.py b/tests/cupyx_tests/scipy_tests/sparse_tests/test_csc.py
--- a/tests/cupyx_tests/scipy_tests/sparse_tests/test_csc.py
+++ b/tests/cupyx_tests/scipy_tests/sparse_tests/test_csc.py
@@ -903,7 +903,34 @@ def test_getnnz(self, xp, sp):
 
 
 @testing.with_requires('scipy>=0.19.0')
-class TestCsrMatrixScipyCompressedMinMax(unittest.TestCase):
+class TestCscMatrixScipyCompressedMinMax(unittest.TestCase):
+
+    def test_min_sparse_axis_none(self):
+        dm_data = numpy.random.random((10, 20))
+        dm_data[dm_data < 0.95] = 0
+
+        dm_data = scipy.sparse.csc_matrix(dm_data)
+        cp_matrix = sparse.csc_matrix((cupy.array(dm_data.data),
+                                       cupy.array(dm_data.indices),
+                                       cupy.array(dm_data.indptr)),
+                                      shape=(10, 20))
+
+        da_cupy_values = cp_matrix.min()
+        da_scipy_values = dm_data.min()
+        testing.assert_array_equal(da_cupy_values, da_scipy_values)
+
+    def test_min_sparse_axis_none_nonzero(self):
+        dm_data = numpy.arange(0, 100, 1).reshape((10, 10)).astype(float)
+
+        dm_sparse = scipy.sparse.csc_matrix(dm_data)
+        cp_matrix = sparse.csc_matrix((cupy.array(dm_sparse.data),
+                                       cupy.array(dm_sparse.indices),
+                                       cupy.array(dm_sparse.indptr)),
+                                      shape=(10, 10))
+
+        da_cupy_values = cp_matrix.min(nonzero=True)
+        da_numpy_values = numpy.array(1).astype(float)
+        testing.assert_array_equal(da_cupy_values, da_numpy_values)
 
     def test_min_sparse_axis_0(self):
         dm_data = numpy.random.random((10, 20))
diff --git a/tests/cupyx_tests/scipy_tests/sparse_tests/test_csr.py b/tests/cupyx_tests/scipy_tests/sparse_tests/test_csr.py
--- a/tests/cupyx_tests/scipy_tests/sparse_tests/test_csr.py
+++ b/tests/cupyx_tests/scipy_tests/sparse_tests/test_csr.py
@@ -1026,6 +1026,33 @@ def test_getnnz(self, xp, sp):
 @testing.with_requires('scipy>=0.19.0')
 class TestCsrMatrixScipyCompressedMinMax(unittest.TestCase):
 
+    def test_min_sparse_axis_none(self):
+        dm_data = numpy.random.random((10, 20))
+        dm_data[dm_data < 0.95] = 0
+
+        dm_data = scipy.sparse.csr_matrix(dm_data)
+        cp_matrix = sparse.csr_matrix((cupy.array(dm_data.data),
+                                       cupy.array(dm_data.indices),
+                                       cupy.array(dm_data.indptr)),
+                                      shape=(10, 20))
+
+        da_cupy_values = cp_matrix.min()
+        da_scipy_values = dm_data.min()
+        testing.assert_array_equal(da_cupy_values, da_scipy_values)
+
+    def test_min_sparse_axis_none_nonzero(self):
+        dm_data = numpy.arange(0, 100, 1).reshape((10, 10)).astype(float)
+
+        dm_sparse = scipy.sparse.csr_matrix(dm_data)
+        cp_matrix = sparse.csr_matrix((cupy.array(dm_sparse.data),
+                                       cupy.array(dm_sparse.indices),
+                                       cupy.array(dm_sparse.indptr)),
+                                      shape=(10, 10))
+
+        da_cupy_values = cp_matrix.min(nonzero=True)
+        da_numpy_values = numpy.array(1).astype(float)
+        testing.assert_array_equal(da_cupy_values, da_numpy_values)
+
     def test_min_sparse_axis_0(self):
         dm_data = numpy.random.random((10, 20))
         dm_data[dm_data < 0.95] = 0