Repository: cupy/cupy

Issue:
RandomState.choice uses global random state
Current master (77c0ac33ada105bc8e97065a5ae4041fea7d5b13)

### Code
```
import cupy

rs = cupy.random.RandomState()

rs.seed(100)
print(rs.uniform())

rs.seed(100)
print(rs.uniform())

rs.seed(100)
print(rs.choice(3, size=10))

rs.seed(100)
print(rs.choice(3, size=10))
```

### Output
```
0.6116323343956134
0.6116323343956134
[0 2 2 2 0 0 1 2 0 1]
[0 0 0 2 0 0 1 0 2 1]
```


Discussion:
These functions are (directly or indirectly) used from `RandomState.choice`. We need to either fix them to allow custom random state or avoid using them.

* `cupy.random.gumbel`
* `cupy.random.uniform`
* `cupy.random.randint`

I found there's already `cupy.RandomState.uniform` method, and NumPy has `numpy.RandomState.gumbel` and `numpy.RandomState.randint` methods. So we can simply move these two generators into `cupy.RandomState`.

Resolving patch:
diff --git a/cupy/random/distributions.py b/cupy/random/distributions.py
--- a/cupy/random/distributions.py
+++ b/cupy/random/distributions.py
@@ -39,12 +39,12 @@ def gumbel(loc=0.0, scale=1.0, size=None, dtype=float):
     Returns:
         cupy.ndarray: Samples drawn from the Gumbel destribution.
 
-    .. seealso:: :func:`numpy.random.gumbel`
+    .. seealso::
+        :func:`cupy.RandomState.gumbel`
+        :func:`numpy.random.gumbel`
     """
-    rs = uniform(size=size, dtype=dtype)
-    # We use `1 - x` as input of `log` method to prevent overflow.
-    # It obeys numpy implementation.
-    return _gumbel_kernel(rs, loc, scale, rs)
+    rs = generator.get_random_state()
+    return rs.gumbel(loc, scale, size, dtype)
 
 
 def lognormal(mean=0.0, sigma=1.0, size=None, dtype=float):
diff --git a/cupy/random/generator.py b/cupy/random/generator.py
--- a/cupy/random/generator.py
+++ b/cupy/random/generator.py
@@ -14,6 +14,20 @@
 from cupy.cuda import curand
 
 
+_gumbel_kernel = None
+
+
+def _get_gumbel_kernel():
+    global _gumbel_kernel
+    if _gumbel_kernel is None:
+        _gumbel_kernel = core.ElementwiseKernel(
+            'T x, T loc, T scale', 'T y',
+            'y = loc - log(-log(1 - x)) * scale',
+            'gumbel_kernel'
+        )
+    return _gumbel_kernel
+
+
 class RandomState(object):
 
     """Portable container of a pseudo-random number generator.
@@ -347,12 +361,12 @@ def choice(self, a, size=None, replace=True, p=None):
         if p is not None:
             p = cupy.broadcast_to(p, (size, a_size))
             index = cupy.argmax(cupy.log(p) +
-                                cupy.random.gumbel(size=(size, a_size)),
+                                self.gumbel(size=(size, a_size)),
                                 axis=1)
             if not isinstance(shape, six.integer_types):
                 index = cupy.reshape(index, shape)
         else:
-            index = cupy.random.randint(0, a_size, size=shape)
+            index = self.randint(0, a_size, size=shape)
             # Align the dtype with NumPy
             index = index.astype(cupy.int64, copy=False)
 
@@ -382,6 +396,51 @@ def shuffle(self, a):
         curand.generate(self._generator, sample.data.ptr, sample.size)
         a[:] = a[cupy.argsort(sample)]
 
