Skip to content

Commit 8262550

Browse files
committed
add Random.jump(rng) API
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.
1 parent 998fff5 commit 8262550

File tree

9 files changed

+381
-224
lines changed

9 files changed

+381
-224
lines changed

NEWS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ Standard library changes
4747

4848
#### Profile
4949

50+
#### Random
51+
52+
* New `Random.jump` function to advance the state ("jump ahead") of `Xoshiro` or `MersenneTwister` RNGs ([#58353]).
53+
5054
#### REPL
5155

5256
#### Test

stdlib/Future/src/Future.jl

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,10 @@ Create an initialized `MersenneTwister` object, whose state is moved forward
3535
One such step corresponds to the generation of two `Float64` numbers.
3636
For each different value of `steps`, a large polynomial has to be generated internally.
3737
One is already pre-computed for `steps=big(10)^20`.
38+
39+
!!! compat "Julia 1.13"
40+
As of Julia 1.13, this functionality is now implemented by `Random.jump`.
3841
"""
39-
function randjump(r::MersenneTwister, steps::Integer)
40-
j = Random._randjump(r, Random.DSFMT.calc_jump(steps))
41-
j.adv_jump += 2*big(steps) # convert to BigInt to prevent overflow
42-
j
43-
end
42+
randjump(r::MersenneTwister, steps::Integer) = Random.jump(r; by=steps)
4443

4544
end # module Future

stdlib/Random/docs/src/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ Random.TaskLocalRNG
8383
Random.Xoshiro
8484
Random.MersenneTwister
8585
Random.RandomDevice
86+
Random.jump
87+
Random.jump!
8688
```
8789

8890
## [Hooking into the `Random` API](@id rand-api-hook)

stdlib/Random/src/MersenneTwister.jl

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ The `seed` may be an integer, a string, or a vector of `UInt32` integers.
5151
If no seed is provided, a randomly generated one is created (using entropy from the system).
5252
See the [`seed!`](@ref) function for reseeding an already existing `MersenneTwister` object.
5353
54+
`MersenneTwister` supports "jumping ahead" via [`Random.jump`](@ref), with an
55+
arbitrary number of steps, where one step represents consuming two `Float64` values.
56+
Given `MersenneTwister`'s huge period of `2^19937-1`, there is a lot of flexibility
57+
for arranging -- via different numbers of jump steps -- computations with multiple
58+
levels of parallelism requiring non-overlapping random subsequences.
59+
Prior to Julia 1.13, the same functionality was available from the `Future` stdlib
60+
with the `Future.randjump` function.
61+
5462
!!! compat "Julia 1.11"
5563
Passing a negative integer seed requires at least Julia 1.11.
5664
@@ -554,7 +562,7 @@ function rand!(r::MersenneTwister, A1::Array{Bool}, sp::SamplerType{Bool})
554562
end
555563

556564

557-
### randjump
565+
### jump
558566

559567
# Old randjump methods are deprecated, the scalar version is in the Future module.
560568

@@ -568,17 +576,16 @@ function _randjump(r::MersenneTwister, jumppoly::DSFMT.GF2X)
568576
s
569577
end
570578

571-
# NON-PUBLIC
572-
function jump(r::MersenneTwister, steps::Integer)
573-
iseven(steps) || throw(DomainError(steps, "steps must be even"))
574-
# steps >= 0 checked in calc_jump (`steps >> 1 < 0` if `steps < 0`)
575-
j = _randjump(r, Random.DSFMT.calc_jump(steps >> 1))
576-
j.adv_jump += steps
577-
j
579+
function jump(rng::MersenneTwister; by::Real=NaN)
580+
isnan(by) && (by = 2.0^128)
581+
steps = BigInt(by)
582+
# steps >= 0 checked in calc_jump
583+
jumped = _randjump(rng, DSFMT.calc_jump(steps))
584+
jumped.adv_jump += steps
585+
jumped
578586
end
579587

580-
# NON-PUBLIC
581-
jump!(r::MersenneTwister, steps::Integer) = copy!(r, jump(r, steps))
588+
jump!(rng::MersenneTwister; by::Real=NaN) = copy!(rng, jump(rng; by))
582589

583590

584591
### constructors matching show (EXPERIMENTAL)
@@ -642,7 +649,7 @@ function advance!(r::MersenneTwister, adv_jump, adv, adv_vals, idxF, adv_ints, i
642649
ms = dsfmt_get_min_array_size() % Int
643650
work = sizehint!(Vector{Float64}(), 2ms)
644651

645-
adv_jump != 0 && jump!(r, adv_jump)
652+
adv_jump != 0 && jump!(r; by=adv_jump)
646653
advF = (adv_vals, idxF) != (0, 0)
647654
advI = (adv_ints, idxI) != (0, 0)
648655

stdlib/Random/src/RNGs.jl

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,3 +306,92 @@ function hash_seed(str::AbstractString, ctx::SHA_CTX)
306306
end
307307
SHA.update!(ctx, (0x05,))
308308
end
309+
310+
311+
## jump
312+
313+
"""
314+
Random.jump(rng::AbstractRNG, [dims...]; [by::Real])
315+
316+
Create a new generator of the same type as `rng`, whose state is advanced forward
317+
("jump ahead") by `by` "steps".
318+
This is equivalent to `Random.jump!(copy(rng); by)`.
319+
320+
When `dims` is specified (as integers or as a tuple), create an array of such
321+
generators `x_1, x_2, ..., x_n`, where `x_1 = jump(rng; by)`, and
322+
`x_i = jump(x_{i-1}; by)`.
323+
324+
This can be used to generate non-overlapping subsequences for parallel computations.
325+
The maximum number of such subsequences is roughly equal to the period
326+
of `rng` divided by `by`.
327+
328+
The default value of `by` should generally be large enough such that the generated
329+
subsequences can accomodate any computation;
330+
it's currently `2.0^128` for `MersenneTwister` and `Xoshiro`, but can change in the
331+
future.
332+
333+
!!! compat "Julia 1.13"
334+
This function was introduced in Julia 1.13.
335+
336+
# Examples
337+
```julia-repl
338+
julia> Random.jump(Xoshiro(0))
339+
Xoshiro(0xe223c163bccd575d, 0xef13c6b1ba8b980b, 0xeb86b37bd43e78ab, 0x2b93c9c43b0815fb, 0x22a21880af5dc689)
340+
341+
julia> m = MersenneTwister(123)
342+
MersenneTwister(123)
343+
344+
julia> Random.jump(m, 3; by=1000)
345+
3-element Vector{MersenneTwister}:
346+
MersenneTwister(123, (1000, 0))
347+
MersenneTwister(123, (2000, 0))
348+
MersenneTwister(123, (3000, 0))
349+
```
350+
351+
See also [`Random.jump!`](@ref).
352+
"""
353+
function jump end
354+
355+
"""
356+
Random.jump!(rng::AbstractRNG; [by::Real]) -> rng
357+
358+
Advance the state of `rng` forward ("jump ahead") by `by` "steps".
359+
What a step represents is specific to each specific type.
360+
For example with `Xoshiro`, advancing by one step means consuming 8 bytes from `rng`.
361+
362+
Note that some approximation in the number of steps is permitted in the implementation.
363+
For example, if the RNG caches some random numbers, it's allowed to discard the cache
364+
and jump ahead by `by` steps without accounting for the specific number of discarded
365+
values. In general, `by` should be specified large enough, with a huge margin, such
366+
that such details do not matter.
367+
368+
`by` should be an integer, but can be expressed via non-`Integer` types for
369+
convenience, e.g. `by = 2.0^128`. While some RNGs like `MersenneTwister` support any
370+
positive integer, others like `Xoshiro` support only specific values.
371+
372+
!!! compat "Julia 1.13"
373+
This function was introduced in Julia 1.13.
374+
375+
# Examples
376+
```julia-repl
377+
julia> m = MersenneTwister(123)
378+
MersenneTwister(123)
379+
380+
julia> Random.jump!(m; by=1000)
381+
MersenneTwister(123, (1000, 0))
382+
```
383+
See also [`Random.jump`](@ref).
384+
"""
385+
function jump! end
386+
387+
jump(rng::AbstractRNG; by::Real=NaN) = jump!(copy(rng); by)
388+
jump(rng::AbstractRNG, d1::Integer, dims::Integer...; by::Real=NaN) =
389+
jump(rng, Dims((d1, dims...)); by)
390+
391+
function jump(rng::AbstractRNG, dims::Dims; by::Real=NaN)
392+
js = Array{typeof(rng)}(undef, dims)
393+
for ii in eachindex(js)
394+
rng = js[ii] = jump(rng; by)
395+
end
396+
js
397+
end

stdlib/Random/src/Random.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export rand!, randn!,
2929
randcycle, randcycle!,
3030
AbstractRNG, MersenneTwister, RandomDevice, TaskLocalRNG, Xoshiro
3131

32-
public seed!, default_rng, Sampler, SamplerType, SamplerTrivial, SamplerSimple
32+
public seed!, default_rng, jump, jump!, Sampler, SamplerType, SamplerTrivial, SamplerSimple
3333

3434
## general definitions
3535

0 commit comments

Comments
 (0)