Repository: cupy/cupy

Issue:
Feature: preserve memory layout order in empty_like, etc?
One thing that was surprising to me on using `cupy.zeros_like`, `cupy.empty_like` or `cupy.ones_like` were that when the input array was Fortran-ordered, the output is C-ordered. This differs from the NumPy behavior which preserves order. I see that it the docstring does state that "this function does not support order and subok options", so I suppose this is to be expected.

Would the CuPy devs be open to a PR that would preserve order 'C' or 'F' for contiguous arrays in these functions?. I think supporting the `order` parameter for values of 'C', 'F' and 'A' should be easy to do and would be willing to make a PR for that. (The underlying `cupy.zeros`, etc. already have the necessary `order` argument).

p.s. 
Congrats on the CuPy project! It is very nice and with the recently added support for custom kernels, I think I would prefer it going forward to PyCUDA in many cases due to the more complete NumPy API coverage.


Discussion:

Resolving patch:
diff --git a/cupy/creation/basic.py b/cupy/creation/basic.py
--- a/cupy/creation/basic.py
+++ b/cupy/creation/basic.py
@@ -1,6 +1,8 @@
 import cupy
 import numpy
 
+from cupy.core.core import _update_order_char, _get_strides_for_order_K
+
 
 def empty(shape, dtype=float, order='C'):
     """Returns an array without initializing the elements.
@@ -20,7 +22,28 @@ def empty(shape, dtype=float, order='C'):
     return cupy.ndarray(shape, dtype, order=order)
 
 
-def empty_like(a, dtype=None):
+def _new_like_order_and_strides(a, dtype, order):
+    """
+    Determine order and strides as in NumPy's PyArray_NewLikeArray.
+
+    (see: numpy/core/src/multiarray/ctors.c)
+    """
+    order = order.upper()
+    if order not in ['C', 'F', 'K', 'A']:
+        raise TypeError('order not understood: {}'.format(order))
+
+    order = chr(_update_order_char(a, ord(order)))
+
+    if order == 'K':
+        strides = _get_strides_for_order_K(a, numpy.dtype(dtype))
+        order = 'C'
+        memptr = cupy.empty(a.size, dtype=dtype).data
+        return order, strides, memptr
+    else:
+        return order, None, None
+
+
+def empty_like(a, dtype=None, order='K'):
     """Returns a new array with same shape and dtype of a given array.
 
     This function currently does not support ``order`` and ``subok`` options.
@@ -28,6 +51,10 @@ def empty_like(a, dtype=None):
     Args:
         a (cupy.ndarray): Base array.
         dtype: Data type specifier. The data type of ``a`` is used by default.
+        order ({'C', 'F', 'A', or 'K'}): Overrides the memory layout of the
+            result. 'C' means C-order, 'F' means F-order, 'A' means 'F' if
+            ``a`` is Fortran contiguous, 'C' otherwise. 'K' means match the
+            layout of ``a`` as closely as possible.
 
     Returns:
         cupy.ndarray: A new array with same shape and dtype of ``a`` with
@@ -36,10 +63,11 @@ def empty_like(a, dtype=None):
     .. seealso:: :func:`numpy.empty_like`
 
     """
-    # TODO(beam2d): Support ordering option
     if dtype is None:
         dtype = a.dtype
-    return cupy.ndarray(a.shape, dtype)
+
+    order, strides, memptr = _new_like_order_and_strides(a, dtype, order)
+    return cupy.ndarray(a.shape, dtype, memptr, strides, order)
 
 
 def eye(N, M=None, k=0, dtype=float):
@@ -106,7 +134,7 @@ def ones(shape, dtype=float):
     return a
 
 
-def ones_like(a, dtype=None):
+def ones_like(a, dtype=None, order='K'):
     """Returns an array of ones with same shape and dtype as a given array.
 
     This function currently does not support ``order`` and ``subok`` options.
@@ -114,6 +142,10 @@ def ones_like(a, dtype=None):
     Args:
         a (cupy.ndarray): Base array.
         dtype: Data type specifier. The dtype of ``a`` is used by default.
+        order ({'C', 'F', 'A', or 'K'}): Overrides the memory layout of the
+            result. 'C' means C-order, 'F' means F-order, 'A' means 'F' if
+            ``a`` is Fortran contiguous, 'C' otherwise. 'K' means match the
+            layout of ``a`` as closely as possible.
 
     Returns:
         cupy.ndarray: An array filled with ones.