+    def gumbel(self, loc=0.0, scale=1.0, size=None, dtype=float):
+        """Returns an array of samples drawn from a Gumbel distribution.
+
+        .. seealso::
+            :func:`cupy.random.gumbel` for full documentation,
+            :meth:`numpy.random.RandomState.gumbel`
+        """
+        x = self.uniform(size=size, dtype=dtype)
+        # We use `1 - x` as input of `log` method to prevent overflow.
+        # It obeys numpy implementation.
+        _get_gumbel_kernel()(x, loc, scale, x)
+        return x
+
+    def randint(self, low, high=None, size=None, dtype='l'):
+        """Returns a scalar or an array of integer values over ``[low, high)``.
+
+        .. seealso::
+            :func:`cupy.random.randint` for full documentation,
+            :meth:`numpy.random.RandomState.randint`
+        """
+        if high is None:
+            lo = 0
+            hi = low
+        else:
+            lo = low
+            hi = high
+
+        if lo >= hi:
+            raise ValueError('low >= high')
+        if lo < cupy.iinfo(dtype).min:
+            raise ValueError(
+                'low is out of bounds for {}'.format(cupy.dtype(dtype).name))
+        if hi > cupy.iinfo(dtype).max + 1:
+            raise ValueError(
+                'high is out of bounds for {}'.format(cupy.dtype(dtype).name))
+
+        diff = hi - lo - 1
+        if diff > cupy.iinfo(cupy.int32).max - cupy.iinfo(cupy.int32).min + 1:
+            raise NotImplementedError(
+                'Sampling from a range whose extent is larger than int32 '
+                'range is currently not supported')
+        x = self.interval(diff, size).astype(dtype, copy=False)
+        cupy.add(x, lo, out=x)
+        return x
+
 
 def seed(seed=None):
     """Resets the state of the random number generator with a seed.
diff --git a/cupy/random/sample.py b/cupy/random/sample.py
--- a/cupy/random/sample.py
+++ b/cupy/random/sample.py
@@ -1,6 +1,5 @@
 import six
 
-import cupy
 from cupy import core
 from cupy.creation import basic
 from cupy.random import distributions
@@ -81,31 +80,8 @@ def randint(low, high=None, size=None, dtype='l'):
         If size is integer, it is the 1D-array of length ``size`` element.
         Otherwise, it is the array whose shape specified by ``size``.
     """
-    if high is None:
-        lo = 0
-        hi = low
-    else:
-        lo = low
-        hi = high
-
-    if lo >= hi:
-        raise ValueError('low >= high')
-    if lo < cupy.iinfo(dtype).min:
-        raise ValueError(
-            'low is out of bounds for {}'.format(cupy.dtype(dtype).name))
-    if hi > cupy.iinfo(dtype).max + 1:
-        raise ValueError(
-            'high is out of bounds for {}'.format(cupy.dtype(dtype).name))
-
-    diff = hi - lo - 1
-    if diff > cupy.iinfo(cupy.int32).max - cupy.iinfo(cupy.int32).min + 1:
-        raise NotImplementedError(
-            'Sampling from a range whose extent is larger than int32 range is '
-            'currently not supported')
     rs = generator.get_random_state()
-    x = rs.interval(diff, size).astype(dtype, copy=False)
-    cupy.add(x, lo, out=x)
-    return x
+    return rs.randint(low, high, size, dtype)
 
 
 def random_integers(low, high=None, size=None):


Test patch:
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,5 +1,4 @@
 import mock
-import operator
 import os
 import threading
 import unittest
@@ -10,172 +9,200 @@
 import cupy
 from cupy import core
 from cupy import cuda
-from cupy.cuda import curand
 from cupy.random import generator
 from cupy import testing
 from cupy.testing import condition
 from cupy.testing import hypothesis
 
 
-class FunctionSwitcher(object):
+class RandomGeneratorTestCase(unittest.TestCase):
 
-    def __init__(self, f):
-        self.tmp = f
-        self.func_name = f.__name__
+    target_method = None
 
-    def __enter__(self):
-        setattr(curand, self.func_name, mock.Mock())
+    def setUp(self):
+        self.rs = generator.RandomState(seed=testing.generate_seed())
+
+    def _get_generator_func(self, *args, **kwargs):
+        assert isinstance(self.target_method, str), (
+            'generate_method must be overridden')
+        f = getattr(self.rs, self.target_method)
+        return lambda: f(*args, **kwargs)
 
