Skip to content

add Random.fork(rng::Xoshiro) to split rng into a new instance #58193

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ Standard library changes

#### Profile

#### Random

* It's now possible to efficiently create a new `Xoshiro` RNG from an existing one via `Random.fork`, which
can be useful in parallel computations ([#58193]).

#### REPL

#### Test
Expand Down
1 change: 1 addition & 0 deletions src/task.c
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,7 @@ main RNG state collision.
[4]:
https://discourse.julialang.org/t/linear-relationship-between-xoshiro-tasks/110454
*/
// if this code is updated, the julia equivalent should be updated as well in Random.fork()
void jl_rng_split(uint64_t dst[JL_RNG_SIZE], uint64_t src[JL_RNG_SIZE]) JL_NOTSAFEPOINT
{
// load and advance the internal LCG state
Expand Down
2 changes: 2 additions & 0 deletions stdlib/Random/docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ Random.TaskLocalRNG
Random.Xoshiro
Random.MersenneTwister
Random.RandomDevice
Random.fork!
Random.fork
```

## [Hooking into the `Random` API](@id rand-api-hook)
Expand Down
2 changes: 1 addition & 1 deletion stdlib/Random/src/Random.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export rand!, randn!,
randcycle, randcycle!,
AbstractRNG, MersenneTwister, RandomDevice, TaskLocalRNG, Xoshiro

public seed!, default_rng, Sampler, SamplerType, SamplerTrivial, SamplerSimple
public fork, fork!, seed!, default_rng, Sampler, SamplerType, SamplerTrivial, SamplerSimple

## general definitions

Expand Down
66 changes: 66 additions & 0 deletions stdlib/Random/src/Xoshiro.jl
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,69 @@ for FT in (Float16, Float32, Float64)
@eval rand(r::Union{TaskLocalRNG, Xoshiro}, ::SamplerTrivial{CloseOpen01{$(FT)}}) =
_uint2float(rand(r, $(UT)), $(FT))
end


## fork Xoshiro RNGs, in the same way that TaskLocalRNG() is forked upon task spawning
## cf. jl_rng_split in src/task.c

const xoshiro_split_a = (
0x214c146c88e47cb7,
0xa66d8cc21285aafa,
0x68c7ef2d7b1a54d4,
0xb053a7d7aa238c61,
)

const xoshiro_split_m = (
0xaef17502108ef2d9,
0xf34026eeb86766af,
0x38fd70ad58dd9fbb,
0x6677f9b93ab0c04d,
)

function _fork(src::Union{Xoshiro, TaskLocalRNG})
s0, s1, s2, s3, s4 = getstate(src)
x = s4
s4 = x * 0xd1342543de82ef95 + 1

state = map((s0, s1, s2, s3), xoshiro_split_a, xoshiro_split_m) do c, ai, mi
w = x ⊻ ai
c += w * (2*c + 1)
c ⊻= c >> ((c >> 59) + 5)
c *= mi
c ⊻= c >> 43
c
end
setstate!(src, (s0, s1, s2, s3, s4)) # only for s4, other values are unchanged
(state..., s4)
end

"""
Random.fork!(dst::Union{Xoshiro, TaskLocalRNG}, src::Union{Xoshiro, TaskLocalRNG} = TaskLocalRNG()) -> dst

Equivalent to `copy!(dst, fork(src))`.
See also [`fork`](@ref).

!!! compat "Julia 1.13"
This function was introduced in Julia 1.13.
"""
fork!(dst::Union{Xoshiro, TaskLocalRNG}, src::Union{Xoshiro, TaskLocalRNG}=TaskLocalRNG()) =
setstate!(dst, _fork(src))

"""
Random.fork(src::Union{Xoshiro, TaskLocalRNG} = TaskLocalRNG())::Xoshiro

Create a new `Xoshiro` object from `src`, in the same way that the task local RNG of a new
task is created from the task local RNG of the parent task.
This is the recommended way to initialize a fresh RNG from an existing one.

!!! note
When `src` is of type `TaskLocalRNG`, this function is guaranteed to return an RNG of
type `Xoshiro` only as long as `Xoshiro` and `TaskLocalRNG` are implementing the same
underlying generator. It may change in the future.

!!! compat "Julia 1.13"
This function was introduced in Julia 1.13.

See also [`Random.fork!`](@ref).
"""
fork(src::Union{Xoshiro, TaskLocalRNG}=TaskLocalRNG()) = Xoshiro(_fork(src)...)
40 changes: 40 additions & 0 deletions stdlib/Random/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1263,3 +1263,43 @@ end
@testset "Docstrings" begin
@test isempty(Docs.undocumented_names(Random))
end

@testset "fork" begin
xx = copy(TaskLocalRNG())
x1 = Random.fork(xx)
x2 = fetch(@async copy(TaskLocalRNG()))
@test x1 isa Xoshiro && x2 isa Xoshiro
@test x1 == x2 # currently, equality involves all 5 UInt64 words of the state
@test xx == TaskLocalRNG()

x3 = Random.fork(TaskLocalRNG())
@test x3 isa Xoshiro
copy!(TaskLocalRNG(), xx) # reset its state
x4 = Random.fork(xx)
@test x4 isa Xoshiro
@test x3 == x4
copy!(xx, TaskLocalRNG())

@test xx == TaskLocalRNG() # check assumptions
x5 = Random.fork()
@test xx != TaskLocalRNG() # TaskLocalRNG() was forked off
copy!(TaskLocalRNG(), xx)
@test x5 == x4

x6 = Xoshiro(0, 0, 0, 0, 0)
@test x6 === Random.fork!(x6, xx)
copy!(xx, TaskLocalRNG())
@test x6 == x5
@test x6 === Random.fork!(x6, TaskLocalRNG())
copy!(TaskLocalRNG(), xx)
@test x6 == x5
@test xx == TaskLocalRNG() # check assumptions
@test x6 === Random.fork!(x6)
@test xx != TaskLocalRNG()
copy!(TaskLocalRNG(), xx)
@test x6 == x5

@test TaskLocalRNG() === Random.fork!(TaskLocalRNG(), copy(xx))
@test x6 === Random.fork!(x6, copy(xx))
@test TaskLocalRNG() == x6
end