Repository: cupy/cupy

Issue:
[WIP] Support Unnormalized inverse FFT
This pull request aims at fixing #944 

With the `"unnormalized"` mode, FFTs become unnormalized in both directions. This is a deviation from the numpy API.

### Done
- added an extra normalization mode `"unnormalize"` on top of `None` and `"ortho"` for FFTs
- tests updated
- documentation

### Not done yet
- ihfft not supported yet. I can not get it to work, I struggle to fix it, there is something wrong there.
- Should the test suite of `cupyx.scipy.fft` and legacy `cupyx.scipy.fftpack` be updated ?

### to discuss
`"unnormalize"` can be changed to `"unnormalized"` if preferred.


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
@@ -189,11 +189,12 @@ def _exec_fft(a, direction, value_type, norm, axis, overwrite_x,
     sz = out.shape[-1]
     if fft_type == cufft.CUFFT_R2C or fft_type == cufft.CUFFT_D2Z:
         sz = n
-    if norm is None:
-        if direction == cufft.CUFFT_INVERSE:
-            out /= sz
-    else:
+    if norm == 'backward' and direction == cufft.CUFFT_INVERSE:
+        out /= sz
+    elif norm == 'ortho':
         out /= math.sqrt(sz)
+    elif norm == 'forward' and direction == cufft.CUFFT_FORWARD:
+        out /= sz
 
     if axis % a.ndim != a.ndim - 1:
         out = out.swapaxes(axis, -1)
@@ -209,15 +210,10 @@ def _fft_c2c(a, direction, norm, axes, overwrite_x, plan=None):
 
 def _fft(a, s, axes, norm, direction, value_type='C2C', overwrite_x=False,
          plan=None):
-    if isinstance(a, np.ndarray):
+    if not isinstance(a, cupy.ndarray):
         raise TypeError('The input array a must be a cupy.ndarray')
-    if norm not in (None, 'ortho'):
-        raise ValueError('Invalid norm value %s, should be None or "ortho".'
-                         % norm)
-
     if (s is not None) and (axes is not None) and len(s) != len(axes):
         raise ValueError('Shape and axes have different lengths.')
-
     if axes is None:
         if s is None:
             dim = a.ndim
@@ -231,6 +227,14 @@ def _fft(a, s, axes, norm, direction, value_type='C2C', overwrite_x=False,
             return a
         else:
             raise IndexError('list index out of range')
+    if norm is None:  # for backward compatibility
+        norm = 'backward'
+    # it is important that we check norm after validating axes for NumPy
+    # compatibility: if axes=(), early return is triggered and norm is not
+    # checked...
+    if norm not in ('backward', 'ortho', 'forward'):
+        raise ValueError('Invalid norm value %s, should be "backward", '
+                         '"ortho", or "forward".' % norm)
     a = _convert_dtype(a, value_type)
     a = _cook_shape(a, s, axes, value_type)
 
@@ -553,22 +557,25 @@ def _exec_fftn(a, direction, value_type, norm, axes, overwrite_x,
     # normalize by the product of the shape along the transformed 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
-    else:
+    if norm == 'backward' and direction == cufft.CUFFT_INVERSE:
+        out /= sz
+    elif norm == 'ortho':
         out /= math.sqrt(sz)
+    elif norm == 'forward' and direction == cufft.CUFFT_FORWARD:
+        out /= sz
 
     return out
 
 
 def _fftn(a, s, axes, norm, direction, value_type='C2C', order='A', plan=None,
           overwrite_x=False, out=None):
-    if isinstance(a, np.ndarray):
+    if not isinstance(a, cupy.ndarray):
         raise TypeError('The input array a must be a cupy.ndarray')
-    if norm not in (None, 'ortho'):
-        raise ValueError('Invalid norm value %s, should be None or "ortho".'
-                         % norm)
+    if norm is None:  # for backward compatibility
+        norm = 'backward'
+    if norm not in ('backward', 'ortho', 'forward'):
+        raise ValueError('Invalid norm value %s, should be "backward", '
+                         '"ortho", or "forward".' % norm)
 
     axes, axes_sorted = _prep_fftn_axes(a.ndim, s, axes, value_type)
     if not axes_sorted:
@@ -657,7 +664,9 @@ def fft(a, n=None, axis=-1, norm=None):
             is not given, the length of the input along the axis specified by
             ``axis`` is used.
         axis (int): Axis over which to compute the FFT.
-        norm (None or ``"ortho"``): Keyword to specify the normalization mode.
+        norm (``"backward"``, ``"ortho"``, or ``"forward"``): Optional keyword
+            to specify the normalization mode. Default is ``None``, which is
+            an alias of ``"backward"``.
 
     Returns:
         cupy.ndarray:
@@ -678,7 +687,9 @@ def ifft(a, n=None, axis=-1, norm=None):
             is not given, the length of the input along the axis specified by
             ``axis`` is used.
         axis (int): Axis over which to compute the FFT.
-        norm (None or ``"ortho"``): Keyword to specify the normalization mode.
+        norm (``"backward"``, ``"ortho"``, or ``"forward"``): Optional keyword
+            to specify the normalization mode. Default is ``None``, which is
+            an alias of ``"backward"``.
 
     Returns:
         cupy.ndarray:
@@ -699,7 +710,9 @@ def fft2(a, s=None, axes=(-2, -1), norm=None):
             output. If ``s`` is not given, the lengths of the input along the
             axes specified by ``axes`` are used.
         axes (tuple of ints): Axes over which to compute the FFT.
-        norm (None or ``"ortho"``): Keyword to specify the normalization mode.
+        norm (``"backward"``, ``"ortho"``, or ``"forward"``): Optional keyword
+            to specify the normalization mode. Default is ``None``, which is
+            an alias of ``"backward"``.
 
     Returns:
         cupy.ndarray:
@@ -721,7 +734,9 @@ def ifft2(a, s=None, axes=(-2, -1), norm=None):
             output. If ``s`` is not given, the lengths of the input along the
             axes specified by ``axes`` are used.
         axes (tuple of ints): Axes over which to compute the FFT.
-        norm (None or ``"ortho"``): Keyword to specify the normalization mode.
+        norm (``"backward"``, ``"ortho"``, or ``"forward"``): Optional keyword
+            to specify the normalization mode. Default is ``None``, which is
+            an alias of ``"backward"``.
 
     Returns:
         cupy.ndarray:
@@ -743,7 +758,9 @@ def fftn(a, s=None, axes=None, norm=None):
             output. If ``s`` is not given, the lengths of the input along the
             axes specified by ``axes`` are used.
         axes (tuple of ints): Axes over which to compute the FFT.
-        norm (None or ``"ortho"``): Keyword to specify the normalization mode.
+        norm (``"backward"``, ``"ortho"``, or ``"forward"``): Optional keyword
+            to specify the normalization mode. Default is ``None``, which is
+            an alias of ``"backward"``.
 
     Returns:
         cupy.ndarray:
@@ -765,7 +782,9 @@ def ifftn(a, s=None, axes=None, norm=None):
             output. If ``s`` is not given, the lengths of the input along the
             axes specified by ``axes`` are used.
         axes (tuple of ints): Axes over which to compute the FFT.
-        norm (None or ``"ortho"``): Keyword to specify the normalization mode.
+        norm (``"backward"``, ``"ortho"``, or ``"forward"``): Optional keyword
+            to specify the normalization mode. Default is ``None``, which is
+            an alias of ``"backward"``.
 
     Returns:
         cupy.ndarray:
@@ -787,7 +806,9 @@ def rfft(a, n=None, axis=-1, norm=None):
             input to use. If ``n`` is not given, the length of the input along
             the axis specified by ``axis`` is used.
         axis (int): Axis over which to compute the FFT.
-        norm (None or ``"ortho"``): Keyword to specify the normalization mode.
+        norm (``"backward"``, ``"ortho"``, or ``"forward"``): Optional keyword
+            to specify the normalization mode. Default is ``None``, which is
+            an alias of ``"backward"``.
 
     Returns:
         cupy.ndarray:
@@ -810,7 +831,9 @@ def irfft(a, n=None, axis=-1, norm=None):
             ``n`` is not given, it is determined from the length of the input
             along the axis specified by ``axis``.
         axis (int): Axis over which to compute the FFT.
-        norm (None or ``"ortho"``): Keyword to specify the normalization mode.
+        norm (``"backward"``, ``"ortho"``, or ``"forward"``): Optional keyword
+            to specify the normalization mode. Default is ``None``, which is
+            an alias of ``"backward"``.
 
     Returns:
         cupy.ndarray:
@@ -833,7 +856,9 @@ def rfft2(a, s=None, axes=(-2, -1), norm=None):
             given, the lengths of the input along the axes specified by
             ``axes`` are used.
         axes (tuple of ints): Axes over which to compute the FFT.
-        norm (None or ``"ortho"``): Keyword to specify the normalization mode.
+        norm (``"backward"``, ``"ortho"``, or ``"forward"``): Optional keyword
+            to specify the normalization mode. Default is ``None``, which is
+            an alias of ``"backward"``.
 
     Returns:
         cupy.ndarray:
@@ -856,7 +881,9 @@ def irfft2(a, s=None, axes=(-2, -1), norm=None):
             they are determined from the lengths of the input along the axes
             specified by ``axes``.
         axes (tuple of ints): Axes over which to compute the FFT.
-        norm (None or ``"ortho"``): Keyword to specify the normalization mode.
+        norm (``"backward"``, ``"ortho"``, or ``"forward"``): Optional keyword
+            to specify the normalization mode. Default is ``None``, which is
+            an alias of ``"backward"``.
 
     Returns:
         cupy.ndarray:
@@ -881,7 +908,9 @@ def rfftn(a, s=None, axes=None, norm=None):
             given, the lengths of the input along the axes specified by
             ``axes`` are used.
         axes (tuple of ints): Axes over which to compute the FFT.
-        norm (None or ``"ortho"``): Keyword to specify the normalization mode.
+        norm (``"backward"``, ``"ortho"``, or ``"forward"``): Optional keyword
+            to specify the normalization mode. Default is ``None``, which is
+            an alias of ``"backward"``.
 
     Returns:
         cupy.ndarray:
@@ -913,7 +942,9 @@ def irfftn(a, s=None, axes=None, norm=None):
             they are determined from the lengths of the input along the axes
             specified by ``axes``.
         axes (tuple of ints): Axes over which to compute the FFT.
-        norm (None or ``"ortho"``): Keyword to specify the normalization mode.
+        norm (``"backward"``, ``"ortho"``, or ``"forward"``): Optional keyword
+            to specify the normalization mode. Default is ``None``, which is
+            an alias of ``"backward"``.
 
     Returns:
         cupy.ndarray:
@@ -935,6 +966,17 @@ def irfftn(a, s=None, axes=None, norm=None):
     return func(a, s, axes, norm, cufft.CUFFT_INVERSE, 'C2R')
 
 
+def _swap_direction(norm):
+    if norm in (None, 'backward'):
+        norm = 'forward'
+    elif norm == 'forward':
+        norm = 'backward'
+    elif norm != 'ortho':
+        raise ValueError('Invalid norm value %s; should be "backward", '
+                         '"ortho", or "forward".' % norm)
+    return norm
+
+
 def hfft(a, n=None, axis=-1, norm=None):
     """Compute the FFT of a signal that has Hermitian symmetry.
 
@@ -945,7 +987,9 @@ def hfft(a, n=None, axis=-1, norm=None):
             ``n`` is not given, it is determined from the length of the input
             along the axis specified by ``axis``.
         axis (int): Axis over which to compute the FFT.
-        norm (None or ``"ortho"``): Keyword to specify the normalization mode.
+        norm (``"backward"``, ``"ortho"``, or ``"forward"``): Optional keyword
+            to specify the normalization mode. Default is ``None``, which is
+            an alias of ``"backward"``.
 
     Returns:
         cupy.ndarray:
@@ -956,9 +1000,7 @@ def hfft(a, n=None, axis=-1, norm=None):
 
     .. seealso:: :func:`numpy.fft.hfft`
     """
-    a = irfft(a.conj(), n, axis)
-    return a * (a.shape[axis] if norm is None else
-                cupy.sqrt(a.shape[axis], dtype=a.dtype))
+    return irfft(a.conj(), n, axis, _swap_direction(norm))
 
 
 def ihfft(a, n=None, axis=-1, norm=None):
@@ -970,7 +1012,9 @@ def ihfft(a, n=None, axis=-1, norm=None):
             input to use. If ``n`` is not given, the length of the input along
             the axis specified by ``axis`` is used.
         axis (int): Axis over which to compute the FFT.
-        norm (None or ``"ortho"``): Keyword to specify the normalization mode.
+        norm (``"backward"``, ``"ortho"``, or ``"forward"``): Optional keyword
+            to specify the normalization mode. Default is ``None``, which is
+            an alias of ``"backward"``.
 
     Returns:
         cupy.ndarray:
@@ -980,9 +1024,7 @@ def ihfft(a, n=None, axis=-1, norm=None):
 
     .. seealso:: :func:`numpy.fft.ihfft`
     """
-    if n is None:
-        n = a.shape[axis]
-    return rfft(a, n, axis, norm).conj() / (n if norm is None else 1)
+    return rfft(a, n, axis, _swap_direction(norm)).conj()
 
 
 def fftfreq(n, d=1.0):


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
@@ -11,6 +11,13 @@
 from cupy.testing.helper import _wraps_partial
 
 
+@pytest.fixture
+def skip_forward_backward(request):
+    if request.instance.norm in ('backward', 'forward'):
+        if not (np.lib.NumpyVersion(np.__version__) >= '1.20.0'):
+            pytest.skip('forward/backward is supported by NumPy 1.20+')
+
+
 def nd_planning_states(states=[True, False], name='enable_nd'):
     """Decorator for parameterized tests with and wihout nd planning
 
@@ -86,10 +93,11 @@ def test_func(self, *args, **kw):
     return decorator
 
 
+@pytest.mark.usefixtures('skip_forward_backward')
 @testing.parameterize(*testing.product({
     'n': [None, 0, 5, 10, 15],
     'shape': [(0,), (10, 0), (10,), (10, 10)],
-    'norm': [None, 'ortho', ''],
+    'norm': [None, 'backward', 'ortho', 'forward', ''],
 }))
 @testing.gpu
 class TestFft:
@@ -101,7 +109,7 @@ def test_fft(self, xp, dtype):
         a = testing.shaped_random(self.shape, xp, dtype)
         out = xp.fft.fft(a, n=self.n, norm=self.norm)
 
-        # np.fft.fft alway returns np.complex128
+        # np.fft.fft always returns np.complex128
         if xp is np and dtype in [np.float16, np.float32, np.complex64]:
             out = out.astype(np.complex64)
 
@@ -140,7 +148,7 @@ def test_fft(self, xp, dtype):
             a = xp.asfortranarray(a)
         out = xp.fft.fft(a, axis=self.axis)
 
-        # np.fft.fft alway returns np.complex128
+        # np.fft.fft always returns np.complex128
         if xp is np and dtype in [np.float16, np.float32, np.complex64]:
             out = out.astype(np.complex64)
 
@@ -175,10 +183,11 @@ def _skip_multi_gpu_bug(shape, gpus):
 # Almost identical to the TestFft class, except that
 # 1. multi-GPU cuFFT is used
 # 2. the tested parameter combinations are adjusted to meet the requirements
+@pytest.mark.usefixtures('skip_forward_backward')
 @testing.parameterize(*testing.product({
     'n': [None, 0, 64],
     'shape': [(0,), (0, 10), (64,), (4, 64)],
-    'norm': [None, 'ortho', ''],
+    'norm': [None, 'backward', 'ortho', 'forward', ''],
 }))
 @testing.multi_gpu(2)
 @pytest.mark.skipif(cupy.cuda.runtime.is_hip,
@@ -195,7 +204,7 @@ def test_fft(self, xp, dtype):
         a = testing.shaped_random(self.shape, xp, dtype)
         out = xp.fft.fft(a, n=self.n, norm=self.norm)
 
-        # np.fft.fft alway returns np.complex128
+        # np.fft.fft always returns np.complex128
         if xp is np and dtype is np.complex64:
             out = out.astype(dtype)
 
@@ -214,7 +223,7 @@ def test_ifft(self, xp, dtype):
         a = testing.shaped_random(self.shape, xp, dtype)
         out = xp.fft.ifft(a, n=self.n, norm=self.norm)
 
-        # np.fft.fft alway returns np.complex128
+        # np.fft.fft always returns np.complex128
         if xp is np and dtype is np.complex64:
             out = out.astype(dtype)
 
@@ -245,7 +254,7 @@ def test_fft(self, xp, dtype):
             a = xp.asfortranarray(a)
         out = xp.fft.fft(a, axis=self.axis)
 
-        # np.fft.fft alway returns np.complex128
+        # np.fft.fft always returns np.complex128
         if xp is np and dtype is np.complex64:
             out = out.astype(dtype)
 
@@ -263,7 +272,7 @@ def test_ifft(self, xp, dtype):
             a = xp.asfortranarray(a)
         out = xp.fft.ifft(a, axis=self.axis)
 
-        # np.fft.fft alway returns np.complex128
+        # np.fft.fft always returns np.complex128
         if xp is np and dtype is np.complex64:
             out = out.astype(dtype)
 
@@ -359,31 +368,36 @@ def test_fft_allocate(self):
         cupy.get_default_memory_pool().free_all_blocks()
 
 
-@testing.parameterize(
-    {'shape': (3, 4), 's': None, 'axes': None, 'norm': None},
-    {'shape': (3, 4), 's': (1, None), 'axes': None, 'norm': None},
-    {'shape': (3, 4), 's': (1, 5), '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': (3, 4), 's': None, 'axes': (), 'norm': None},
-    {'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), 's': None, 'axes': (), 'norm': None},
-    {'shape': (2, 3, 4), 's': (2, 3), 'axes': (0, 1, 2), 'norm': 'ortho'},
-    {'shape': (2, 3, 4, 5), 's': None, 'axes': None, 'norm': None},
-    {'shape': (0, 5), 's': None, 'axes': None, 'norm': None},
-    {'shape': (2, 0, 5), 's': None, 'axes': None, 'norm': None},
-    {'shape': (0, 0, 5), 's': None, 'axes': None, 'norm': None},
-    {'shape': (3, 4), 's': (0, 5), 'axes': None, 'norm': None},
-    {'shape': (3, 4), 's': (1, 0), 'axes': None, 'norm': None},
-)
+@pytest.mark.usefixtures('skip_forward_backward')
+@testing.parameterize(*(
+    testing.product_dict([
+        {'shape': (3, 4), 's': None, 'axes': None},
+        {'shape': (3, 4), 's': (1, None), 'axes': None},
+        {'shape': (3, 4), 's': (1, 5), 'axes': None},
+        {'shape': (3, 4), 's': None, 'axes': (-2, -1)},
+        {'shape': (3, 4), 's': None, 'axes': (-1, -2)},
+        {'shape': (3, 4), 's': None, 'axes': (0,)},
+        {'shape': (3, 4), 's': None, 'axes': None},
+        {'shape': (3, 4), 's': None, 'axes': ()},
+        {'shape': (2, 3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4), 's': (1, 4, None), 'axes': None},
+        {'shape': (2, 3, 4), 's': (1, 4, 10), 'axes': None},
+        {'shape': (2, 3, 4), 's': None, 'axes': (-3, -2, -1)},
+        {'shape': (2, 3, 4), 's': None, 'axes': (-1, -2, -3)},
+        {'shape': (2, 3, 4), 's': None, 'axes': (0, 1)},
+        {'shape': (2, 3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4), 's': None, 'axes': ()},
+        {'shape': (2, 3, 4), 's': (2, 3), 'axes': (0, 1, 2)},
+        {'shape': (2, 3, 4, 5), 's': None, 'axes': None},
+        {'shape': (0, 5), 's': None, 'axes': None},
+        {'shape': (2, 0, 5), 's': None, 'axes': None},
+        {'shape': (0, 0, 5), 's': None, 'axes': None},
+        {'shape': (3, 4), 's': (0, 5), 'axes': None},
+        {'shape': (3, 4), 's': (1, 0), 'axes': None},
+    ],
+        testing.product({'norm': [None, 'backward', 'ortho', 'forward', '']})
+    )
+))
 @testing.gpu
 class TestFft2:
 
@@ -430,32 +444,37 @@ def test_ifft2(self, xp, dtype, order, enable_nd):
         return out
 
 
-@testing.parameterize(
-    {'shape': (3, 4), 's': None, 'axes': None, 'norm': None},
-    {'shape': (3, 4), 's': (1, None), 'axes': None, 'norm': None},
-    {'shape': (3, 4), 's': (1, 5), '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': [-1, -2], 'norm': None},
-    {'shape': (3, 4), 's': None, 'axes': (0,), 'norm': None},
-    {'shape': (3, 4), 's': None, 'axes': (), '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': (-1, -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), 's': None, 'axes': (), 'norm': 'ortho'},
-    {'shape': (2, 3, 4), 's': (2, 3), 'axes': (0, 1, 2), 'norm': 'ortho'},
-    {'shape': (2, 3, 4), 's': (4, 3, 2), 'axes': (2, 0, 1), 'norm': 'ortho'},
-    {'shape': (2, 3, 4, 5), 's': None, 'axes': None, 'norm': None},
-    {'shape': (0, 5), 's': None, 'axes': None, 'norm': None},
-    {'shape': (2, 0, 5), 's': None, 'axes': None, 'norm': None},
-    {'shape': (0, 0, 5), 's': None, 'axes': None, 'norm': None},
-)
+@pytest.mark.usefixtures('skip_forward_backward')
+@testing.parameterize(*(
+    testing.product_dict([
+        {'shape': (3, 4), 's': None, 'axes': None},
+        {'shape': (3, 4), 's': (1, None), 'axes': None},
+        {'shape': (3, 4), 's': (1, 5), 'axes': None},
+        {'shape': (3, 4), 's': None, 'axes': (-2, -1)},
+        {'shape': (3, 4), 's': None, 'axes': (-1, -2)},
+        {'shape': (3, 4), 's': None, 'axes': [-1, -2]},
+        {'shape': (3, 4), 's': None, 'axes': (0,)},
+        {'shape': (3, 4), 's': None, 'axes': ()},
+        {'shape': (3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4), 's': (1, 4, None), 'axes': None},
+        {'shape': (2, 3, 4), 's': (1, 4, 10), 'axes': None},
+        {'shape': (2, 3, 4), 's': None, 'axes': (-3, -2, -1)},
+        {'shape': (2, 3, 4), 's': None, 'axes': (-1, -2, -3)},
+        {'shape': (2, 3, 4), 's': None, 'axes': (-1, -3)},
+        {'shape': (2, 3, 4), 's': None, 'axes': (0, 1)},
+        {'shape': (2, 3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4), 's': None, 'axes': ()},
+        {'shape': (2, 3, 4), 's': (2, 3), 'axes': (0, 1, 2)},
+        {'shape': (2, 3, 4), 's': (4, 3, 2), 'axes': (2, 0, 1)},
+        {'shape': (2, 3, 4, 5), 's': None, 'axes': None},
+        {'shape': (0, 5), 's': None, 'axes': None},
+        {'shape': (2, 0, 5), 's': None, 'axes': None},
+        {'shape': (0, 0, 5), 's': None, 'axes': None},
+    ],
+        testing.product({'norm': [None, 'backward', 'ortho', 'forward', '']})
+    )
+))
 @testing.gpu
 class TestFftn:
 
@@ -502,25 +521,30 @@ def test_ifftn(self, xp, dtype, order, enable_nd):
         return out
 
 
-@testing.parameterize(
-    {'shape': (3, 4), 's': None, 'axes': None, 'norm': None},
-    {'shape': (3, 4), 's': (1, 5), '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, 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), 's': (2, 3), 'axes': None, 'norm': 'ortho'},
-    {'shape': (2, 3, 4), 's': (2, 3), 'axes': (0, 1, 2), 'norm': 'ortho'},
-    {'shape': (0, 5), 's': None, 'axes': None, 'norm': None},
-    {'shape': (2, 0, 5), 's': None, 'axes': None, 'norm': None},
-    {'shape': (0, 0, 5), 's': None, 'axes': None, 'norm': None},
-)
+@pytest.mark.usefixtures('skip_forward_backward')
+@testing.parameterize(*(
+    testing.product_dict([
+        {'shape': (3, 4), 's': None, 'axes': None},
+        {'shape': (3, 4), 's': (1, 5), 'axes': None},
+        {'shape': (3, 4), 's': None, 'axes': (-2, -1)},
+        {'shape': (3, 4), 's': None, 'axes': (-1, -2)},
+        {'shape': (3, 4), 's': None, 'axes': (0,)},
+        {'shape': (3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4), 's': (1, 4, 10), 'axes': None},
+        {'shape': (2, 3, 4), 's': None, 'axes': (-3, -2, -1)},
+        {'shape': (2, 3, 4), 's': None, 'axes': (-1, -2, -3)},
+        {'shape': (2, 3, 4), 's': None, 'axes': (0, 1)},
+        {'shape': (2, 3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4), 's': (2, 3), 'axes': None},
+        {'shape': (2, 3, 4), 's': (2, 3), 'axes': (0, 1, 2)},
+        {'shape': (0, 5), 's': None, 'axes': None},
+        {'shape': (2, 0, 5), 's': None, 'axes': None},
+        {'shape': (0, 0, 5), 's': None, 'axes': None},
+    ],
+        testing.product({'norm': [None, 'backward', 'ortho', 'forward']})
+    )
+))
 @testing.gpu
 class TestPlanCtxManagerFftn:
 
@@ -529,9 +553,9 @@ def skip_buggy(self):
         if cupy.cuda.runtime.is_hip:
             # TODO(leofang): test newer ROCm versions
             if (self.axes == (0, 1) and self.shape == (2, 3, 4)):
-                raise pytest.skip("hipFFT's PlanNd for this case "
-                                  "is buggy, so Plan1d is generated "
-                                  "instead")
+                pytest.skip("hipFFT's PlanNd for this case "
+                            "is buggy, so Plan1d is generated "
+                            "instead")
 
     @nd_planning_states()
     @testing.for_complex_dtypes()
@@ -607,10 +631,11 @@ def test_fftn_error_on_wrong_plan(self, dtype, enable_nd):
         assert 'The cuFFT plan and a.shape do not match' in str(ex.value)
 
 
+@pytest.mark.usefixtures('skip_forward_backward')
 @testing.parameterize(*testing.product({
     'n': [None, 5, 10, 15],
     'shape': [(10,), ],
-    'norm': [None, 'ortho'],
+    'norm': [None, 'backward', 'ortho', 'forward'],
 }))
 @testing.gpu
 class TestPlanCtxManagerFft:
@@ -630,7 +655,7 @@ def test_fft(self, xp, dtype):
         else:
             out = xp.fft.fft(a, n=self.n, norm=self.norm)
 
-        # np.fft.fft alway returns np.complex128
+        # np.fft.fft always returns np.complex128
         if xp is np and dtype is np.complex64:
             out = out.astype(np.complex64)
 
@@ -678,15 +703,17 @@ def test_fft_error_on_wrong_plan(self, dtype):
 # Almost identical to the TestPlanCtxManagerFft class, except that
 # 1. multi-GPU cuFFT is used
 # 2. the tested parameter combinations are adjusted to meet the requirements
+@pytest.mark.usefixtures('skip_forward_backward')
 @testing.parameterize(*testing.product({
     'n': [None, 64],
     'shape': [(64,), (128,)],
-    'norm': [None, 'ortho'],
+    'norm': [None, 'backward', 'ortho', 'forward', ''],
 }))
 @testing.multi_gpu(2)
 @pytest.mark.skipif(cupy.cuda.runtime.is_hip,
                     reason='hipFFT does not support multi-GPU FFT')
 class TestMultiGpuPlanCtxManagerFft:
+
     @multi_gpu_config(gpu_configs=[[0, 1], [1, 0]])
     @testing.for_complex_dtypes()
     @testing.numpy_cupy_allclose(rtol=1e-4, atol=1e-7, accept_error=ValueError,
@@ -705,7 +732,7 @@ def test_fft(self, xp, dtype):
         else:
             out = xp.fft.fft(a, n=self.n, norm=self.norm)
 
-        # np.fft.fft alway returns np.complex128
+        # np.fft.fft always returns np.complex128
         if xp is np and dtype is np.complex64:
             out = out.astype(np.complex64)
 
@@ -751,23 +778,32 @@ def test_fft_error_on_wrong_plan(self, dtype):
         with pytest.raises(ValueError) as ex, plan_wrong:
             fft(a, n=self.n, norm=self.norm)
         # targeting a particular error
+        if self.norm == '':
+            # if norm is invalid, we still get ValueError, but it's raised
+            # when checking norm, earlier than the plan check
+            return  # skip
         assert 'Target array size does not match the plan.' in str(ex.value)
 
 
-@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': (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': (-3, -2, -1), 'norm': None},
-)
+@pytest.mark.usefixtures('skip_forward_backward')
+@testing.parameterize(*(
+    testing.product_dict([
+        {'shape': (3, 4), 's': None, 'axes': None},
+        {'shape': (3, 4), 's': None, 'axes': (-2, -1)},
+        {'shape': (3, 4), 's': None, 'axes': (-1, -2)},
+        {'shape': (3, 4), 's': None, 'axes': (0,)},
+        {'shape': (3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4), 's': (1, 4, None), 'axes': None},
+        {'shape': (2, 3, 4), 's': (1, 4, 10), 'axes': None},
+        {'shape': (2, 3, 4), 's': None, 'axes': (-3, -2, -1)},
+        {'shape': (2, 3, 4), 's': None, 'axes': (-1, -2, -3)},
+        {'shape': (2, 3, 4), 's': None, 'axes': (0, 1)},
+        {'shape': (2, 3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4, 5), 's': None, 'axes': (-3, -2, -1)},
+    ],
+        testing.product({'norm': [None, 'backward', 'ortho', 'forward', '']})
+    )
+))
 @testing.gpu
 class TestFftnContiguity:
 
@@ -809,16 +845,18 @@ def test_ifftn_orders(self, dtype, enable_nd):
                 pass
 
 
+@pytest.mark.usefixtures('skip_forward_backward')
 @testing.parameterize(*testing.product({
     'n': [None, 5, 10, 15],
     'shape': [(10,), (10, 10)],
-    'norm': [None, 'ortho'],
+    'norm': [None, 'backward', 'ortho', 'forward', ''],
 }))
 @testing.gpu
 class TestRfft:
 
     @testing.for_all_dtypes(no_complex=True)
-    @testing.numpy_cupy_allclose(rtol=1e-4, atol=1e-7, contiguous_check=False)
+    @testing.numpy_cupy_allclose(rtol=1e-4, atol=1e-7, accept_error=ValueError,
+                                 contiguous_check=False)
     def test_rfft(self, xp, dtype):
         a = testing.shaped_random(self.shape, xp, dtype)
         out = xp.fft.rfft(a, n=self.n, norm=self.norm)
@@ -829,7 +867,8 @@ def test_rfft(self, xp, dtype):
         return out
 
     @testing.for_all_dtypes()
-    @testing.numpy_cupy_allclose(rtol=1e-4, atol=1e-7, contiguous_check=False)
+    @testing.numpy_cupy_allclose(rtol=1e-4, atol=1e-7, accept_error=ValueError,
+                                 contiguous_check=False)
     def test_irfft(self, xp, dtype):
         a = testing.shaped_random(self.shape, xp, dtype)
         out = xp.fft.irfft(a, n=self.n, norm=self.norm)
@@ -840,10 +879,11 @@ def test_irfft(self, xp, dtype):
         return out
 
 
+@pytest.mark.usefixtures('skip_forward_backward')
 @testing.parameterize(*testing.product({
     'n': [None, 5, 10, 15],
     'shape': [(10,)],
-    'norm': [None, 'ortho'],
+    'norm': [None, 'backward', 'ortho', 'forward'],
 }))
 @testing.gpu
 class TestPlanCtxManagerRfft:
@@ -853,6 +893,7 @@ class TestPlanCtxManagerRfft:
                                  contiguous_check=False)
     def test_rfft(self, xp, dtype):
         a = testing.shaped_random(self.shape, xp, dtype)
+
         if xp is cupy:
             from cupyx.scipy.fftpack import get_fft_plan
             shape = (self.n,) if self.n is not None else None
@@ -908,24 +949,29 @@ def test_rfft_error_on_wrong_plan(self, dtype):
         assert 'Target array size does not match the plan.' in str(ex.value)
 
 
-@testing.parameterize(
-    {'shape': (3, 4), 's': None, 'axes': None, 'norm': None},
-    {'shape': (3, 4), 's': (1, None), 'axes': None, 'norm': None},
-    {'shape': (3, 4), 's': (1, 5), '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), 's': (2, 3), 'axes': (0, 1, 2), 'norm': 'ortho'},
-    {'shape': (2, 3, 4, 5), 's': None, 'axes': None, 'norm': None},
-)
+@pytest.mark.usefixtures('skip_forward_backward')
+@testing.parameterize(*(
+    testing.product_dict([
+        {'shape': (3, 4), 's': None, 'axes': None},
+        {'shape': (3, 4), 's': (1, None), 'axes': None},
+        {'shape': (3, 4), 's': (1, 5), 'axes': None},
+        {'shape': (3, 4), 's': None, 'axes': (-2, -1)},
+        {'shape': (3, 4), 's': None, 'axes': (-1, -2)},
+        {'shape': (3, 4), 's': None, 'axes': (0,)},
+        {'shape': (3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4), 's': (1, 4, None), 'axes': None},
+        {'shape': (2, 3, 4), 's': (1, 4, 10), 'axes': None},
+        {'shape': (2, 3, 4), 's': None, 'axes': (-3, -2, -1)},
+        {'shape': (2, 3, 4), 's': None, 'axes': (-1, -2, -3)},
+        {'shape': (2, 3, 4), 's': None, 'axes': (0, 1)},
+        {'shape': (2, 3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4), 's': (2, 3), 'axes': (0, 1, 2)},
+        {'shape': (2, 3, 4, 5), 's': None, 'axes': None},
+    ],
+        testing.product({'norm': [None, 'backward', 'ortho', 'forward', '']})
+    )
+))
 @testing.gpu
 class TestRfft2:
 
@@ -990,24 +1036,29 @@ def test_irfft2(self, dtype):
                 xp.fft.irfft2(a, s=self.s, axes=self.axes, norm=self.norm)
 
 
-@testing.parameterize(
-    {'shape': (3, 4), 's': None, 'axes': None, 'norm': None},
-    {'shape': (3, 4), 's': (1, None), 'axes': None, 'norm': None},
-    {'shape': (3, 4), 's': (1, 5), '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), 's': (2, 3), 'axes': (0, 1, 2), 'norm': 'ortho'},
-    {'shape': (2, 3, 4, 5), 's': None, 'axes': None, 'norm': None},
-)
+@pytest.mark.usefixtures('skip_forward_backward')
+@testing.parameterize(*(
+    testing.product_dict([
+        {'shape': (3, 4), 's': None, 'axes': None},
+        {'shape': (3, 4), 's': (1, None), 'axes': None},
+        {'shape': (3, 4), 's': (1, 5), 'axes': None},
+        {'shape': (3, 4), 's': None, 'axes': (-2, -1)},
+        {'shape': (3, 4), 's': None, 'axes': (-1, -2)},
+        {'shape': (3, 4), 's': None, 'axes': (0,)},
+        {'shape': (3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4), 's': (1, 4, None), 'axes': None},
+        {'shape': (2, 3, 4), 's': (1, 4, 10), 'axes': None},
+        {'shape': (2, 3, 4), 's': None, 'axes': (-3, -2, -1)},
+        {'shape': (2, 3, 4), 's': None, 'axes': (-1, -2, -3)},
+        {'shape': (2, 3, 4), 's': None, 'axes': (0, 1)},
+        {'shape': (2, 3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4), 's': (2, 3), 'axes': (0, 1, 2)},
+        {'shape': (2, 3, 4, 5), 's': None, 'axes': None},
+    ],
+        testing.product({'norm': [None, 'backward', 'ortho', 'forward', '']})
+    )
+))
 @testing.gpu
 class TestRfftn:
 
@@ -1053,21 +1104,26 @@ def test_irfftn(self, xp, dtype, order, enable_nd):
 
 
 # Only those tests in which a legit plan can be obtained are kept
-@testing.parameterize(
-    {'shape': (3, 4), 's': None, 'axes': None, 'norm': None},
-    {'shape': (3, 4), 's': (1, None), 'axes': None, 'norm': None},
-    {'shape': (3, 4), 's': (1, 5), 'axes': None, 'norm': None},
-    {'shape': (3, 4), 's': None, 'axes': (-2, -1), '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': (0, 1), 'norm': None},
-    {'shape': (2, 3, 4), 's': None, 'axes': None, 'norm': 'ortho'},
-    {'shape': (2, 3, 4), 's': (2, 3), 'axes': (0, 1, 2), 'norm': 'ortho'},
-)
+@pytest.mark.usefixtures('skip_forward_backward')
+@testing.parameterize(*(
+    testing.product_dict([
+        {'shape': (3, 4), 's': None, 'axes': None},
+        {'shape': (3, 4), 's': (1, None), 'axes': None},
+        {'shape': (3, 4), 's': (1, 5), 'axes': None},
+        {'shape': (3, 4), 's': None, 'axes': (-2, -1)},
+        {'shape': (3, 4), 's': None, 'axes': (0,)},
+        {'shape': (3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4), 's': (1, 4, None), 'axes': None},
+        {'shape': (2, 3, 4), 's': (1, 4, 10), 'axes': None},
+        {'shape': (2, 3, 4), 's': None, 'axes': (-3, -2, -1)},
+        {'shape': (2, 3, 4), 's': None, 'axes': (0, 1)},
+        {'shape': (2, 3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4), 's': (2, 3), 'axes': (0, 1, 2)},
+    ],
+        testing.product({'norm': [None, 'backward', 'ortho', 'forward', '']})
+    )
+))
 @testing.gpu
 class TestPlanCtxManagerRfftn:
 
@@ -1126,21 +1182,26 @@ def test_irfftn(self, xp, dtype, enable_nd):
     # TODO(leofang): write test_rfftn_error_on_wrong_plan()?
 
 
-@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},
-)
+@pytest.mark.usefixtures('skip_forward_backward')
+@testing.parameterize(*(
+    testing.product_dict([
+        {'shape': (3, 4), 's': None, 'axes': None},
+        {'shape': (3, 4), 's': None, 'axes': (-2, -1)},
+        {'shape': (3, 4), 's': None, 'axes': (-1, -2)},
+        {'shape': (3, 4), 's': None, 'axes': (0,)},
+        {'shape': (3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4), 's': (1, 4, None), 'axes': None},
+        {'shape': (2, 3, 4), 's': (1, 4, 10), 'axes': None},
+        {'shape': (2, 3, 4), 's': None, 'axes': (-3, -2, -1)},
+        {'shape': (2, 3, 4), 's': None, 'axes': (-1, -2, -3)},
+        {'shape': (2, 3, 4), 's': None, 'axes': (0, 1)},
+        {'shape': (2, 3, 4), 's': None, 'axes': None},
+        {'shape': (2, 3, 4, 5), 's': None, 'axes': None},
+    ],
+        testing.product({'norm': [None, 'backward', 'ortho', 'forward', '']})
+    )
+))
 @testing.gpu
 class TestRfftnContiguity:
 
@@ -1206,16 +1267,18 @@ def test_irfftn(self, dtype):
                 xp.fft.irfftn(a, s=self.s, axes=self.axes, norm=self.norm)
 
 
+@pytest.mark.usefixtures('skip_forward_backward')
 @testing.parameterize(*testing.product({
     'n': [None, 5, 10, 15],
     'shape': [(10,), (10, 10)],
-    'norm': [None, 'ortho'],
+    'norm': [None, 'backward', 'ortho', 'forward', ''],
 }))
 @testing.gpu
 class TestHfft:
 
     @testing.for_all_dtypes()
-    @testing.numpy_cupy_allclose(rtol=1e-4, atol=1e-7, contiguous_check=False)
+    @testing.numpy_cupy_allclose(rtol=1e-4, atol=1e-7, accept_error=ValueError,
+                                 contiguous_check=False)
     def test_hfft(self, xp, dtype):
         a = testing.shaped_random(self.shape, xp, dtype)
         out = xp.fft.hfft(a, n=self.n, norm=self.norm)
@@ -1226,7 +1289,8 @@ def test_hfft(self, xp, dtype):
         return out
 
     @testing.for_all_dtypes(no_complex=True)
-    @testing.numpy_cupy_allclose(rtol=1e-4, atol=1e-7, contiguous_check=False)
+    @testing.numpy_cupy_allclose(rtol=1e-4, atol=1e-7, accept_error=ValueError,
+                                 contiguous_check=False)
     def test_ihfft(self, xp, dtype):
         a = testing.shaped_random(self.shape, xp, dtype)
         out = xp.fft.ihfft(a, n=self.n, norm=self.norm)