Repository: cupy/cupy

Issue:
Make a copy for input when calling FFT on CUDA 10.1+
#3763 adds a warning that inputs may be modified (because cuFFT performs in-place operation), but it's better to make a copy for input before calling cuFFT.

* Make a copy by default.
* Provide an option to avoid copy, e.g., `cupy.fft(..., *, overwrite_x=False)` for users needing performance. (using keyword argument as this is not numpy-compatible)

This feature is not to be backported to v7.


Discussion:
> * Make a copy by default.

I think we only need to do this for certain kinds of FFT (like C2R). I am digging into the docs.
If cuFFT aims to be interchangable with FFTW, then we only need to worry about C2R transforms (i.e., `irfft`, `irfft2`, `irfftn`). See https://github.com/cupy/cupy/issues/3736#issuecomment-672073361 and the [cuFFT documentation](https://docs.nvidia.com/cuda/cufft/index.html#data-layout):
> Note that in-place complex-to-real FFTs may overwrite arbitrary imaginary input point values when non-unit input and output strides are chosen. Out-of-place complex-to-real FFT will overwrite input buffer if custom strides are set by the user.

Resolving patch:
diff --git a/cupy/fft/fft.py b/cupy/fft/fft.py
--- a/cupy/fft/fft.py
+++ b/cupy/fft/fft.py
@@ -86,6 +86,11 @@ def _exec_fft(a, direction, value_type, norm, axis, overwrite_x,
 
     if a.base is not None or not a.flags.c_contiguous:
         a = a.copy()
+    elif (value_type == 'C2R' and not overwrite_x and
+            10010 <= cupy.cuda.runtime.runtimeGetVersion()):
+        # The input array may be modified in CUDA 10.1 and above.
+        # See #3763 for the discussion.
+        a = a.copy()
 
     n = a.shape[-1]
     if n < 1:
@@ -413,6 +418,12 @@ def _exec_fftn(a, direction, value_type, norm, axes, overwrite_x,
     else:
         raise ValueError('a must be contiguous')
 
+    if (value_type == 'C2R' and not overwrite_x and
+            10010 <= cupy.cuda.runtime.runtimeGetVersion()):
+        # The input array may be modified in CUDA 10.1 and above.
+        # See #3763 for the discussion.
+        a = a.copy()
+
     curr_plan = cufft.get_current_plan()
     if curr_plan is not None:
         plan = curr_plan
@@ -717,8 +728,6 @@ def irfft(a, n=None, axis=-1, norm=None):
             given, the length of the transformed axis is`2*(m-1)` where `m`
             is the length of the transformed axis of the input.
 
-    .. warning:: The input array may be modified in CUDA 10.1 and above.
-
     .. seealso:: :func:`numpy.fft.irfft`
     """
     return _fft(a, (n,), (axis,), norm, cufft.CUFFT_INVERSE, 'C2R')
@@ -766,8 +775,6 @@ def irfft2(a, s=None, axes=(-2, -1), norm=None):
             `2*(m-1)` where `m` is the length of the final transformed axis of
             the input.
 
-    .. warning:: The input array may be modified in CUDA 10.1 and above.
-
     .. seealso:: :func:`numpy.fft.irfft2`
     """
     func = _default_fft_func(a, s, axes, value_type='C2R')
@@ -825,8 +832,6 @@ def irfftn(a, s=None, axes=None, norm=None):
             ``2*(m-1)`` where `m` is the length of the final transformed axis
             of the input.
 
-    .. warning:: The input array may be modified in CUDA 10.1 and above.
-
     .. seealso:: :func:`numpy.fft.irfftn`
     """
     if (10020 >= cupy.cuda.runtime.runtimeGetVersion() >= 10010
diff --git a/cupyx/scipy/fft/fft.py b/cupyx/scipy/fft/fft.py
--- a/cupyx/scipy/fft/fft.py
+++ b/cupyx/scipy/fft/fft.py
@@ -333,9 +333,6 @@ def irfft(x, n=None, axis=-1, norm=None, overwrite_x=False, *, plan=None):
         cupy.ndarray:
             The transformed array.
 
-    .. warning:: The input array may be modified in CUDA 10.1 and above, even
-                 when `overwrite_x is False`.
-
     .. seealso:: :func:`scipy.fft.irfft`
     """
     return _fft(x, (n,), (axis,), norm, cufft.CUFFT_INVERSE, 'C2R',
@@ -405,9 +402,6 @@ def irfft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, *,
             `2*(m-1)` where `m` is the length of the final transformed axis of
             the input.
 
-    .. warning:: The input array may be modified in CUDA 10.1 and above, even
-                 when `overwrite_x is False`.
-
     .. seealso:: :func:`scipy.fft.irfft2`
     """
     return irfftn(x, s, axes, norm, overwrite_x, plan=plan)
@@ -478,9 +472,6 @@ def irfftn(x, s=None, axes=None, norm=None, overwrite_x=False, *, plan=None):
             ``2*(m-1)`` where `m` is the length of the final transformed axis
             of the input.
 
-    .. warning:: The input array may be modified in CUDA 10.1 and above, even
-                 when `overwrite_x is False`.
-
     .. seealso:: :func:`scipy.fft.irfftn`
     """
     s = _assequence(s)
diff --git a/cupyx/scipy/fftpack/fft.py b/cupyx/scipy/fftpack/fft.py
--- a/cupyx/scipy/fftpack/fft.py
+++ b/cupyx/scipy/fftpack/fft.py
@@ -426,9 +426,6 @@ def irfft(x, n=None, axis=-1, overwrite_x=False):
        This function does not support a precomputed `plan`. If you need this
        capability, please consider using :func:`cupy.fft.irfft` or :func:`
        cupyx.scipy.fft.irfft`.
-
-    .. warning:: The input array may be modified in CUDA 10.1 and above, even
-                 when `overwrite_x is False`.
     """
     if n is None:
         n = x.shape[axis]


Test patch:
diff --git a/tests/cupyx_tests/scipy_tests/fft_tests/test_fft.py b/tests/cupyx_tests/scipy_tests/fft_tests/test_fft.py
--- a/tests/cupyx_tests/scipy_tests/fft_tests/test_fft.py
+++ b/tests/cupyx_tests/scipy_tests/fft_tests/test_fft.py
@@ -1103,11 +1103,7 @@ def test_irfft2(self, xp, dtype):
         x_orig = x.copy()
         out = _fft_module(xp).irfft2(x, s=self.s, axes=self.axes,
                                      norm=self.norm)
-
-        # CUDA 10.1 and above may modify input, this fails for complex64
-        if (cp.cuda.runtime.runtimeGetVersion() <= 10000 or
-                dtype not in [np.complex64, np.complex128]):
-            testing.assert_array_equal(x, x_orig)
+        testing.assert_array_equal(x, x_orig)
 
         return _correct_np_dtype(xp, dtype, out)
 
@@ -1146,11 +1142,7 @@ def test_irfft2_plan(self, xp, dtype):
             kw = {}
         out = _fft_module(xp).irfft2(
             x, s=self.s, axes=self.axes, norm=self.norm, **kw)
-
-        # CUDA 10.1 and above may modify input, this fails for complex64
-        if (cp.cuda.runtime.runtimeGetVersion() <= 10000 or
-                dtype not in [np.complex64, np.complex128]):
-            testing.assert_array_equal(x, x_orig)
+        testing.assert_array_equal(x, x_orig)
 
         return _correct_np_dtype(xp, dtype, out)
 
@@ -1201,11 +1193,7 @@ def test_irfft2_plan_manager(self, xp, dtype):
             assert get_current_plan() is None
         else:
             out = _fft_module(xp).irfft2(x, s=self.s, axes=self.axes)
-
-        # CUDA 10.1 and above may modify input, this fails for complex64
-        if (cp.cuda.runtime.runtimeGetVersion() <= 10000 or
-                dtype not in [np.complex64, np.complex128]):
-            testing.assert_array_equal(x, x_orig)
+        testing.assert_array_equal(x, x_orig)
 
         return _correct_np_dtype(xp, dtype, out)
 
@@ -1359,11 +1347,7 @@ def test_irfftn(self, xp, dtype):
         x_orig = x.copy()
         out = _fft_module(xp).irfftn(x, s=self.s, axes=self.axes,
                                      norm=self.norm)
-
-        # CUDA 10.1 and above may modify input, this fails for complex64
-        if (cp.cuda.runtime.runtimeGetVersion() <= 10000 or
-                dtype not in [np.complex64, np.complex128]):
-            testing.assert_array_equal(x, x_orig)
+        testing.assert_array_equal(x, x_orig)
 
         return _correct_np_dtype(xp, dtype, out)
 
@@ -1402,11 +1386,7 @@ def test_irfftn_plan(self, xp, dtype):
             kw = {}
         out = _fft_module(xp).irfftn(
             x, s=self.s, axes=self.axes, norm=self.norm, **kw)
-
-        # CUDA 10.1 and above may modify input, this fails for complex64
-        if (cp.cuda.runtime.runtimeGetVersion() <= 10000 or
-                dtype not in [np.complex64, np.complex128]):
-            testing.assert_array_equal(x, x_orig)
+        testing.assert_array_equal(x, x_orig)
 
         return _correct_np_dtype(xp, dtype, out)
 
@@ -1457,11 +1437,7 @@ def test_irfftn_plan_manager(self, xp, dtype):
             assert get_current_plan() is None
         else:
             out = _fft_module(xp).irfftn(x, s=self.s, axes=self.axes)
-
-        # CUDA 10.1 and above may modify input, this fails for complex64
-        if (cp.cuda.runtime.runtimeGetVersion() <= 10000 or
-                dtype not in [np.complex64, np.complex128]):
-            testing.assert_array_equal(x, x_orig)
+        testing.assert_array_equal(x, x_orig)
 
         return _correct_np_dtype(xp, dtype, out)