Skip to content

BitGenerator support#499

Open
flying-sheep wants to merge 53 commits into
PyO3:mainfrom
flying-sheep:pa/bitgen
Open

BitGenerator support#499
flying-sheep wants to merge 53 commits into
PyO3:mainfrom
flying-sheep:pa/bitgen

Conversation

@flying-sheep

@flying-sheep flying-sheep commented Jun 6, 2025

Copy link
Copy Markdown

See

Fixes #498

The idea is to have a safe wrapper around the npy_bitgen struct that implements rand::RngCore. That way pyo3 functions could be passed a np.random.Generator, get that wrapper from it, and pass it to Rust APIs, which could then call its methods repeatedly.

The way it’s implemented, the workflow would look like this:

  1. acquire GIL
  2. cast a np.random.BitGenerator instance into a numpy::random::PyBitGenerator.
  3. call .lock() on it to get a numpy::random::PyBitGeneratorGuard.
  4. release GIL
  5. call functions on guard object without needing to hold the GIL

TODO:

  • I see local crashes when running all tests, so there’s probably some UB, I’d appreciate help to fix it.

Safety

If somebody releases the threading lock of the BitGenerator while we’re using it, this isn’t safe 🤔

API design options

I could make this more complex by adding a new trait that is implemented by both PyBitGenerator and PyBitGeneratorGuard, allowing to choose if someone wants to

  • use the PyBitGenerator’s random_* methods directly on that object while holding the GIL and without locking it
  • use it like it’s used now, by locking the np.random.BitGenerator and returning a GIL-free object that can be used.

but for now I just implemented the use case that’s actually desired.

@flying-sheep flying-sheep changed the title BItGenerator support BitGenerator support Jun 6, 2025
@flying-sheep
flying-sheep marked this pull request as ready for review June 8, 2025 12:44

@Icxolu Icxolu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks like a useful addition! Thanks for working on it. I'm definitely not an expert here, but I left a few comment about things that stood out to me. Let me know what you think.
Also, are there any differences between numpy v1 and v2 that we need to consider?

Comment thread .vscode/settings.json

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be removed

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

sure, will do when I’m done. I like working on multiple machines, and I don’t like re-doing settings for individual projects

Comment thread src/random.rs Outdated
Comment thread src/random.rs Outdated
Comment thread src/random.rs Outdated
Comment thread src/random.rs Outdated
Comment thread src/random.rs Outdated
Comment thread src/random.rs Outdated
Comment thread .vscode/settings.json
Comment thread src/npyffi/random.rs
Comment thread Cargo.toml Outdated
Comment thread src/npyffi/random.rs Outdated
Comment thread src/random.rs Outdated
//! # use pyo3::prelude::*;
//! use rand::Rng as _;
//! # use numpy::random::{PyBitGenerator, PyBitGeneratorMethods as _};
//! # // TODO: reuse function definition from above?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It feels like there should be a convenient way to get this. I'm thinking about something like