-    def __exit__(self, *_):
-        setattr(curand, self.func_name, self.tmp)
+    def _generate_check_repro(self, func, seed=0):
+        # Sample a random array while checking reproducibility
+        self.rs.seed(seed)
+        x = func()
+        self.rs.seed(seed)
+        y = func()
+        testing.assert_array_equal(
+            x, y,
+            'Randomly generated arrays with the same seed did not match')
+        return x
+
+    def generate(self, *args, **kwargs):
+        # Pick one sample from generator.
+        # Reproducibility is checked by repeating seed-and-sample cycle twice.
+        func = self._get_generator_func(*args, **kwargs)
+        return self._generate_check_repro(func, seed=0)
+
+    def generate_many(self, *args, **kwargs):
+        # Pick many samples from generator.
+        # Reproducibility is checked only for the first sample,
+        # because it's very slow to set seed every time.
+        _count = kwargs.pop('_count', None)
+        assert _count is not None, '_count is required'
+        func = self._get_generator_func(*args, **kwargs)
+
+        if _count == 0:
+            return []
+
+        vals = [self._generate_check_repro(func, seed=0)]
+        for i in range(1, _count):
+            vals.append(func())
+        return vals
 
 
 @testing.fix_random()
 @testing.gpu
 class TestRandomState(unittest.TestCase):
 
-    _multiprocess_can_split_ = True
-    args = (0.0, 1.0)
-    size = None
-
     def setUp(self):
         self.rs = generator.RandomState(seed=testing.generate_seed())
 
-    def check_lognormal(self, curand_func, dtype):
-        shape = core.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)
-        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):
-        with FunctionSwitcher(curand.generateLogNormalDouble):
-            self.check_lognormal(curand.generateLogNormalDouble, float)
-
-    def test_lognormal_float32(self):
-        with FunctionSwitcher(curand.generateLogNormal):
-            self.check_lognormal(curand.generateLogNormal, numpy.float32)
+    def check_seed(self, seed):
+        rs = self.rs
 
-    def test_lognormal_float64(self):
-        with FunctionSwitcher(curand.generateLogNormalDouble):
-            self.check_lognormal(curand.generateLogNormalDouble, numpy.float64)
-
-    def check_normal(self, curand_func, dtype):
-        shape = core.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)
-        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):
-        with FunctionSwitcher(curand.generateNormal):
-            self.check_normal(curand.generateNormal, numpy.float32)
-
-    def test_normal_float64(self):
-        with FunctionSwitcher(curand.generateNormalDouble):
-            self.check_normal(curand.generateNormalDouble, numpy.float64)
-
-    def check_random_sample(self, curand_func, dtype):
-        out = self.rs.random_sample(self.size, dtype)
-        curand_func.assert_called_once_with(
-            self.rs._generator, out.data.ptr, out.size)
-
-    def test_random_sample_float32(self):
-        with FunctionSwitcher(curand.generateUniform):
-            self.check_random_sample(curand.generateUniform, numpy.float32)
+        rs.seed(seed)
+        xs1 = [rs.uniform() for _ in range(100)]
 
-    def test_random_sample_float64(self):
-        with FunctionSwitcher(curand.generateUniformDouble):
-            self.check_random_sample(
-                curand.generateUniformDouble, numpy.float64)
+        rs.seed(seed)
+        xs2 = [rs.uniform() for _ in range(100)]
 
-    def check_seed(self, curand_func, seed):
-        self.rs.seed(seed)
-        call_args_list = curand_func.call_args_list
-        self.assertEqual(1, len(call_args_list))
-        call_args = call_args_list[0][0]
-        self.assertEqual(2, len(call_args))
-        self.assertIs(self.rs._generator, call_args[0])
-        self.assertEqual(numpy.uint64, call_args[1].dtype)
+        rs.seed(seed)
+        rs.seed(None)
+        xs3 = [rs.uniform() for _ in range(100)]
 
-    def test_seed_none(self):
-        with FunctionSwitcher(curand.setPseudoRandomGeneratorSeed):
-            self.check_seed(curand.setPseudoRandomGeneratorSeed, None)
+        # Random state must be reproducible
+        assert xs1 == xs2
+        # Random state must be initialized randomly with seed=None
+        assert xs1 != xs3
 
     @testing.for_int_dtypes()
     def test_seed_not_none(self, dtype):