@@ -121,10 +153,10 @@ def ones_like(a, dtype=None):
     .. seealso:: :func:`numpy.ones_like`
 
     """
-    # TODO(beam2d): Support ordering option
     if dtype is None:
         dtype = a.dtype
-    a = cupy.ndarray(a.shape, dtype)
+    order, strides, memptr = _new_like_order_and_strides(a, dtype, order)
+    a = cupy.ndarray(a.shape, dtype, memptr, strides, order)
     a.fill(1)
     return a
 
@@ -149,7 +181,7 @@ def zeros(shape, dtype=float, order='C'):
     return a
 
 
-def zeros_like(a, dtype=None):
+def zeros_like(a, dtype=None, order='K'):
     """Returns an array of zeros with same shape and dtype as a given array.
 
     This function currently does not support ``order`` and ``subok`` options.
@@ -157,6 +189,10 @@ def zeros_like(a, dtype=None):
     Args:
         a (cupy.ndarray): Base array.
         dtype: Data type specifier. The dtype of ``a`` is used by default.
+        order ({'C', 'F', 'A', or 'K'}): Overrides the memory layout of the
+            result. 'C' means C-order, 'F' means F-order, 'A' means 'F' if
+            ``a`` is Fortran contiguous, 'C' otherwise. 'K' means match the
+            layout of ``a`` as closely as possible.\
 
     Returns:
         cupy.ndarray: An array filled with zeros.
@@ -164,10 +200,10 @@ def zeros_like(a, dtype=None):
     .. seealso:: :func:`numpy.zeros_like`
 
     """
-    # TODO(beam2d): Support ordering option
     if dtype is None:
         dtype = a.dtype
-    a = cupy.ndarray(a.shape, dtype)
+    order, strides, memptr = _new_like_order_and_strides(a, dtype, order)
+    a = cupy.ndarray(a.shape, dtype, memptr, strides, order)
     a.data.memset_async(0, a.nbytes)
     return a
 
@@ -199,7 +235,7 @@ def full(shape, fill_value, dtype=None):
     return a
 
 
-def full_like(a, fill_value, dtype=None):
+def full_like(a, fill_value, dtype=None, order='K'):
     """Returns a full array with same shape and dtype as a given array.
 
     This function currently does not support ``order`` and ``subok`` options.
@@ -208,6 +244,10 @@ def full_like(a, fill_value, dtype=None):
         a (cupy.ndarray): Base array.
         fill_value: A scalar value to fill a new array.
         dtype: Data type specifier. The dtype of ``a`` is used by default.
+        order ({'C', 'F', 'A', or 'K'}): Overrides the memory layout of the
+            result. 'C' means C-order, 'F' means F-order, 'A' means 'F' if
+            ``a`` is Fortran contiguous, 'C' otherwise. 'K' means match the
+            layout of ``a`` as closely as possible.
 
     Returns:
         cupy.ndarray: An array filled with ``fill_value``.
