Repository: chainer/chainer

Issue:
cupy.random.randn raises an error when generating an array with an odd number of elements
For example:

```
>>> from chainer import cuda
>>> cuda.cupy.random.randn(1,2)
array([[-0.25047453,  0.7825017 ]])
>>> cuda.cupy.random.randn(1,3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../chainer-1.3.0-py2.7.egg/cupy/random/sample.py", line 52, in randn
    return distributions.normal(size=size, dtype=dtype)
  File .../chainer-1.3.0-py2.7.egg/cupy/random/distributions.py", line 47, in normal
    return rs.normal(loc, scale, size=size, dtype=dtype)
  File ".../chainer-1.3.0-py2.7.egg/cupy/random/generator.py", line 89, in normal
    func(self._generator, out.data.ptr, out.size, loc, scale)
  File ".../chainer-1.3.0-py2.7.egg/cupy/cuda/curand.py", line 196, in generateNormalDouble
    check_status(status)
  File ".../chainer-1.3.0-py2.7.egg/cupy/cuda/curand.py", line 79, in check_status
    raise CURANDError(status)
cupy.cuda.curand.CURANDError: CURAND_STATUS_LENGTH_NOT_MULTIPLE
>>> cuda.cupy.random.randn(1,4)
array([[-2.93901085, -0.69427899,  0.18251298,  1.91443934]])
>>> cuda.cupy.random.randn(2,2)
array([[-1.75610197,  0.79795696],
       [-1.04317749, -0.50339785]])
>>> cuda.cupy.random.randn(3,3)
[Snip identical error output]
```

I've done a search over a range of different shapes, and the one constant seems to be that an odd number of elements reliably results in the above error.



Discussion:

Resolving patch:
diff --git a/cupy/__init__.py b/cupy/__init__.py
--- a/cupy/__init__.py
+++ b/cupy/__init__.py
@@ -66,6 +66,17 @@
 float64 = numpy.float64
 
 
+def _get_size(size):
+    if size is None:
+        return ()
+    elif isinstance(size, collections.Sequence):
+        return tuple(size)
+    elif isinstance(size, int):
+        return size,
+    else:
+        raise ValueError('size should be None, collections.Sequence, or int')
+
+
 class ndarray(object):
 
     """Multi-dimensional array on a CUDA device.
@@ -87,7 +98,7 @@ class ndarray(object):
 
     """
     def __init__(self, shape, dtype=float, memptr=None, strides=None):
-        self._shape = shape = tuple(shape)
+        self._shape = shape = _get_size(shape)
         self._dtype = dtype = numpy.dtype(dtype)
         size = 1
         for s in shape:
diff --git a/cupy/cuda/curand.py b/cupy/cuda/curand.py
--- a/cupy/cuda/curand.py
+++ b/cupy/cuda/curand.py
@@ -208,6 +208,10 @@ def generateNormalDouble(generator, outputPtr, n, mean, stddev):
 
 
 def generateLogNormal(generator, outputPtr, n, mean, stddev):
+    if n % 2 == 1:
+        msg = 'curandGenerateLogNormal can only generate even number of '\
+              'random variables simultaneously. See issue #390 for detail.'
+        raise ValueError(msg)
     status = _curand.curandGenerateLogNormal(generator, outputPtr, n,
                                              mean, stddev)
     check_status(status)
@@ -217,6 +221,10 @@ def generateLogNormal(generator, outputPtr, n, mean, stddev):
 
 
 def generateLogNormalDouble(generator, outputPtr, n, mean, stddev):
+    if n % 2 == 1:
+        msg = 'curandGenerateLogNormalDouble can only generate even number of '\
+              'random variables simultaneously. See issue #390 for detail.'
+        raise ValueError(msg)
     status = _curand.curandGenerateLogNormalDouble(generator, outputPtr, n,
                                                    mean, stddev)
     check_status(status)
diff --git a/cupy/random/generator.py b/cupy/random/generator.py
--- a/cupy/random/generator.py
+++ b/cupy/random/generator.py
@@ -1,10 +1,11 @@
 import atexit
 import binascii
-import collections
+import operator
 import os
 import time
 
 import numpy
+import six
 
 import cupy
 from cupy import cuda
@@ -51,6 +52,23 @@ def set_stream(self, stream=None):
             stream = cuda.Stream()
         curand.setStream(self._generator, stream.ptr)
 