-        with FunctionSwitcher(curand.setPseudoRandomGeneratorSeed):
-            self.check_seed(curand.setPseudoRandomGeneratorSeed, dtype(0))
+        self.check_seed(dtype(0))
 
     @testing.for_dtypes([numpy.complex_])
     def test_seed_invalid_type_complex(self, dtype):
         with self.assertRaises(TypeError):
-            with FunctionSwitcher(curand.setPseudoRandomGeneratorSeed):
-                self.check_seed(curand.setPseudoRandomGeneratorSeed, dtype(0))
+            self.rs.seed(dtype(0))
 
     @testing.for_float_dtypes()
     def test_seed_invalid_type_float(self, dtype):
         with self.assertRaises(TypeError):
-            with FunctionSwitcher(curand.setPseudoRandomGeneratorSeed):
-                self.check_seed(curand.setPseudoRandomGeneratorSeed, dtype(0))
+            self.rs.seed(dtype(0))
 
 
 @testing.gpu
-class TestRandomState2(TestRandomState):
+@testing.parameterize(*[
+    {'args': (0.0, 1.0), 'size': None},
+    {'args': (10.0, 20.0), 'size': None},
+    {'args': (0.0, 1.0), 'size': 10},
+    {'args': (0.0, 1.0), 'size': (1, 2, 3)},
+    {'args': (0.0, 1.0), 'size': 3},
+    {'args': (0.0, 1.0), 'size': (3, 3)},
+    {'args': (0.0, 1.0), 'size': ()},
+])
+@testing.fix_random()
+class TestLogNormal(RandomGeneratorTestCase):
 
-    args = (10.0, 20.0)
-    size = None
+    target_method = 'lognormal'
 
+    def check_lognormal(self, dtype):
+        vals = self.generate_many(
+            self.args[0], self.args[1], self.size, dtype, _count=10)
 
-@testing.gpu
-class TestRandomState3(TestRandomState):
+        shape = core.get_size(self.size)
+        for val in vals:
+            assert isinstance(val, cupy.ndarray)
+            assert val.dtype == dtype
+            assert val.shape == shape
+            assert (0 <= val).all()
+        # TODO(niboshi): Distribution test
 
-    args = (0.0, 1.0)
-    size = 10
+    def test_lognormal_float(self):
+        self.check_lognormal(float)
+
+    def test_lognormal_float32(self):
+        self.check_lognormal(numpy.float32)
+
+    def test_lognormal_float64(self):
+        self.check_lognormal(numpy.float64)
 
 
 @testing.gpu
-class TestRandomState4(TestRandomState):
+@testing.parameterize(*[
+    {'args': (0.0, 1.0), 'size': None},
+    {'args': (10.0, 20.0), 'size': None},
+    {'args': (0.0, 1.0), 'size': 10},
+    {'args': (0.0, 1.0), 'size': (1, 2, 3)},
+    {'args': (0.0, 1.0), 'size': 3},
+    {'args': (0.0, 1.0), 'size': (3, 3)},
+    {'args': (0.0, 1.0), 'size': ()},
+])
+@testing.fix_random()
+class TestNormal(RandomGeneratorTestCase):
 
-    args = (0.0, 1.0)
-    size = (1, 2, 3)
+    target_method = 'normal'
 
+    def check_normal(self, dtype):
+        vals = self.generate_many(
+            self.args[0], self.args[1], self.size, dtype, _count=10)
 
-@testing.gpu
-class TestRandomState6(TestRandomState):
+        shape = core.get_size(self.size)
+        for val in vals:
+            assert isinstance(val, cupy.ndarray)
+            assert val.dtype == dtype
+            assert val.shape == shape
+        # TODO(niboshi): Distribution test
+
+    def test_normal_float32(self):
+        self.check_normal(numpy.float32)
 
-    args = (0.0, 1.0)
-    size = 3
+    def test_normal_float64(self):
+        self.check_normal(numpy.float64)
 
 
 @testing.gpu
