Repository: cupy/cupy

Issue:
Higher order interpolation modes in future (v5.0) map_coordinates function
I realize I am jumping the gun here but I noticed that in the future CuPy v5.0 you aim to implement scipy's map_coordinate function. I am very excited for this as my code spends a lot of its time in that function at present. 

I noticed that you also state that spline orders higher than 1 (linear) will not be supported initially but you aim to implement these higher orders at a later stage. Just putting my hand up now to show that at least one person is interested in this functionality. This library has already given 5-6 fold performance boosts on my ffts. So thanks very much!


Discussion:
Thank you for your comment.
It is good to implement higher order interpolation, however, it is in a difficult situation because SciPy's one will be changed (See scipy/scipy#8465). I think that it is better to implement it after SciPy's one is fixed.
I have started working on this. So far, I have implemented the spline prefiltering functions `spline_filter1d` and `spline_filter` using `RawKernel`. These now pass the test cases from SciPy.

The remaining step is performing the higher order interpolation, but this should be a relatively straightforward extension to the kernels proposed in #3166.
Did this make it into the project? Im really interested in using a cupy CubicSpline. Awesome work, cupy helps me and my colleagues regularly. 
> Did this make it into the project? Im really interested in using a cupy CubicSpline. Awesome work, cupy helps me and my colleagues regularly.

There is a preliminary implementation available at [cupyimg](https://github.com/mritools/cupyimg), but this is not an officially supported project. The intention is to make PRs here for the handful of things that are currently in `cupyimg.scipy`, but not yet present here in `cupyx.scipy`. We have also made preliminary plans to move the scikit-image related functions from `cupyimg.skimage` to a new `RAPIDS/cuImage` project, although I don't have a timeline for that yet. 

That said, if you want to try out and/or help with this initial implementation, you would just import the desired function from `cupyimg.scipy.ndimage` instead of `cupyx.scipy.ndimage`. The package is not currently on PyPI, so you would have to install directly from GitHub using something like: `python -m pip install git+https://github.com/mritools/cupyimg.git`.

One caveat is that the relative acceleration over SciPy for the nearest and linear interpolation functions as currently available here are substantially greater than the acceleration of spline order 3-5.  I get only around 10x speedup over SciPy for the spline cases and think this may be due to excessive register use in the kernels, but I have not profiled it in detail.  There is probably some further optimization that could be done.

Thank you for a detailed answer, I'll try and implement these and give you feedback on how I go.
Work on fixing the boundary handling upstream in SciPy has begun. See for example: scipy/scipy#12767 and meta-issue scipy/scipy#12773

I plan to make corresponding PRs here in the future.

Resolving patch:
diff --git a/cupyx/scipy/ndimage/_interp_kernels.py b/cupyx/scipy/ndimage/_interp_kernels.py
--- a/cupyx/scipy/ndimage/_interp_kernels.py
+++ b/cupyx/scipy/ndimage/_interp_kernels.py
@@ -3,6 +3,8 @@
 import cupy
 import cupy.core.internal
 
+from cupyx.scipy.ndimage import _spline_prefilter_core
+from cupyx.scipy.ndimage import _spline_kernel_weights
 from cupyx.scipy.ndimage import _util
 
 math_constants_preamble = r'''
@@ -10,8 +12,10 @@
 #include <cupy/math_constants.h>
 '''
 
+spline_weights_inline = _spline_kernel_weights.spline_weights_inline
 
-def _get_coord_map(ndim):
+
+def _get_coord_map(ndim, nprepad=0):
     """Extract target coordinate from coords array (for map_coordinates).
 
     Notes
@@ -33,13 +37,14 @@ def _get_coord_map(ndim):
     """
     ops = []
     ops.append('ptrdiff_t ncoords = _ind.size();')
+    pre = f" + (W){nprepad}" if nprepad > 0 else ''
     for j in range(ndim):
         ops.append(f'''
-    W c_{j} = coords[i + {j} * ncoords];''')
+    W c_{j} = coords[i + {j} * ncoords]{pre};''')
     return ops
 
 
-def _get_coord_zoom_and_shift(ndim):
+def _get_coord_zoom_and_shift(ndim, nprepad=0):
     """Compute target coordinate based on a shift followed by a zoom.
 
     This version zooms from the center of the edge pixels.
@@ -58,13 +63,14 @@ def _get_coord_zoom_and_shift(ndim):
 
     """
     ops = []
+    pre = f" + (W){nprepad}" if nprepad > 0 else ''
     for j in range(ndim):
         ops.append(f'''
-    W c_{j} = zoom[{j}] * ((W)in_coord[{j}] - shift[{j}]);''')
+    W c_{j} = zoom[{j}] * ((W)in_coord[{j}] - shift[{j}]){pre};''')
     return ops
 
 
-def _get_coord_zoom_and_shift_grid(ndim):
+def _get_coord_zoom_and_shift_grid(ndim, nprepad=0):
     """Compute target coordinate based on a shift followed by a zoom.
 
     This version zooms from the outer edges of the grid pixels.
@@ -83,9 +89,10 @@ def _get_coord_zoom_and_shift_grid(ndim):
 
     """
     ops = []
