From 80d37a668165ac7ee519da55b36d6e9c03a8bcc8 Mon Sep 17 00:00:00 2001 From: Nikita Grigorian Date: Wed, 4 Nov 2026 11:46:56 -0800 Subject: [PATCH 1/4] Add the PCG64DXSM and MRG32k3a bit generators to dpnp.random PCG64DXSM is a custom engine bit-identical with NumPy's for raw values, random, uniform and bytes, drawn in a sequential layout for the samplers taking a fixed number of draws. MRG32k3a uses oneMKL's device engine, with work-item w on substream w. sycl::mul_hi is replaced with a limb-based umul_hi, as DPC++ 2026.2 miscompiles it on Intel GPUs. --- CHANGELOG.md | 2 + doc/known_words.txt | 6 + doc/reference/random.rst | 27 +- dpnp/AGENTS.md | 2 +- dpnp/libtensor/extensions/rng/common.hpp | 35 +- .../extensions/rng/kernels/engines.hpp | 236 +++++++++- .../extensions/rng/kernels/sample.hpp | 45 +- .../extensions/rng/kernels/samplers.hpp | 11 +- dpnp/libtensor/extensions/rng/rng_py.cpp | 8 +- dpnp/random/__init__.py | 10 +- dpnp/random/_bit_generator.py | 407 ++++++++++++++++++ dpnp/random/_generator.py | 8 +- dpnp/tests/AGENTS.md | 2 +- dpnp/tests/test_random_generator.py | 346 ++++++++++++++- dpnp/tests/test_sycl_queue.py | 5 +- dpnp/tests/test_usm_type.py | 5 +- .../cupy/random_tests/test_bit_generator.py | 1 - 17 files changed, 1121 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9506cb4ec1a..56b4640d2cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ This release drops support for Python 3.10, making Python 3.11 the minimum requi * Added the continuous distributions of `numpy.random.Generator`, such as `gamma`, `beta` and `vonmises`, to `dpnp.random.Generator` [#3158](https://github.com/IntelPython/dpnp/pull/3158) * Added `integers` and the discrete distributions of `numpy.random.Generator`, such as `poisson`, `binomial` and `hypergeometric`, to `dpnp.random.Generator`, along with `noncentral_chisquare` and `noncentral_f` [#3159](https://github.com/IntelPython/dpnp/pull/3159) * Added the multivariate distributions, `permutation`, `shuffle`, `permuted`, `choice` and `bytes` to `dpnp.random.Generator` [#3160](https://github.com/IntelPython/dpnp/pull/3160) +* Added the `dpnp.random.PCG64DXSM` bit generator, which matches `numpy.random.PCG64DXSM` for raw values, `random`, `uniform` and `bytes` [#3161](https://github.com/IntelPython/dpnp/pull/3161) +* Added the `dpnp.random.MRG32k3a` bit generator [#3161](https://github.com/IntelPython/dpnp/pull/3161) ### Changed diff --git a/doc/known_words.txt b/doc/known_words.txt index 2d2ca98a9b6..63125e07643 100644 --- a/doc/known_words.txt +++ b/doc/known_words.txt @@ -43,9 +43,11 @@ Hanning histogrammed Hypergeometric kwargs +Kelton iaxis iinfo Infs +L'Ecuyer intp ints iterable @@ -90,17 +92,21 @@ reentrant representable resampling rng +RngStreams runtimes scikit se SeedSequence signbit signum +Simard sinc strided subarray subarrays subclasses +substream +substreams subtype SyclDevice SyclQueue diff --git a/doc/reference/random.rst b/doc/reference/random.rst index f0ff7bc801f..1f2962c7886 100644 --- a/doc/reference/random.rst +++ b/doc/reference/random.rst @@ -5,11 +5,13 @@ Random sampling (:mod:`dpnp.random`) .. hint:: `NumPy API Reference: Random sampling `_ -DPNP draws random numbers on the SYCL device, with the counter-based -Philox4x32x10 generator of the oneMKL device RNG API (oneMath's in a -oneMath build). It provides the :class:`Generator` API of NumPy, -recommended for new code. The legacy :class:`RandomState` API and the -module-level functions still use the host-side oneMKL RNG. +DPNP draws random numbers on the SYCL device, by default with the +counter-based Philox4x32x10 generator of the oneMKL device RNG API, or with +NumPy's PCG64DXSM generator or the MRG32k3a generator of the oneMKL device +RNG API. A oneMath build takes them from oneMath's device RNG API instead. +It provides the :class:`Generator` API of NumPy, recommended for new code. +The legacy :class:`RandomState` API and the module-level functions still +use the host-side oneMKL RNG. .. code-block:: python @@ -28,7 +30,10 @@ A generator draws all its samples on one SYCL queue, chosen with the Differences from NumPy ---------------------- -* The streams differ from NumPy's. For a given seed, output size and data +* The streams differ from NumPy's, except that :class:`PCG64DXSM` gives + NumPy's raw values, :meth:`Generator.random`, :meth:`Generator.uniform` + and :meth:`Generator.bytes`, and can exchange states with + :class:`numpy.random.PCG64DXSM`. For a given seed, output size and data type, the raw bits and the uniform floats are the same on every device. The other distributions use transcendental functions, which may round differently on another device: their samples agree to a few ULP, or @@ -37,7 +42,8 @@ Differences from NumPy precision, as noted below. * The values depend on the output size: a draw is not a prefix of a larger one, and two draws of ``n`` values differ from one draw of - ``2 * n``. Code splitting its draws into chunks gets other values. + ``2 * n``, but for the methods :class:`PCG64DXSM` draws in the order of + its stream. Code splitting its draws into chunks gets other values. * The floating-point distributions take a ``dtype`` keyword, as in CuPy, which defaults to the default floating-point type of the device, so on a device without double precision the same seed gives other values by @@ -69,8 +75,9 @@ Differences from NumPy * ``hypergeometric`` requires ``ngood`` and ``nbad`` below ``10**9``. * :meth:`Generator.shuffle` only accepts a :class:`dpnp.ndarray`, as in CuPy. -* Generators and bit generators cannot be pickled or deep-copied, and the - bit generators have no ``advance`` or ``jumped`` method. +* Generators and bit generators cannot be pickled or deep-copied, + and :class:`Philox4x3210` and :class:`MRG32k3a` have no ``advance`` or + ``jumped`` method. Random Generator ---------------- @@ -177,6 +184,8 @@ Bit generators BitGenerator.sycl_device BitGenerator.sycl_queue Philox4x3210 + PCG64DXSM + MRG32k3a SeedSequence Legacy random generation diff --git a/dpnp/AGENTS.md b/dpnp/AGENTS.md index 6009def4f08..3f545cb7ef4 100644 --- a/dpnp/AGENTS.md +++ b/dpnp/AGENTS.md @@ -6,7 +6,7 @@ - `dpnp_algo/dpnp_elementwise_common.py`: ufunc wrapper classes (`DPNPUnaryFunc`, `DPNPBinaryFunc`, two-output and specialised variants) - `dpnp_utils/`: shared helpers (`get_usm_allocations`, `map_dtype_to_device`, reduction wrappers) - `linalg/`, `fft/`, `random/`, `scipy/`, `lib/`: subpackages, each exporting through its own `__init__.py` -- `random/`: `_bit_generator.py` (`BitGenerator`, `Philox4x3210`; `SeedSequence` is NumPy's) and `_generator.py` (`Generator`, which implements every distribution). The kernels are in `dpnp.libtensor._rng_impl`. +- `random/`: `_bit_generator.py` (`BitGenerator`, `Philox4x3210`, `PCG64DXSM`, `MRG32k3a`; `SeedSequence` is NumPy's) and `_generator.py` (`Generator`, which implements every distribution). The kernels are in `dpnp.libtensor._rng_impl`. - Legacy, don't extend: the Cython `dpnp_algo` path and `dpnp_backend_c` (still used by `dpnp.partition` and `dpnp.random`), and the NumPy fallback `call_origin`. New kernels go into `dpnp/libtensor` (see `dpnp/libtensor/AGENTS.md`). ## Adding a Public Function diff --git a/dpnp/libtensor/extensions/rng/common.hpp b/dpnp/libtensor/extensions/rng/common.hpp index 94f228727ae..f86698d0490 100644 --- a/dpnp/libtensor/extensions/rng/common.hpp +++ b/dpnp/libtensor/extensions/rng/common.hpp @@ -130,7 +130,7 @@ sycl::event submit_sample(sycl::queue &exec_q, const std::size_t nelems, const std::vector &depends) { - const std::size_t n_wi = rng_ns::num_work_items(nelems); + const std::size_t n_wi = rng_ns::num_work_items(nelems, L); return exec_q.submit([&](sycl::handler &cgh) { cgh.depends_on(depends); @@ -184,7 +184,12 @@ sycl::event sample_impl(sycl::queue &exec_q, T *res = reinterpret_cast(result); - if constexpr (rng_ns::can_draw_blocked) { + if constexpr (rng_ns::can_draw_sequential) { + return detail::submit_sample( + exec_q, state, sampler, res, nelems, depends); + } + else if constexpr (rng_ns::can_draw_blocked) { // the blocked layout gives the same values, faster on GPUs but // slower on CPUs if (exec_q.get_device().is_gpu()) { @@ -244,7 +249,7 @@ void init_sample_dispatch_table(sample_dispatch_table_t &table) { using ext::common::init_dispatch_vector; - static_assert(rng_ns::num_engines == 1, + static_assert(rng_ns::num_engines == 3, "Every engine must have a row in the dispatch table"); constexpr int philox = static_cast(rng_ns::engine_id::philox4x32x10); @@ -252,18 +257,40 @@ void init_sample_dispatch_table(sample_dispatch_table_t &table) SampleFactoryFor::template type>( table[philox]); + + constexpr int pcg = static_cast(rng_ns::engine_id::pcg64dxsm); + init_dispatch_vector::template type>( + table[pcg]); + + constexpr int mrg = static_cast(rng_ns::engine_id::mrg32k3a); + init_dispatch_vector::template type>( + table[mrg]); } template