+    def _generate_normal(self, func, size, dtype, *args):
+        # curand funcitons below don't support odd size.
+        # * curand.generateNormal
+        # * curand.generateNormalDouble
+        # * curand.generateLogNormal
+        # * curand.generateLogNormalDouble
+        size = cupy._get_size(size)
+        element_size = six.moves.reduce(operator.mul, size, 1)
+        if element_size % 2 == 0:
+            out = cupy.empty(size, dtype=dtype)
+            func(self._generator, out.data.ptr, out.size, *args)
+            return out
+        else:
+            out = cupy.empty((element_size + 1,), dtype=dtype)
+            func(self._generator, out.data.ptr, out.size, *args)
+            return out[:element_size].reshape(size)
+
     # NumPy compatible functions
 
     def lognormal(self, mean=0.0, sigma=1.0, size=None, dtype=float):
@@ -62,14 +80,11 @@ def lognormal(self, mean=0.0, sigma=1.0, size=None, dtype=float):
 
         """
         dtype = _check_and_get_dtype(dtype)
-        size = _get_size(size)
-        out = cupy.empty(size, dtype=dtype)
         if dtype.char == 'f':
             func = curand.generateLogNormal
         else:
             func = curand.generateLogNormalDouble
-        func(self._generator, out.data.ptr, out.size, mean, sigma)
-        return out
+        return self._generate_normal(func, size, dtype, mean, sigma)
 
     def normal(self, loc=0.0, scale=1.0, size=None, dtype=float):
         """Returns an array of normally distributed samples.
@@ -80,14 +95,11 @@ def normal(self, loc=0.0, scale=1.0, size=None, dtype=float):
 
         """
         dtype = _check_and_get_dtype(dtype)
-        size = _get_size(size)
-        out = cupy.empty(size, dtype=dtype)
         if dtype.char == 'f':
             func = curand.generateNormal
         else:
             func = curand.generateNormalDouble
-        func(self._generator, out.data.ptr, out.size, loc, scale)
-        return out
+        return self._generate_normal(func, size, dtype, loc, scale)
 
     def rand(self, *size, **kwarg):
         """Returns uniform random values over the interval ``[0, 1)``.
@@ -129,7 +141,6 @@ def random_sample(self, size=None, dtype=float):
 
         """
         dtype = _check_and_get_dtype(dtype)
-        size = _get_size(size)
         out = cupy.empty(size, dtype=dtype)
         if dtype.char == 'f':
             func = curand.generateUniform
@@ -177,7 +188,6 @@ def uniform(self, low=0.0, high=1.0, size=None, dtype=float):
 
         """
         dtype = numpy.dtype(dtype)
-        size = _get_size(size)
         rand = self.random_sample(size=size, dtype=dtype)
         return dtype.type(low) + rand * dtype.type(high - low)
 
@@ -231,17 +241,6 @@ def get_random_state():
     return rs
 
 
-def _get_size(size):
-    if size is None:
-        return ()
-    elif isinstance(size, collections.Sequence):
-        return tuple(size)
-    elif isinstance(size, int):
-        return size,
-    else:
-        raise ValueError('size should be None, collections.Sequence, or int')
-
-
 def _check_and_get_dtype(dtype):
     dtype = numpy.dtype(dtype)
     if dtype.char not in ('f', 'd'):


Test patch:
diff --git a/tests/cupy_tests/cuda_tests/test_curand.py b/tests/cupy_tests/cuda_tests/test_curand.py
--- a/tests/cupy_tests/cuda_tests/test_curand.py
+++ b/tests/cupy_tests/cuda_tests/test_curand.py
@@ -12,14 +12,26 @@ def setUp(self):
         self.generator = curand.createGenerator(
             curand.CURAND_RNG_PSEUDO_DEFAULT)
 
-    def test_invalid_argument_float(self):
+    def test_invalid_argument_normal_float(self):
         out = cupy.empty((1,), dtype=numpy.float32)
         with self.assertRaises(ValueError):
             curand.generateNormal(
                 self.generator, out.data.ptr, 1, 0.0, 1.0)
 
-    def test_invalid_argument_double(self):
+    def test_invalid_argument_normal_double(self):
         out = cupy.empty((1,), dtype=numpy.float64)
         with self.assertRaises(ValueError):
             curand.generateNormalDouble(
                 self.generator, out.data.ptr, 1, 0.0, 1.0)
+
+    def test_invalid_argument_log_normal_float(self):
+        out = cupy.empty((1,), dtype=numpy.float32)
+        with self.assertRaises(ValueError):
+            curand.generateLogNormal(
+                self.generator, out.data.ptr, 1, 0.0, 1.0)
+
+    def test_invalid_argument_log_normal_double(self):
+        out = cupy.empty((1,), dtype=numpy.float64)
+        with self.assertRaises(ValueError):
+            curand.generateLogNormalDouble(
+                self.generator, out.data.ptr, 1, 0.0, 1.0)
diff --git a/tests/cupy_tests/random_tests/test_generator.py b/tests/cupy_tests/random_tests/test_generator.py
--- a/tests/cupy_tests/random_tests/test_generator.py
+++ b/tests/cupy_tests/random_tests/test_generator.py
@@ -1,9 +1,12 @@
 import mock
