Skip to content

Add dpnp.random.Generator on the oneMKL device RNG API - #3157

Open
ndgrigorian wants to merge 2 commits into
masterfrom
feature/device-rng-generator
Open

ndgrigorian wants to merge 2 commits into
masterfrom
feature/device-rng-generator

Conversation

@ndgrigorian

@ndgrigorian ndgrigorian commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

This PR is the first of a series splitting #3147 into smaller pieces. It adds the new framework for device-side random number generation and NumPy's Generator API, with only the base distributions

The new dpnp.libtensor._rng_impl extension samples on the device using the oneMKL device RNG API (header-only, so no oneMKL RNG library is linked). A bit generator keeps its state on the host, and each kernel launch rebuilds its engines from that state, so sampling never waits on the device. For a given seed, output size and dtype, the raw bits and the uniform floats are the same on every device, and the normal samples agree to a few ULP

Adds

  • dpnp.random.Generator with random, uniform, standard_normal, normal and spawn, and default_rng; with usm_type=None (the default) the samples take the coerced USM type of the array parameters
  • dpnp.random.BitGenerator (with random_raw, spawn, state and lock), SeedSequence (NumPy's) and the Philox4x3210 bit generator on oneMKL's device philox4x32x10
  • Strided and blocked sample layouts; the blocked layout generates the words of a work-item in one engine call and is used on GPUs, giving the same values

The legacy RandomState and the module-level functions are untouched until later in the series

Series:

  1. Add dpnp.random.Generator on the oneMKL device RNG API #3157 (this PR): framework, Philox4x3210 and the base distributions
  2. Add continuous distributions to dpnp.random.Generator #3158: Continuous distributions
  3. Add discrete distributions to dpnp.random.Generator #3159: Discrete distributions
  4. Add multivariate distributions and permutations to dpnp.random.Generator #3160: Multivariate distributions and permutations
  5. Add PCG64DXSM and MRG32k3a bit generators to dpnp.random #3161: PCG64DXSM and MRG32k3a bit generators
  6. Reimplement dpnp.random.RandomState on the device RNG #3162: RandomState and the module functions on the device RNG
  7. Remove the legacy backend RNG #3163: Removal of the legacy backend RNG
  • Have you provided a meaningful PR description?
  • Have you added a test, reproducer or referred to an issue with a reproducer?
  • Have you tested your changes locally for CPU and GPU devices?
  • Have you made sure that new changes do not introduce compiler warnings?
  • Have you checked performance impact of proposed changes?
  • Have you added documentation for your changes, if necessary?
  • Have you added your changes to the changelog?

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

View rendered docs @ https://intelpython.github.io/dpnp/pull/3157/index.html

@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Array API standard conformance tests for dpnp=0.22.0dev1=np2py314h8d9cdd5_3 ran successfully.
Passed: 1491
Failed: 0
Skipped: 6

@coveralls

coveralls commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Coverage Status

coverage: 84.226% (-0.2%) from 84.397% — feature/device-rng-generator into master

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved RNG correctness issues can deadlock state access, duplicate random streams through races or invalid sizes, and produce incorrect results for large draws or overflowing float32 ranges.

5 open issues
What changed in this PR

Introduces the foundation for device-side random generation through NumPy’s Generator API, the first step in the RNG migration series. Legacy RandomState and module-level functions remain unchanged.

Changes:

  • Adds Generator, default_rng, and Philox bit generators with state management and spawning.
  • Implements base distributions in a header-only oneMKL device RNG extension, with strided and GPU-optimized blocked sampling.
  • Adds API documentation and tests for distributions, reproducibility, queues, and allocation types.
File Description
dpnp/​tests/​test_usm_type.py Tests RNG allocation types.
dpnp/​tests/​test_sycl_queue.py Tests queue placement and mismatch errors.
dpnp/​tests/​test_random_generator.py Tests generators, distributions, state, and reproducibility.
dpnp/​tests/​AGENTS.md Adds RNG testing guidance.
dpnp/​random/​_generator.py Implements Generator and base distributions.
dpnp/​random/​_bit_generator.py Implements Philox state, raw draws, and spawning.
dpnp/​random/​__init__.py Exports the new random APIs.
dpnp/​libtensor/​extensions/​rng/​uniform.cpp Registers uniform and raw-bit samplers.
dpnp/​libtensor/​extensions/​rng/​sample.cpp Validates inputs and manages sampling lifetimes.
dpnp/​libtensor/​extensions/​rng/​rng_py.cpp Defines the Python extension module.
dpnp/​libtensor/​extensions/​rng/​normal.cpp Registers the normal sampler.
dpnp/​libtensor/​extensions/​rng/​kernels/​samplers.hpp Defines distribution sampler adapters.
dpnp/​libtensor/​extensions/​rng/​kernels/​sample.hpp Implements strided and blocked sampling layouts.
dpnp/​libtensor/​extensions/​rng/​kernels/​engines.hpp Wraps Philox engines and uniform-bit conversion.
dpnp/​libtensor/​extensions/​rng/​kernels/​distributions.hpp Implements parameters and Box–Muller sampling.
dpnp/​libtensor/​extensions/​rng/​distributions.hpp Declares sampler registration functions.
dpnp/​libtensor/​extensions/​rng/​common.hpp Provides dispatch and kernel submission helpers.
dpnp/​libtensor/​extensions/​rng/​CMakeLists.txt Configures the header-only RNG extension.
dpnp/​libtensor/​AGENTS.md Documents RNG architecture and stream invariants.
dpnp/​CMakeLists.txt Includes the RNG extension in the build.
dpnp/​AGENTS.md Adds Python RNG implementation guidance.
doc/​reference/​random.rst Documents modern RNG APIs alongside legacy functions.
CHANGELOG.md Records the new device RNG API.

Review effort: Balanced

Comment thread dpnp/random/_bit_generator.py Resolved
Comment thread dpnp/random/_bit_generator.py Resolved
Comment thread dpnp/random/_bit_generator.py Resolved
Comment thread dpnp/random/_generator.py Resolved
@antonwolfy antonwolfy added this to the 0.22.0 release milestone Nov 6, 2026
@antonwolfy

This comment was marked as off-topic.

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sampling core is right: the stream matches Philox4x32-10 bit for bit on GPU and CPU, and it passes PractRand and TestU01. The problems are at the edges, in the Python layer, the docs and the tests. Copilot's five points all hold; of the inline comments below, I'd fix the overlap check, the device claim in the docs and the skipped CuPy tests before merge.

Open bot points I confirm:

  • Copilot, lock deadlock (_bit_generator.py:110). New evidence: a thread running with bg.lock: bg.state was still alive after a 3 s join.
  • Copilot, random_raw(output=False) rewind (_bit_generator.py:242). New evidence: random_raw(16), then random_raw(-16, output=False), then random_raw(16) returns the same 16 values, while NumPy's Philox.random_raw(-16, output=False) leaves the counter unchanged. random_raw((2, -8), output=False) rewinds too, and random_raw((2**40, 2**40), output=False) wraps numpy.prod to 0 and leaves the state alone.
  • Copilot, key read after the lock is released (_bit_generator.py:391).
  • Copilot, LARGE_RANGES (rng/CMakeLists.txt:40). It takes more than 1.7e10 elements to reach.
  • Copilot, float32 uniform range (_generator.py:129). New evidence: uniform(-3e38, 3e38, size=1000, dtype=numpy.float32) returns 1000 infs.

What I ran, on a Panther Lake U iGPU (Level Zero and OpenCL) and the OpenCL CPU device, with the PR built in place:

  • test_random_generator.py: 102 passed on level_zero:gpu; 100 passed and 2 skipped (the two-device tests) on opencl:cpu. The new test_sycl_queue.py and test_usm_type.py tests, third_party/cupy/random_tests/ as shipped, test_random.py and test_random_state.py pass on both, with no pytest warnings. The build had no compiler warnings.
  • A NumPy implementation of Philox4x32-10, checked against the Random123 known-answer vectors, reproduces random_raw bit for bit in 274 checks on both devices: sizes from 1 to 220+3, a counter carrying into the high word, the wrap at 2128, consecutive calls and output=False. random and uniform match its float conversion bit for bit.
  • PractRand 0.94: 256 GB of the GPU stream with no anomaly. 4-16 GB each, also with none: the CPU stream; keys 0-3 with the same counter, seeds 0-3 and four spawn children, each interleaved by word and by chunk; and streams of 1-, 9- and 17-value calls (built with the reference, which matches the device).
  • TestU01 1.2.3: SmallCrush passes on the GPU stream, the CPU stream and the interleaved related keys. Crush passes all 144 tests on two seeds; a third seed had p = 6.4e-4 (CouponCollector) and 0.9992 (CollisionPermut), which the other two seeds didn't repeat. BigCrush is still running.
  • Distributions, 1e8 draws per case on both devices: KS and chi-square for random, uniform (ranges up to ±1e30 in float32 and ±1e300 in float64), standard_normal and normal, in float32 and float64, all in range. Over 6.7e9 normals there is no correlation between the two values of a Box-Muller pair or between neighbouring work-items.

Not inline:

  1. uniform skips the checks of its host parameters when any parameter is on the device. uniform(dpnp.zeros(1), numpy.array([numpy.inf])) returns [inf], where NumPy raises OverflowError: Range exceeds valid bounds. _check_arrays (_generator.py:142-152) returns None for every parameter as soon as one is a device array, while normal checks each parameter on its own (normal(dpnp.zeros(1), -1.0) raises). Suggestion: check that the host parameters are finite even when the range can't be computed on the host.
  2. The output ignores the parameters' USM type, and the new dpnp/AGENTS.md line says something else about temporaries. normal(dpnp.zeros(4, usm_type="shared"), 1.0).usm_type is "device". The line says "Temporaries take the output's usm_type", but _to_kernel_param (_generator.py:360) calls dpnp.asarray without usm_type, so a parameter given as a list becomes a "device" array even with usm_type="shared". Suggestion: pass usm_type=usm_type there, and either follow the inputs' USM type as other dpnp functions do or list the keyword-only rule under "Differences from NumPy".
  3. The method docstrings don't describe dtype, usm_type and out. They appear only as one-line Limitations, and the methods have no Parameters, Returns or Examples, unlike the legacy dpnp.random.uniform. random.rst doesn't list Generator.spawn, Generator.bit_generator, BitGenerator.random_raw or BitGenerator.state, and the device text at _bit_generator.py:87 refers to dpctl.tensor.Device, which is dpnp.tensor.Device elsewhere in dpnp now.
  4. Nits:
    • Nit: the _random_raw doc string (uniform.cpp:46-47) says "low half first", but BitGen::next_uint64 returns (first_word << 32) | second_word, so the first 32-bit word is the high half.
    • Nit: NormalSampler::operator() (kernels/samplers.hpp) lacks the #pragma clang fp contract(off) that UniformSampler has, so loc + scale * z may become an FMA on one device and not on another (icpx defaults to -ffp-contract=fast-honor-pragmas).
    • Nit: the Sphinx spelling step reports 41 new words from these docstrings (Philox, BitGenerator, SeedSequence, ziggurat, ...); they belong in doc/known_words.txt.
    • Non-blocking: pickle and copy.deepcopy of a Generator or a BitGenerator fail with TypeError (from the SyclQueue and the Lock), and advance/jumped are absent; NumPy has all of them. Worth a line under "Differences from NumPy" until a follow-up adds them.

Checked, no issues

  • Engine setup: {key, counter_lo, counter_hi + w} maps to oneMKL's key k0/k1 and counter c0-c3 the way _launch assumes, and one generate on philox4x32x10<8> or <16> yields the words of 8 or 16 calls on <1>. Raw and uniform values are bit-identical between the GPU (blocked) and CPU (strided) layouts.
  • Stream separation: each work-item owns 264 counters, and the carry from the low word keeps the blocks disjoint up to the wrap at 2128.
  • Lifetimes and validation: keep_args_alive holds the result and the parameters until the kernel ends; the queue, writability, contiguity, size, dtype, engine and seed-length checks are in place.
  • fp64: the double kernels are guarded and split per kernel, and the float paths use only float literals.
  • NumPy parity: size normalisation, dtype handling, the scale < 0, -0.0, NaN and None checks, broadcasting and the ExecutionPlacementError cases match NumPy.
  • Statistical tests: the mean and variance checks of test_moments gave no failure over 200 other seeds.
  • CI: GitHub Actions and the required Jenkins lanes are green. The failed Jenkins Windows py3.14 wheel build is an icx out-of-memory crash in copy_and_cast_usm_to_usm.cpp, which this PR doesn't touch.

Comment thread doc/reference/random.rst Resolved
Comment thread dpnp/tests/AGENTS.md Resolved
Comment thread dpnp/random/_generator.py Resolved
Comment thread dpnp/random/_generator.py Resolved
Comment thread dpnp/random/_generator.py Resolved
Comment thread dpnp/random/_bit_generator.py Resolved
Comment thread dpnp/random/_generator.py Resolved
Add the dpnp.libtensor._rng_impl extension, which samples on the device
from oneMKL's device philox4x32x10 engine, and the Generator API of
NumPy on top of it: BitGenerator, Philox4x3210, SeedSequence (NumPy's),
default_rng, and the random, uniform, standard_normal and normal methods.

A bit generator keeps its state on the host and every launch rebuilds
its engines from it, so the values depend only on the seed and the
output size, not on the device. The legacy RandomState and module
functions are unchanged.
@ndgrigorian
ndgrigorian force-pushed the feature/device-rng-generator branch from 9fef498 to 1bd3cee Compare November 6, 2026 14:58
- Make the bit generator lock reentrant and read the Philox key under it
- Validate `size` in `random_raw(output=False)` and count values without
  overflow
- Drop the seed sequence of a Philox4x3210 given its key, as NumPy does
- Check the `uniform` range in the sampling dtype, and host bounds next to
  device ones
- Raise TypeError for parameters that don't cast to the sampling dtype
- Follow the USM type of the array parameters when `usm_type` is None
- Accept a usm_ndarray and, for `random` and `standard_normal`, an
  F-contiguous `out`
- Reject a parameter array overlapping the result, and build the module
  with LARGE_RANGES
- Take the float32 Box-Muller u1 from a whole 32-bit word, and keep FMA
  contraction off in the normal sampler
- Correct the device-independence claims, document the output-size
  dependence, and expand the method docstrings
- Enable the CuPy Generator and Philox tests, skipping the classes of
  later distributions and bit generators
@ndgrigorian

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review. All of the inline points are addressed in cab62a2, with replies on each thread. On the points from the review body:

  1. uniform host checks next to device parameters: a host bound that isn't finite (in float64 or in the sampling dtype) now raises OverflowError even when the other bound is on the device (test_uniform_device_and_host_bounds).
  2. USM type: the Generator methods now default to usm_type=None, so the output and the temporaries take the coerced USM type of the array parameters (and out), or "device" when there are none; an explicit usm_type still wins. _to_kernel_param passes it to dpnp.asarray. This is documented in the class docstring and in "Differences from NumPy", and tested in test_usm_type.py::test_generator_coerced_usm_type. It applies to every method up the stack.
  3. Docstrings: random, uniform, standard_normal and normal now have Parameters, Returns, Limitations and Examples. random.rst lists Generator.bit_generator, Generator.spawn, BitGenerator.random_raw, BitGenerator.spawn and BitGenerator.state, and dpctl.tensor.Device is now dpnp.tensor.Device. The methods of the later PRs still have the short form; I'll expand them in those PRs.
  4. Nits: the _random_raw doc now says "high half first", NormalSampler has fp contract(off), and pickling, copying and advance/jumped are listed under "Differences from NumPy" (PCG64DXSM in Add PCG64DXSM and MRG32k3a bit generators to dpnp.random #3161 has advance and jumped). I couldn't run make spelling here, so known_words.txt only gains the words I could identify (BitGenerator, Philox, reentrant, SeedSequence, strided, ziggurat). If you share your list of 41, I'll add the rest.

The six PRs above are rebased onto this one, carrying the changes where they apply: usm_type=None for every method, out overlap copies and the per-PR CuPy unskips.

@antonwolfy

Copy link
Copy Markdown
Contributor

BigCrush (TestU01 1.2.3) has finished on the GPU stream: all 160 statistics of its 106 tests passed, over 3.57e11 32-bit words. That completes the randomness testing from my review: no battery found a problem with the generator.

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-checked at this head on level_zero:gpu and opencl:cpu: all the earlier points are fixed, and the tests pass on both devices. A few new points inline: the first two are worth fixing, the rest are nits.

// no fused multiply-add, to round as NumPy does
#pragma clang fp contract(off)
const T low = low_[i];
return low + (high_[i] - low) * bg.template next_uniform<T>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On opencl:cpu, uniform over a tiny range returns values above high.
uniform(0, 1.5 * finfo(float32).tiny, 4099, dtype=float32) on opencl:cpu returns values up to 1.333 × high, while level_zero:gpu stays below high. The CPU output equals k * (range * 2**-24) bit for bit, with k = w >> 8, instead of (k * 2**-24) * range: the two products are reassociated, so range * 2**-24 underflows into a denormal and is rounded. float64 does the same with 2**-53.

  • Any range below 2**-102 (about 2e-31) in float32 or 2**-969 in float64 can give other values than the GPU, which contradicts "the uniform floats are the same on every device" (random.rst:31-33).
  • A range below about 4 × tiny gives values outside [low, high); at 7 × tiny the largest is still 1.14 × high.

_rng_impl is built with icpx's default fast floating-point model: its compile lines add only -fno-approx-func -fno-finite-math-only. fp contract(off) turns off contraction but still allows reassociation. libtensor builds its precision-sensitive sources with -fno-fast-math (dpnp/libtensor/source/CMakeLists.txt:324-329). This sampler is unchanged since the first commit.

Suggestion: build the rng sources with -fno-fast-math (or add #pragma clang fp reassociate(off) here), and add a test that compares uniform(0, 1.5 * tiny) with NumPy's low + (high - low) * u, where u is random() of the same seed.


def test_float32_normal_reference(self):
# float32 normals take u1 from a whole 32-bit word
key, counter, n = 0x0123456789ABCDEF, 3, 32

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test also passes with the old 24-bit u1.
At counter=3 all 16 u1 words are far from 0, where the mappings differ by less than 1e-6 relative in the output, well inside rtol=1e-5. Against the device output on level_zero:gpu and opencl:cpu, the test still passes when the reference uses the old ((w >> 8) + 0.5) * 2**-24, or float32(w) * 2**-32 without the + 0.5.

With counter=14589945 (same key and n), the second u1 word of work-item 0 is 6, so elements 8 and 12 come from u1 = 6.5 * 2**-32, where the Box-Muller radius is 6.37. There the current mapping passes on both devices, and the two mutants fail, with relative differences of 8.3e-2 and 2.0e-3.

Suggestion: use that counter, so the test covers the tail it is meant to pin.

Python integer when `size` is ``None``. With ``output=False``, the
state advances by the number of elements of `size`, and an invalid
`size` raises as it does with ``output=True``. NumPy advances by
the sum of the dimensions of `size` instead, and ignores a negative

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NumPy doesn't ignore a negative dimension here, only a negative total.
With output=False NumPy advances by np.asarray(size).sum() (numpy/random/_common.pyx:88), so a negative dimension lowers the count: with Philox(1), random_raw((5, -2), output=False) advances by 3 values, while (2, -8) and (-16,) don't advance it.

Suggestion: "NumPy advances by the sum of the dimensions of size instead, and not at all when that sum is negative."

Comment thread doc/reference/random.rst
checked as in NumPy. Parameters in :class:`dpnp.ndarray` are
not, since that would wait for the device.
Invalid ones give undefined values, such as NaN.
* Generators and bit generators cannot be pickled or copied, and the bit

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy.copy works; only pickle and copy.deepcopy fail.
copy.copy of a Generator returns one that shares the bit generator, and copy.copy of a Philox4x3210 an independent one in the same state, as in NumPy (it also shares the original's lock, which NumPy's copy doesn't). pickle.dumps and copy.deepcopy of either raise TypeError. _generator.py:202 says the same as this line.

Suggestion: "cannot be pickled or deep-copied".

def_sampler<rng_ns::RawSampler, RawTypes>(
m, "_random_raw",
"Fill the result with the raw 64-bit output of the engine, or with "
"its 32-bit halves, high half first",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 32-bit output isn't the halves of the 64-bit one.
Element i comes from work-item i % ceil(n / 8) for both dtypes, so a uint32 draw of 2 * n elements equals the high-first halves of a uint64 draw of n elements only for n <= 4. Each matches the Philox reference on its own.

Suggestion: "or with one 32-bit word per element".

Comment thread doc/known_words.txt
oneAPI
ord
orthonormal
Philox

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Philox belongs after Penrose.

On the 41 words from my review: that was the number of new warning lines from the spelling step, not of words. At this head the step reports 10 lines more than master. The only new word is rng, from the dpnp.random.default_rng page; oneMKL, sycl and reproducibility are flagged on master already. So rng is the only word left to add.

with bg.lock:
bg.state = bg.state

t = threading.Thread(target=_use_state)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: make this thread a daemon.
If the lock stops being reentrant, the assert fails but t stays blocked, and as a non-daemon thread it keeps the pytest process from exiting. threading.Thread(target=_use_state, daemon=True) avoids that.

Comment thread dpnp/random/_generator.py
@bit_generator.setter
def bit_generator(self, value):
if not isinstance(value, BitGenerator):
raise TypeError(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: a few lines of the Python layer have no test.
The coverage job at this head shows _generator.py at 96.6% and _bit_generator.py at 95.3%. The lines no test reaches:

  • here, _generator.py:238: the TypeError of the bit_generator setter, e.g. rng.bit_generator = 5 (the same check in __init__ is covered);
  • _bit_generator.py:154: a seed given as a dpnp array, which goes through dpnp.asnumpy;
  • _generator.py:251 and _bit_generator.py:192: the sycl_device properties, used only in a docstring example;
  • _generator.py:318-319: the C-only out check. No method of this PR reaches it, since random and standard_normal take the path that allows an F-contiguous out; standard_gamma reaches it in Add continuous distributions to dpnp.random.Generator #3158, so its test can land there;
  • __repr__ and __str__ (_generator.py:220, 223, _bit_generator.py:158, 161), and the NotImplementedError of the base BitGenerator.state and _launch (_bit_generator.py:197, 212).

Suggestion: tests for the setter error, the dpnp-array seed and sycl_device; the rest is optional.

{
}

void operator()(sycl::id<1> id) const

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: the kernel headers in rng/kernels/ count in the coverage report.
scripts/gen_coverage.py:325 drops the SYCL kernel headers from the C++ report with -ignore-filename-regex=.*/libtensor/include/kernels/.*\.hpp$, since host coverage can't see code that runs in a kernel (#2623, #2816). These headers are in extensions/rng/kernels/, which the pattern doesn't match, so Coveralls lists all four:

  • only their host lines count as covered: the functor constructors and the n_wi helper, 13 lines;
  • the 105 device lines, this operator() body and the others, count as missed: engines.hpp and distributions.hpp are at 0%, this file at 18% and samplers.hpp at 27%.

That is most of this PR's -0.2%: without these headers, line coverage would be about 0.26 points higher.

Suggestion: widen the pattern to .*/libtensor/(include|extensions/[^/]+)/kernels/.*\.hpp$, or move the headers to libtensor/include/kernels/rng/. The latter ships them with dpnp.get_include(), where they would need oneMKL's device RNG headers.


#include <sycl/sycl.hpp>

#include <oneapi/mkl/rng/device.hpp>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: with DPNP_USE_ONEMATH, this could include oneMath's device RNG header.
_rng_impl always includes oneMKL's device RNG API, while blas, fft, lapack and sparse switch to oneMath with DPNP_USE_ONEMATH. oneMath's device API (oneapi/math/rng/device.hpp) is header-only, generic SYCL code. At the v0.9 tag that dpnp pins (CMakeLists.txt:274) and at current develop, it has the same engines and distributions as oneMKL 2026.2's. Across the series this extension uses only philox4x32x10<N>, mrg32k3a (#3161), bits<std::uint32_t> and generate, and oneMath has all of them. Its distributions don't matter here, since the samplers are dpnp's own. The only part missing from oneMath is oneMKL's engine_helpers.hpp (engine_descriptor), which isn't used.

This matters mainly for the CUDA and HIP builds, which go through oneMath. I haven't checked whether oneMKL's header compiles for those targets.

Suggestion: on USE_ONEMATH, include <oneapi/math/rng/device.hpp>, alias oneapi::math::rng::device instead of oneapi::mkl::rng::device, take the include directories from ${ONEMATH_LIB}, and run the Philox reference tests on a oneMath build to confirm the streams are the same. This is fine for a follow-up PR.

Comment thread dpnp/random/_generator.py
a :class:`dpnp.random.Generator` is returned unaltered.

Default: ``None``.
device : {None, string, SyclDevice, SyclQueue, Device}, optional

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The device and sycl_queue texts here aren't the standard ones.
dpnp/AGENTS.md:35 asks to copy them from dpnp.empty, as BitGenerator's device (_bit_generator.py:119) and RandomState do. This one doesn't list what device accepts (a filter selector string, a SyclDevice, a SyclQueue or a dpnp.tensor.Device), and sycl_queue doesn't say that None takes the queue from device or uses a default one. The same holds for Philox4x3210 (_bit_generator.py:353, 357), whose device and sycl_queue only say "See :class:dpnp.random.BitGenerator.", and for the sycl_queue of BitGenerator (_bit_generator.py:128).

Suggestion: use the dpnp.empty text in all three, keeping the extra sentence here ("Ignored when seed is a bit generator or a generator.").

Comment thread doc/reference/random.rst
BitGenerator
BitGenerator.random_raw
BitGenerator.spawn
BitGenerator.state

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some documented properties have no page.
dpnp's class pages don't list their members (the RandomState page doesn't either), so a member is rendered only when this file lists it. Generator.sycl_queue and Generator.sycl_device, and BitGenerator.lock, BitGenerator.seed_seq, BitGenerator.sycl_queue and BitGenerator.sycl_device have docstrings but aren't listed, so they appear nowhere in the rendered docs. lock matters most: its docstring is the only place that says the lock is reentrant and how to use it. ndarray.rst:73-75 lists ndarray.sycl_device, ndarray.sycl_queue and ndarray.usm_type.

Suggestion: add them to the Generator and BitGenerator autosummary lists.


class BitGenerator:
"""
Base class for the bit generators of :class:`dpnp.random.Generator`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the "For full documentation" line is missing.
Generator and its methods have it, and NumPy has this class. dpnp/AGENTS.md:31 puts it after the summary: For full documentation refer to :obj:`numpy.random.BitGenerator`.

Comment thread dpnp/random/_generator.py

def spawn(self, n_children):
"""
Create new independent child generators on the same SYCL queue.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

n_children isn't described.
This docstring has only the summary and the reference line, while BitGenerator.spawn (_bit_generator.py:301) has Parameters and Returns. So the rendered Generator.spawn page doesn't say what n_children is or what is returned.

Suggestion: add the same Parameters and Returns, with out : list of Generator.

Comment thread doc/reference/random.rst
* Generators and bit generators cannot be pickled or copied, and the bit
generators have no ``advance`` or ``jumped`` method.

Random Generator

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this heading and the legacy "Random generator" (line 181) differ only by one capital letter.
The page's table of contents shows "Random Generator" for the new API and "Random generator" for RandomState under "Legacy random generation". The other headings on this page are in sentence case ("Accessing the bit generator and spawning", "Bit generators", "Legacy random generation"); "Random Generator" follows NumPy's page title.

Suggestion: rename one of them, e.g. the legacy one to "RandomState", so the two sections are easy to tell apart.

// Most of the sampling algorithms below are adapted from NumPy's
// numpy/random/src/distributions/distributions.c, which carries this notice:
//
// Copyright (c) 2005-2025, NumPy Developers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This NumPy notice isn't needed in this PR.

The code below it is Param and the Box-Muller StandardNormal, which isn't NumPy's algorithm: NumPy's Generator.standard_normal uses the ziggurat, and the legacy gauss the polar method. So "Most of the sampling algorithms below are adapted from ..." isn't true yet, and the notice isn't needed here.

And I believe we need to follow the procedure and get approval if we would like to add the notice.

bg = Philox4x3210(key=key, counter=counter)
res = bg.random_raw(n)
assert res.dtype == dpnp.uint64
assert_array_equal(dpnp.asnumpy(res), _reference_raw(key, counter, n))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dpnp.asnumpy() is not needed here and below:

Suggested change
assert_array_equal(dpnp.asnumpy(res), _reference_raw(key, counter, n))
assert_array_equal(res, _reference_raw(key, counter, n))

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants