Repository: cupy/cupy

Issue:
cufftPlanMany() argument support in cupyx.scipy.fftpack.get_fft_plan
I'm looking for the ability to do multiple overlapped FFTs on a 1D array of data. With cufftPlanMany() function in cuFFT I can set the istride/ostride and idist/odist arguments to accomplish this. I can also set the type to R2C, C2R, C2C (and other datatype equivalents).

I appreciate that cupyx.scipy.fftpack.get_fft_plan gives me the ability to set a plan prior to running multiple FFTs. However, it doesn't seem to give the same functionality as cufftPlanMany does. It is also currently restricted to C2C or Z2Z. Can get_fft_plan be expanded to give the user the same options as cufftPlanMany offers?


Discussion:

Resolving patch:
diff --git a/cupy/fft/fft.py b/cupy/fft/fft.py
--- a/cupy/fft/fft.py
+++ b/cupy/fft/fft.py
@@ -1,4 +1,3 @@
-import copy
 import functools
 import math
 import warnings
@@ -9,6 +8,7 @@
 from cupy.cuda import cufft
 from cupy.fft import config
 
+
 _reduce = functools.reduce
 _prod = cupy.core.internal.prod
 
@@ -71,8 +71,10 @@ def _convert_fft_type(a, value_type):
         return cufft.CUFFT_Z2Z
     elif value_type == 'R2C' and a.dtype == np.float64:
         return cufft.CUFFT_D2Z
-    else:
+    elif value_type == 'C2R' and a.dtype == np.complex128:
         return cufft.CUFFT_Z2D