impl PyBitGenerator {
     fn new(py: Python<'_>) -> PyResult<Bound<..>>;
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

there are many implementations, we’d have to cover all of them.

I’d rather leave this minimal until this PR is mostly done.

Comment thread src/random.rs Outdated
Comment thread src/random.rs Outdated
Comment thread src/random.rs Outdated
Comment thread src/random.rs Outdated
Comment thread src/random.rs Outdated
Comment thread src/random.rs Outdated
Comment thread src/random.rs Outdated
Comment thread src/random.rs Outdated
@flying-sheep

Copy link
Copy Markdown
Author

OK, with the release attr and changing the parallel test to use the explicit release as well, the UB now sometimes manifests as a lock poisoning error. progress?

@Icxolu

Icxolu commented Jun 10, 2025

Copy link
Copy Markdown
Member

I may have found a problem:

This fails as intended:

Python::with_gil(|py| {
    let obj = get_bit_generator(py)?;
    let a = obj.lock()?;
    let b = obj.lock()?;

    Ok::<_, PyErr>(())
})
.unwrap();

returning

called `Result::unwrap()` on an `Err` value: PyErr { type: <class 'RuntimeError'>, value: RuntimeError('BitGenerator is already locked'), traceback: None }

But this does not fail:

Python::with_gil(|py| {
    let a = get_bit_generator(py)?.lock()?;
    let b = get_bit_generator(py)?.lock()?;

    Ok::<_, PyErr>(())
})
.unwrap();

and crucially it gives the same pointers:

[src/random.rs:113:18] ptr = 0x00007b9f6be44cc0
[src/random.rs:113:18] *ptr = bitgen_t {
    state: 0x00007b9f6be44d08,
    next_uint64: 0x00007b9f6837d320,
    next_uint32: 0x00007b9f6837d370,
    next_double: 0x00007b9f6837d3f0,
    next_raw: 0x00007b9f6837d320,
}
[src/random.rs:113:18] ptr = 0x00007b9f6be44cc0
[src/random.rs:113:18] *ptr = bitgen_t {
    state: 0x00007b9f6be44d08,
    next_uint64: 0x00007b9f6837d320,
    next_uint32: 0x00007b9f6837d370,
    next_double: 0x00007b9f6837d3f0,
    next_raw: 0x00007b9f6837d320,
}

So when using multiple threads, for example multiple tests running in parallel, we have a data race on the state. I think we need a lock across all instances to make this work.

@flying-sheep

This comment was marked as outdated.

@mejrs

mejrs commented Jun 11, 2025

Copy link
Copy Markdown
Member

Maybe we should skip the guard part and just lock and unlock within the RngCore implementation itself. Can you give an example for why you'd want this, and why the api has this form? Why would someone want to use this rather than the RngCore impl that rand ships with? Maybe we can come up with a better design.

@flying-sheep

flying-sheep commented Jun 11, 2025

Copy link
Copy Markdown
Author

When implementing Python-facing APIs, having a rng: np.random.Generator parameter is common. I want to write code that actually respects that parameter and uses it instead of ignoring it or calling it once to seed the actual generator.

@juntyr

juntyr commented Jun 24, 2025

Copy link
Copy Markdown

Would it also possible to go the other way, i.e. provide a rand rng from Rust to Python as a numpy BitGenerator?

@flying-sheep

Copy link
Copy Markdown
Author

Yes, that's part of numpy's API as well!

@flying-sheep

flying-sheep commented Jul 22, 2026

Copy link
Copy Markdown
Author

OK, done.

UB fixed

turns out the BitGenerator itself has allocated memory, not just the _capsule, so letting that be freed was the bug. Now we bump its refcount and keep it around until we’re done.

double locking issue fixed

@Icxolu as you found in #499 (comment), the lock is re-entrant, so now we just keep a HashSet of locked bitgens so same-thread guards are unique.

PS: MSRV is 1.83, and in 1.84 ptr.addr() was stabilized, which would be a slightly better API than as usize here

@flying-sheep

flying-sheep commented Jul 22, 2026

Copy link
Copy Markdown
Author

And done! Please take a look!

@Icxolu I followed your initial advice and converted it from a lock() -> Guard API into a lock(|guard| ...) API, getting rid of the silently-fallible Drop implementation.

I have gathered some experience using numpy’s generators and figured that using .spawn_one() or .spawn(n) to get more freedom here is totally acceptable.

outstanding questions & tasks

  • naming: we have PyBitGenerator for the maybe-shared one and BitGenerator for the exclusive access one (&mut when locked, by value when owned by us). These names could be better I think.
  • add new()/from_* functions. With the new API it/they would return a owned-by-us BitGenerator (BitGenerator support #499 (comment)) could maybe be combined with this, and change the BitGenerator so it can hold one of multiple backends
  • finally: remove .vscode stuff (BitGenerator support #499 (comment))

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.

rand::RngCore implementation for numpy.random.Generator

4 participants