BitGenerator support#499
Conversation
Icxolu
left a comment
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
sure, will do when I’m done. I like working on multiple machines, and I don’t like re-doing settings for individual projects
| //! # use pyo3::prelude::*; | ||
| //! use rand::Rng as _; | ||
| //! # use numpy::random::{PyBitGenerator, PyBitGeneratorMethods as _}; | ||
| //! # // TODO: reuse function definition from above? |
There was a problem hiding this comment.
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<..>>;
}There was a problem hiding this comment.
there are many implementations, we’d have to cover all of them.
I’d rather leave this minimal until this PR is mostly done.
|
OK, with the |
|
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 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: 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. |
This comment was marked as outdated.
This comment was marked as outdated.
|
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. |
|
When implementing Python-facing APIs, having a |
|
Would it also possible to go the other way, i.e. provide a rand rng from Rust to Python as a numpy BitGenerator? |
|
Yes, that's part of numpy's API as well! |
# Conflicts: # Cargo.toml
|
OK, done. UB fixedturns out the 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 |
|
And done! Please take a look! @Icxolu I followed your initial advice and converted it from a I have gathered some experience using numpy’s generators and figured that using outstanding questions & tasks
|
See
Fixes #498
The idea is to have a safe wrapper around the
npy_bitgenstruct that implementsrand::RngCore. That way pyo3 functions could be passed anp.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:
castanp.random.BitGeneratorinstance into anumpy::random::PyBitGenerator..lock()on it to get anumpy::random::PyBitGeneratorGuard.TODO:
Safety
If somebody releases the threading lock of the
BitGeneratorwhile 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
PyBitGeneratorandPyBitGeneratorGuard, allowing to choose if someone wants toPyBitGenerator’srandom_*methods directly on that object while holding the GIL and without locking itnp.random.BitGeneratorand returning a GIL-free object that can be used.but for now I just implemented the use case that’s actually desired.