-class TestRandomState7(TestRandomState):
+@testing.parameterize(*[
+    {'size': None},
+    {'size': 10},
+    {'size': (1, 2, 3)},
+    {'size': 3},
+    {'size': ()},
+])
+@testing.fix_random()
+class TestRandomSample(unittest.TestCase):
+
+    def setUp(self):
+        self.rs = generator.RandomState(seed=testing.generate_seed())
 
-    args = (0.0, 1.0)
-    size = (3, 3)
+    def check_random_sample(self, dtype):
+        vals = [self.rs.random_sample(self.size, dtype) for _ in range(10)]
 
+        shape = core.get_size(self.size)
+        for val in vals:
+            assert isinstance(val, cupy.ndarray)
+            assert val.dtype == dtype
+            assert val.shape == shape
+            assert (0 <= val).all()
+            assert (val < 1).all()
+        # TODO(niboshi): Distribution test
 
-@testing.gpu
-class TestRandomState8(TestRandomState):
+    def test_random_sample_float32(self):
+        self.check_random_sample(numpy.float32)
 
-    args = (0.0, 1.0)
-    size = ()
+    def test_random_sample_float64(self):
+        self.check_random_sample(numpy.float64)
 
 
 @testing.fix_random()
@@ -185,22 +212,10 @@ class TestRandAndRandN(unittest.TestCase):
     def setUp(self):
         self.rs = generator.RandomState(seed=testing.generate_seed())
 
-    def test_rand(self):
-        self.rs.random_sample = mock.Mock()
-        self.rs.rand(1, 2, 3, dtype=numpy.float32)
-        self.rs.random_sample.assert_called_once_with(
-            size=(1, 2, 3), dtype=numpy.float32)
-
     def test_rand_invalid_argument(self):
         with self.assertRaises(TypeError):
             self.rs.rand(1, 2, 3, unnecessary='unnecessary_argument')
 
-    def test_randn(self):
-        self.rs.normal = mock.Mock()
-        self.rs.randn(1, 2, 3, dtype=numpy.float32)
-        self.rs.normal.assert_called_once_with(
-            size=(1, 2, 3), dtype=numpy.float32)
-
     def test_randn_invalid_argument(self):
         with self.assertRaises(TypeError):
             self.rs.randn(1, 2, 3, unnecessary='unnecessary_argument')
@@ -208,65 +223,92 @@ def test_randn_invalid_argument(self):
 
 @testing.fix_random()
 @testing.gpu
-class TestInterval(unittest.TestCase):
+class TestInterval(RandomGeneratorTestCase):
 
-    def setUp(self):
-        self.rs = cupy.random.get_random_state()
-        self.rs.seed(testing.generate_seed())
+    target_method = 'interval'
 
     def test_zero(self):
-        numpy.testing.assert_array_equal(
-            self.rs.interval(0, (2, 3)).get(), numpy.zeros((2, 3)))
+        shape = (2, 3)
+        vals = self.generate_many(0, shape, _count=10)
+        for val in vals:
+            assert isinstance(val, cupy.ndarray)
+            assert val.dtype == numpy.int32
+            assert val.shape == shape
+            assert (val == 0).all()
 
     def test_shape_zero(self):
-        v = self.rs.interval(10, None)
-        self.assertEqual(v.dtype, numpy.int32)
-        self.assertEqual(v.shape, ())
+        mx = 10
+        vals = self.generate_many(mx, None, _count=10)
+        for val in vals:
+            assert isinstance(val, cupy.ndarray)
+            assert val.dtype == numpy.int32
+            assert val.shape == ()
+            assert (0 <= val).all()
+            assert (val <= mx).all()
+        # TODO(niboshi): Distribution test
 
     def test_shape_one_dim(self):
-        v = self.rs.interval(10, 10)
-        self.assertEqual(v.dtype, numpy.int32)
-        self.assertEqual(v.shape, (10,))
+        mx = 10
+        size = 20
+        vals = self.generate_many(mx, size, _count=10)
+        for val in vals:
+            assert isinstance(val, cupy.ndarray)
+            assert val.dtype == numpy.int32
+            assert val.shape == (size,)
+            assert (0 <= val).all()
+            assert (val <= mx).all()
+        # TODO(niboshi): Distribution test
 
     def test_shape_multi_dim(self):
