Skip to content

Commit 60d7bdb

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 edceb0b commit 60d7bdb

File tree

6 files changed

+111
-63
lines changed

6 files changed

+111
-63
lines changed

NEWS.md

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

5050
#### Random
5151

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

5555
#### 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
@@ -306,3 +306,61 @@ function hash_seed(str::AbstractString, ctx::SHA_CTX)
306306
end
307307
SHA.update!(ctx, (0x05,))
308308
end
309+
310+
311+
## forking
312+
313+
"""
314+
Random.fork(rng::AbstractRNG, [dims...])::typeof(rng)
315+
316+
Create deterministically a new independent RNG object from an existing one, of the same type.
317+
When `dims` is specified (as integers or a tuple of integers),
318+
an array of such independent RNGs is created.
319+
320+
This is the recommended way to initialize fresh RNG instances when reproducibility might be
321+
required, and can be useful in particular in multi-threaded contexts, where race conditions
322+
must be avoided. For example:
323+
```julia
324+
function dotask(rng::Xoshiro)
325+
num_subtasks = 10
326+
rngs = Random.fork(rng, num_subtasks)
327+
result = Vector(undef, num_subtasks)
328+
Threads.@threads for i=1:num_subtasks
329+
result[i] = dosubtask(i, rngs[i])
330+
end
331+
result
332+
end
333+
```
334+
Note that this functions generally mutates its `rng` argument, so calling it on the same `rng`
335+
must not be done concurrently from multiple threads.
336+
337+
Currently, a method is only implemented for `Xoshiro`.
338+
339+
!!! note
340+
`Random.fork(::Xoshiro)` uses the same algorithm as when the task-local RNG
341+
of a new task is created from the task-local RNG of the parent task.
342+
343+
!!! compat "Julia 1.13"
344+
This function was introduced in Julia 1.13.
345+
346+
# Examples
347+
```jldoctest
348+
julia> x = Xoshiro(0)
349+
Xoshiro(0xdb2fa90498613fdf, 0x48d73dc42d195740, 0x8c49bc52dc8a77ea, 0x1911b814c02405e8, 0x22a21880af5dc689)
350+
351+
julia> [x, fork(x)] # x is mutated
352+
2-element Vector{Xoshiro}:
353+
Xoshiro(0xdb2fa90498613fdf, 0x48d73dc42d195740, 0x8c49bc52dc8a77ea, 0x1911b814c02405e8, 0x26b589433d8074be)
354+
Xoshiro(0x545f53b997598dfb, 0xac80f92d91bb35a5, 0xb6eb3382e8c409ca, 0xa9aa6968fbdd5e83, 0x26b589433d8074be)
355+
356+
julia> fork(x, 3)
357+
3-element Vector{Xoshiro}:
358+
Xoshiro(0xfc86733bafa6df6d, 0x5ff051ecb5937fcf, 0x935c4e55a82ca686, 0xa57b44768cdb84e9, 0x845f4ebfc53d5497)
359+
Xoshiro(0x9c49327a59542654, 0x7c2f821b7716e6b7, 0x586a3fe58fed92f7, 0x28bbf526c1aca281, 0x425e2dc6f55934e4)
360+
Xoshiro(0xd5cbe598083243e0, 0xfba09a94aaf998af, 0xa674c207f3796c54, 0x084d4986ad49c4eb, 0x52a722b1a914a4b5)
361+
```
362+
"""
363+
fork
364+
365+
fork(rng::AbstractRNG, dims::Dims) = typeof(rng)[fork(rng) for _=LinearIndices(dims)]
366+
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: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,40 +1302,57 @@ 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
1341+
1342+
# arrays
1343+
y2 = fork(xx, 2)
1344+
@test y2 isa Vector{Xoshiro} && size(y2) == (2,)
1345+
y34 = fork(xx, 3, 0x4)
1346+
@test y34 isa Matrix{Xoshiro} && size(y34) == (3, 4)
1347+
y123 = fork(xx, (1, 2, 3))
1348+
@test y123 isa Array{Xoshiro, 3} && size(y123) == (1, 2, 3)
1349+
y0 = fork(xx, ())
1350+
@test y0 isa Array{Xoshiro, 0} && size(y0) == ()
1351+
@test allunique([y2..., y34..., y123..., y0...])
1352+
1353+
# fork is currently unsupported for other RNGs
1354+
for rng = (RandomDevice(), MersenneTwister(0), TaskLocalRNG(), SeedHasher(0))
1355+
@test_throws MethodError fork(rng)
1356+
@test_throws MethodError fork(rng, (2, 3))
1357+
end
13411358
end

0 commit comments

Comments
 (0)