+    pre = f" + (W){nprepad}" if nprepad > 0 else ''
     for j in range(ndim):
         ops.append(f'''
-    W c_{j} = zoom[{j}] * ((W)in_coord[{j}] - shift[j] + 0.5) - 0.5;''')
+    W c_{j} = zoom[{j}] * ((W)in_coord[{j}] - shift[j] + 0.5) - 0.5{pre};''')
     return ops
 
 
@@ -107,9 +114,10 @@ def _get_coord_zoom(ndim, nprepad=0):
 
     """
     ops = []
+    pre = f" + (W){nprepad}" if nprepad > 0 else ''
     for j in range(ndim):
         ops.append(f'''
-    W c_{j} = zoom[{j}] * (W)in_coord[{j}];''')
+    W c_{j} = zoom[{j}] * (W)in_coord[{j}]{pre};''')
     return ops
 
 
@@ -131,9 +139,10 @@ def _get_coord_zoom_grid(ndim, nprepad=0):
 
     """
     ops = []
+    pre = f" + (W){nprepad}" if nprepad > 0 else ''
     for j in range(ndim):
         ops.append(f'''
-    W c_{j} = zoom[{j}] * ((W)in_coord[{j}] + 0.5) - 0.5;''')
+    W c_{j} = zoom[{j}] * ((W)in_coord[{j}] + 0.5) - 0.5{pre};''')
     return ops
 
 
@@ -153,13 +162,14 @@ def _get_coord_shift(ndim, nprepad=0):
 
     """
     ops = []
+    pre = f" + (W){nprepad}" if nprepad > 0 else ''
     for j in range(ndim):
         ops.append(f'''
-    W c_{j} = (W)in_coord[{j}] - shift[{j}];''')
+    W c_{j} = (W)in_coord[{j}] - shift[{j}]{pre};''')
     return ops
 
 
-def _get_coord_affine(ndim):
+def _get_coord_affine(ndim, nprepad=0):
     """Compute target coordinate based on a homogeneous transformation matrix.
 
     The homogeneous matrix has shape (ndim, ndim + 1). It corresponds to
@@ -180,6 +190,7 @@ def _get_coord_affine(ndim):
 
     """
     ops = []
+    pre = f" + (W){nprepad}" if nprepad > 0 else ''
     ncol = ndim + 1
     for j in range(ndim):
         ops.append(f'''
@@ -188,7 +199,7 @@ def _get_coord_affine(ndim):
             ops.append(f'''
             c_{j} += mat[{ncol * j + k}] * (W)in_coord[{k}];''')
         ops.append(f'''
-            c_{j} += mat[{ncol * j + ndim}];''')
+            c_{j} += mat[{ncol * j + ndim}]{pre};''')
     return ops
 
 
@@ -213,7 +224,7 @@ def _unravel_loop_index(shape, uint_t='unsigned int'):
 
 
 def _generate_interp_custom(coord_func, ndim, large_int, yshape, mode, cval,
-                            order, name='', integer_output=False):
+                            order, name='', integer_output=False, nprepad=0):
     """
     Args:
         coord_func (function): generates code to do the coordinate
@@ -226,6 +237,8 @@ def _generate_interp_custom(coord_func, ndim, large_int, yshape, mode, cval,
         name (str): base name for the interpolation kernel
         integer_output (bool): boolean indicating whether the output has an
             integer type.
+        nprepad (int): integer indicating the amount of prepadding at the
+            boundaries.
 
     Returns:
         operation (str): code body for the ElementwiseKernel
@@ -253,7 +266,7 @@ def _generate_interp_custom(coord_func, ndim, large_int, yshape, mode, cval,
     ops.append(_unravel_loop_index(yshape, uint_t))
 
     # compute the transformed (target) coordinates, c_j
-    ops = ops + coord_func(ndim)
+    ops = ops + coord_func(ndim, nprepad)
 
     if cval is numpy.nan:
         cval = 'CUDART_NAN'
@@ -374,6 +387,64 @@ def _generate_interp_custom(coord_func, ndim, large_int, yshape, mode, cval,
                         w_{j} = c_{j} - (W)cf_{j};
                         ic_{j} = cc_bounded_{j} * sx_{j};
                     }}''')
+    elif order > 1:
+        if mode == 'grid-constant':
+            spline_mode = 'constant'
+        elif mode == 'nearest':
+            spline_mode = 'nearest'
+        else:
+            spline_mode = _spline_prefilter_core._get_spline_mode(mode)
+
+        # wx, wy are temporary variables used during spline weight computation
+        ops.append(f'''
+            W wx, wy;
+            {int_t} start;''')
+        for j in range(ndim):
+            # determine weights along the current axis
+            ops.append(f'''
+            W weights_{j}[{order + 1}];''')
+            ops.append(spline_weights_inline[order].format(j=j, order=order))
+
+            # get starting coordinate for spline interpolation along axis j
+            if mode in ['wrap']:
+                ops.append(f'double dcoord = c_{j};')
+                coord_var = 'dcoord'
+                ops.append(
+                    _util._generate_boundary_condition_ops(
+                        mode, coord_var, f'xsize_{j}', int_t, True))
+            else:
+                coord_var = f'(double)c_{j}'
+
+            if order & 1:
+                op_str = '''
+                start = ({int_t})floor({coord_var}) - {order_2};'''
+            else:
+                op_str = '''
+                start = ({int_t})floor({coord_var} + 0.5) - {order_2};'''
+            ops.append(
+                op_str.format(
+                    int_t=int_t, coord_var=coord_var, order_2=order // 2
+                ))
+
+            # set of coordinate values within spline footprint along axis j
+            ops.append(f'''{int_t} ci_{j}[{order + 1}];''')
+            for k in range(order + 1):
+                ixvar = f'ci_{j}[{k}]'
+                ops.append(f'''
+                {ixvar} = start + {k};''')
+                ops.append(
+                    _util._generate_boundary_condition_ops(
+                        spline_mode, ixvar, f'xsize_{j}', int_t))
+
+            # loop over the order + 1 values in the spline filter
+            ops.append(f'''
+            W w_{j};
+            {int_t} ic_{j};
+            for (int k_{j} = 0; k_{j} <= {order}; k_{j}++)
+                {{
+                    w_{j} = weights_{j}[k_{j}];
+                    ic_{j} = ci_{j}[k_{j}] * sx_{j};
+            ''')
 
     if order > 0:
 