@@ -215,9 +255,9 @@ def full_like(a, fill_value, dtype=None):
     .. seealso:: :func:`numpy.full_like`
 
     """
-    # TODO(beam2d): Support ordering option
     if dtype is None:
         dtype = a.dtype
-    a = cupy.ndarray(a.shape, dtype)
+    order, strides, memptr = _new_like_order_and_strides(a, dtype, order)
+    a = cupy.ndarray(a.shape, dtype, memptr, strides, order)
     a.fill(fill_value)
     return a


Test patch:
diff --git a/tests/cupy_tests/creation_tests/test_basic.py b/tests/cupy_tests/creation_tests/test_basic.py
--- a/tests/cupy_tests/creation_tests/test_basic.py
+++ b/tests/cupy_tests/creation_tests/test_basic.py
@@ -69,14 +69,86 @@ def test_empty_int_huge_size_fill0(self):
         del a
         cupy.get_default_memory_pool().free_all_blocks()
 
+    @testing.for_orders('CFAK')
     @testing.for_all_dtypes()
     @testing.numpy_cupy_array_equal()
-    def test_empty_like(self, xp, dtype):
+    def test_empty_like(self, xp, dtype, order):
         a = testing.shaped_arange((2, 3, 4), xp, dtype)
-        b = xp.empty_like(a)
+        b = xp.empty_like(a, order=order)
         b.fill(0)
         return b
 
+    @testing.for_orders('CFAK')
+    @testing.for_all_dtypes()
+    @testing.numpy_cupy_array_equal()
+    def test_empty_like_contiguity(self, xp, dtype, order):
+        a = testing.shaped_arange((2, 3, 4), xp, dtype)
+        b = xp.empty_like(a, order=order)
+        b.fill(0)
+        if order in ['f', 'F']:
+            self.assertTrue(b.flags.f_contiguous)
+        else:
+            self.assertTrue(b.flags.c_contiguous)
+        return b
+
+    @testing.for_orders('CFAK')
+    @testing.for_all_dtypes()
+    @testing.numpy_cupy_array_equal()
+    def test_empty_like_contiguity2(self, xp, dtype, order):
+        a = testing.shaped_arange((2, 3, 4), xp, dtype)
+        a = xp.asfortranarray(a)
+        b = xp.empty_like(a, order=order)
+        b.fill(0)
+        if order in ['c', 'C']:
+            self.assertTrue(b.flags.c_contiguous)
+        else:
+            self.assertTrue(b.flags.f_contiguous)
+        return b
+
+    @testing.for_orders('CFAK')
+    @testing.for_all_dtypes()
+    @testing.numpy_cupy_array_equal()
+    def test_empty_like_contiguity3(self, xp, dtype, order):
+        a = testing.shaped_arange((2, 3, 4), xp, dtype)
+        # test strides that are both non-contiguous and non-descending
+        a = a[:, ::2, :].swapaxes(0, 1)
+        b = xp.empty_like(a, order=order)
+        b.fill(0)
+        if order in ['k', 'K', None]:
+            self.assertFalse(b.flags.c_contiguous)
+            self.assertFalse(b.flags.f_contiguous)
+        elif order in ['f', 'F']:
+            self.assertFalse(b.flags.c_contiguous)
+            self.assertTrue(b.flags.f_contiguous)
+        else:
+            self.assertTrue(b.flags.c_contiguous)
+            self.assertFalse(b.flags.f_contiguous)
+        return b
+
+    @testing.for_all_dtypes()
+    def test_empty_like_K_strides(self, dtype):
+        # test strides that are both non-contiguous and non-descending
+        a = testing.shaped_arange((2, 3, 4), numpy, dtype)
+        a = a[:, ::2, :].swapaxes(0, 1)
+        b = numpy.empty_like(a, order='K')
+        b.fill(0)
+
+        # GPU case
+        ag = testing.shaped_arange((2, 3, 4), cupy, dtype)
+        ag = ag[:, ::2, :].swapaxes(0, 1)
+        bg = cupy.empty_like(ag, order='K')
+        bg.fill(0)
+
+        # make sure NumPy and CuPy strides agree
+        self.assertEqual(b.strides, bg.strides)
+        return
+
+    @testing.numpy_cupy_raises()
+    def test_empty_like_invalid_order(self, xp, dtype):
+        a = testing.shaped_arange((2, 3, 4), xp, dtype)
+        b = xp.empty_like(a, order='Q')
+        return b
+
     @testing.for_CF_orders()
     def test_empty_zero_sized_array_strides(self, order):
         a = numpy.empty((1, 0, 2), dtype='d', order=order)
@@ -117,22 +189,24 @@ def test_zeros_strides(self, order):
         b = cupy.zeros((2, 3), dtype='d', order=order)
         self.assertEqual(b.strides, a.strides)
 
+    @testing.for_orders('CFAK')
     @testing.for_all_dtypes()
     @testing.numpy_cupy_array_equal()
-    def test_zeros_like(self, xp, dtype):
+    def test_zeros_like(self, xp, dtype, order):
         a = xp.ndarray((2, 3, 4), dtype=dtype)
-        return xp.zeros_like(a)
+        return xp.zeros_like(a, order=order)
 
     @testing.for_all_dtypes()
     @testing.numpy_cupy_array_equal()
     def test_ones(self, xp, dtype):
         return xp.ones((2, 3, 4), dtype=dtype)
 
+    @testing.for_orders('CFAK')
     @testing.for_all_dtypes()
     @testing.numpy_cupy_array_equal()
-    def test_ones_like(self, xp, dtype):
+    def test_ones_like(self, xp, dtype, order):
         a = xp.ndarray((2, 3, 4), dtype=dtype)
-        return xp.ones_like(a)
+        return xp.ones_like(a, order=order)
 
     @testing.for_all_dtypes()
     @testing.numpy_cupy_array_equal()
@@ -151,8 +225,9 @@ def test_full_default_dtype(self, xp, dtype):
     def test_full_default_dtype_cpu_input(self, xp, dtype):
         return xp.full((2, 3, 4), numpy.array(1, dtype=dtype))
 
+    @testing.for_orders('CFAK')
     @testing.for_all_dtypes()
     @testing.numpy_cupy_array_equal()
-    def test_full_like(self, xp, dtype):
+    def test_full_like(self, xp, dtype, order):
         a = xp.ndarray((2, 3, 4), dtype=dtype)
-        return xp.full_like(a, 1)
+        return xp.full_like(a, 1, order=order)