Repository: cupy/cupy

Issue:
cupyx.scipy.signal.convolve2d: integer overflow when using mixed dtypes
### Description

CuPy's `convolve2d` behaves differently from SciPy's `convolve2d` when using two arrays with different dtypes. When using integers with different precision, CuPy's `convolve2d` leads to integer overflow.

#### SciPy's behaviour

If the precisions of the convolved integer arrays are different, the higest precision is used to prevent integer overflow.

```py
# scipy's convolve2d
import numpy as np
from scipy.signal import convolve2d
a = np.array([[257]], dtype="uint64")
b = np.array([[1]], dtype="uint8")
result = convolve2d(a, b, mode="same")
print(result, result.dtype)
# [[257]] uint64 : expected result
```

#### CuPy's behaviour

If the precisions of the convolved integer arrays are different, the calculation is limited by the lowest precision and integer overflow occurs. The dtype of the resulting array is that of the first array passed to `convolve2d`, but the overflow occurs before: the error is present even when the first array has the higher precision.

```py
# cupy's convolve2d
import cupy as cp
from cupyx.scipy.signal import convolve2d
a = cp.asarray(a) # defined above
b = cp.asarray(b)
result = convolve2d(a, b, mode="same")
print(result, result.dtype)
# [[1]] uint64 : correct dtype, but uint8 integer overflow
# reversed convolution
result = convolve2d(b, a, mode="same")
print(result, result.dtype)
# [[1]] uint8 : wrong dtype and uint8 integer overflow
```

#### Expected behaviour

The expected behaviour is SciPy's behaviour: when convolving arrays of mixed dtypes, the calculations should be performed on the highest-precision dtype.

### To Reproduce

```py
# scipy's convolve2d
import numpy as np
from scipy.signal import convolve2d
a = np.array([[257]], dtype="uint64")
b = np.array([[1]], dtype="uint8")
result = convolve2d(a, b, mode="same")
print(result, result.dtype)
# [[257]] uint64 : expected result

# cupy's convolve2d
import cupy as cp
from cupyx.scipy.signal import convolve2d
a = cp.asarray(a)
b = cp.asarray(b)
result = convolve2d(a, b, mode="same")
print(result, result.dtype)
# [[1]] uint64 : correct dtype, but uint8 integer overflow
# reversed convolution
result = convolve2d(b, a, mode="same")
print(result, result.dtype)
# [[1]] uint8 : wrong dtype and uint8 integer overflow
```


### Installation

Wheel (`pip install cupy-***`)

### Environment

```
OS                           : Windows-10-10.0.19041-SP0
Python Version               : 3.7.4
CuPy Version                 : 9.5.0
CuPy Platform                : NVIDIA CUDA
NumPy Version                : 1.21.2
SciPy Version                : 1.7.1
Cython Build Version         : 0.29.24
Cython Runtime Version       : 0.29.13
CUDA Root                    : C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.2
nvcc PATH                    : C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.2\bin\nvcc.EXE
CUDA Build Version           : 11020
CUDA Driver Version          : 11020
CUDA Runtime Version         : 11020
cuBLAS Version               : (available)
cuFFT Version                : 10401
cuRAND Version               : 10203
cuSOLVER Version             : (11, 1, 0)
cuSPARSE Version             : (available)
NVRTC Version                : (11, 2)
Thrust Version               : 101000
CUB Build Version            : 101000
Jitify Build Version         : 60e9e72
cuDNN Build Version          : None
cuDNN Version                : None
NCCL Build Version           : None
NCCL Runtime Version         : None
cuTENSOR Version             : None
cuSPARSELt Build Version     : None
Device 0 Name                : GeForce MX150
Device 0 Compute Capability  : 61
Device 0 PCI Bus ID          : 0000:01:00.0
```


### Additional Information

_No response_


Discussion:

Resolving patch:
diff --git a/cupyx/scipy/signal/_signaltools_core.py b/cupyx/scipy/signal/_signaltools_core.py
--- a/cupyx/scipy/signal/_signaltools_core.py
+++ b/cupyx/scipy/signal/_signaltools_core.py
@@ -61,10 +61,23 @@ def _direct_correlate(in1, in2, mode='full', output=float, convolution=False,
                             for x1, x2 in zip(in1.shape, in2.shape))
 
     # Check the output
+    # In SciPy, the output dtype is determined by inputs' dtypes
+    out_dtype = cupy.promote_types(in1, in2)
     if not isinstance(output, cupy.ndarray):
-        output = cupy.empty(out_shape, output)
+        if not cupy.can_cast(output, out_dtype):
+            raise ValueError('not available for this type')
+        output = cupy.empty(out_shape, out_dtype)
     elif output.shape != out_shape:
         raise ValueError('out has wrong shape')
+    elif output.dtype != out_dtype:
+        raise ValueError('out has wrong dtype')
+
+    # Check input dtypes
+    # Internally, the kernel accumulates in in2's type, so if in2 has lower
+    # precision (can_cast = True!) we hit overflow easier
+    # TODO(leofang): this is a band-aid fix for cupy/cupy#6047
+    if cupy.can_cast(in2, in1):
+        in2 = in2.astype(out_dtype)  # make a copy while upcasting
 
     # Get and run the CuPy kernel
     int_type = _util._get_inttype(in1)


Test patch: