Skip to content

Commit 0e56550

Browse files
committed
ditch fork!, add array version, do not support TaskLocalRNG
Now that seed! accepts an rng as 2nd arg, fork! is replaced by seed!. Array version is useful for `@threads` (cf. docstring). `fork(TaskLocalRNG())` is ambiguous: return a Xoshiro or TaskLocalRNG() ? Leave that question for later. Also, returning Xoshiro was seriously complicating the docstring.
1 parent c088083 commit 0e56550

File tree

6 files changed

+94
-63
lines changed

6 files changed

+94
-63
lines changed

NEWS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Standard library changes
4646

4747
#### Random
4848

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

5252
#### REPL

stdlib/Random/docs/src/index.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ Random.TaskLocalRNG
8383
Random.Xoshiro
8484
Random.MersenneTwister
8585
Random.RandomDevice
86-
Random.fork!
8786
Random.fork
8887
```
8988

stdlib/Random/src/RNGs.jl

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,3 +920,61 @@ function advance!(r::MersenneTwister, adv_jump, adv, adv_vals, idxF, adv_ints, i
920920
_advance_to!(r, adv, work)
921921
r
922922
end
923+
924+
925+
## forking
926+
927+
"""
928+
Random.fork(rng::AbstractRNG, [dims...])::typeof(rng)
929+
930+
Create deterministically a new independent RNG object from an existing one, of the same type.
931+
When `dims` is specified (as integers or a tuple of integers),
932+
an array of such independent RNGs is created.
933+
934+
This is the recommended way to initialize fresh RNG instances when reproducibility is
935+
required, and can be useful in particular in multi-threaded contexts, where race conditions
936+
must be avoided. For example:
937+
```julia
938+
function dotask(rng::Xoshiro)
939+
num_subtasks = 10
940+
rngs = Random.fork(rng, num_subtasks)
941+
result = Vector(undef, num_subtasks)
942+
Threads.@threads for i=1:num_subtasks
943+
result[i] = dosubtask(i, rngs[i])
944+
end
945+
result
946+
end
947+
```
948+
Note that this functions does mutate its `rng` argument, so calling it on the same `rng`
949+
must not be done concurrently from multiple threads.
950+
951+
Currently, a method is only implemented for `Xoshiro`.
952+
953+
!!! note
954+
`Random.fork(::Xoshiro)` uses the same algorithm as when the task-local RNG
955+
of a new task is created from the task-local RNG of the parent task.
956+
957+
!!! compat "Julia 1.13"
958+
This function was introduced in Julia 1.13.
959+
960+
# Examples
961+
```jldoctest
962+
julia> x = Xoshiro(0)
963+
Xoshiro(0xdb2fa90498613fdf, 0x48d73dc42d195740, 0x8c49bc52dc8a77ea, 0x1911b814c02405e8, 0x22a21880af5dc689)
964+
965+
julia> [x, fork(x)] # x is mutated
966+
2-element Vector{Xoshiro}:
967+
Xoshiro(0xdb2fa90498613fdf, 0x48d73dc42d195740, 0x8c49bc52dc8a77ea, 0x1911b814c02405e8, 0x26b589433d8074be)
968+
Xoshiro(0x545f53b997598dfb, 0xac80f92d91bb35a5, 0xb6eb3382e8c409ca, 0xa9aa6968fbdd5e83, 0x26b589433d8074be)
969+
970+
julia> fork(x, 3)
971+
3-element Vector{Xoshiro}:
972+
Xoshiro(0xfc86733bafa6df6d, 0x5ff051ecb5937fcf, 0x935c4e55a82ca686, 0xa57b44768cdb84e9, 0x845f4ebfc53d5497)
973+
Xoshiro(0x9c49327a59542654, 0x7c2f821b7716e6b7, 0x586a3fe58fed92f7, 0x28bbf526c1aca281, 0x425e2dc6f55934e4)
974+
Xoshiro(0xd5cbe598083243e0, 0xfba09a94aaf998af, 0xa674c207f3796c54, 0x084d4986ad49c4eb, 0x52a722b1a914a4b5)
975+
```
976+
"""
977+
fork
978+
979+
fork(rng::AbstractRNG, dims::Dims) = typeof(rng)[fork(rng) for _=LinearIndices(dims)]
980+
fork(rng::AbstractRNG, d1::Integer, dims::Integer...) = fork(rng, Dims((d1, dims...)))

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 fork, fork!, seed!, default_rng, Sampler, SamplerType, SamplerTrivial, SamplerSimple
32+
public fork, seed!, default_rng, Sampler, SamplerType, SamplerTrivial, SamplerSimple
3333

3434
## general definitions
3535

stdlib/Random/src/Xoshiro.jl

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,9 @@ hash(x::Union{TaskLocalRNG, Xoshiro}, h::UInt) = hash(getstate(x), h + 0x49a62c2
246246
seed!(rng::Union{TaskLocalRNG, Xoshiro}, seeder::AbstractRNG) =
247247
initstate!(rng, rand(seeder, NTuple{4, UInt64}))
248248

249+
# when seeder is a Xoshiro, use forking instead of regular seeding, to avoid correlations
250+
seed!(rng::Union{TaskLocalRNG, Xoshiro}, seeder::Union{TaskLocalRNG, Xoshiro}) =
251+
setstate!(rng, _fork(seeder))
249252

250253
@inline function rand(x::Union{TaskLocalRNG, Xoshiro}, ::SamplerType{UInt64})
251254
s0, s1, s2, s3 = getstate(x)
@@ -332,33 +335,4 @@ function _fork(src::Union{Xoshiro, TaskLocalRNG})
332335
(state..., s4)
333336
end
334337

335-
"""
336-
Random.fork!(dst::Union{Xoshiro, TaskLocalRNG}, src::Union{Xoshiro, TaskLocalRNG} = TaskLocalRNG()) -> dst
337-
338-
Equivalent to `copy!(dst, fork(src))`.
339-
See also [`fork`](@ref).
340-
341-
!!! compat "Julia 1.13"
342-
This function was introduced in Julia 1.13.
343-
"""
344-
fork!(dst::Union{Xoshiro, TaskLocalRNG}, src::Union{Xoshiro, TaskLocalRNG}=TaskLocalRNG()) =
345-
setstate!(dst, _fork(src))
346-
347-
"""
348-
Random.fork(src::Union{Xoshiro, TaskLocalRNG} = TaskLocalRNG())::Xoshiro
349-
350-
Create a new `Xoshiro` object from `src`, in the same way that the task local RNG of a new
351-
task is created from the task local RNG of the parent task.
352-
This is the recommended way to initialize a fresh RNG from an existing one.
353-
354-
!!! note
355-
When `src` is of type `TaskLocalRNG`, this function is guaranteed to return an RNG of
356-
type `Xoshiro` only as long as `Xoshiro` and `TaskLocalRNG` are implementing the same
357-
underlying generator. It may change in the future.
358-
359-
!!! compat "Julia 1.13"
360-
This function was introduced in Julia 1.13.
361-
362-
See also [`Random.fork!`](@ref).
363-
"""
364-
fork(src::Union{Xoshiro, TaskLocalRNG}=TaskLocalRNG()) = Xoshiro(_fork(src)...)
338+
fork(src::Xoshiro) = Xoshiro(_fork(src)...)

stdlib/Random/test/runtests.jl

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,40 +1302,40 @@ end
13021302

13031303
@testset "fork" begin
13041304
xx = copy(TaskLocalRNG())
1305+
x0 = copy(xx)
13051306
x1 = Random.fork(xx)
13061307
x2 = fetch(@async copy(TaskLocalRNG()))
13071308
@test x1 isa Xoshiro && x2 isa Xoshiro
13081309
@test x1 == x2 # currently, equality involves all 5 UInt64 words of the state
13091310
@test xx == TaskLocalRNG()
13101311

1311-
x3 = Random.fork(TaskLocalRNG())
1312-
@test x3 isa Xoshiro
1313-
copy!(TaskLocalRNG(), xx) # reset its state
1314-
x4 = Random.fork(xx)
1315-
@test x4 isa Xoshiro
1316-
@test x3 == x4
1317-
copy!(xx, TaskLocalRNG())
1318-
1319-
@test xx == TaskLocalRNG() # check assumptions
1320-
x5 = Random.fork()
1321-
@test xx != TaskLocalRNG() # TaskLocalRNG() was forked off
1322-
copy!(TaskLocalRNG(), xx)
1323-
@test x5 == x4
1324-
1325-
x6 = Xoshiro(0, 0, 0, 0, 0)
1326-
@test x6 === Random.fork!(x6, xx)
1327-
copy!(xx, TaskLocalRNG())
1328-
@test x6 == x5
1329-
@test x6 === Random.fork!(x6, TaskLocalRNG())
1330-
copy!(TaskLocalRNG(), xx)
1331-
@test x6 == x5
1332-
@test xx == TaskLocalRNG() # check assumptions
1333-
@test x6 === Random.fork!(x6)
1334-
@test xx != TaskLocalRNG()
1335-
copy!(TaskLocalRNG(), xx)
1336-
@test x6 == x5
1337-
1338-
@test TaskLocalRNG() === Random.fork!(TaskLocalRNG(), copy(xx))
1339-
@test x6 === Random.fork!(x6, copy(xx))
1340-
@test TaskLocalRNG() == x6
1312+
# fork is deterministic
1313+
copy!(xx, x0)
1314+
x3 = Random.fork(xx)
1315+
@test x3 == x2
1316+
@test rand(x3, UInt128) == rand(x2, UInt128)
1317+
1318+
# seed! uses the same mechanism
1319+
copy!(xx, x0)
1320+
x4 = Xoshiro(0, 0, 0, 0, 0)
1321+
@test x4 === Random.seed!(x4, xx)
1322+
@test x4 == x1
1323+
copy!(TaskLocalRNG(), x0)
1324+
x5 = Xoshiro(0, 0, 0, 0, 0)
1325+
@test x5 === Random.seed!(x5, TaskLocalRNG())
1326+
@test x5 == x1
1327+
@test xx == TaskLocalRNG() # both are in the same state after being forked off
1328+
1329+
@test TaskLocalRNG() == Random.seed!(TaskLocalRNG(), copy!(xx, x0))
1330+
@test TaskLocalRNG() == x4
1331+
copy!(TaskLocalRNG(), x0)
1332+
@test TaskLocalRNG() == Random.seed!(TaskLocalRNG(), TaskLocalRNG())
1333+
1334+
# self-seeding
1335+
copy!(xx, x0)
1336+
copy!(TaskLocalRNG(), x0)
1337+
@test xx === Random.seed!(xx, xx)
1338+
@test TaskLocalRNG() === Random.seed!(TaskLocalRNG(), TaskLocalRNG())
1339+
@test xx == TaskLocalRNG()
1340+
@test xx == x1
13411341
end

0 commit comments

Comments
 (0)