-        v = self.rs.interval(10, (1, 2))
-        self.assertEqual(v.dtype, numpy.int32)
-        self.assertEqual(v.shape, (1, 2))
+        mx = 10
+        shape = (1, 2)
+        vals = self.generate_many(mx, shape, _count=10)
+        for val in vals:
+            assert isinstance(val, cupy.ndarray)
+            assert val.dtype == numpy.int32
+            assert val.shape == shape
+            assert (0 <= val).all()
+            assert (val <= mx).all()
+        # TODO(niboshi): Distribution test
 
     def test_int32_range(self):
-        v = self.rs.interval(0x00000000, 2)
-        self.assertEqual(v.dtype, numpy.int32)
+        v = self.generate(0x00000000, 2)
+        assert v.dtype == numpy.int32
 
-        v = self.rs.interval(0x7fffffff, 2)
-        self.assertEqual(v.dtype, numpy.int32)
+        v = self.generate(0x7fffffff, 2)
+        assert v.dtype == numpy.int32
 
     def test_uint32_range(self):
-        v = self.rs.interval(0x80000000, 2)
-        self.assertEqual(v.dtype, numpy.uint32)
+        v = self.generate(0x80000000, 2)
+        assert v.dtype == numpy.uint32
 
-        v = self.rs.interval(0xffffffff, 2)
-        self.assertEqual(v.dtype, numpy.uint32)
+        v = self.generate(0xffffffff, 2)
+        assert v.dtype == numpy.uint32
 
-    @condition.repeat(3, 10)
     def test_bound_1(self):
-        vals = [self.rs.interval(10, (2, 3)).get() for _ in range(10)]
+        vals = self.generate_many(10, (2, 3), _count=10)
         for val in vals:
-            self.assertEqual(val.shape, (2, 3))
-        self.assertEqual(min(_.min() for _ in vals), 0)
-        self.assertEqual(max(_.max() for _ in vals), 10)
+            assert isinstance(val, cupy.ndarray)
+            assert val.dtype == numpy.int32
+            assert val.shape == (2, 3)
+            assert (0 <= val).all()
+            assert (val <= 10).all()
 
-    @condition.repeat(3, 10)
     def test_bound_2(self):
-        vals = [self.rs.interval(2, None).get() for _ in range(20)]
-        self.assertEqual(min(vals), 0)
-        self.assertEqual(max(vals), 2)
+        vals = self.generate_many(2, None, _count=20)
+        for val in vals:
+            assert isinstance(val, cupy.ndarray)
+            assert val.dtype == numpy.int32
+            assert val.shape == ()
+            assert (0 <= val).all()
+            assert (val <= 2).all()
 
     @condition.repeat(3, 10)
     def test_goodness_of_fit(self):
         mx = 5
         trial = 100
-        vals = [self.rs.interval(mx, None).get()
-                for _ in six.moves.xrange(trial)]
+        vals = self.generate_many(mx, None, _count=trial)
+        vals = [val.get() for val in vals]
         counts = numpy.histogram(vals, bins=numpy.arange(mx + 2))[0]
         expected = numpy.array([float(trial) / (mx + 1)] * (mx + 1))
         self.assertTrue(hypothesis.chi_square_test(counts, expected))
@@ -274,7 +316,7 @@ def test_goodness_of_fit(self):
     @condition.repeat(3)
     def test_goodness_of_fit_2(self):
         mx = 5
-        vals = self.rs.interval(mx, (5, 5)).get()
+        vals = self.generate(mx, (5, 5)).get()
         counts = numpy.histogram(vals, bins=numpy.arange(mx + 2))[0]
         expected = numpy.array([float(vals.size) / (mx + 1)] * (mx + 1))
         self.assertTrue(hypothesis.chi_square_test(counts, expected))
@@ -282,25 +324,24 @@ def test_goodness_of_fit_2(self):
 
 @testing.fix_random()
 @testing.gpu
-class TestTomaxint(unittest.TestCase):
+class TestTomaxint(RandomGeneratorTestCase):
 
-    def setUp(self):
-        self.rs = generator.RandomState(seed=testing.generate_seed())
+    target_method = 'tomaxint'
 
     def test_tomaxint_none(self):