@@ -415,7 +486,7 @@ def _generate_interp_custom(coord_func, ndim, large_int, yshape, mode, cval,
 
 @cupy._util.memoize(for_each_device=True)
 def _get_map_kernel(ndim, large_int, yshape, mode, cval=0.0, order=1,
-                    integer_output=False):
+                    integer_output=False, nprepad=0):
     in_params = 'raw X x, raw W coords'
     out_params = 'Y y'
     operation, name = _generate_interp_custom(
@@ -428,6 +499,7 @@ def _get_map_kernel(ndim, large_int, yshape, mode, cval=0.0, order=1,
         order=order,
         name='shift',
         integer_output=integer_output,
+        nprepad=nprepad,
     )
     return cupy.ElementwiseKernel(in_params, out_params, operation, name,
                                   preamble=math_constants_preamble)
@@ -435,7 +507,7 @@ def _get_map_kernel(ndim, large_int, yshape, mode, cval=0.0, order=1,
 
 @cupy._util.memoize(for_each_device=True)
 def _get_shift_kernel(ndim, large_int, yshape, mode, cval=0.0, order=1,
-                      integer_output=False):
+                      integer_output=False, nprepad=0):
     in_params = 'raw X x, raw W shift'
     out_params = 'Y y'
     operation, name = _generate_interp_custom(
@@ -448,6 +520,7 @@ def _get_shift_kernel(ndim, large_int, yshape, mode, cval=0.0, order=1,
         order=order,
         name='shift',
         integer_output=integer_output,
+        nprepad=nprepad,
     )
     return cupy.ElementwiseKernel(in_params, out_params, operation, name,
                                   preamble=math_constants_preamble)
@@ -455,7 +528,7 @@ def _get_shift_kernel(ndim, large_int, yshape, mode, cval=0.0, order=1,
 
 @cupy._util.memoize(for_each_device=True)
 def _get_zoom_shift_kernel(ndim, large_int, yshape, mode, cval=0.0, order=1,
-                           integer_output=False, grid_mode=False):
+                           integer_output=False, grid_mode=False, nprepad=0):
     in_params = 'raw X x, raw W shift, raw W zoom'
     out_params = 'Y y'
     if grid_mode:
@@ -472,6 +545,7 @@ def _get_zoom_shift_kernel(ndim, large_int, yshape, mode, cval=0.0, order=1,
         order=order,
         name="zoom_shift_grid" if grid_mode else "zoom_shift",
         integer_output=integer_output,
+        nprepad=nprepad,
     )
     return cupy.ElementwiseKernel(in_params, out_params, operation, name,
                                   preamble=math_constants_preamble)
@@ -479,7 +553,7 @@ def _get_zoom_shift_kernel(ndim, large_int, yshape, mode, cval=0.0, order=1,
 
 @cupy._util.memoize(for_each_device=True)
 def _get_zoom_kernel(ndim, large_int, yshape, mode, cval=0.0, order=1,
-                     integer_output=False, grid_mode=False):
+                     integer_output=False, grid_mode=False, nprepad=0):
     in_params = 'raw X x, raw W zoom'
     out_params = 'Y y'
     operation, name = _generate_interp_custom(
@@ -492,6 +566,7 @@ def _get_zoom_kernel(ndim, large_int, yshape, mode, cval=0.0, order=1,
         order=order,
         name="zoom_grid" if grid_mode else "zoom",
         integer_output=integer_output,
+        nprepad=nprepad,
     )
     return cupy.ElementwiseKernel(in_params, out_params, operation, name,
                                   preamble=math_constants_preamble)
@@ -499,7 +574,7 @@ def _get_zoom_kernel(ndim, large_int, yshape, mode, cval=0.0, order=1,
 
 @cupy._util.memoize(for_each_device=True)
 def _get_affine_kernel(ndim, large_int, yshape, mode, cval=0.0, order=1,
-                       integer_output=False):
+                       integer_output=False, nprepad=0):
     in_params = 'raw X x, raw W mat'
     out_params = 'Y y'
     operation, name = _generate_interp_custom(
@@ -512,6 +587,7 @@ def _get_affine_kernel(ndim, large_int, yshape, mode, cval=0.0, order=1,
         order=order,
         name='affine',
         integer_output=integer_output,
+        nprepad=nprepad,
     )
     return cupy.ElementwiseKernel(in_params, out_params, operation, name,
                                   preamble=math_constants_preamble)
diff --git a/cupyx/scipy/ndimage/_spline_kernel_weights.py b/cupyx/scipy/ndimage/_spline_kernel_weights.py
new file mode 100644
--- /dev/null
+++ b/cupyx/scipy/ndimage/_spline_kernel_weights.py
@@ -0,0 +1,73 @@
+"""Determination of spline kernel weights (adapted from SciPy)
+
+See more verbose comments for each case there:
+https://github.com/scipy/scipy/blob/eba29d69846ab1299976ff4af71c106188397ccc/scipy/ndimage/src/ni_splines.c#L7   # NOQA
+
+``spline_weights_inline`` is a dict where the key is the spline order and the
+value is the spline weight initialization code.
+"""
+
+spline_weights_inline = {}
+
+# Note: This order = 1 case is currently unused (order = 1 has a different code
+# path in _interp_kernels.py). I think that existing code is a bit more
+# efficient.
+spline_weights_inline[1] = '''
+wx = c_{j} - floor({order} & 1 ? c_{j} : c_{j} + 0.5);
+weights_{j}[0] = 1.0 - wx;
+weights_{j}[1] = wx;
+'''
+
+spline_weights_inline[2] = '''
+wx = c_{j} - floor({order} & 1 ? c_{j} : c_{j} + 0.5);
+weights_{j}[1] = 0.75 - wx * wx;
+wy = 0.5 - wx;
+weights_{j}[0] = 0.5 * wy * wy;
+weights_{j}[2] = 1.0 - weights_{j}[0] - weights_{j}[1];
+'''
+
+spline_weights_inline[3] = '''
+wx = c_{j} - floor({order} & 1 ? c_{j} : c_{j} + 0.5);
+wy = 1.0 - wx;
+weights_{j}[1] = (wx * wx * (wx - 2.0) * 3.0 + 4.0) / 6.0;
+weights_{j}[2] = (wy * wy * (wy - 2.0) * 3.0 + 4.0) / 6.0;
+weights_{j}[0] = wy * wy * wy / 6.0;
+weights_{j}[3] = 1.0 - weights_{j}[0] - weights_{j}[1] - weights_{j}[2];
+'''
+
+spline_weights_inline[4] = '''
+wx = c_{j} - floor({order} & 1 ? c_{j} : c_{j} + 0.5);
+wy = wx * wx;
+weights_{j}[2] = wy * (wy * 0.25 - 0.625) + 115.0 / 192.0;
+wy = 1.0 + wx;
+weights_{j}[1] = wy * (wy * (wy * (5.0 - wy) / 6.0 - 1.25) + 5.0 / 24.0) +
+             55.0 / 96.0;
+wy = 1.0 - wx;
+weights_{j}[3] = wy * (wy * (wy * (5.0 - wy) / 6.0 - 1.25) + 5.0 / 24.0) +
+             55.0 / 96.0;
+wy = 0.5 - wx;
+wy = wy * wy;
+weights_{j}[0] = wy * wy / 24.0;
+weights_{j}[4] = 1.0 - weights_{j}[0] - weights_{j}[1]
+                     - weights_{j}[2] - weights_{j}[3];
+'''
+
+spline_weights_inline[5] = '''
+wx = c_{j} - floor({order} & 1 ? c_{j} : c_{j} + 0.5);
+wy = wx * wx;
+weights_{j}[2] = wy * (wy * (0.25 - wx / 12.0) - 0.5) + 0.55;
+wy = 1.0 - wx;
+wy = wy * wy;
+weights_{j}[3] = wy * (wy * (0.25 - (1.0 - wx) / 12.0) - 0.5) + 0.55;
+wy = wx + 1.0;
+weights_{j}[1] = wy * (wy * (wy * (wy * (wy / 24.0 - 0.375) + 1.25) - 1.75)
+                   + 0.625) + 0.425;
+wy = 2.0 - wx;
+weights_{j}[4] = wy * (wy * (wy * (wy * (wy / 24.0 - 0.375) + 1.25) - 1.75)
+                  + 0.625) + 0.425;
+wy = 1.0 - wx;
+wy = wy * wy;
+weights_{j}[0] = (1.0 - wx) * wy * wy / 120.0;
+weights_{j}[5] = 1.0 - weights_{j}[0] - weights_{j}[1] - weights_{j}[2]
+                     - weights_{j}[3] - weights_{j}[4];
+'''
diff --git a/cupyx/scipy/ndimage/interpolation.py b/cupyx/scipy/ndimage/interpolation.py
--- a/cupyx/scipy/ndimage/interpolation.py
+++ b/cupyx/scipy/ndimage/interpolation.py
@@ -15,16 +15,11 @@
 
 def _check_parameter(func_name, order, mode):
     if order is None:
-        warnings.warn('In the current feature the default order of {} is 1. '
-                      'It is different from scipy.ndimage and can change in '
-                      'the future.'.format(func_name))
+        warnings.warn(f'Currently the default order of {func_name} is 1. In a '
+                      'future release this may change to 3 to match '
+                      'scipy.ndimage ')
     elif order < 0 or 5 < order:
         raise ValueError('spline order is not supported')
-    elif 1 < order:
-        # SciPy supports order 0-5, but CuPy supports only order 0 and 1. Other
-        # orders will be implemented, therefore it raises NotImplementedError
-        # instead of ValueError.
-        raise NotImplementedError('spline order is not supported')
 
     if mode in ['grid-mirror', 'grid-wrap', 'grid-reflect', 'wrap', 'reflect']:
         cupy._util.experimental(f"mode '{mode}'")
@@ -84,9 +79,8 @@ def spline_filter1d(input, order=3, axis=-1, output=cupy.float64,
 
     Args:
         input (cupy.ndarray): The input array.
-        order (int): The order of the spline interpolation. If it is not given,
-            order 1 is used. It is different from :mod:`scipy.ndimage` and can
-            change in the future. Currently it supports only order 0 and 1.
+        order (int): The order of the spline interpolation, default is 3. Must
+            be in the range 0-5.
         axis (int): The axis along which the spline filter is applied. Default
             is the last axis.
         output (cupy.ndarray or dtype, optional): The array in which to place
@@ -164,9 +158,8 @@ def spline_filter(input, order=3, output=cupy.float64, mode='mirror'):
 
     Args:
         input (cupy.ndarray): The input array.
-        order (int): The order of the spline interpolation. If it is not given,
-            order 1 is used. It is different from :mod:`scipy.ndimage` and can
-            change in the future. Currently it supports only order 0 and 1.
+        order (int): The order of the spline interpolation, default is 3. Must
+            be in the range 0-5.
         output (cupy.ndarray or dtype, optional): The array in which to place
             the output, or the dtype of the returned array. Default is
             ``numpy.float64``.
@@ -198,7 +191,66 @@ def spline_filter(input, order=3, output=cupy.float64, mode='mirror'):
     return output
 
 
-def map_coordinates(input, coordinates, output=None, order=None,
+def _check_coordinates(coordinates, order, allow_float32=True):
+    if coordinates.dtype.kind == 'f':
+        if allow_float32:
+            coord_dtype = cupy.promote_types(coordinates.dtype, cupy.float32)
+        else:
+            coord_dtype = cupy.promote_types(coordinates.dtype, cupy.float64)
+        coordinates = coordinates.astype(coord_dtype, copy=False)
+    elif coordinates.dtype.kind in 'iu':
+        if order > 1:
+            # order > 1 (spline) kernels require floating-point coordinates
+            if allow_float32:
+                coord_dtype = cupy.promote_types(
+                    coordinates.dtype, cupy.float32
+                )
+            else:
+                coord_dtype = cupy.promote_types(
+                    coordinates.dtype, cupy.float64
+                )
+            coordinates = coordinates.astype(coord_dtype)
+    else:
+        raise ValueError('coordinates should have floating point dtype')
+    if not coordinates.flags.c_contiguous:
+        coordinates = cupy.ascontiguousarray(coordinates)
+    return coordinates
+
+
+def _prepad_for_spline_filter(input, mode, cval):
+    if mode in ['nearest', 'grid-constant']:
+        # these modes need padding to get accurate boundary values
+        npad = 12  # empirical factor chosen by SciPy
+        if mode == 'grid-constant':
+            kwargs = dict(mode='constant', constant_values=cval)
+        else:
+            kwargs = dict(mode='edge')
+        padded = cupy.pad(input, npad, **kwargs)
+    else:
+        npad = 0
+        padded = input
+    return padded, npad
+
+
+def _filter_input(image, prefilter, mode, cval, order):
+    """Perform spline prefiltering when needed.
+
+    Spline orders > 1 need a prefiltering stage to preserve resolution.
+
+    For boundary modes without analytical spline boundary conditions, some
+    prepadding of the input with cupy.pad is used to maintain accuracy.
+    ``npad`` is an integer corresponding to the amount of padding at each edge
+    of the array.
+    """
+    if not prefilter or order < 2:
+        return (cupy.ascontiguousarray(image), 0)
+    padded, npad = _prepad_for_spline_filter(image, mode, cval)
+    float_dtype = cupy.promote_types(image.dtype, cupy.float32)
+    filtered = spline_filter(padded, order, output=float_dtype, mode=mode)
+    return cupy.ascontiguousarray(filtered), npad
+
+
+def map_coordinates(input, coordinates, output=None, order=3,
                     mode='constant', cval=0.0, prefilter=True):
     """Map the input array to new coordinates by interpolation.
 
@@ -216,9 +268,8 @@ def map_coordinates(input, coordinates, output=None, order=None,
             evaluated.
         output (cupy.ndarray or ~cupy.dtype): The array in which to place the
             output, or the dtype of the returned array.
-        order (int): The order of the spline interpolation. If it is not given,
-            order 1 is used. It is different from :mod:`scipy.ndimage` and can
-            change in the future. Currently it supports only order 0 and 1.
+        order (int): The order of the spline interpolation, default is 3. Must
+            be in the range 0-5.
         mode (str): Points outside the boundaries of the input are filled
             according to the given mode (``'constant'``, ``'nearest'``,
             ``'mirror'``, ``'reflect'``, ``'wrap'``, ``'grid-mirror'``,
@@ -251,17 +302,18 @@ def map_coordinates(input, coordinates, output=None, order=None,
 
     if input.dtype.kind in 'iu':
         input = input.astype(cupy.float32)
-
+    coordinates = _check_coordinates(coordinates, order)
+    filtered, nprepad = _filter_input(input, prefilter, mode, cval, order)
     large_int = max(_prod(input.shape), coordinates.shape[0]) > 1 << 31
     kern = _interp_kernels._get_map_kernel(
         input.ndim, large_int, yshape=coordinates.shape, mode=mode, cval=cval,
-        order=order, integer_output=integer_output)
-    kern(input, coordinates, ret)
+        order=order, integer_output=integer_output, nprepad=nprepad)
+    kern(filtered, coordinates, ret)
     return ret
 
 
 def affine_transform(input, matrix, offset=0.0, output_shape=None, output=None,
-                     order=None, mode='constant', cval=0.0, prefilter=True):
+                     order=3, mode='constant', cval=0.0, prefilter=True):
     """Apply an affine transformation.
 
     Given an output image pixel index vector ``o``, the pixel value is
@@ -293,9 +345,8 @@ def affine_transform(input, matrix, offset=0.0, output_shape=None, output=None,
         output_shape (tuple of ints): Shape tuple.
         output (cupy.ndarray or ~cupy.dtype): The array in which to place the
             output, or the dtype of the returned array.
-        order (int): The order of the spline interpolation. If it is not given,
-            order 1 is used. It is different from :mod:`scipy.ndimage` and can
-            change in the future. Currently it supports only order 0 and 1.
+        order (int): The order of the spline interpolation, default is 3. Must
+            be in the range 0-5.
         mode (str): Points outside the boundaries of the input are filled
             according to the given mode (``'constant'``, ``'nearest'``,
             ``'mirror'``, ``'reflect'``, ``'wrap'``, ``'grid-mirror'``,
@@ -356,12 +407,11 @@ def affine_transform(input, matrix, offset=0.0, output_shape=None, output=None,
         return ret
 
     matrix = matrix.astype(cupy.float64, copy=False)
-    if order is None:
-        order = 1
     ndim = input.ndim
     output = _util._get_output(output, input, shape=output_shape)
     if input.dtype.kind in 'iu':
         input = input.astype(cupy.float32)
+    filtered, nprepad = _filter_input(input, prefilter, mode, cval, order)
 
     integer_output = output.dtype.kind in 'iu'
     _util._check_cval(mode, cval, integer_output)
@@ -371,16 +421,16 @@ def affine_transform(input, matrix, offset=0.0, output_shape=None, output=None,
         offset = -offset / matrix
         kern = _interp_kernels._get_zoom_shift_kernel(
             ndim, large_int, output_shape, mode, cval=cval, order=order,
-            integer_output=integer_output)
-        kern(input, offset, matrix, output)
+            integer_output=integer_output, nprepad=nprepad)
+        kern(filtered, offset, matrix, output)
     else:
         kern = _interp_kernels._get_affine_kernel(
             ndim, large_int, output_shape, mode, cval=cval, order=order,
-            integer_output=integer_output)
+            integer_output=integer_output, nprepad=nprepad)
         m = cupy.zeros((ndim, ndim + 1), dtype=cupy.float64)
         m[:, :-1] = matrix
         m[:, -1] = cupy.asarray(offset, dtype=cupy.float64)
-        kern(input, m, output)
+        kern(filtered, m, output)
     return output
 
 
@@ -396,7 +446,7 @@ def _minmax(coor, minc, maxc):
     return minc, maxc
 
 
-def rotate(input, angle, axes=(1, 0), reshape=True, output=None, order=None,
+def rotate(input, angle, axes=(1, 0), reshape=True, output=None, order=3,
            mode='constant', cval=0.0, prefilter=True):
     """Rotate an array.
 
@@ -413,9 +463,8 @@ def rotate(input, angle, axes=(1, 0), reshape=True, output=None, order=None,
             is True.
         output (cupy.ndarray or ~cupy.dtype): The array in which to place the
             output, or the dtype of the returned array.
-        order (int): The order of the spline interpolation. If it is not given,
-            order 1 is used. It is different from :mod:`scipy.ndimage` and can
-            change in the future. Currently it supports only order 0 and 1.
+        order (int): The order of the spline interpolation, default is 3. Must
+            be in the range 0-5.
         mode (str): Points outside the boundaries of the input are filled
             according to the given mode (``'constant'``, ``'nearest'``,
             ``'mirror'``, ``'reflect'``, ``'wrap'``, ``'grid-mirror'``,
@@ -483,7 +532,6 @@ def rotate(input, angle, axes=(1, 0), reshape=True, output=None, order=None,
     matrix[axes[1], axes[0]] = -sin
     matrix[axes[1], axes[1]] = cos
 
-    iy = input.shape[axes[0]]
     offset = numpy.zeros(ndim, dtype=cupy.float64)
     offset[axes] = in_center - out_center
 
@@ -494,7 +542,7 @@ def rotate(input, angle, axes=(1, 0), reshape=True, output=None, order=None,
                             mode, cval, prefilter)
 
 
-def shift(input, shift, output=None, order=None, mode='constant', cval=0.0,
+def shift(input, shift, output=None, order=3, mode='constant', cval=0.0,
           prefilter=True):
     """Shift an array.
 
@@ -509,9 +557,8 @@ def shift(input, shift, output=None, order=None, mode='constant', cval=0.0,
             should contain one value for each axis.
         output (cupy.ndarray or ~cupy.dtype): The array in which to place the
             output, or the dtype of the returned array.
-        order (int): The order of the spline interpolation. If it is not given,
-            order 1 is used. It is different from :mod:`scipy.ndimage` and can
-            change in the future. Currently it supports only order 0 and 1.
+        order (int): The order of the spline interpolation, default is 3. Must
+            be in the range 0-5.
         mode (str): Points outside the boundaries of the input are filled
             according to the given mode (``'constant'``, ``'nearest'``,
             ``'mirror'``, ``'reflect'``, ``'wrap'``, ``'grid-mirror'``,
@@ -548,23 +595,26 @@ def shift(input, shift, output=None, order=None, mode='constant', cval=0.0,
             prefilter,
         )
     else:
-        if order is None:
-            order = 1
         output = _util._get_output(output, input)
         if input.dtype.kind in 'iu':
             input = input.astype(cupy.float32)
+        filtered, nprepad = _filter_input(input, prefilter, mode, cval, order)
         integer_output = output.dtype.kind in 'iu'
         _util._check_cval(mode, cval, integer_output)
         large_int = _prod(input.shape) > 1 << 31
         kern = _interp_kernels._get_shift_kernel(
             input.ndim, large_int, input.shape, mode, cval=cval, order=order,
-            integer_output=integer_output)
-        shift = cupy.asarray(shift, dtype=cupy.float64)
-        kern(input, shift, output)
+            integer_output=integer_output, nprepad=nprepad)
+        shift = cupy.asarray(shift, dtype=cupy.float64, order='C')
+        if shift.ndim != 1:
+            raise ValueError('shift must be 1d')
+        if shift.size != filtered.ndim:
+            raise ValueError('len(shift) must equal input.ndim')
+        kern(filtered, shift, output)
     return output
 
 
-def zoom(input, zoom, output=None, order=None, mode='constant', cval=0.0,
+def zoom(input, zoom, output=None, order=3, mode='constant', cval=0.0,
          prefilter=True, *, grid_mode=False):
     """Zoom an array.
 
@@ -577,9 +627,8 @@ def zoom(input, zoom, output=None, order=None, mode='constant', cval=0.0,
             contain one value for each axis.
         output (cupy.ndarray or ~cupy.dtype): The array in which to place the
             output, or the dtype of the returned array.
-        order (int): The order of the spline interpolation. If it is not given,
-            order 1 is used. It is different from :mod:`scipy.ndimage` and can
-            change in the future. Currently it supports only order 0 and 1.
+        order (int): The order of the spline interpolation, default is 3. Must
+            be in the range 0-5.
         mode (str): Points outside the boundaries of the input are filled
             according to the given mode (``'constant'``, ``'nearest'``,
             ``'mirror'``, ``'reflect'``, ``'wrap'``, ``'grid-mirror'``,
@@ -645,9 +694,6 @@ def zoom(input, zoom, output=None, order=None, mode='constant', cval=0.0,
             prefilter,
         )
     else:
-        if order is None:
-            order = 1
-
         if grid_mode:
             cupy._util.experimental("grid_mode=True")
 
@@ -675,12 +721,14 @@ def zoom(input, zoom, output=None, order=None, mode='constant', cval=0.0,
         output = _util._get_output(output, input, shape=output_shape)
         if input.dtype.kind in 'iu':
             input = input.astype(cupy.float32)
+        filtered, nprepad = _filter_input(input, prefilter, mode, cval, order)
         integer_output = output.dtype.kind in 'iu'
         _util._check_cval(mode, cval, integer_output)
         large_int = max(_prod(input.shape), _prod(output_shape)) > 1 << 31
         kern = _interp_kernels._get_zoom_kernel(
             input.ndim, large_int, output_shape, mode, order=order,
-            integer_output=integer_output, grid_mode=grid_mode)
+            integer_output=integer_output, grid_mode=grid_mode,
+            nprepad=nprepad)
         zoom = cupy.asarray(zoom, dtype=cupy.float64)
-        kern(input, zoom, output)
+        kern(filtered, zoom, output)
     return output


Test patch:
diff --git a/tests/cupyx_tests/scipy_tests/ndimage_tests/test_interpolation.py b/tests/cupyx_tests/scipy_tests/ndimage_tests/test_interpolation.py
--- a/tests/cupyx_tests/scipy_tests/ndimage_tests/test_interpolation.py
+++ b/tests/cupyx_tests/scipy_tests/ndimage_tests/test_interpolation.py
@@ -25,10 +25,17 @@
 legacy_modes = ['constant', 'nearest', 'mirror']
 
 
+def _conditional_scipy_version_skip(mode, order):
+    if ((mode in scipy16_modes or (mode != 'mirror' and order > 1)) and
+            (scipy_version < '1.6.0')):
+        pytest.skip(
+            'SciPy >= 1.6.0 needed to test this mode/order combination.')
+
+
 @testing.parameterize(*testing.product({
     'output': [None, numpy.float64, 'f', float, 'empty'],
-    'order': [0, 1],
-    'mode': ['constant', 'nearest', 'mirror'],
+    'order': [0, 1, 2, 3, 4, 5],
+    'mode': ['constant', 'nearest', 'mirror'] + scipy16_modes,
     'cval': [1.0],
     'prefilter': [True],
 }))
@@ -39,6 +46,7 @@ class TestMapCoordinates:
     _multiprocess_can_split = True
 
     def _map_coordinates(self, xp, scp, a, coordinates):
+        _conditional_scipy_version_skip(self.mode, self.order)
         map_coordinates = scp.ndimage.map_coordinates
         if self.output == 'empty':
             output = xp.empty(coordinates.shape[1:], dtype=a.dtype)
@@ -52,14 +60,14 @@ def _map_coordinates(self, xp, scp, a, coordinates):
                                    self.mode, self.cval, self.prefilter)
 
     @testing.for_float_dtypes(no_float16=True)
-    @testing.numpy_cupy_allclose(atol=1e-5, scipy_name='scp')
+    @testing.numpy_cupy_allclose(atol=1e-4, scipy_name='scp')
     def test_map_coordinates_float(self, xp, scp, dtype):
         a = testing.shaped_random((100, 100), xp, dtype)
         coordinates = testing.shaped_random((a.ndim, 100), xp, dtype)
         return self._map_coordinates(xp, scp, a, coordinates)
 
     @testing.for_float_dtypes(no_float16=True)
-    @testing.numpy_cupy_allclose(atol=1e-5, scipy_name='scp')
+    @testing.numpy_cupy_allclose(atol=1e-4, scipy_name='scp')
     def test_map_coordinates_fortran_order(self, xp, scp, dtype):
         a = testing.shaped_random((100, 100), xp, dtype)
         coordinates = testing.shaped_random((a.ndim, 100), xp, dtype)
@@ -68,7 +76,7 @@ def test_map_coordinates_fortran_order(self, xp, scp, dtype):
         return self._map_coordinates(xp, scp, a, coordinates)
 
     @testing.for_float_dtypes(no_float16=True)
-    @testing.numpy_cupy_allclose(atol=1e-5, scipy_name='scp')
+    @testing.numpy_cupy_allclose(atol=1e-4, scipy_name='scp')
     def test_map_coordinates_float_nd_coords(self, xp, scp, dtype):
         a = testing.shaped_random((100, 100), xp, dtype)
         coordinates = testing.shaped_random((a.ndim, 10, 10), xp, dtype,
@@ -76,7 +84,7 @@ def test_map_coordinates_float_nd_coords(self, xp, scp, dtype):
         return self._map_coordinates(xp, scp, a, coordinates)
 
     @testing.for_int_dtypes(no_bool=True)
-    @testing.numpy_cupy_allclose(atol=1e-5, scipy_name='scp')
+    @testing.numpy_cupy_allclose(atol=1e-4, scipy_name='scp')
     def test_map_coordinates_int(self, xp, scp, dtype):
         if numpy.lib.NumpyVersion(scipy.__version__) < '1.0.0':
             if dtype in (numpy.dtype('l'), numpy.dtype('q')):
@@ -99,10 +107,10 @@ def test_map_coordinates_int(self, xp, scp, dtype):
     'offset': [0.3, [-1.3, 1.3]],
     'output_shape': [None],
     'output': [None, numpy.float64, 'empty'],
-    'order': [0, 1],
+    'order': [0, 1, 2, 3, 4, 5],
     'mode': legacy_modes + scipy16_modes,
     'cval': [1.0],
-    'prefilter': [True],
+    'prefilter': [False, True],
 }))
 @testing.gpu
 @testing.with_requires('scipy')
@@ -111,8 +119,7 @@ class TestAffineTransform:
     _multiprocess_can_split = True
 
     def _affine_transform(self, xp, scp, a, matrix):
-        if self.mode in scipy16_modes and scipy_version < '1.6.0':
-            pytest.skip('specified mode requires SciPy >= 1.6.0')
+        _conditional_scipy_version_skip(self.mode, self.order)
         ver = numpy.lib.NumpyVersion(scipy.__version__)
         if ver < '1.0.0' and matrix.ndim == 2 and matrix.shape[1] == 3:
             return xp.empty(0)
@@ -231,7 +238,7 @@ def test_affine_transform_opencv(self, xp, dtype):
         'axes': [(1, 0)],
         'reshape': [False],
         'output': [None],
-        'order': [0, 1],
+        'order': [0, 1, 3],
         'mode': legacy_modes + scipy16_modes,
         'cval': [1.0],
         'prefilter': [True],
@@ -244,8 +251,7 @@ class TestRotate:
     _multiprocess_can_split = True
 
     def _rotate(self, xp, scp, a):
-        if self.mode in scipy16_modes and scipy_version < '1.6.0':
-            pytest.skip('specified mode requires SciPy >= 1.6.0')
+        _conditional_scipy_version_skip(self.mode, self.order)
         rotate = scp.ndimage.rotate
         if self.output == 'empty':
             output = rotate(a, self.angle, self.axes,
@@ -349,14 +355,14 @@ def test_rotate_opencv(self, xp, dtype):
     testing.product({
         'shift': [0.1, -10, (5, -5)],
         'output': [None, numpy.float64, 'empty'],
-        'order': [0, 1],
+        'order': [0, 1, 3],
         'mode': legacy_modes + scipy16_modes,
         'cval': [1.0],
         'prefilter': [True],
     }) + testing.product({
         'shift': [0.1, ],
         'output': [None, numpy.float64, 'empty'],
-        'order': [0, 1],
+        'order': [0, 1, 3],
         'mode': ['constant', ],
         'cval': [cupy.nan, cupy.inf, -cupy.inf],
         'prefilter': [True],
@@ -370,8 +376,7 @@ class TestShift:
 
     def _shift(self, xp, scp, a):
         shift = scp.ndimage.shift
-        if self.mode in scipy16_modes and scipy_version < '1.6.0':
-            pytest.skip('specified mode requires SciPy >= 1.6.0')
+        _conditional_scipy_version_skip(self.mode, self.order)
         if self.output == 'empty':
             output = xp.empty_like(a)
             return_value = shift(a, self.shift, output, self.order,
@@ -537,6 +542,7 @@ class TestZoom:
     _multiprocess_can_split = True
 
     def _zoom(self, xp, scp, a):
+        _conditional_scipy_version_skip(self.mode, self.order)
         zoom = scp.ndimage.zoom
         if self.output == 'empty':
             output = zoom(a, self.zoom, None, self.order,
@@ -585,7 +591,7 @@ def test_zoom_int(self, xp, scp, dtype):
     'mode': ['nearest', 'reflect', 'mirror', 'grid-wrap', 'grid-constant'],
 }))
 @testing.gpu
-class TestZoomIntegerGrid():
+class TestZoomOrder0IntegerGrid():
 
     def test_zoom_grid_by_int_order0(self):
         # When grid_mode is True,  order 0 zoom should be the same as