+    else:
+        raise ValueError
 
 
 def _exec_fft(a, direction, value_type, norm, axis, overwrite_x,
@@ -104,7 +106,7 @@ def _exec_fft(a, direction, value_type, norm, axis, overwrite_x,
         if not isinstance(plan, cufft.Plan1d):
             raise ValueError('expected plan to have type cufft.Plan1d')
         if fft_type != plan.fft_type:
-            raise ValueError('CUFFT plan dtype mismatch.')
+            raise ValueError('cuFFT plan dtype mismatch.')
         if out_size != plan.nx:
             raise ValueError('Target array size does not match the plan.')
         if batch != plan.batch:
@@ -179,8 +181,9 @@ def _fft(a, s, axes, norm, direction, value_type='C2C', overwrite_x=False,
     elif value_type == 'R2C':
         a = _exec_fft(a, direction, value_type, norm, axes[-1], overwrite_x)
         a = _fft_c2c(a, direction, norm, axes[:-1], overwrite_x)
-    else:
+    else:  # C2R
         a = _fft_c2c(a, direction, norm, axes[:-1], overwrite_x)
+        # _cook_shape tells us input shape only, and no output shape
         if (s is None) or (s[-1] is None):
             out_size = a.shape[axes[-1]] * 2 - 2
         else:
@@ -191,7 +194,7 @@ def _fft(a, s, axes, norm, direction, value_type='C2C', overwrite_x=False,
     return a
 
 
-def _prep_fftn_axes(ndim, s=None, axes=None):
+def _prep_fftn_axes(ndim, s=None, axes=None, value_type='C2C'):
     """Configure axes argument for an n-dimensional FFT.
 
     The axes to be transformed are returned in ascending order.
@@ -214,7 +217,14 @@ def _prep_fftn_axes(ndim, s=None, axes=None):
             return (), ()
         if _reduce(min, axes) < -ndim or _reduce(max, axes) > ndim - 1:
             raise ValueError("The specified axes exceed the array dimensions.")
-        axes_sorted = tuple(sorted([ax % ndim for ax in axes]))
+        if value_type == 'C2C':
+            axes_sorted = tuple(sorted([ax % ndim for ax in axes]))
+        else:  # C2R or R2C
+            # The last axis is special, need to isolate it and append
+            # to the rest of (sorted) axes
+            axes_sorted = sorted([ax % ndim for ax in axes[:-1]])
+            axes_sorted.append(axes[-1] % ndim)
+            axes_sorted = tuple(axes_sorted)
 
     # unsorted axes for _cook_shape, sorted ones are otherwise used
     return axes, axes_sorted
@@ -230,35 +240,41 @@ def _nd_plan_is_possible(axes_sorted, ndim):
                 for n in range(len(axes_sorted) - 1)]))
 
 
-def _get_cufft_plan_nd(shape, fft_type, axes=None, order='C'):
+def _get_cufft_plan_nd(shape, fft_type, axes=None, order='C', out_size=None):
     """Generate a CUDA FFT plan for transforming up to three axes.
 
     Args:
         shape (tuple of int): The shape of the array to transform
-        fft_type ({cufft.CUFFT_C2C, cufft.CUFFT_Z2Z}): The FFT type to perform.
-            Currently only complex-to-complex transforms are supported.
+        fft_type (int): The FFT type to perform. Supported values are:
+            `cufft.CUFFT_C2C`, `cufft.CUFFT_C2R`, `cufft.CUFFT_R2C`,
+            `cufft.CUFFT_Z2Z`, `cufft.CUFFT_Z2D`, and `cufft.CUFFT_D2Z`.
         axes (None or int or tuple of int):  The axes of the array to
             transform. Currently, these must be a set of up to three adjacent
             axes and must include either the first or the last axis of the
             array.  If `None`, it is assumed that all axes are transformed.
         order ({'C', 'F'}): Specify whether the data to be transformed has C or
             Fortran ordered data layout.
+        out_size (int): The output length along the last axis for R2C/C2R FFTs.
+            For C2C FFT, this is ignored (and set to `None`).
 
     Returns:
-        plan (cufft.PlanNd): The CUFFT Plan. This can be used with
-            cufft.fft.fftn or cufft.fft.ifftn.
+        plan (cufft.PlanNd): A cuFFT Plan for the chosen `fft_type`.
     """
     ndim = len(shape)
 
-    if fft_type not in [cufft.CUFFT_C2C, cufft.CUFFT_Z2Z]:
-        raise NotImplementedError(
-            'Only cufft.CUFFT_C2C and cufft.CUFFT_Z2Z are supported.')
+    if fft_type in (cufft.CUFFT_C2C, cufft.CUFFT_Z2Z):
+        value_type = 'C2C'
+    elif fft_type in (cufft.CUFFT_C2R, cufft.CUFFT_Z2D):
+        value_type = 'C2R'
+    else:  # CUFFT_R2C or CUFFT_D2Z
+        value_type = 'R2C'
 
     if axes is None:
         # transform over all axes
         fft_axes = tuple(range(ndim))
     else:
-        _, fft_axes = _prep_fftn_axes(ndim, s=None, axes=axes)
+        _, fft_axes = _prep_fftn_axes(ndim, s=None, axes=axes,
+                                      value_type=value_type)
 
     if not _nd_plan_is_possible(fft_axes, ndim):
         raise ValueError(
@@ -286,33 +302,43 @@ def _get_cufft_plan_nd(shape, fft_type, axes=None, order='C'):
     input[b * idist + ((x * inembed[1] + y) * inembed[2] + z) * istride]
     output[b * odist + ((x * onembed[1] + y) * onembed[2] + z) * ostride]
     """
+    # At this point, _default_fft_func() guarantees that for F-order arrays
+    # we only need to consider C2C, and not C2R or R2C.
+    # TODO(leofang): figure out if we really have to skip F-order?
+    in_dimensions = [shape[d] for d in fft_axes]
+    if order == 'F':
+        in_dimensions = in_dimensions[::-1]
+    in_dimensions = tuple(in_dimensions)
+    if fft_type in (cufft.CUFFT_C2C, cufft.CUFFT_Z2Z):
+        out_dimensions = in_dimensions
+        plan_dimensions = in_dimensions
+    else:
+        out_dimensions = list(in_dimensions)
+        if out_size is not None:  # for C2R & R2C
+            out_dimensions[-1] = out_size  # only valid for C order!
+        out_dimensions = tuple(out_dimensions)
+        if fft_type in (cufft.CUFFT_R2C, cufft.CUFFT_D2Z):
+            plan_dimensions = in_dimensions
+        else:  # CUFFT_C2R or CUFFT_Z2D
+            plan_dimensions = out_dimensions
+    inembed = in_dimensions
+    onembed = out_dimensions
+
     if fft_axes == tuple(range(ndim)):
         # tranfsorm over all axes
-        plan_dimensions = copy.copy(shape)
-        if order == 'F':
-            plan_dimensions = plan_dimensions[::-1]
-        idist = np.intp(_prod(shape))
-        odist = np.intp(_prod(shape))
-        istride = ostride = 1
-        inembed = onembed = None
         nbatch = 1
+        idist = odist = 1  # doesn't matter since nbatch = 1
+        istride = ostride = 1
     else:
-        plan_dimensions = []
-        for d in range(ndim):
-            if d in fft_axes:
-                plan_dimensions.append(shape[d])
-        plan_dimensions = tuple(plan_dimensions)
-        if order == 'F':
-            plan_dimensions = plan_dimensions[::-1]
-        inembed = onembed = plan_dimensions
+        # batch along the first or the last axis
         if 0 not in fft_axes:
             # don't FFT along the first min_axis_fft axes
             min_axis_fft = _reduce(min, fft_axes)
             nbatch = _prod(shape[:min_axis_fft])
             if order == 'C':
                 # C-ordered GPU array with batch along first dim
-                idist = _prod(plan_dimensions)
-                odist = _prod(plan_dimensions)
+                idist = _prod(in_dimensions)
+                odist = _prod(out_dimensions)
                 istride = 1
                 ostride = 1
             elif order == 'F':
@@ -333,8 +359,8 @@ def _get_cufft_plan_nd(shape, fft_type, axes=None, order='C'):
                 ostride = nbatch
             elif order == 'F':
                 # F-ordered GPU array with batch along last dim
-                idist = _prod(plan_dimensions)
-                odist = _prod(plan_dimensions)
+                idist = _prod(in_dimensions)
+                odist = _prod(out_dimensions)
                 istride = 1
                 ostride = 1
         else:
@@ -344,23 +370,24 @@ def _get_cufft_plan_nd(shape, fft_type, axes=None, order='C'):
                 'spatial axes).')
 
     plan = cufft.PlanNd(shape=plan_dimensions,
-                        istride=istride,
-                        ostride=ostride,
                         inembed=inembed,
-                        onembed=onembed,
+                        istride=istride,
                         idist=idist,
+                        onembed=onembed,
+                        ostride=ostride,
                         odist=odist,
                         fft_type=fft_type,
-                        batch=nbatch)
+                        batch=nbatch,
+                        order=order,
+                        last_axis=fft_axes[-1],
+                        last_size=out_size)
     return plan
 
 
 def _exec_fftn(a, direction, value_type, norm, axes, overwrite_x,
-               plan=None, out=None):
+               plan=None, out=None, out_size=None):
 
     fft_type = _convert_fft_type(a, value_type)
-    if fft_type not in [cufft.CUFFT_C2C, cufft.CUFFT_Z2Z]:
-        raise NotImplementedError('Only C2C and Z2Z are supported.')
 
     if a.flags.c_contiguous:
         order = 'C'
@@ -375,10 +402,15 @@ def _exec_fftn(a, direction, value_type, norm, axes, overwrite_x,
         # don't check repeated usage; it's done in _default_fft_func()
     if plan is None:
         # generate a plan
-        plan = _get_cufft_plan_nd(a.shape, fft_type, axes=axes, order=order)
+        plan = _get_cufft_plan_nd(a.shape, fft_type, axes=axes, order=order,
+                                  out_size=out_size)
     else:
         if not isinstance(plan, cufft.PlanNd):
             raise ValueError('expected plan to have type cufft.PlanNd')
+        if order != plan.order:
+            raise ValueError('array orders mismatch (plan: {}, input: {})'
+                             .format(plan.order, order))
+        # TODO(leofang): this shape check needs to be modified for C2R
         if a.flags.c_contiguous:
             expected_shape = tuple(a.shape[ax] for ax in axes)
         else:
@@ -386,13 +418,19 @@ def _exec_fftn(a, direction, value_type, norm, axes, overwrite_x,
             expected_shape = tuple(a.shape[ax] for ax in axes[::-1])
         if expected_shape != plan.shape:
             raise ValueError(
-                'The CUFFT plan and a.shape do not match: '
+                'The cuFFT plan and a.shape do not match: '
                 'plan.shape = {}, expected_shape={}, a.shape = {}'.format(
                     plan.shape, expected_shape, a.shape))
         if fft_type != plan.fft_type:
-            raise ValueError('CUFFT plan dtype mismatch.')
-        # TODO: also check the strides and axes of the plan?
-
+            raise ValueError('cuFFT plan dtype mismatch.')
+        if value_type != 'C2C':
+            if axes[-1] != plan.last_axis:
+                raise ValueError('The last axis for R2C/C2R mismatch')
+            if out_size != plan.last_size:
+                raise ValueError('The size along the last R2C/C2R axis '
+                                 'mismatch')
+
+    # TODO(leofang): support in-place transform for R2C/C2R
     if overwrite_x and value_type == 'C2C':
         out = a
     elif out is None:
@@ -402,7 +440,8 @@ def _exec_fftn(a, direction, value_type, norm, axes, overwrite_x,
     plan.fft(a, out, direction)
 
     # normalize by the product of the shape along the transformed axes
-    sz = _prod([out.shape[ax] for ax in axes])
+    arr = a if fft_type in (cufft.CUFFT_R2C, cufft.CUFFT_D2Z) else out
+    sz = _prod([arr.shape[ax] for ax in axes])
     if norm is None:
         if direction == cufft.CUFFT_INVERSE:
             out /= sz
@@ -418,7 +457,7 @@ def _fftn(a, s, axes, norm, direction, value_type='C2C', order='A', plan=None,
         raise ValueError('Invalid norm value %s, should be None or "ortho".'
                          % norm)
 
-    axes, axes_sorted = _prep_fftn_axes(a.ndim, s, axes)
+    axes, axes_sorted = _prep_fftn_axes(a.ndim, s, axes, value_type)
     if not axes_sorted:
         if value_type == 'C2C':
             return a
@@ -437,7 +476,7 @@ def _fftn(a, s, axes, norm, direction, value_type='C2C', order='A', plan=None,
     elif order not in ['C', 'F']:
         raise ValueError('Unsupported order: {}'.format(order))
 
-    # Note: need to call_cook_shape prior to sorting the axes
+    # Note: need to call _cook_shape prior to sorting the axes
     a = _cook_shape(a, s, axes, value_type, order=order)
 
     if order == 'C' and not a.flags.c_contiguous:
@@ -445,12 +484,24 @@ def _fftn(a, s, axes, norm, direction, value_type='C2C', order='A', plan=None,
     elif order == 'F' and not a.flags.f_contiguous:
         a = cupy.asfortranarray(a)
 
+    # _cook_shape tells us input shape only, and no output shape
+    if value_type == 'C2R':
+        if (s is None) or (s[-1] is None):
+            out_size = 2 * (a.shape[axes_sorted[-1]] - 1)
+        else:
+            out_size = s[-1]
+    elif value_type == 'R2C':
+        out_size = a.shape[axes_sorted[-1]] // 2 + 1
+    else:  # C2C
+        out_size = None
+
     a = _exec_fftn(a, direction, value_type, norm=norm, axes=axes_sorted,
-                   overwrite_x=overwrite_x, plan=plan, out=out)
+                   overwrite_x=overwrite_x, plan=plan, out=out,
+                   out_size=out_size)
     return a
 
 
-def _default_fft_func(a, s=None, axes=None, plan=None):
+def _default_fft_func(a, s=None, axes=None, plan=None, value_type='C2C'):
     curr_plan = cufft.get_current_plan()
     if curr_plan is not None:
         if plan is None:
@@ -465,7 +516,11 @@ def _default_fft_func(a, s=None, axes=None, plan=None):
           a.ndim == 1 or not config.enable_nd_planning):
         return _fft
 
-    _, axes_sorted = _prep_fftn_axes(a.ndim, s, axes)
+    # cuFFT's N-D C2R/R2C transforms may not agree with NumPy's outcomes
+    if a.flags.f_contiguous and value_type != 'C2C':
+        return _fft
+
+    _, axes_sorted = _prep_fftn_axes(a.ndim, s, axes, value_type)
     if len(axes_sorted) > 1 and _nd_plan_is_possible(axes_sorted, a.ndim):
         # prefer Plan1D in the 1D case
         return _fftn
@@ -667,7 +722,8 @@ def rfft2(a, s=None, axes=(-2, -1), norm=None):
 
     .. seealso:: :func:`numpy.fft.rfft2`
     """
-    return _fft(a, s, axes, norm, cufft.CUFFT_FORWARD, 'R2C')
+    func = _default_fft_func(a, s, axes, value_type='R2C')
+    return func(a, s, axes, norm, cufft.CUFFT_FORWARD, 'R2C')
 
 
 def irfft2(a, s=None, axes=(-2, -1), norm=None):
@@ -691,7 +747,8 @@ def irfft2(a, s=None, axes=(-2, -1), norm=None):
 
     .. seealso:: :func:`numpy.fft.irfft2`
     """
-    return _fft(a, s, axes, norm, cufft.CUFFT_INVERSE, 'C2R')
+    func = _default_fft_func(a, s, axes, value_type='C2R')
+    return func(a, s, axes, norm, cufft.CUFFT_INVERSE, 'C2R')
 
 
 def rfftn(a, s=None, axes=None, norm=None):
@@ -713,7 +770,8 @@ def rfftn(a, s=None, axes=None, norm=None):
 
     .. seealso:: :func:`numpy.fft.rfftn`
     """
-    return _fft(a, s, axes, norm, cufft.CUFFT_FORWARD, 'R2C')
+    func = _default_fft_func(a, s, axes, value_type='R2C')
+    return func(a, s, axes, norm, cufft.CUFFT_FORWARD, 'R2C')
 
 
 def _size_last_transform_axis(shape, s, axes):
@@ -752,7 +810,8 @@ def irfftn(a, s=None, axes=None, norm=None):
         warnings.warn('Output of irfftn might not be correct due to issue '
                       'of cuFFT in CUDA 10.1/10.2 on Pascal or older GPUs.')
 
-    return _fft(a, s, axes, norm, cufft.CUFFT_INVERSE, 'C2R')
+    func = _default_fft_func(a, s, axes, value_type='C2R')
+    return func(a, s, axes, norm, cufft.CUFFT_INVERSE, 'C2R')
 
 
 def hfft(a, n=None, axis=-1, norm=None):
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
@@ -6,7 +6,7 @@
 import cupy
 from cupy.cuda import cufft
 from cupy.fft.fft import (_fft, _default_fft_func, hfft as _hfft,
-                          ihfft as _ihfft)
+                          ihfft as _ihfft, _size_last_transform_axis)
 from cupy.fft.fft import fftshift, ifftshift, fftfreq, rfftfreq
 
 from cupyx.scipy.fftpack import get_fft_plan
@@ -420,7 +420,9 @@ def rfftn(x, s=None, axes=None, norm=None, overwrite_x=False, *, plan=None):
         raise NotImplementedError('rfftn plan is currently not yet supported')
     s = _assequence(s)
     axes = _assequence(axes)
-    return _fft(x, s, axes, norm, cufft.CUFFT_FORWARD, 'R2C', overwrite_x)
+    func = _default_fft_func(x, s, axes, value_type='R2C')
+    return func(x, s, axes, norm, cufft.CUFFT_FORWARD, 'R2C',
+                overwrite_x=overwrite_x)
 
 
 @_implements(_scipy_fft.irfftn)
@@ -452,7 +454,14 @@ def irfftn(x, s=None, axes=None, norm=None, overwrite_x=False, *, plan=None):
         raise NotImplementedError('irfftn plan is currently not yet supported')
     s = _assequence(s)
     axes = _assequence(axes)
-    return _fft(x, s, axes, norm, cufft.CUFFT_INVERSE, 'C2R', overwrite_x)
+    if (10020 >= cupy.cuda.runtime.runtimeGetVersion() >= 10010
+            and int(cupy.cuda.device.get_compute_capability()) < 70
+            and _size_last_transform_axis(x.shape, s, axes) == 2):
+        warnings.warn('Output of irfftn might not be correct due to issue '
+                      'of cuFFT in CUDA 10.1/10.2 on Pascal or older GPUs.')
+    func = _default_fft_func(x, s, axes, value_type='C2R')
+    return func(x, s, axes, norm, cufft.CUFFT_INVERSE, 'C2R',
+                overwrite_x=overwrite_x)
 
 
 @_implements(_scipy_fft.hfft)
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
@@ -96,6 +96,7 @@ def get_fft_plan(a, shape=None, axes=None, value_type='C2C'):
             shape[axis] = s
     shape = tuple(shape)
 
+    # TODO(leofang): support R2C/C2R transforms
     # check value_type
     fft_type = _convert_fft_type(a, value_type)
     if n > 1 and fft_type not in [cufft.CUFFT_C2C, cufft.CUFFT_Z2Z]:


Test patch:
diff --git a/tests/cupy_tests/fft_tests/test_fft.py b/tests/cupy_tests/fft_tests/test_fft.py
--- a/tests/cupy_tests/fft_tests/test_fft.py
+++ b/tests/cupy_tests/fft_tests/test_fft.py
@@ -7,7 +7,8 @@
 import cupy
 from cupy import testing
 from cupy.fft import config
-from cupy.fft.fft import _default_fft_func, _fft, _fftn
+from cupy.fft.fft import (_default_fft_func, _fft, _fftn,
+                          _size_last_transform_axis)
 
 
 def nd_planning_states(states=[True, False], name='enable_nd'):
@@ -83,15 +84,6 @@ def test_func(self, *args, **kw):
     return decorator
 
 
-def _size_last_transform_axis(shape, s, axes):
-    if s is not None:
-        if s[-1] is not None:
-            return s[-1]
-    elif axes is not None:
-        return shape[axes[-1]]
-    return shape[-1]
-
-
 @testing.parameterize(*testing.product({
     'n': [None, 0, 5, 10, 15],
     'shape': [(10,), (10, 10)],
@@ -175,7 +167,6 @@ def test_ifft(self, xp, dtype):
     'shape': [(64,), (4, 64)],
     'norm': [None, 'ortho', ''],
 }))
-@testing.with_requires('numpy>=1.10.0')
 @testing.multi_gpu(2)
 class TestMultiGpuFft(unittest.TestCase):
     @multi_gpu_config(gpu_configs=[[0, 1], [1, 0]])
@@ -218,7 +209,6 @@ def test_ifft(self, xp, dtype):
     'data_order': ['F', 'C'],
     'axis': [0, 1, -1],
 }))
-@testing.with_requires('numpy>=1.10.0')
 @testing.multi_gpu(2)
 class TestMultiGpuFftOrder(unittest.TestCase):
     @multi_gpu_config(gpu_configs=[[0, 1], [1, 0]])
@@ -259,7 +249,7 @@ class TestDefaultPlanType(unittest.TestCase):
 
     @nd_planning_states()
     def test_default_fft_func(self, enable_nd):
-        # test cases where nd CUFFT plan is possible
+        # test cases where nd cuFFT plan is possible
         ca = cupy.ones((16, 16, 16))
         for axes in [(0, 1), (1, 2), None, (0, 1, 2)]:
             fft_func = _default_fft_func(ca, axes=axes)
@@ -282,6 +272,32 @@ def test_default_fft_func(self, enable_nd):
         # first or last axis not included -> nd plan not possible
         assert _default_fft_func(ca, axes=(1, )) is _fft
 
+        # for rfftn
+        ca = cupy.random.random((4, 2, 6))
+        for s, axes in zip([(3, 4), None, (8, 7, 5)],
+                           [(-2, -1), (0, 1), None]):
+            fft_func = _default_fft_func(ca, s=s, axes=axes, value_type='R2C')
+            if enable_nd:
+                assert fft_func is _fftn
+            else:
+                assert fft_func is _fft
+
+        # nd plan not possible if last axis is not 0 or ndim-1
+        assert _default_fft_func(ca, axes=(2, 1), value_type='R2C') is _fft
+
+        # for irfftn
+        ca = cupy.random.random((4, 2, 6)).astype(cupy.complex128)
+        for s, axes in zip([(3, 4), None, (8, 7, 5)],
+                           [(-2, -1), (0, 1), None]):
+            fft_func = _default_fft_func(ca, s=s, axes=axes, value_type='C2R')
+            if enable_nd:
+                assert fft_func is _fftn
+            else:
+                assert fft_func is _fft
+
+        # nd plan not possible if last axis is not 0 or ndim-1
+        assert _default_fft_func(ca, axes=(2, 1), value_type='C2R') is _fft
+
 
 @testing.gpu
 @testing.slow
@@ -326,13 +342,20 @@ def test_fft_allocate(self):
 class TestFft2(unittest.TestCase):
 
     @nd_planning_states()
+    @testing.for_orders('CF')
     @testing.for_all_dtypes()
     @testing.numpy_cupy_allclose(rtol=1e-4, atol=1e-7, accept_error=ValueError,
                                  contiguous_check=False)
-    def test_fft2(self, xp, dtype, enable_nd):
+    def test_fft2(self, xp, dtype, order, enable_nd):
         assert config.enable_nd_planning == enable_nd
         a = testing.shaped_random(self.shape, xp, dtype)
-        out = xp.fft.fft2(a, s=self.s, norm=self.norm)
+        if order == 'F':
+            a = xp.asfortranarray(a)
+        out = xp.fft.fft2(a, s=self.s, axes=self.axes, norm=self.norm)
+
+        if self.axes is not None and not self.axes:
+            assert out is a
+            return out
 
         if xp is np and dtype in [np.float16, np.float32, np.complex64]:
             out = out.astype(np.complex64)
@@ -340,13 +363,20 @@ def test_fft2(self, xp, dtype, enable_nd):
         return out
 
     @nd_planning_states()
+    @testing.for_orders('CF')
     @testing.for_all_dtypes()
     @testing.numpy_cupy_allclose(rtol=1e-4, atol=1e-7, accept_error=ValueError,
                                  contiguous_check=False)
-    def test_ifft2(self, xp, dtype, enable_nd):
+    def test_ifft2(self, xp, dtype, order, enable_nd):
         assert config.enable_nd_planning == enable_nd
         a = testing.shaped_random(self.shape, xp, dtype)
-        out = xp.fft.ifft2(a, s=self.s, norm=self.norm)
+        if order == 'F':
+            a = xp.asfortranarray(a)
+        out = xp.fft.ifft2(a, s=self.s, axes=self.axes, norm=self.norm)
+
+        if self.axes is not None and not self.axes:
+            assert out is a
+            return out
 
         if xp is np and dtype in [np.float16, np.float32, np.complex64]:
             out = out.astype(np.complex64)
@@ -381,12 +411,15 @@ def test_ifft2(self, xp, dtype, enable_nd):
 class TestFftn(unittest.TestCase):
 
     @nd_planning_states()
+    @testing.for_orders('CF')
     @testing.for_all_dtypes()
     @testing.numpy_cupy_allclose(rtol=1e-4, atol=1e-7, accept_error=ValueError,
                                  contiguous_check=False)
-    def test_fftn(self, xp, dtype, enable_nd):
+    def test_fftn(self, xp, dtype, order, enable_nd):
         assert config.enable_nd_planning == enable_nd
         a = testing.shaped_random(self.shape, xp, dtype)
+        if order == 'F':
+            a = xp.asfortranarray(a)
         out = xp.fft.fftn(a, s=self.s, axes=self.axes, norm=self.norm)
 
         if self.axes is not None and not self.axes:
@@ -399,12 +432,15 @@ def test_fftn(self, xp, dtype, enable_nd):
         return out
 
     @nd_planning_states()
+    @testing.for_orders('CF')
     @testing.for_all_dtypes()
     @testing.numpy_cupy_allclose(rtol=1e-4, atol=1e-7, accept_error=ValueError,
                                  contiguous_check=False)
-    def test_ifftn(self, xp, dtype, enable_nd):
+    def test_ifftn(self, xp, dtype, order, enable_nd):
         assert config.enable_nd_planning == enable_nd
         a = testing.shaped_random(self.shape, xp, dtype)
+        if order == 'F':
+            a = xp.asfortranarray(a)
         out = xp.fft.ifftn(a, s=self.s, axes=self.axes, norm=self.norm)
 
         if self.axes is not None and not self.axes:
@@ -505,7 +541,7 @@ def test_fftn_error_on_wrong_plan(self, dtype, enable_nd):
         with pytest.raises(ValueError) as ex, plan_wrong:
             fftn(a, s=self.s, axes=self.axes, norm=self.norm)
         # targeting a particular error
-        assert 'The CUFFT plan and a.shape do not match' in str(ex.value)
+        assert 'The cuFFT plan and a.shape do not match' in str(ex.value)
 
 
 @testing.parameterize(*testing.product({
@@ -584,7 +620,6 @@ def test_fft_error_on_wrong_plan(self, dtype):
     'shape': [(64,), (128,)],
     'norm': [None, 'ortho'],
 }))
-@testing.with_requires('numpy>=1.10.0')
 @testing.multi_gpu(2)
 class TestMultiGpuPlanCtxManagerFft(unittest.TestCase):
     @multi_gpu_config(gpu_configs=[[0, 1], [1, 0]])
@@ -762,28 +797,39 @@ def test_irfft(self, xp, dtype):
 @testing.gpu
 class TestRfft2(unittest.TestCase):
 
+    @nd_planning_states()
+    @testing.for_orders('CF')
     @testing.for_all_dtypes(no_complex=True)
     @testing.numpy_cupy_allclose(rtol=1e-4, atol=1e-7, accept_error=ValueError,
                                  contiguous_check=False)
-    def test_rfft2(self, xp, dtype):
-        # the scaling of old Numpy is incorrect
-        if np.__version__ < np.lib.NumpyVersion('1.13.0'):
-            if self.s is not None:
-                return xp.empty(0)
-
+    def test_rfft2(self, xp, dtype, order, enable_nd):
+        assert config.enable_nd_planning == enable_nd
         a = testing.shaped_random(self.shape, xp, dtype)
-        out = xp.fft.rfft2(a, s=self.s, norm=self.norm)
+        if order == 'F':
+            a = xp.asfortranarray(a)
+        out = xp.fft.rfft2(a, s=self.s, axes=self.axes, norm=self.norm)
 
         if xp is np and dtype in [np.float16, np.float32, np.complex64]:
             out = out.astype(np.complex64)
         return out
 
+    @nd_planning_states()
+    @testing.for_orders('CF')
     @testing.for_all_dtypes()
     @testing.numpy_cupy_allclose(rtol=1e-4, atol=1e-7, accept_error=ValueError,
                                  contiguous_check=False)
-    def test_irfft2(self, xp, dtype):
+    def test_irfft2(self, xp, dtype, order, enable_nd):
+        assert config.enable_nd_planning == enable_nd
+        if (10020 >= cupy.cuda.runtime.runtimeGetVersion() >= 10010
+                and int(cupy.cuda.device.get_compute_capability()) < 70
+                and _size_last_transform_axis(
+                    self.shape, self.s, self.axes) == 2):
+            raise unittest.SkipTest('work-around for cuFFT issue')
+
         a = testing.shaped_random(self.shape, xp, dtype)
-        out = xp.fft.irfft2(a, s=self.s, norm=self.norm)
+        if order == 'F':
+            a = xp.asfortranarray(a)
+        out = xp.fft.irfft2(a, s=self.s, axes=self.axes, norm=self.norm)
 
         if xp is np and dtype in [np.float16, np.float32, np.complex64]:
             out = out.astype(np.float32)
@@ -833,11 +879,16 @@ def test_irfft2(self, dtype):
 @testing.gpu
 class TestRfftn(unittest.TestCase):
 
+    @nd_planning_states()
+    @testing.for_orders('CF')
     @testing.for_all_dtypes(no_complex=True)
     @testing.numpy_cupy_allclose(rtol=1e-4, atol=1e-7, accept_error=ValueError,
                                  contiguous_check=False)
-    def test_rfftn(self, xp, dtype):
+    def test_rfftn(self, xp, dtype, order, enable_nd):
+        assert config.enable_nd_planning == enable_nd
         a = testing.shaped_random(self.shape, xp, dtype)
+        if order == 'F':
+            a = xp.asfortranarray(a)
         out = xp.fft.rfftn(a, s=self.s, axes=self.axes, norm=self.norm)
 
         if xp is np and dtype in [np.float16, np.float32, np.complex64]:
@@ -845,10 +896,13 @@ def test_rfftn(self, xp, dtype):
 
         return out
 
+    @nd_planning_states()
+    @testing.for_orders('CF')
     @testing.for_all_dtypes()
     @testing.numpy_cupy_allclose(rtol=1e-4, atol=1e-7, accept_error=ValueError,
                                  contiguous_check=False)
-    def test_irfftn(self, xp, dtype):
+    def test_irfftn(self, xp, dtype, order, enable_nd):
+        assert config.enable_nd_planning == enable_nd
         if (10020 >= cupy.cuda.runtime.runtimeGetVersion() >= 10010
                 and int(cupy.cuda.device.get_compute_capability()) < 70
                 and _size_last_transform_axis(
@@ -856,6 +910,8 @@ def test_irfftn(self, xp, dtype):
             raise unittest.SkipTest('work-around for cuFFT issue')
 
         a = testing.shaped_random(self.shape, xp, dtype)
+        if order == 'F':
+            a = xp.asfortranarray(a)
         out = xp.fft.irfftn(a, s=self.s, axes=self.axes, norm=self.norm)
 
         if xp is np and dtype in [np.float16, np.float32, np.complex64]:
@@ -864,6 +920,67 @@ def test_irfftn(self, xp, dtype):
         return out
 
 
+# TODO(leofang): write a TestPlanCtxManagerRfftn class
+
+
+@testing.parameterize(
+    {'shape': (3, 4), 's': None, 'axes': None, 'norm': None},
+    {'shape': (3, 4), 's': None, 'axes': (-2, -1), 'norm': None},
+    {'shape': (3, 4), 's': None, 'axes': (-1, -2), 'norm': None},
+    {'shape': (3, 4), 's': None, 'axes': (0,), 'norm': None},
+    {'shape': (3, 4), 's': None, 'axes': None, 'norm': 'ortho'},
+    {'shape': (2, 3, 4), 's': None, 'axes': None, 'norm': None},
+    {'shape': (2, 3, 4), 's': (1, 4, None), 'axes': None, 'norm': None},
+    {'shape': (2, 3, 4), 's': (1, 4, 10), 'axes': None, 'norm': None},
+    {'shape': (2, 3, 4), 's': None, 'axes': (-3, -2, -1), 'norm': None},
+    {'shape': (2, 3, 4), 's': None, 'axes': (-1, -2, -3), 'norm': None},
+    {'shape': (2, 3, 4), 's': None, 'axes': (0, 1), 'norm': None},
+    {'shape': (2, 3, 4), 's': None, 'axes': None, 'norm': 'ortho'},
+    {'shape': (2, 3, 4, 5), 's': None, 'axes': None, 'norm': None},
+)
+@testing.gpu
+class TestRfftnContiguity(unittest.TestCase):
+
+    @nd_planning_states([True])
+    @testing.for_float_dtypes()
+    def test_rfftn_orders(self, dtype, enable_nd):
+        for order in ['C', 'F']:
+            a = testing.shaped_random(self.shape, cupy, dtype)
+            if order == 'F':
+                a = cupy.asfortranarray(a)
+            out = cupy.fft.rfftn(a, s=self.s, axes=self.axes)
+
+            fft_func = _default_fft_func(a, s=self.s, axes=self.axes,
+                                         value_type='R2C')
+            if fft_func is _fftn:
+                # nd plans have output with contiguity matching the input
+                self.assertEqual(out.flags.c_contiguous, a.flags.c_contiguous)
+                self.assertEqual(out.flags.f_contiguous, a.flags.f_contiguous)
+            else:
+                # 1d planning case doesn't guarantee preserved contiguity
+                pass
+
+    @nd_planning_states([True])
+    @testing.for_all_dtypes()
+    def test_ifftn_orders(self, dtype, enable_nd):
+        for order in ['C', 'F']:
+
+            a = testing.shaped_random(self.shape, cupy, dtype)
+            if order == 'F':
+                a = cupy.asfortranarray(a)
+            out = cupy.fft.irfftn(a, s=self.s, axes=self.axes)
+
+            fft_func = _default_fft_func(a, s=self.s, axes=self.axes,
+                                         value_type='C2R')
+            if fft_func is _fftn:
+                # nd plans have output with contiguity matching the input
+                self.assertEqual(out.flags.c_contiguous, a.flags.c_contiguous)
+                self.assertEqual(out.flags.f_contiguous, a.flags.f_contiguous)
+            else:
+                # 1d planning case doesn't guarantee preserved contiguity
+                pass
+
+
 @testing.parameterize(
     {'shape': (3, 4), 's': None, 'axes': (), 'norm': None},
     {'shape': (2, 3, 4), 's': None, 'axes': (), 'norm': None},