Repository: cupy/cupy

Issue:
Implement 'circular' or 'wrap' mode for cupy.pad
I'm working on 2D convolution problem but need circular (wrap) padding. Would that be hard implement a wrap mode like numpy? 

https://docs.scipy.org/doc/numpy/reference/generated/numpy.pad.html


Discussion:
This already exists via `cupy.pad` or am I misunderstanding what you are asking for?
Nevermind, I had forgotten I had implemented this locally, but never made a PR for it. 
From what I can tell, pad is able to do expand with constant values with ‘constant’, replication with ‘edge’, and reflection with ‘reflect’. I need wrap, sometimes considered ‘circular’. As an example, if I were to expand the right side by one column, it would be the same as the first column on the left side of the matrix. This is for cyclic convolution.

Get Outlook for iOS<https://aka.ms/o0ukef>
________________________________
From: Gregory R. Lee <notifications@github.com>
Sent: Wednesday, August 7, 2019 6:42:10 PM
To: cupy/cupy <cupy@noreply.github.com>
Cc: Matthew Nicely <mnicely@nvidia.com>; Author <author@noreply.github.com>
Subject: Re: [cupy/cupy] Implement 'circular' or 'wrap' mode for cupy.pad (#2367)


This already exists via cupy.pad or am I misunderstanding what you are asking for?

—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub<https://github.com/cupy/cupy/issues/2367?email_source=notifications&email_token=AL5UKAUBRWAXEECVC3TIJVLQDNFUFA5CNFSM4IKEUOBKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD3Z47FQ#issuecomment-519294870>, or mute the thread<https://github.com/notifications/unsubscribe-auth/AL5UKAQQD4YICVYLB5RQ64TQDNFUFANCNFSM4IKEUOBA>.

-----------------------------------------------------------------------------------
This email message is for the sole use of the intended recipient(s) and may contain
confidential information.  Any unauthorized review, use, disclosure or distribution
is prohibited.  If you are not the intended recipient, please contact the sender by
reply email and destroy all copies of the original message.
-----------------------------------------------------------------------------------

Here is a gist with an implementation of `pad` that I had been using locally. I never made a full set of tests or started a PR here for it, but it should work. It is nearly a 1:1 copy of numpy.pad from NumPy 1.17.
https://gist.github.com/grlee77/ea0c34f2b68cd376b4d1013b9cdcd06d

example (navigate to the folder containing `arraypad.py` from the gist above:
```Python
import cupy
from arraypad import pad
pad(cupy.arange(6), 4, mode='wrap')
```
gives:
```array([2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3])```

If there is interest from the cupy devs, I can try to find time to finish a PR for this here.

@grlee77 Yes, we're interested! Please do make a PR for this.

Resolving patch:
diff --git a/cupy/padding/pad.py b/cupy/padding/pad.py
--- a/cupy/padding/pad.py
+++ b/cupy/padding/pad.py
@@ -1,321 +1,744 @@
 import numpy
-import six
 
 import cupy
 
 
-def _prepend_const(narray, pad_amount, value, axis=-1):
-    if pad_amount == 0:
-        return narray
-    padshape = tuple([x if i != axis else pad_amount
-                      for i, x in enumerate(narray.shape)])
-    return cupy.concatenate((cupy.full(padshape, value, narray.dtype),
-                             narray), axis=axis)
+###############################################################################
+# Private utility functions.
 
 
-def _append_const(narray, pad_amount, value, axis=-1):
-    if pad_amount == 0:
-        return narray
-    padshape = tuple([x if i != axis else pad_amount
-                      for i, x in enumerate(narray.shape)])
-    return cupy.concatenate((narray,
-                             cupy.full(padshape, value, narray.dtype)),
-                            axis=axis)
+def _round_if_needed(arr, dtype):
+    """Rounds arr inplace if the destination dtype is an integer.
+    """
+    if cupy.issubdtype(dtype, cupy.integer):
+        arr.round(out=arr)  # bug in round so use rint (cupy/cupy#2330)
+
+
+def _slice_at_axis(sl, axis):
+    """Constructs a tuple of slices to slice an array in the given dimension.
+
+    Args:
+      sl(slice): The slice for the given dimension.
+      axis(int): The axis to which `sl` is applied. All other dimensions are
+          left "unsliced".
 
+    Returns:
+      tuple of slices: A tuple with slices matching `shape` in length.
+    """
+    return (slice(None),) * axis + (sl,) + (Ellipsis,)
 
-def _prepend_edge(arr, pad_amt, axis=-1):
-    """Prepend `pad_amt` to `arr` along `axis` by extending edge values.
 
-    Parameters
-    ----------
-    arr : ndarray
-        Input array of arbitrary shape.
-    pad_amt : int
-        Amount of padding to prepend.
-    axis : int
-        Axis along which to pad `arr`.
+def _view_roi(array, original_area_slice, axis):
+    """Gets a view of the current region of interest during iterative padding.
 
-    Returns
-    -------
-    padarr : ndarray
-        Output array, extended by `pad_amt` edge values appended along `axis`.
+    When padding multiple dimensions iteratively corner values are
+    unnecessarily overwritten multiple times. This function reduces the
+    working area for the first dimensions so that corners are excluded.
 
+    Args:
+      array(ndarray): The array with the region of interest.
+      original_area_slice(tuple of slices): Denotes the area with original
+          values of the unpadded array.
+      axis(int): The currently padded dimension assuming that `axis` is padded
+          before `axis` + 1.
+
+    Returns:
     """
-    if pad_amt == 0:
-        return arr
-
-    edge_slice = tuple([slice(None) if i != axis else 0
-                        for i, x in enumerate(arr.shape)])
-
-    # Shape to restore singleton dimension after slicing
-    pad_singleton = tuple([x if i != axis else 1
-                           for i, x in enumerate(arr.shape)])
-    edge_arr = arr[edge_slice].reshape(pad_singleton)
-    return cupy.concatenate((edge_arr.repeat(pad_amt, axis=axis), arr),
-                            axis=axis)
-
-
-def _append_edge(arr, pad_amt, axis=-1):
-    """Append `pad_amt` to `arr` along `axis` by extending edge values.
-
-    Parameters
-    ----------
-    arr : ndarray
-        Input array of arbitrary shape.
-    pad_amt : int
-        Amount of padding to append.
-    axis : int
-        Axis along which to pad `arr`.
-
-    Returns
-    -------
-    padarr : ndarray
-        Output array, extended by `pad_amt` edge values prepended along
-        `axis`.
+    axis += 1
+    sl = (slice(None),) * axis + original_area_slice[axis:]
+    return array[sl]
+
+
+def _pad_simple(array, pad_width, fill_value=None):
+    """Pads an array on all sides with either a constant or undefined values.
 
+    Args:
+      array(ndarray): Array to grow.
+      pad_width(sequence of tuple[int, int]): Pad width on both sides for each
+          dimension in `arr`.
+      fill_value(scalar, optional): If provided the padded area is
+          filled with this value, otherwise the pad area left undefined.
+          (Default value = None)
     """
-    if pad_amt == 0:
-        return arr
-
-    edge_slice = tuple([slice(None) if i != axis else arr.shape[axis] - 1
-                        for i, x in enumerate(arr.shape)])
-
-    # Shape to restore singleton dimension after slicing
-    pad_singleton = tuple([x if i != axis else 1
-                           for i, x in enumerate(arr.shape)])
-    edge_arr = arr[edge_slice].reshape(pad_singleton)
-    return cupy.concatenate((arr, edge_arr.repeat(pad_amt, axis=axis)),
-                            axis=axis)
-
-
-def _pad_ref(arr, pad_amt, method, axis=-1):
-    """Pad `axis` of `arr` by reflection.
-
-    Parameters
-    ----------
-    arr : ndarray
-        Input array of arbitrary shape.
-    pad_amt : tuple of ints, length 2
-        Padding to (prepend, append) along `axis`.
-    method : str
-        Controls method of reflection; options are 'even' or 'odd'.
-    axis : int
-        Axis along which to pad `arr`.
-
-    Returns
-    -------
-    padarr : ndarray
-        Output array, with `pad_amt[0]` values prepended and `pad_amt[1]`
-        values appended along `axis`. Both regions are padded with reflected
-        values from the original array.
-
-    Notes
-    -----
-    This algorithm does not pad with repetition, i.e. the edges are not
-    repeated in the reflection. For that behavior, use `mode='symmetric'`.
-
-    The modes 'reflect', 'symmetric', and 'wrap' must be padded with a
-    single function, lest the indexing tricks in non-integer multiples of the
-    original shape would violate repetition in the final iteration.
+    # Allocate grown array
+    new_shape = tuple(
+        left + size + right
+        for size, (left, right) in zip(array.shape, pad_width)
+    )
+    order = 'F' if array.flags.fnc else 'C'  # Fortran and not also C-order
+    padded = cupy.empty(new_shape, dtype=array.dtype, order=order)
+
+    if fill_value is not None:
+        padded.fill(fill_value)
+
+    # Copy old array into correct space
+    original_area_slice = tuple(
+        slice(left, left + size)
+        for size, (left, right) in zip(array.shape, pad_width)
+    )
+    padded[original_area_slice] = array
 
+    return padded, original_area_slice
+
+
+def _set_pad_area(padded, axis, width_pair, value_pair):
+    """Set an empty-padded area in given dimension.
+    """
+    left_slice = _slice_at_axis(slice(None, width_pair[0]), axis)
+    padded[left_slice] = value_pair[0]
+
+    right_slice = _slice_at_axis(
+        slice(padded.shape[axis] - width_pair[1], None), axis
+    )
+    padded[right_slice] = value_pair[1]
+
+
+def _get_edges(padded, axis, width_pair):
+    """Retrieves edge values from an empty-padded array along a given axis.
+
+    Args:
+      padded(ndarray): Empty-padded array.
+      axis(int): Dimension in which the edges are considered.
+      width_pair((int, int)): Pair of widths that mark the pad area on both
+          sides in the given dimension.
     """
-    # Implicit booleanness to test for zero (or None) in any scalar type
-    if pad_amt[0] == 0 and pad_amt[1] == 0:
-        return arr
-
-    ##########################################################################
-    # Prepended region
-
-    # Slice off a reverse indexed chunk from near edge to pad `arr` before
-    ref_slice = tuple([slice(None) if i != axis else slice(pad_amt[0], 0, -1)
-                       for i, x in enumerate(arr.shape)])
-
-    ref_chunk1 = arr[ref_slice]
-
-    # Shape to restore singleton dimension after slicing
-    pad_singleton = tuple([x if i != axis else 1
-                           for i, x in enumerate(arr.shape)])
-    if pad_amt[0] == 1:
-        ref_chunk1 = ref_chunk1.reshape(pad_singleton)
-
-    # Memory/computationally more expensive, only do this if `method='odd'`
-    if 'odd' in method and pad_amt[0] > 0:
-        edge_slice1 = tuple([slice(None) if i != axis else 0
-                             for i, x in enumerate(arr.shape)])
-        edge_chunk = arr[edge_slice1].reshape(pad_singleton)
-        ref_chunk1 = 2 * edge_chunk - ref_chunk1
-        del edge_chunk
-
-    ##########################################################################
-    # Appended region
-
-    # Slice off a reverse indexed chunk from far edge to pad `arr` after
-    start = arr.shape[axis] - pad_amt[1] - 1
-    end = arr.shape[axis] - 1
-    ref_slice = tuple([slice(None) if i != axis else slice(start, end)
-                       for i, x in enumerate(arr.shape)])
-    rev_idx = tuple([slice(None) if i != axis else slice(None, None, -1)
-                     for i, x in enumerate(arr.shape)])
-    ref_chunk2 = arr[ref_slice][rev_idx]
-
-    if pad_amt[1] == 1:
-        ref_chunk2 = ref_chunk2.reshape(pad_singleton)
-
-    if 'odd' in method:
-        edge_slice2 = tuple([slice(None) if i != axis else -1
-                             for i, x in enumerate(arr.shape)])
-        edge_chunk = arr[edge_slice2].reshape(pad_singleton)
-        ref_chunk2 = 2 * edge_chunk - ref_chunk2
-        del edge_chunk
-
-    # Concatenate `arr` with both chunks, extending along `axis`
-    return cupy.concatenate((ref_chunk1, arr, ref_chunk2), axis=axis)
-
-
-def _normalize_shape(ndarray, shape, cast_to_int=True):
-    ndims = ndarray.ndim
-    if shape is None:
-        return ((None, None), ) * ndims
-    ndshape = numpy.asarray(shape)
-    if ndshape.size == 1:
-        ndshape = numpy.repeat(ndshape, 2)
-    if ndshape.ndim == 1:
-        ndshape = numpy.tile(ndshape, (ndims, 1))
-    if ndshape.shape != (ndims, 2):
-        message = 'Unable to create correctly shaped tuple from %s' % shape
-        raise ValueError(message)
-    if cast_to_int:
-        ndshape = numpy.rint(ndshape).astype(int)
-    return tuple([tuple(axis) for axis in ndshape])
-
-
-def _validate_lengths(narray, number_elements):
-    shape = _normalize_shape(narray, number_elements)
-    for axis_shape in shape:
-        axis_shape = [1 if x is None else x for x in axis_shape]
-        axis_shape = [1 if x >= 0 else -1 for x in axis_shape]
-        if axis_shape[0] < 0 or axis_shape[1] < 0:
-            message = '%s cannot contain negative values.' % number_elements
-            raise ValueError(message)
-    return shape
-
-
-def pad(array, pad_width, mode, **keywords):
-    """Returns padded array. You can specify the padded widths and values.
-
-    This function currently supports only ``mode=constant`` .
+    left_index = width_pair[0]
+    left_slice = _slice_at_axis(slice(left_index, left_index + 1), axis)
+    left_edge = padded[left_slice]
+
+    right_index = padded.shape[axis] - width_pair[1]
+    right_slice = _slice_at_axis(slice(right_index - 1, right_index), axis)
+    right_edge = padded[right_slice]
+
+    return left_edge, right_edge
+
+
+def _get_linear_ramps(padded, axis, width_pair, end_value_pair):
+    """Constructs linear ramps for an empty-padded array along a given axis.
 
     Args:
-        array (array-like): Input array of rank N.
-        pad_width (int or array-like): Number of values padded
-            to the edges of each axis.
-            ((before_1, after_1), ... (before_N, after_N)) uniquely pad widths
-            for each axis.
-            ((before, after),) yields same before and after pad for each axis.
-            (pad,) or int is a shortcut for before = after = pad width for all
-            axes.
-            You cannot specify ``cupy.ndarray`` .
-        mode (str):
-            'constant'
-                Pads with a constant values.
-            'edge'
-                Pads with the edge values of array.
-            'reflect'
-                Pads with the reflection of the vector mirrored on the first
-                and last values of the vector along each axis.
-        constant_values (int or array-like): Used in
-            ``constant``.
-            The values are padded for each axis.
-            ((before_1, after_1), ... (before_N, after_N)) uniquely pad
-            constants for each axis.
-            ((before, after),) yields same before and after constants for each
-            axis.
-            (constant,) or int is a shortcut for before = after = constant for
-            all axes.
-            Default is 0. You cannot specify ``cupy.ndarray`` .
-        reflect_type : {'even', 'odd'}, optional
-            Used in 'reflect', and 'symmetric'.  The 'even' style is the
-            default with an unaltered reflection around the edge value.  For
-            the 'odd' style, the extented part of the array is created by
-            subtracting the reflected values from two times the edge value.
+      padded(ndarray): Empty-padded array.
+      axis(int): Dimension in which the ramps are constructed.
+      width_pair((int, int)): Pair of widths that mark the pad area on both
+          sides in the given dimension.
+      end_value_pair((scalar, scalar)): End values for the linear ramps which
+          form the edge of the fully padded array. These values are included in
+          the linear ramps.
+    """
+    edge_pair = _get_edges(padded, axis, width_pair)
+
+    # TODO (grlee77): implement axis argument to cupy.linspace to avoid numpy
+    if numpy.lib.NumpyVersion(numpy.__version__) < '1.16.0':
+        # TODO (grlee77): remove once axis support is added to cupy.linspace
+        raise NotImplementedError(
+            "padding with 'linear_ramp' currently requires numpy >= 1.16")
+
+    left_ramp = cupy.asarray(numpy.linspace(
+        start=end_value_pair[0],
+        # squeeze axis replaced by linspace
+        stop=cupy.asnumpy(edge_pair[0].squeeze(axis)),
+        num=width_pair[0],
+        endpoint=False,
+        dtype=padded.dtype,
+        axis=axis,
+    ))
+
+    right_ramp = cupy.asarray(numpy.linspace(
+        start=end_value_pair[1],
+        # squeeze axis replaced by linspace
+        stop=cupy.asnumpy(edge_pair[1].squeeze(axis)),
+        num=width_pair[1],
+        endpoint=False,
+        dtype=padded.dtype,
+        axis=axis,
+    ))
+    # Reverse linear space in appropriate dimension
+    right_ramp = right_ramp[_slice_at_axis(slice(None, None, -1), axis)]
+
+    return left_ramp, right_ramp
+
+
+def _get_stats(padded, axis, width_pair, length_pair, stat_func):
+    """Calculates a statistic for an empty-padded array along a given axis.
+
+    Args:
+      padded(ndarray): Empty-padded array.
+      axis(int): Dimension in which the statistic is calculated.
+      width_pair((int, int)): Pair of widths that mark the pad area on both
+          sides in the given dimension.
+      length_pair(2-element sequence of None or int): Gives the number of
+          values in valid area from each side that is taken into account when
+          calculating the statistic. If None the entire valid area in `padded`
+          is considered.
+      stat_func(function): Function to compute statistic. The expected
+          signature is
+          ``stat_func(x: ndarray, axis: int, keepdims: bool) -> ndarray``.
+    """
+    # Calculate indices of the edges of the area with original values
+    left_index = width_pair[0]
+    right_index = padded.shape[axis] - width_pair[1]
+    # as well as its length
+    max_length = right_index - left_index
+
+    # Limit stat_lengths to max_length
+    left_length, right_length = length_pair
+    if left_length is None or max_length < left_length:
+        left_length = max_length
+    if right_length is None or max_length < right_length:
+        right_length = max_length
+
+    # Calculate statistic for the left side
+    left_slice = _slice_at_axis(
+        slice(left_index, left_index + left_length), axis
+    )
+    left_chunk = padded[left_slice]
+    left_stat = stat_func(left_chunk, axis=axis, keepdims=True)
+    _round_if_needed(left_stat, padded.dtype)
+
+    if left_length == right_length == max_length:
+        # return early as right_stat must be identical to left_stat
+        return left_stat, left_stat
+
+    # Calculate statistic for the right side
+    right_slice = _slice_at_axis(
+        slice(right_index - right_length, right_index), axis
+    )
+    right_chunk = padded[right_slice]
+    right_stat = stat_func(right_chunk, axis=axis, keepdims=True)
+    _round_if_needed(right_stat, padded.dtype)
+    return left_stat, right_stat
+
+
+def _set_reflect_both(padded, axis, width_pair, method, include_edge=False):
+    """Pads an `axis` of `arr` using reflection.
+
+    Args:
+      padded(ndarray): Input array of arbitrary shape.
+      axis(int): Axis along which to pad `arr`.
+      width_pair((int, int)): Pair of widths that mark the pad area on both
+          sides in the given dimension.
+      method(str): Controls method of reflection; options are 'even' or 'odd'.
+      include_edge(bool, optional): If true, edge value is included in
+          reflection, otherwise the edge value forms the symmetric axis to the
+          reflection. (Default value = False)
+    """
+    left_pad, right_pad = width_pair
+    old_length = padded.shape[axis] - right_pad - left_pad
+
+    if include_edge:
+        # Edge is included, we need to offset the pad amount by 1
+        edge_offset = 1
+    else:
+        edge_offset = 0  # Edge is not included, no need to offset pad amount
+        old_length -= 1  # but must be omitted from the chunk
+
+    if left_pad > 0:
+        # Pad with reflected values on left side:
+        # First limit chunk size which can't be larger than pad area
+        chunk_length = min(old_length, left_pad)
+        # Slice right to left, stop on or next to edge, start relative to stop
+        stop = left_pad - edge_offset
+        start = stop + chunk_length
+        left_slice = _slice_at_axis(slice(start, stop, -1), axis)
+        left_chunk = padded[left_slice]
+
+        if method == 'odd':
+            # Negate chunk and align with edge
+            edge_slice = _slice_at_axis(slice(left_pad, left_pad + 1), axis)
+            left_chunk = 2 * padded[edge_slice] - left_chunk
+
+        # Insert chunk into padded area
+        start = left_pad - chunk_length
+        stop = left_pad
+        pad_area = _slice_at_axis(slice(start, stop), axis)
+        padded[pad_area] = left_chunk
+        # Adjust pointer to left edge for next iteration
+        left_pad -= chunk_length
+
+    if right_pad > 0:
+        # Pad with reflected values on right side:
+        # First limit chunk size which can't be larger than pad area
+        chunk_length = min(old_length, right_pad)
+        # Slice right to left, start on or next to edge, stop relative to start
+        start = -right_pad + edge_offset - 2
+        stop = start - chunk_length
+        right_slice = _slice_at_axis(slice(start, stop, -1), axis)
+        right_chunk = padded[right_slice]
+
+        if method == 'odd':
+            # Negate chunk and align with edge
+            edge_slice = _slice_at_axis(
+                slice(-right_pad - 1, -right_pad), axis
+            )
+            right_chunk = 2 * padded[edge_slice] - right_chunk
+
+        # Insert chunk into padded area
+        start = padded.shape[axis] - right_pad
+        stop = start + chunk_length
+        pad_area = _slice_at_axis(slice(start, stop), axis)
+        padded[pad_area] = right_chunk
+        # Adjust pointer to right edge for next iteration
+        right_pad -= chunk_length
+
+    return left_pad, right_pad
+
+
+def _set_wrap_both(padded, axis, width_pair):
+    """Pads an `axis` of `arr` with wrapped values.
+
+    Args:
+      padded(ndarray): Input array of arbitrary shape.
+      axis(int): Axis along which to pad `arr`.
+      width_pair((int, int)): Pair of widths that mark the pad area on both
+          sides in the given dimension.
+    """
+    left_pad, right_pad = width_pair
+    period = padded.shape[axis] - right_pad - left_pad
+
+    # If the current dimension of `arr` doesn't contain enough valid values
+    # (not part of the undefined pad area) we need to pad multiple times.
+    # Each time the pad area shrinks on both sides which is communicated with
+    # these variables.
+    new_left_pad = 0
+    new_right_pad = 0
+
+    if left_pad > 0:
+        # Pad with wrapped values on left side
+        # First slice chunk from right side of the non-pad area.
+        # Use min(period, left_pad) to ensure that chunk is not larger than
+        # pad area
+        right_slice = _slice_at_axis(
+            slice(
+                -right_pad - min(period, left_pad),
+                -right_pad if right_pad != 0 else None,
+            ),
+            axis,
+        )
+        right_chunk = padded[right_slice]
+
+        if left_pad > period:
+            # Chunk is smaller than pad area
+            pad_area = _slice_at_axis(slice(left_pad - period, left_pad), axis)
+            new_left_pad = left_pad - period
+        else:
+            # Chunk matches pad area
+            pad_area = _slice_at_axis(slice(None, left_pad), axis)
+        padded[pad_area] = right_chunk
+
+    if right_pad > 0:
+        # Pad with wrapped values on right side
+        # First slice chunk from left side of the non-pad area.
+        # Use min(period, right_pad) to ensure that chunk is not larger than
+        # pad area
+        left_slice = _slice_at_axis(
+            slice(left_pad, left_pad + min(period, right_pad)), axis
+        )
+        left_chunk = padded[left_slice]
+
+        if right_pad > period:
+            # Chunk is smaller than pad area
+            pad_area = _slice_at_axis(
+                slice(-right_pad, -right_pad + period), axis
+            )
+            new_right_pad = right_pad - period
+        else:
+            # Chunk matches pad area
+            pad_area = _slice_at_axis(slice(-right_pad, None), axis)
+        padded[pad_area] = left_chunk
+
+    return new_left_pad, new_right_pad
+
+
+def _as_pairs(x, ndim, as_index=False):
+    """Broadcasts `x` to an array with shape (`ndim`, 2).
+
+    A helper function for `pad` that prepares and validates arguments like
+    `pad_width` for iteration in pairs.
+
+    Args:
+      x(None, scalar or array-like): The object to broadcast to the shape
+          (`ndim`, 2).
+      ndim(int): Number of pairs the broadcasted `x` will have.
+      as_index(bool, optional): If `x` is not None, try to round each
+          element of `x` to an integer (dtype `cupy.intp`) and ensure every
+          element is positive. (Default value = False)
 
     Returns:
-        cupy.ndarray:
-        Padded array of rank equal to ``array`` with shape increased according
-        to ``pad_width`` .
+      nested iterables, shape (`ndim`, 2): The broadcasted version of `x`.
+    """
+    if x is None:
+        # Pass through None as a special case, otherwise cupy.round(x) fails
+        # with an AttributeError
+        return ((None, None),) * ndim
+
+    x = numpy.array(x)
+    if as_index:
+        x = numpy.asarray(numpy.round(x), dtype=numpy.intp)
+
+    if x.ndim < 3:
+        # Optimization: Possibly use faster paths for cases where `x` has
+        # only 1 or 2 elements. `numpy.broadcast_to` could handle these as well
+        # but is currently slower
+
+        if x.size == 1:
+            # x was supplied as a single value
+            x = x.ravel()  # Ensure x[0] works for x.ndim == 0, 1, 2
+            if as_index and x < 0:
+                raise ValueError("index can't contain negative values")
+            return ((x[0], x[0]),) * ndim
+
+        if x.size == 2 and x.shape != (2, 1):
+            # x was supplied with a single value for each side
+            # but except case when each dimension has a single value
+            # which should be broadcasted to a pair,
+            # e.g. [[1], [2]] -> [[1, 1], [2, 2]] not [[1, 2], [1, 2]]
+            x = x.ravel()  # Ensure x[0], x[1] works
+            if as_index and (x[0] < 0 or x[1] < 0):
+                raise ValueError("index can't contain negative values")
+            return ((x[0], x[1]),) * ndim
+
+    if as_index and x.min() < 0:
+        raise ValueError("index can't contain negative values")
+
+    # Converting the array with `tolist` seems to improve performance
+    # when iterating and indexing the result (see usage in `pad`)
+    x_view = x.view()
+    x_view.shape = (ndim, 2)
+    return x_view.tolist()
+
+# def _pad_dispatcher(array, pad_width, mode=None, **kwargs):
+#    return (array,)
+
+
+###############################################################################
+# Public functions
+
+
+# @array_function_dispatch(_pad_dispatcher, module='numpy')
+def pad(array, pad_width, mode='constant', **kwargs):
+    """Pads an array with specified widths and values.
 
-    .. seealso:: :func:`numpy.pad`
+    Args:
+      array(cupy.ndarray): The array to pad.
+      pad_width(sequence, array_like or int): Number of values padded to the
+          edges of each axis. ((before_1, after_1), ... (before_N, after_N))
+          unique pad widths for each axis. ((before, after),) yields same
+          before and after pad for each axis. (pad,) or int is a shortcut for
+          before = after = pad width for all axes. You cannot specify
+          ``cupy.ndarray``.
+      mode(str or function, optional): One of the following string values or a
+          user supplied function
+
+          'constant' (default)
+              Pads with a constant value.
+          'edge'
+              Pads with the edge values of array.
+          'linear_ramp'
+              Pads with the linear ramp between end_value and the array edge
+              value.
+          'maximum'
+              Pads with the maximum value of all or part of the vector along
+              each axis.
+          'mean'
+              Pads with the mean value of all or part of the vector along each
+              axis.
+          'median'
+              Pads with the median value of all or part of the vector along
+              each axis. (Not Implemented)
+          'minimum'
+              Pads with the minimum value of all or part of the vector along
+              each axis.
+          'reflect'
+              Pads with the reflection of the vector mirrored on the first and
+              last values of the vector along each axis.
+          'symmetric'
+               Pads with the reflection of the vector mirrored along the edge
+               of the array.
+          'wrap'
+              Pads with the wrap of the vector along the axis. The first
+              values are used to pad the end and the end values are used to
+              pad the beginning.
+          'empty'
+              Pads with undefined values.
+          <function>
+              Padding function, see Notes.
+      stat_length(sequence or int, optional): Used in 'maximum', 'mean',
+          'median', and 'minimum'.  Number of values at edge of each axis used
+          to calculate the statistic value.
+          ((before_1, after_1), ... (before_N, after_N)) unique statistic
+          lengths for each axis. ((before, after),) yields same before and
+          after statistic lengths for each axis. (stat_length,) or int is a
+          shortcut for before = after = statistic length for all axes.
+          Default is ``None``, to use the entire axis. You cannot specify
+          ``cupy.ndarray``.
+      constant_values(sequence or scalar, optional): Used in 'constant'. The
+          values to set the padded values for each axis.
+          ((before_1, after_1), ... (before_N, after_N)) unique pad constants
+          for each axis.
+          ((before, after),) yields same before and after constants for each
+          axis.
+          (constant,) or constant is a shortcut for before = after = constant
+          for all axes.
+          Default is 0. You cannot specify ``cupy.ndarray``.
+      end_values(sequence or scalar, optional): Used in 'linear_ramp'. The
+          values used for the ending value of the linear_ramp and that will
+          form the edge of the padded array.
+          ((before_1, after_1), ... (before_N, after_N)) unique end values
+          for each axis.
+          ((before, after),) yields same before and after end
+          values for each axis.
+          (constant,) or constant is a shortcut for before = after = constant
+          for all axes.
+          Default is 0. You cannot specify ``cupy.ndarray``.
+      reflect_type({'even', 'odd'}, optional): Used in 'reflect', and
+          'symmetric'.  The 'even' style is the default with an unaltered
+          reflection around the edge value.  For the 'odd' style, the extended
+          part of the array is created by subtracting the reflected values from
+          two times the edge value.
 
+    Returns:
+      cupy.ndarray: Padded array with shape extended by ``pad_width``.
+
+    .. note::
+        For an array with rank greater than 1, some of the padding of later
+        axes is calculated from padding of previous axes.  This is easiest to
+        think about with a rank 2 array where the corners of the padded array
+        are calculated by using padded values from the first axis.
+
+        The padding function, if used, should modify a rank 1 array in-place.
+        It has the following signature:
+
+        ``padding_func(vector, iaxis_pad_width, iaxis, kwargs)``
+
+        where
+
+        vector (cupy.ndarray)
+            A rank 1 array already padded with zeros.  Padded values are
+            ``vector[:iaxis_pad_width[0]]`` and
+            ``vector[-iaxis_pad_width[1]:]``.
+        iaxis_pad_width (tuple)
+            A 2-tuple of ints, ``iaxis_pad_width[0]`` represents the number of
+            values padded at the beginning of vector where
+            ``iaxis_pad_width[1]`` represents the number of values padded at
+            the end of vector.
+        iaxis (int)
+            The axis currently being calculated.
+        kwargs (dict)
+            Any keyword arguments the function requires.
+
+    Examples
+    --------
+    >>> a = [1, 2, 3, 4, 5]
+    >>> cupy.pad(a, (2, 3), 'constant', constant_values=(4, 6))
+    array([4, 4, 1, ..., 6, 6, 6])
+
+    >>> cupy.pad(a, (2, 3), 'edge')
+    array([1, 1, 1, ..., 5, 5, 5])
+
+    >>> cupy.pad(a, (2, 3), 'linear_ramp', end_values=(5, -4))
+    array([ 5,  3,  1,  2,  3,  4,  5,  2, -1, -4])
+
+    >>> cupy.pad(a, (2,), 'maximum')
+    array([5, 5, 1, 2, 3, 4, 5, 5, 5])
+
+    >>> cupy.pad(a, (2,), 'mean')
+    array([3, 3, 1, 2, 3, 4, 5, 3, 3])
+
+    >>> a = [[1, 2], [3, 4]]
+    >>> cupy.pad(a, ((3, 2), (2, 3)), 'minimum')
+    array([[1, 1, 1, 2, 1, 1, 1],
+           [1, 1, 1, 2, 1, 1, 1],
+           [1, 1, 1, 2, 1, 1, 1],
+           [1, 1, 1, 2, 1, 1, 1],
+           [3, 3, 3, 4, 3, 3, 3],
+           [1, 1, 1, 2, 1, 1, 1],
+           [1, 1, 1, 2, 1, 1, 1]])
+
+    >>> a = [1, 2, 3, 4, 5]
+    >>> cupy.pad(a, (2, 3), 'reflect')
+    array([3, 2, 1, 2, 3, 4, 5, 4, 3, 2])
+
+    >>> cupy.pad(a, (2, 3), 'reflect', reflect_type='odd')
+    array([-1,  0,  1,  2,  3,  4,  5,  6,  7,  8])
+
+    >>> cupy.pad(a, (2, 3), 'symmetric')
+    array([2, 1, 1, 2, 3, 4, 5, 5, 4, 3])
+
+    >>> cupy.pad(a, (2, 3), 'symmetric', reflect_type='odd')
+    array([0, 1, 1, 2, 3, 4, 5, 5, 6, 7])
+
+    >>> cupy.pad(a, (2, 3), 'wrap')
+    array([4, 5, 1, 2, 3, 4, 5, 1, 2, 3])
+
+    >>> def pad_with(vector, pad_width, iaxis, kwargs):
+    ...     pad_value = kwargs.get('padder', 10)
+    ...     vector[:pad_width[0]] = pad_value
+    ...     vector[-pad_width[1]:] = pad_value
+    >>> a = cupy.arange(6)
+    >>> a = a.reshape((2, 3))
+    >>> cupy.pad(a, 2, pad_with)
+    array([[10, 10, 10, 10, 10, 10, 10],
+           [10, 10, 10, 10, 10, 10, 10],
+           [10, 10,  0,  1,  2, 10, 10],
+           [10, 10,  3,  4,  5, 10, 10],
+           [10, 10, 10, 10, 10, 10, 10],
+           [10, 10, 10, 10, 10, 10, 10]])
+    >>> cupy.pad(a, 2, pad_with, padder=100)
+    array([[100, 100, 100, 100, 100, 100, 100],
+           [100, 100, 100, 100, 100, 100, 100],
+           [100, 100,   0,   1,   2, 100, 100],
+           [100, 100,   3,   4,   5, 100, 100],
+           [100, 100, 100, 100, 100, 100, 100],
+           [100, 100, 100, 100, 100, 100, 100]])
     """
-    if not numpy.asarray(pad_width).dtype.kind == 'i':
-        raise TypeError('pad_width must be of integral type.')
-    narray = cupy.array(array)
-    pad_width = _validate_lengths(narray, pad_width)
-    allowed_keywords = {
-        'constant': ['constant_values'],
+    array = cupy.asarray(array)
+    pad_width = numpy.asarray(pad_width)
+
+    if not pad_width.dtype.kind == 'i':
+        raise TypeError('`pad_width` must be of integral type.')
+
+    # Broadcast to shape (array.ndim, 2)
+    pad_width = _as_pairs(pad_width, array.ndim, as_index=True)
+
+    if callable(mode):
+        # Old behavior: Use user-supplied function with numpy.apply_along_axis
+        function = mode
+        # Create a new zero padded array
+        padded, _ = _pad_simple(array, pad_width, fill_value=0)
+        # And apply along each axis
+
+        for axis in range(padded.ndim):
+            # Iterate using ndindex as in apply_along_axis, but assuming that
+            # function operates inplace on the padded array.
+
+            # view with the iteration axis at the end
+            view = cupy.moveaxis(padded, axis, -1)
+
+            # compute indices for the iteration axes, and append a trailing
+            # ellipsis to prevent 0d arrays decaying to scalars (gh-8642)
+            inds = numpy.ndindex(view.shape[:-1])
+            inds = (ind + (Ellipsis,) for ind in inds)
+            for ind in inds:
+                function(view[ind], pad_width[axis], axis, kwargs)
+
+        return padded
+
+    # Make sure that no unsupported keywords were passed for the current mode
+    allowed_kwargs = {
+        'empty': [],
         'edge': [],
+        'wrap': [],
+        'constant': ['constant_values'],
+        'linear_ramp': ['end_values'],
+        'maximum': ['stat_length'],
+        'mean': ['stat_length'],
+        # 'median': ['stat_length'],
+        'minimum': ['stat_length'],
         'reflect': ['reflect_type'],
+        'symmetric': ['reflect_type'],
     }
-    keyword_defaults = {
-        'constant_values': 0,
-        'reflect_type': 'even',
+    try:
+        unsupported_kwargs = set(kwargs) - set(allowed_kwargs[mode])
+    except KeyError:
+        raise ValueError("mode '{}' is not supported".format(mode))
+    if unsupported_kwargs:
+        raise ValueError(
+            "unsupported keyword arguments for mode '{}': {}".format(
+                mode, unsupported_kwargs
+            )
+        )
+
+    stat_functions = {
+        'maximum': cupy.max,
+        'minimum': cupy.min,
+        'mean': cupy.mean,
+        # 'median': cupy.median,
     }
-    if mode not in allowed_keywords:
-        raise NotImplementedError
-    for key in keywords:
-        if key not in allowed_keywords[mode]:
-            raise ValueError('%s keyword not in allowed keywords %s' %
-                             (key, allowed_keywords[mode]))
-    for allowed_keyword in allowed_keywords[mode]:
-        keywords.setdefault(allowed_keyword, keyword_defaults[allowed_keyword])
-    for key in keywords:
-        if key == 'constant_values':
-            keywords[key] = _normalize_shape(narray, keywords[key],
-                                             cast_to_int=False)
-    newmatrix = narray.copy()
+
+    # Create array with final shape and original values
+    # (padded area is undefined)
+    padded, original_area_slice = _pad_simple(array, pad_width)
+    # And prepare iteration over all dimensions
+    # (zipping may be more readable than using enumerate)
+    axes = range(padded.ndim)
+
     if mode == 'constant':
-        for axis, ((pad_before, pad_after), (before_value, after_value)) \
-                in enumerate(six.moves.zip(pad_width,
-                                           keywords['constant_values'])):
-            newmatrix = _prepend_const(
-                newmatrix, pad_before, before_value, axis)
-            newmatrix = _append_const(newmatrix, pad_after, after_value, axis)
-    elif mode == 'edge':
-        for axis, (pad_before, pad_after) in enumerate(pad_width):
-            newmatrix = _prepend_edge(newmatrix, pad_before, axis)
-            newmatrix = _append_edge(newmatrix, pad_after, axis)
-    elif mode == 'reflect':
-        for axis, (pad_before, pad_after) in enumerate(pad_width):
-            if narray.shape[axis] == 0:
-                # Axes with non-zero padding cannot be empty.
-                if pad_before > 0 or pad_after > 0:
-                    raise ValueError('There aren\'t any elements to reflect'
-                                     ' in axis {} of `array`'.format(axis))
-                # Skip zero padding on empty axes.
-                continue
+        values = kwargs.get('constant_values', 0)
+        values = _as_pairs(values, padded.ndim)
+        for axis, width_pair, value_pair in zip(axes, pad_width, values):
+            roi = _view_roi(padded, original_area_slice, axis)
+            _set_pad_area(roi, axis, width_pair, value_pair)
+
+    elif mode == 'empty':
+        pass  # Do nothing as _pad_simple already returned the correct result
+
+    elif array.size == 0:
+        # Only modes 'constant' and 'empty' can extend empty axes, all other
+        # modes depend on `array` not being empty
+        # -> ensure every empty axis is only 'padded with 0'
+        for axis, width_pair in zip(axes, pad_width):
+            if array.shape[axis] == 0 and any(width_pair):
+                raise ValueError(
+                    "can't extend empty axis {} using modes other than "
+                    "'constant' or 'empty'".format(axis)
+                )
+        # passed, don't need to do anything more as _pad_simple already
+        # returned the correct result
 
-            # Recursive padding along any axis where `pad_amt` is too large
-            # for indexing tricks. We can only safely pad the original axis
-            # length, to keep the period of the reflections consistent.
-            if ((pad_before > 0) or
-                    (pad_after > 0)) and newmatrix.shape[axis] == 1:
+    elif mode == 'edge':
+        for axis, width_pair in zip(axes, pad_width):
+            roi = _view_roi(padded, original_area_slice, axis)
+            edge_pair = _get_edges(roi, axis, width_pair)
+            _set_pad_area(roi, axis, width_pair, edge_pair)
+
+    elif mode == 'linear_ramp':
+        end_values = kwargs.get('end_values', 0)
+        end_values = _as_pairs(end_values, padded.ndim)
+        for axis, width_pair, value_pair in zip(axes, pad_width, end_values):
+            roi = _view_roi(padded, original_area_slice, axis)
+            ramp_pair = _get_linear_ramps(roi, axis, width_pair, value_pair)
+            _set_pad_area(roi, axis, width_pair, ramp_pair)
+
+    elif mode in stat_functions:
+        func = stat_functions[mode]
+        length = kwargs.get('stat_length', None)
+        length = _as_pairs(length, padded.ndim, as_index=True)
+        for axis, width_pair, length_pair in zip(axes, pad_width, length):
+            roi = _view_roi(padded, original_area_slice, axis)
+            stat_pair = _get_stats(roi, axis, width_pair, length_pair, func)
+            _set_pad_area(roi, axis, width_pair, stat_pair)
+
+    elif mode in {'reflect', 'symmetric'}:
+        method = kwargs.get('reflect_type', 'even')
+        include_edge = True if mode == 'symmetric' else False
+        for axis, (left_index, right_index) in zip(axes, pad_width):
+            if array.shape[axis] == 1 and (left_index > 0 or right_index > 0):
                 # Extending singleton dimension for 'reflect' is legacy
                 # behavior; it really should raise an error.
-                newmatrix = _prepend_edge(newmatrix, pad_before, axis)
-                newmatrix = _append_edge(newmatrix, pad_after, axis)
+                edge_pair = _get_edges(padded, axis, (left_index, right_index))
+                _set_pad_area(
+                    padded, axis, (left_index, right_index), edge_pair
+                )
                 continue
 
-            method = keywords['reflect_type']
-            safe_pad = newmatrix.shape[axis] - 1
-            while ((pad_before > safe_pad) or (pad_after > safe_pad)):
-                pad_iter_b = min(safe_pad,
-                                 safe_pad * (pad_before // safe_pad))
-                pad_iter_a = min(safe_pad, safe_pad * (pad_after // safe_pad))
-                newmatrix = _pad_ref(
-                    newmatrix, (pad_iter_b, pad_iter_a), method, axis)
-                pad_before -= pad_iter_b
-                pad_after -= pad_iter_a
-                safe_pad += pad_iter_b + pad_iter_a
-            newmatrix = _pad_ref(
-                newmatrix, (pad_before, pad_after), method, axis)
-    return newmatrix
+            roi = _view_roi(padded, original_area_slice, axis)
+            while left_index > 0 or right_index > 0:
+                # Iteratively pad until dimension is filled with reflected
+                # values. This is necessary if the pad area is larger than
+                # the length of the original values in the current dimension.
+                left_index, right_index = _set_reflect_both(
+                    roi, axis, (left_index, right_index), method, include_edge
+                )
+
+    elif mode == 'wrap':
+        for axis, (left_index, right_index) in zip(axes, pad_width):
+            roi = _view_roi(padded, original_area_slice, axis)
+            while left_index > 0 or right_index > 0:
+                # Iteratively pad until dimension is filled with wrapped
+                # values. This is necessary if the pad area is larger than
+                # the length of the original values in the current dimension.
+                left_index, right_index = _set_wrap_both(
+                    roi, axis, (left_index, right_index)
+                )
+
+    return padded


Test patch:
diff --git a/tests/cupy_tests/padding_tests/test_pad.py b/tests/cupy_tests/padding_tests/test_pad.py
--- a/tests/cupy_tests/padding_tests/test_pad.py
+++ b/tests/cupy_tests/padding_tests/test_pad.py
@@ -10,7 +10,9 @@
     *testing.product({
         'array': [numpy.arange(6).reshape([2, 3])],
         'pad_width': [1, [1, 2], [[1, 2], [3, 4]]],
-        'mode': ['constant', 'edge', 'reflect'],
+        # mode 'mean' is non-exact, so it is tested in a separate class
+        'mode': ['constant', 'edge', 'linear_ramp', 'maximum',
+                 'minimum', 'reflect', 'symmetric', 'wrap'],
     })
 )
 @testing.gpu
@@ -21,6 +23,17 @@ class TestPadDefault(unittest.TestCase):
     def test_pad_default(self, xp, dtype):
         array = xp.array(self.array, dtype=dtype)
 
+        if (xp.dtype(dtype).kind in ['i', 'u'] and
+                self.mode == 'linear_ramp'):
+            # TODO: can remove this skip once cupy/cupy/#2330 is merged
+            return array
+
+        if (self.mode == 'linear_ramp' and
+                numpy.lib.NumpyVersion(numpy.__version__) < '1.16.0'):
+            # skip linear_ramp test on older NumPy until pad is updated to
+            # use cupy.linspace with axis argument (cupy/cupy#2461).
+            return array
+
         # Older version of NumPy(<1.12) can emit ComplexWarning
         def f():
             return xp.pad(array, self.pad_width, mode=self.mode)
@@ -33,6 +46,36 @@ def f():
             return f()
 
 
+@testing.parameterize(
+    *testing.product({
+        'array': [numpy.arange(6).reshape([2, 3])],
+        'pad_width': [1, [1, 2], [[1, 2], [3, 4]]],
+    })
+)
+@testing.gpu
+class TestPadDefaultMean(unittest.TestCase):
+
+    @testing.for_all_dtypes(no_bool=True)
+    @testing.numpy_cupy_array_almost_equal(decimal=5)
+    def test_pad_default(self, xp, dtype):
+        array = xp.array(self.array, dtype=dtype)
+
+        if xp.dtype(dtype).kind in ['i', 'u']:
+            # TODO: can remove this skip once cupy/cupy/#2330 is merged
+            return array
+
+        # Older version of NumPy(<1.12) can emit ComplexWarning
+        def f():
+            return xp.pad(array, self.pad_width, mode='mean')
+
+        if xp is numpy:
+            with warnings.catch_warnings():
+                warnings.simplefilter('ignore', numpy.ComplexWarning)
+                return f()
+        else:
+            return f()
+
+
 @testing.parameterize(
     # mode='constant'
     {'array': numpy.arange(6).reshape([2, 3]), 'pad_width': 1,
@@ -51,6 +94,36 @@ def f():
     {'array': numpy.arange(6).reshape([2, 3]),
      'pad_width': [[1, 2], [3, 4]], 'mode': 'reflect',
      'reflect_type': 'odd'},
+    # mode='symmetric'
+    {'array': numpy.arange(6).reshape([2, 3]), 'pad_width': 1,
+     'mode': 'symmetric', 'reflect_type': 'odd'},
+    {'array': numpy.arange(6).reshape([2, 3]),
+     'pad_width': [1, 2], 'mode': 'symmetric', 'reflect_type': 'odd'},
+    {'array': numpy.arange(6).reshape([2, 3]),
+     'pad_width': [[1, 2], [3, 4]], 'mode': 'symmetric',
+     'reflect_type': 'odd'},
+    # mode='minimum'
+    {'array': numpy.arange(60).reshape([5, 12]), 'pad_width': 1,
+     'mode': 'minimum', 'stat_length': 2},
+    {'array': numpy.arange(60).reshape([5, 12]),
+     'pad_width': [1, 2], 'mode': 'minimum', 'stat_length': (2, 4)},
+    {'array': numpy.arange(60).reshape([5, 12]),
+     'pad_width': [[1, 2], [3, 4]], 'mode': 'minimum',
+     'stat_length': ((2, 4), (3, 5))},
+    {'array': numpy.arange(60).reshape([5, 12]),
+     'pad_width': [[1, 2], [3, 4]], 'mode': 'minimum',
+     'stat_length': None},
+    # mode='maximum'
+    {'array': numpy.arange(60).reshape([5, 12]), 'pad_width': 1,
+     'mode': 'maximum', 'stat_length': 2},
+    {'array': numpy.arange(60).reshape([5, 12]),
+     'pad_width': [1, 2], 'mode': 'maximum', 'stat_length': (2, 4)},
+    {'array': numpy.arange(60).reshape([5, 12]),
+     'pad_width': [[1, 2], [3, 4]], 'mode': 'maximum',
+     'stat_length': ((2, 4), (3, 5))},
+    {'array': numpy.arange(60).reshape([5, 12]),
+     'pad_width': [[1, 2], [3, 4]], 'mode': 'maximum',
+     'stat_length': None},
 )
 @testing.gpu
 # Old numpy does not work with multi-dimensional constant_values
@@ -67,7 +140,10 @@ def f():
             if self.mode == 'constant':
                 return xp.pad(array, self.pad_width, mode=self.mode,
                               constant_values=self.constant_values)
-            elif self.mode == 'reflect':
+            elif self.mode in ['minimum', 'maximum']:
+                return xp.pad(array, self.pad_width, mode=self.mode,
+                              stat_length=self.stat_length)
+            elif self.mode in ['reflect', 'symmetric']:
                 return xp.pad(array, self.pad_width, mode=self.mode,
                               reflect_type=self.reflect_type)
 
@@ -79,6 +155,46 @@ def f():
             return f()
 
 
+@testing.parameterize(
+    # mode='mean'
+    {'array': numpy.arange(60).reshape([5, 12]), 'pad_width': 1,
+     'mode': 'mean', 'stat_length': 2},
+    {'array': numpy.arange(60).reshape([5, 12]),
+     'pad_width': [1, 2], 'mode': 'mean', 'stat_length': (2, 4)},
+    {'array': numpy.arange(60).reshape([5, 12]),
+     'pad_width': [[1, 2], [3, 4]], 'mode': 'mean',
+     'stat_length': ((2, 4), (3, 5))},
+    {'array': numpy.arange(60).reshape([5, 12]),
+     'pad_width': [[1, 2], [3, 4]], 'mode': 'mean',
+     'stat_length': None},
+)
+@testing.gpu
+# Old numpy does not work with multi-dimensional constant_values
+@testing.with_requires('numpy>=1.11.1')
+class TestPadMean(unittest.TestCase):
+
+    @testing.for_all_dtypes(no_bool=True)
+    @testing.numpy_cupy_array_almost_equal(decimal=5)
+    def test_pad(self, xp, dtype):
+        array = xp.array(self.array, dtype=dtype)
+
+        if xp.dtype(dtype).kind in ['i', 'u']:
+            # TODO: can remove this skip once cupy/cupy/#2330 is merged
+            return array
+
+        # Older version of NumPy(<1.12) can emit ComplexWarning
+        def f():
+            return xp.pad(array, self.pad_width, mode=self.mode,
+                          stat_length=self.stat_length)
+
+        if xp is numpy:
+            with warnings.catch_warnings():
+                warnings.simplefilter('ignore', numpy.ComplexWarning)
+                return f()
+        else:
+            return f()
+
+
 @testing.gpu
 class TestPadNumpybug(unittest.TestCase):
 
@@ -94,6 +210,46 @@ def test_pad_highdim_default(self, xp, dtype):
         return a
 
 
+@testing.gpu
+class TestPadEmpty(unittest.TestCase):
+
+    @testing.with_requires('numpy>=1.17')
+    @testing.for_all_dtypes(no_bool=True)
+    @testing.numpy_cupy_array_equal()
+    def test_pad_empty(self, xp, dtype):
+        array = xp.arange(6, dtype=dtype).reshape([2, 3])
+        pad_width = 2
+        a = xp.pad(array, pad_width=pad_width, mode='empty')
+        # omit uninitialized "empty" boundary from the comparison
+        return a[pad_width:-pad_width, pad_width:-pad_width]
+
+
+@testing.gpu
+class TestPadCustomFunction(unittest.TestCase):
+
+    @testing.with_requires('numpy>=1.12')
+    @testing.for_all_dtypes(no_bool=True)
+    @testing.numpy_cupy_array_equal()
+    def test_pad_via_func_modifying_inplace(self, xp, dtype):
+        def _padwithtens(vector, pad_width, iaxis, kwargs):
+            vector[:pad_width[0]] = 10
+            vector[-pad_width[1]:] = 10
+        a = xp.arange(6, dtype=dtype).reshape(2, 3)
+        a = xp.pad(a, 2, _padwithtens)
+        return a
+
+    @testing.for_all_dtypes(no_bool=True)
+    @testing.numpy_cupy_array_equal()
+    def test_pad_via_func_returning_vector(self, xp, dtype):
+        def _padwithtens(vector, pad_width, iaxis, kwargs):
+            vector[:pad_width[0]] = 10
+            vector[-pad_width[1]:] = 10
+            return vector  # returning vector required by old NumPy (<=1.12)
+        a = xp.arange(6, dtype=dtype).reshape(2, 3)
+        a = xp.pad(a, 2, _padwithtens)
+        return a
+
+
 @testing.parameterize(
     # mode='constant'
     {'array': [], 'pad_width': 1, 'mode': 'constant', 'constant_values': 3},