-        x = self.rs.tomaxint()
+        x = self.generate()
         self.assertEqual(x.shape, ())
         self.assertTrue((0 <= x).all())
         self.assertTrue((x <= cupy.iinfo(cupy.int_).max).all())
 
     def test_tomaxint_int(self):
-        x = self.rs.tomaxint(3)
+        x = self.generate(3)
         self.assertEqual(x.shape, (3,))
         self.assertTrue((0 <= x).all())
         self.assertTrue((x <= cupy.iinfo(cupy.int_).max).all())
 
     def test_tomaxint_tuple(self):
-        x = self.rs.tomaxint((2, 3))
+        x = self.generate((2, 3))
         self.assertEqual(x.shape, (2, 3))
         self.assertTrue((0 <= x).all())
         self.assertTrue((x <= cupy.iinfo(cupy.int_).max).all())
@@ -316,14 +357,12 @@ def test_tomaxint_tuple(self):
 )
 @testing.fix_random()
 @testing.gpu
-class TestChoice1(unittest.TestCase):
+class TestChoice1(RandomGeneratorTestCase):
 
-    def setUp(self):
-        self.rs = cupy.random.get_random_state()
-        self.rs.seed(testing.generate_seed())
+    target_method = 'choice'
 
     def test_dtype_shape(self):
-        v = self.rs.choice(a=self.a, size=self.size, p=self.p)
+        v = self.generate(a=self.a, size=self.size, p=self.p)
         if isinstance(self.size, six.integer_types):
             expected_shape = (self.size,)
         else:
@@ -337,13 +376,14 @@ def test_dtype_shape(self):
 
     @condition.repeat(3, 10)
     def test_bound(self):
-        vals = [self.rs.choice(a=self.a, size=self.size, p=self.p).get()
-                for _ in range(20)]
+        vals = self.generate_many(
+            a=self.a, size=self.size, p=self.p, _count=20)
+        vals = [val.get() for val in vals]
         size_ = self.size if isinstance(self.size, tuple) else (self.size,)
         for val in vals:
             self.assertEqual(val.shape, size_)
-        self.assertEqual(min(_.min() for _ in vals), 0)
-        self.assertEqual(max(_.max() for _ in vals), 2)
+        self.assertEqual(min(val.min() for val in vals), 0)
+        self.assertEqual(max(val.max() for val in vals), 2)
 
 
 @testing.parameterize(
@@ -351,14 +391,12 @@ def test_bound(self):
 )
 @testing.fix_random()
 @testing.gpu
-class TestChoice2(unittest.TestCase):
+class TestChoice2(RandomGeneratorTestCase):
 
-    def setUp(self):
-        self.rs = cupy.random.get_random_state()
-        self.rs.seed(testing.generate_seed())
+    target_method = 'choice'
 
     def test_dtype_shape(self):
-        v = self.rs.choice(a=self.a, size=self.size, p=self.p)
+        v = self.generate(a=self.a, size=self.size, p=self.p)
         if isinstance(self.size, six.integer_types):
             expected_shape = (self.size,)
         else:
@@ -372,35 +410,34 @@ def test_dtype_shape(self):
 
     @condition.repeat(3, 10)
     def test_bound(self):
-        vals = [self.rs.choice(a=self.a, size=self.size, p=self.p).get()
-                for _ in range(20)]
+        vals = self.generate_many(
+            a=self.a, size=self.size, p=self.p, _count=20)
+        vals = [val.get() for val in vals]
         size_ = self.size if isinstance(self.size, tuple) else (self.size,)
         for val in vals:
             self.assertEqual(val.shape, size_)
-        self.assertEqual(min(_.min() for _ in vals), 0)
-        self.assertEqual(max(_.max() for _ in vals), 2)
+        self.assertEqual(min(val.min() for val in vals), 0)
+        self.assertEqual(max(val.max() for val in vals), 2)
 
 
 @testing.fix_random()
 @testing.gpu
-class TestChoiceChi(unittest.TestCase):
+class TestChoiceChi(RandomGeneratorTestCase):
 
