-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
add Random.jump(rng)
API
#58353
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
add Random.jump(rng)
API
#58353
Conversation
We have long had methods for RNG "jumps ahead", i.e. advancing the state by a given number of "steps", but no good API for that. The only public API is `Future.randjump(r::MersenneTwister, steps::Integer)`, and there are also functions for `Xoshiro` which are not public (`Random.jump_128` and friends). The following generic API is implemented here: * `Random.jump(rng)` to jump by a reasonable default number of steps * `Random.jump(rng; by::Real)` to jump by `by` steps * `Random.jump!(rng; [by])` to equivalently jump in-place * `Random.jump(rng, dims...; [by])` to create an array of jumped RNGs In old julia versions, there also existed a method of `randjump` returning an array, but the 1st element of this array was the passed argument; the version here does not do this aliasing. There are two kinds of integers one would wish to pass: dimensions for the array version, and the number of steps. Using jumps is relatively "niche", but needing to fidle with the number of steps is even more niche. It's expected that in the vast majority of cases, a good default is enough. Some APIs in other languages have `jump` (e.g. 2^128 steps) and `long_jump` (e.g. 2^192 steps), or `leap` in java, for more complicated cases; for example each process gets a jumped RNG via `long_jump`, and within each process, each thread gets a jumped RNG via `jump`. But this is not very scalable if more kind of jumps are needed: should `huge_jump` be introduced? For these rare cases where the default number of steps is not sufficient, it seems better to let the programmer explicitly specify the number of steps via an integer. There is even a third kind of integers one might want to pass: in `Random.jump_128(x::Xoshiro, i::Integer)`, `i` represents the number of times a jump of size `2^128` is applied; this is because `Xoshiro` doesn't support arbitrary number of steps; this is not supported in the proposed API, because 1) it's trivial for the user to implement herself, and 2) in probably most use cases, using the array version will be a valid alternative, and more efficient because previous computations are not wasted (like in `[Random.jump_128(x, i) for i=1:num_tasks]` vs `Random.jump(x, num_tasks)`). Another argument in favor of this API is that it mirrors the proposed `Random.fork(rng, dims...)` function from #58193.
j = _randjump(r, Random.DSFMT.calc_jump(steps >> 1)) | ||
j.adv_jump += steps | ||
j | ||
function jump(rng::MersenneTwister; by::Real=NaN) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why make this Real
, and not default to e.g. -1
? Does jumping half of a step make sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For convenience; I typed big(2)^128
or big(10)^20
(for MersenneTwister) too often. Jump would often be a power of 2, so accepting 2.0^128
is nice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see - so what actually happens when e.g. by=1.5
is passed in?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An error is thrown; in this very method for example, the error happens when trying to convert by
to BigInt
. I put this in this docstring:
by
should be an integer, but can be expressed via non-
Integertypes for convenience, e.g.
by = 2.0^128`.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I missed that part of the docstring - I think it would be good to explicitly mention the error case when the conversion fails.
elseif by == 2.0^192 | ||
jump_192!(rng) | ||
else | ||
throw(ArgumentError("$(typeof(rng)) RNGs can be jumped only by 2^128 or 2^192 steps")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of throwing an error, would it make sense to use the step
argument as "how many multiples of the stepsize (2^128) should be jumped"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather not. I think there needs to be a way to specify the number of steps (different RNGs will have different number of steps available), and to have the API as simple as possible, I prefer not having another integer specify the multiple. If needed, we could eventually support Random.jump(xoshiro, by=3*big(2)^128)
automatically detect that it's 3 times of jump of 2.0^128
. But maybe I misunderstood your suggestion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was only referring to the special case of Xoshiro
, not the general case. I agree that limiting the general case doesn't really make sense. I also meant interpreting the existing by
/step
argument from this PR as that "multiple of 2^128", not adding another argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would prevent using the 2^192
jump, but also it complicates the API:
by is usually the number of steps, except when specified otherwise, where it's interpreted as being a multiple of a pre-defined number of steps
(This formulation above is not clear enough, but that was to give the idea...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, reading that, I can see that this is confusing.. Is it possible to make the jump for Xoshiro arbitrary instead? It just feels a bit weird to have this specific limitation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it would be possible, but I don't currently plan to implement that myself. I agree it feels somewhat weird, but it's not really in practice. Typically libraries only provide jump
and long_jump
for two different number of steps, and its plenty enough for the vast majority of use-cases. We could easily add a few more specific ones though (2^64, etc.)
Do we need this now that we have the PR for |
That's an interesting idea, and indeed I believe One thing is that However, as the code is already here, and jumping ahead is relatively standard, keeping it in |
j.adv_jump += steps | ||
j | ||
function jump(rng::MersenneTwister; by::Real=NaN) | ||
isnan(by) && (by = 2.0^128) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not set the default value for by
to be 2.0^128 rather than NaN
if we're already changing the value to be that when the default is provided?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. The weak reason is that the array version takes the same default NaN
, and passes it out unchanged to the non-array version (this method above), which then needs to handle NaN
anyway. The alternative would be, in the array version, to either call jump(rng)
or jump(rng; by)
depending on whether or not by
was passed explicitly.
We have long had methods for RNG "jumps ahead", i.e. advancing the state by a given number of "steps", but no good API for that.
The only public API is
Future.randjump(r::MersenneTwister, steps::Integer)
, and there are also functions forXoshiro
which are not public (Random.jump_128
and friends).The following generic API is implemented here:
Random.jump(rng)
to jump by a reasonable default number of stepsRandom.jump(rng; by::Real)
to jump byby
stepsRandom.jump!(rng; [by])
to equivalently jump in-placeRandom.jump(rng, dims...; [by])
to create an array of jumped RNGsIn old julia versions, there also existed a method of
randjump
returning an array, but the 1st element of this array was the passed argument; the version here does not do this aliasing.There are two kinds of integers one would wish to pass: dimensions for the array version, and the number of steps.
Using jumps is relatively "niche", but needing to fidle with the number of steps is even more niche. It's expected that in the vast majority of cases, a good default is enough.
Some APIs in other languages have
jump
(e.g. 2^128 steps) andlong_jump
(e.g. 2^192 steps), orleap
in java, for more complicated cases; for example each process gets a jumped RNG vialong_jump
, and within each process, each thread gets a jumped RNG viajump
. But this is not very scalable if more kind of jumps are needed: shouldhuge_jump
be introduced? For these rare cases where the default number of steps is not sufficient, it seems better to let the programmer explicitly specify the number of steps via an integer.There is even a third kind of integers one might want to pass: in
Random.jump_128(x::Xoshiro, i::Integer)
,i
represents the number of times a jump of size2^128
is applied; this is becauseXoshiro
doesn't support arbitrary number of steps; this is not supported in the proposed API, because 1) it's trivial for the user to implement herself, and 2) in probably most use cases, using the array version will be a valid alternative, and more efficient because previous computations are not wasted(like in
[Random.jump_128(x, i) for i=1:num_tasks]
vsRandom.jump(x, num_tasks)
).Another argument in favor of this API is that it mirrors the proposed
Random.fork(rng, dims...)
function from #58193.