Repository: chainer/chainer

Issue:
ChainerX take allows OOB access
I think we should not allow this?
```
>>> x = chx.array([1,2])
>>> y = x.take(chx.array([3]), axis=0)
>>> y
array([2], shape=(1,), dtype=int64, device='native:0')
```
numpy raises an exception for this.


Discussion:
I will take a deep look at it
Ideally should behave the same way as numpy.

OOB checking is not being done.
It is easy to do if the indexes is a native or numpy array.
But seems tricky when using device arrays (Cuda) since it will incur in synchronization.

Resolving patch:
diff --git a/chainerx/_docs/routines.py b/chainerx/_docs/routines.py
--- a/chainerx/_docs/routines.py
+++ b/chainerx/_docs/routines.py
@@ -542,6 +542,10 @@ def _docs_indexing():
         The indices of the values to extract. When indices are out of bounds,
         they are wrapped around.
     axis (int): The axis over which to select values.
+    mode (str): Specifies how out-of-bounds indices will behave.
+        'raise' - raise an error
+        'wrap' - wrap around
+        'clip' - clip to the range
 
 Returns:
     :func:`~chainerx.ndarray`: Output array.
@@ -553,6 +557,11 @@ def _docs_indexing():
     During backpropagation, this function propagates the gradient of the
     output array to the input array ``a``.
 
+Note:
+   The default mode for the native backend is 'raise', while for the cuda
+   backend is 'wrap' in order to prevent device synchronization.
+   'raise' mode is currently not supported in the CUDA backend.
+
 .. seealso:: :func:`numpy.take`
 """)
 


Test patch:
diff --git a/chainerx_cc/chainerx/array_test.cc b/chainerx_cc/chainerx/array_test.cc
--- a/chainerx_cc/chainerx/array_test.cc
+++ b/chainerx_cc/chainerx/array_test.cc
@@ -33,6 +33,7 @@
 #include "chainerx/indexable_array.h"
 #include "chainerx/indexer.h"
 #include "chainerx/op_node.h"
+#include "chainerx/routines/indexing.h"
 #include "chainerx/scalar.h"
 #include "chainerx/shape.h"
 #include "chainerx/slice.h"
@@ -1168,7 +1169,7 @@ TEST_P(ArrayTest, Take) {
     Shape output_shape{2, 2, 3};
     Array a = testing::BuildArray(input_shape).WithLinearData<T>().WithPadding(1);
     Array indices = testing::BuildArray(indices_shape).WithData<int64_t>({0, 14, 3, 1, -10, 1});
-    Array b = a.Take(indices, 1);
+    Array b = a.Take(indices, 1, IndexBoundsMode::kWrap);
 
     EXPECT_EQ(output_shape, b.shape());
     Array e = testing::BuildArray(output_shape).WithData<T>({0, 2, 3, 1, 2, 1, 4, 6, 7, 5, 6, 5});
diff --git a/tests/chainerx_tests/unit_tests/routines_tests/test_indexing.py b/tests/chainerx_tests/unit_tests/routines_tests/test_indexing.py
--- a/tests/chainerx_tests/unit_tests/routines_tests/test_indexing.py
+++ b/tests/chainerx_tests/unit_tests/routines_tests/test_indexing.py
@@ -208,6 +208,8 @@ def test_getitem_zero_sized_offsets(device):
 # wasteful.
 @chainer.testing.parameterize_pytest(
     'indices_dtype', chainerx.testing.integral_dtypes)
+@chainer.testing.parameterize_pytest(
+    'mode', ['raise', 'wrap', 'clip'])
 @chainer.testing.parameterize_pytest(
     'a_dtype', chainerx.testing.all_dtypes)
 class TestTake(op_utils.NumpyOpTest):
@@ -216,7 +218,8 @@ class TestTake(op_utils.NumpyOpTest):
     forward_accept_errors = (chainerx.DimensionError, numpy.AxisError)
 
     def setup(self):
-        if (numpy.dtype(self.indices_dtype).kind == 'u'
+        if (self.mode == 'raise'
+                and numpy.dtype(self.indices_dtype).kind == 'u'
                 and (numpy.array(self.indices, 'int64') < 0).any()):
             raise unittest.SkipTest(
                 'Indices underflows and index out of bounds cannot be tested.')
@@ -236,6 +239,9 @@ def forward_xp(self, inputs, xp):
         axis = self.axis
         indices_type = self.indices_type
         a, = inputs
+        if (xp is chainerx and self.mode == 'raise'
+                and 'cuda' in xp.get_default_device().name):
+            pytest.skip('CUDA is not supportted with mode="raise"')
 
         assert isinstance(indices, list)
         if indices_type == 'list':
@@ -248,12 +254,28 @@ def forward_xp(self, inputs, xp):
             assert False, indices_type
 
         if self.is_module:
-            b = xp.take(a, indices, axis)
+            b = xp.take(a, indices, axis, mode=self.mode)
         else:
-            b = a.take(indices, axis)
+            b = a.take(indices, axis, mode=self.mode)
         return b,
 
 
+@pytest.mark.parametrize_device(['native:0', 'cuda:0'])
+@pytest.mark.parametrize('shape,indices,axis', [
+    # Invalid: Index out of bounds
+    ((2, 3), [2], 0),
+    ((2, 3), [-3], 0),
+])
+def test_take_index_error(device, shape, indices, axis):
+    a = array_utils.create_dummy_ndarray(chainerx, shape, 'float32')
+    indices = numpy.array(indices).astype(numpy.int32)
+    error = IndexError
+    if device.backend.name == 'cuda':
+        error = chainerx.BackendError  # Not supported in CUDA
+    with pytest.raises(error):
+        chainerx.take(a, indices, axis, mode='raise')
+
+
 def _random_condition(shape, dtype):
     size = int(numpy.prod(shape))
     mask = numpy.random.randint(0, 1, size).astype('bool_').reshape(shape)