-    def setUp(self):
-        self.rs = cupy.random.get_random_state()
-        self.rs.seed(testing.generate_seed())
+    target_method = 'choice'
 
     @condition.repeat(3, 10)
     def test_goodness_of_fit(self):
         trial = 100
-        vals = [self.rs.choice(3, 1, True, [0.3, 0.3, 0.4]).get()
-                for _ in six.moves.xrange(trial)]
+        vals = self.generate_many(3, 1, True, [0.3, 0.3, 0.4], _count=trial)
+        vals = [val.get() for val in vals]
         counts = numpy.histogram(vals, bins=numpy.arange(4))[0]
         expected = numpy.array([30, 30, 40])
         self.assertTrue(hypothesis.chi_square_test(counts, expected))
 
     @condition.repeat(3, 10)
     def test_goodness_of_fit_2(self):
-        vals = self.rs.choice(3, (5, 20), True, [0.3, 0.3, 0.4]).get()
+        vals = self.generate(3, (5, 20), True, [0.3, 0.3, 0.4]).get()
         counts = numpy.histogram(vals, bins=numpy.arange(4))[0]
         expected = numpy.array([30, 30, 40])
         self.assertTrue(hypothesis.chi_square_test(counts, expected))
@@ -452,14 +489,12 @@ def test_choice_invalid_value(self):
 )
 @testing.fix_random()
 @testing.gpu
-class TestChoiceReplaceFalse(unittest.TestCase):
+class TestChoiceReplaceFalse(RandomGeneratorTestCase):
 
-    def setUp(self):
-        self.rs = cupy.random.get_random_state()
-        self.rs.seed(testing.generate_seed())
+    target_method = 'choice'
 
     def test_dtype_shape(self):
-        v = self.rs.choice(a=self.a, size=self.size, replace=False)
+        v = self.generate(a=self.a, size=self.size, replace=False)
         if isinstance(self.size, six.integer_types):
             expected_shape = (self.size,)
         else:
@@ -473,7 +508,7 @@ def test_dtype_shape(self):
 
     @condition.repeat(3, 10)
     def test_bound(self):
-        val = self.rs.choice(a=self.a, size=self.size, replace=False).get()
+        val = self.generate(a=self.a, size=self.size, replace=False).get()
         size = self.size if isinstance(self.size, tuple) else (self.size,)
         self.assertEqual(val.shape, size)
         self.assertTrue((0 <= val).all())
@@ -481,12 +516,53 @@ def test_bound(self):
         val = numpy.asarray(val)
         self.assertEqual(numpy.unique(val).size, val.size)
 
-    def test_reproduce(self):
-        rs1 = cupy.random.RandomState(1)
-        v1 = rs1.choice(a=self.a, size=self.size, replace=False)
-        rs2 = cupy.random.RandomState(1)
-        v2 = rs2.choice(a=self.a, size=self.size, replace=False)
-        self.assertTrue((v1 == v2).all())
+
+@testing.gpu
+@testing.fix_random()
+class TestGumbel(RandomGeneratorTestCase):
+    # TODO(niboshi):
+    #   Test soundness of distribution.
+    #   Currently only reprocibility is checked.
+
+    target_method = 'gumbel'
+
+    def test_gumbel_1(self):
+        self.generate()
+
+    def test_gumbel_2(self):
+        self.generate(0.0, 1.0, size=(3, 2))
+
+
+@testing.gpu
+@testing.fix_random()
+class TestRandint(RandomGeneratorTestCase):
+    # TODO(niboshi):
+    #   Test soundness of distribution.
+    #   Currently only reprocibility is checked.
+
+    target_method = 'randint'
+
+    def test_randint_1(self):
+        self.generate(3)
+
+    def test_randint_2(self):
+        self.generate(3, 4, size=(3, 2))
+
+
+@testing.gpu
+@testing.fix_random()
+class TestUniform(RandomGeneratorTestCase):
+    # TODO(niboshi):
+    #   Test soundness of distribution.
+    #   Currently only reprocibility is checked.
+
+    target_method = 'uniform'
+
+    def test_uniform_1(self):
+        self.generate()
+
+    def test_uniform_2(self):
+        self.generate(-4.2, 2.4, size=(3, 2))
 
 
 @testing.parameterize(