+import operator
 import os
 import unittest
 
 import numpy
+import six
 
+import cupy
 from cupy import cuda
 from cupy.cuda import curand
 from cupy.random import generator
@@ -21,10 +24,19 @@ def setUp(self):
         self.rs = generator.RandomState()
 
     def check_lognormal(self, curand_func, dtype):
+        shape = cupy._get_size(self.size)
+        exp_size = six.moves.reduce(operator.mul, shape, 1)
+        if exp_size % 2 == 1:
+            exp_size += 1
+
+        curand_func.return_value = cupy.zeros(exp_size, dtype=dtype)
         out = self.rs.lognormal(self.args[0], self.args[1], self.size, dtype)
-        curand_func.assert_called_once_with(
-            self.rs._generator, out.data.ptr,
-            out.size, self.args[0], self.args[1])
+        gen, _, size, mean, sigma = curand_func.call_args[0]
+        self.assertIs(gen, self.rs._generator)
+        self.assertEqual(size, exp_size)
+        self.assertIs(mean, self.args[0])
+        self.assertIs(sigma, self.args[1])
+        self.assertEqual(out.shape, shape)
 
     def test_lognormal_float(self):
         curand.generateLogNormalDouble = mock.Mock()
@@ -39,10 +51,19 @@ def test_lognormal_float64(self):
         self.check_lognormal(curand.generateLogNormalDouble, numpy.float64)
 
     def check_normal(self, curand_func, dtype):
+        shape = cupy._get_size(self.size)
+        exp_size = six.moves.reduce(operator.mul, shape, 1)
+        if exp_size % 2 == 1:
+            exp_size += 1
+
+        curand_func.return_value = cupy.zeros(exp_size, dtype=dtype)
         out = self.rs.normal(self.args[0], self.args[1], self.size, dtype)
-        curand_func.assert_called_once_with(
-            self.rs._generator, out.data.ptr,
-            out.size, self.args[0], self.args[1])
+        gen, _, size, loc, scale = curand_func.call_args[0]
+        self.assertIs(gen, self.rs._generator)
+        self.assertEqual(size, exp_size)
+        self.assertIs(loc, self.args[0])
+        self.assertIs(scale, self.args[1])
+        self.assertEqual(out.shape, shape)
 
     def test_normal_float32(self):
         curand.generateNormal = mock.Mock()
@@ -105,6 +126,27 @@ class TestRandomState4(TestRandomState):
     size = (1, 2, 3)
 
 
+@testing.gpu
+class TestRandomState6(TestRandomState):
+
+    args = (0.0, 1.0)
+    size = 3
+
+
+@testing.gpu
+class TestRandomState7(TestRandomState):
+
+    args = (0.0, 1.0)
+    size = (3, 3)
+
+
+@testing.gpu
+class TestRandomState8(TestRandomState):
+
+    args = (0.0, 1.0)
+    size = ()
+
+
 @testing.gpu
 class TestRandAndRandN(unittest.TestCase):
 
@@ -188,28 +230,6 @@ def test_get_random_state_with_chainer_seed(self):
         generator.RandomState.assert_called_with('1')
 
 
-class TestGetSize(unittest.TestCase):
-
-    def test_none(self):
-        self.assertEqual(generator._get_size(None), ())
-
-    def check_collection(self, a):
-        self.assertEqual(generator._get_size(a), tuple(a))
-
-    def test_list(self):
-        self.check_collection([1, 2, 3])
-
-    def test_tuple(self):
-        self.check_collection((1, 2, 3))
-
-    def test_int(self):
-        self.assertEqual(generator._get_size(1), (1,))
-
-    def test_float(self):
-        with self.assertRaises(ValueError):
-            generator._get_size(1.0)
-
-
 class TestCheckAndGetDtype(unittest.TestCase):
 
     @testing.for_float_dtypes(no_float16=True)
diff --git a/tests/cupy_tests/test_ndarray.py b/tests/cupy_tests/test_ndarray.py
new file mode 100644
--- /dev/null
+++ b/tests/cupy_tests/test_ndarray.py
@@ -0,0 +1,25 @@
+import unittest
+
+import cupy
+
+
+class TestGetSize(unittest.TestCase):
+
+    def test_none(self):
+        self.assertEqual(cupy._get_size(None), ())
+
+    def check_collection(self, a):
+        self.assertEqual(cupy._get_size(a), tuple(a))
+
+    def test_list(self):
+        self.check_collection([1, 2, 3])
+
+    def test_tuple(self):
+        self.check_collection((1, 2, 3))
+
+    def test_int(self):
+        self.assertEqual(cupy._get_size(1), (1,))
+
+    def test_float(self):
+        with self.assertRaises(ValueError):
+            cupy._get_size(1.0)