Skip to content

Commit

Permalink
define Random.GLOBAL_RNG = TaskLocalRNG() (#51388)
Browse files Browse the repository at this point in the history
`GLOBAL_RNG` is a relic from pre-v1.3 when our global RNG was not
thread-safe:
```
const GLOBAL_RNG = MersenneTwister()
```

`GLOBAL_RNG` was never really specified, but was referred to in
docstrings (of `rand` etc.), and people had to use it in packages in
order to provide a default RNG to their functions. So we have to keep
defining `Random.GLOBAL_RNG` for the time being.

In v1.3, we didn't have anymore a unique global RNG, but instead one per
thread; in order to keep code using `GLOBAL_RNG` working, we got this
new definition as a clever hack:
```
struct _GLOBAL_RNG <: AbstractRNG end
const GLOBAL_RNG = _GLOBAL_RNG()
```
I.e. `GLOBAL_RNG` is just a "handle", which refers to the actual
threaded-RNG when passed to `rand` functions. This entails defining most
functions taking a `default_rng()` to also accept `_GLOBAL_RNG`. See
#41123, and #41235 which is not even merged.

But since v1.7, we have `Random.default_rng()` (our now official way to
refer to the default RNG) returning `TaskLocalRNG()`, which is itself a
"handle" (singleton). So we can as well redefine `GLOBAL_RNG` to be
`TaskLocalRNG()` instead of `_GLOBAL_RNG()`, with the advantage that all
the relevant methods are already defined for `TaskLocalRNG`.

The only expected difference in behavior is `seed!(GLOBAL_RNG, seed)`,
which previously would update `Random.GLOBAL_SEED` (a hack used by
`@testset`), but now is equivalent to `seed!(TaskLocalRNG(), seed)`,
which doesn't update this global seed. This precise behavior was never
documented (`GLOBAL_SEED` is purely internal), so this should be fine.
  • Loading branch information
rfourquet committed Sep 29, 2023
1 parent 29f2b2f commit b74daf5
Show file tree
Hide file tree
Showing 6 changed files with 40 additions and 86 deletions.
60 changes: 18 additions & 42 deletions stdlib/Random/src/RNGs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -323,13 +323,6 @@ seed!(r::MersenneTwister, n::Integer) = seed!(r, make_seed(n))

### Global RNG

struct _GLOBAL_RNG <: AbstractRNG
global const GLOBAL_RNG = _GLOBAL_RNG.instance
end

# GLOBAL_RNG currently uses TaskLocalRNG
typeof_rng(::_GLOBAL_RNG) = TaskLocalRNG

"""
Random.default_rng() -> rng
Expand All @@ -354,48 +347,31 @@ e.g. `rand(default_rng())`.
@inline default_rng() = TaskLocalRNG()
@inline default_rng(tid::Int) = TaskLocalRNG()

copy!(dst::Xoshiro, ::_GLOBAL_RNG) = copy!(dst, default_rng())
copy!(::_GLOBAL_RNG, src::Xoshiro) = copy!(default_rng(), src)
copy(::_GLOBAL_RNG) = copy(default_rng())

# defined only for backward compatibility with pre-v1.3 code when `default_rng()` didn't exist;
# `GLOBAL_RNG` was never really documented, but was appearing in the docstring of `rand`
const GLOBAL_RNG = default_rng()

# In v1.0, the GLOBAL_RNG was storing the seed which was used to initialize it; this seed was used to implement
# the following feature of `@testset`:
# > Before the execution of the body of a `@testset`, there is an implicit
# > call to `Random.seed!(seed)` where `seed` is the current seed of the global RNG.
# But the global RNG is now TaskLocalRNG() and doesn't store its seed; in order to not break `@testset`, we now
# store the seed used in a call like `seed!(seed)` *without* an explicit RNG in `GLOBAL_SEED`; the wording of the
# feature above was sufficiently unprecise (e.g. what exactly is the "global RNG"?) that this solution seems fine
GLOBAL_SEED = 0
# only the Test module is allowed to use this function!
set_global_seed!(seed) = global GLOBAL_SEED = seed

function seed!(::_GLOBAL_RNG, seed=rand(RandomDevice(), UInt64, 4))
global GLOBAL_SEED = seed
# seed the "global" RNG
function seed!(seed=nothing)
# the seed is not left as `nothing`, as storing `nothing` as the global seed wouldn't lead to reproducible streams
seed = @something seed rand(RandomDevice(), UInt128)
set_global_seed!(seed)
seed!(default_rng(), seed)
end

seed!(rng::_GLOBAL_RNG, ::Nothing) = seed!(rng) # to resolve ambiguity

seed!(seed::Union{Nothing,Integer,Vector{UInt32},Vector{UInt64}}=nothing) =
seed!(GLOBAL_RNG, seed)

rng_native_52(::_GLOBAL_RNG) = rng_native_52(default_rng())
rand(::_GLOBAL_RNG, sp::SamplerBoolBitInteger) = rand(default_rng(), sp)
for T in (:(SamplerTrivial{UInt52Raw{UInt64}}),
:(SamplerTrivial{UInt2x52Raw{UInt128}}),
:(SamplerTrivial{UInt104Raw{UInt128}}),
:(SamplerTrivial{CloseOpen01_64}),
:(SamplerTrivial{CloseOpen12_64}),
:(SamplerUnion(Int64, UInt64, Int128, UInt128)),
:(SamplerUnion(Bool, Int8, UInt8, Int16, UInt16, Int32, UInt32)),
)
@eval rand(::_GLOBAL_RNG, x::$T) = rand(default_rng(), x)
end

rand!(::_GLOBAL_RNG, A::AbstractArray{Float64}, I::SamplerTrivial{<:FloatInterval_64}) = rand!(default_rng(), A, I)
rand!(::_GLOBAL_RNG, A::Array{Float64}, I::SamplerTrivial{<:FloatInterval_64}) = rand!(default_rng(), A, I)
for T in (Float16, Float32)
@eval rand!(::_GLOBAL_RNG, A::Array{$T}, I::SamplerTrivial{CloseOpen12{$T}}) = rand!(default_rng(), A, I)
@eval rand!(::_GLOBAL_RNG, A::Array{$T}, I::SamplerTrivial{CloseOpen01{$T}}) = rand!(default_rng(), A, I)
end
for T in BitInteger_types
@eval rand!(::_GLOBAL_RNG, A::Array{$T}, I::SamplerType{$T}) = rand!(default_rng(), A, I)
end

function __init__()
seed!(GLOBAL_RNG)
seed!()
ccall(:jl_gc_init_finalizer_rng_state, Cvoid, ())
end

Expand Down
6 changes: 2 additions & 4 deletions stdlib/Random/src/Random.jl
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,9 @@ the amount of precomputation, if applicable.
*types* and *values*, respectively. [`Random.SamplerSimple`](@ref) can be used to store
pre-computed values without defining extra types for only this purpose.
"""
Sampler(rng::AbstractRNG, x, r::Repetition=Val(Inf)) = Sampler(typeof_rng(rng), x, r)
Sampler(rng::AbstractRNG, x, r::Repetition=Val(Inf)) = Sampler(typeof(rng), x, r)
Sampler(rng::AbstractRNG, ::Type{X}, r::Repetition=Val(Inf)) where {X} =
Sampler(typeof_rng(rng), X, r)

typeof_rng(rng::AbstractRNG) = typeof(rng)
Sampler(typeof(rng), X, r)

# this method is necessary to prevent rand(rng::AbstractRNG, X) from
# recursively constructing nested Sampler types.
Expand Down
32 changes: 7 additions & 25 deletions stdlib/Random/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -711,31 +711,13 @@ end
Random.seed!(seed)
@test Random.GLOBAL_SEED === seed
end
# two separate loops as otherwise we are no sure that the second call (with GLOBAL_RNG)
# actually sets GLOBAL_SEED
for seed=seeds
Random.seed!(Random.GLOBAL_RNG, seed)
@test Random.GLOBAL_SEED === seed
end

Random.seed!(nothing)
seed1 = Random.GLOBAL_SEED
@test seed1 isa Vector{UInt64} # could change, but must not be nothing

Random.seed!(Random.GLOBAL_RNG, nothing)
seed2 = Random.GLOBAL_SEED
@test seed2 isa Vector{UInt64}
@test seed2 != seed1

Random.seed!()
seed3 = Random.GLOBAL_SEED
@test seed3 isa Vector{UInt64}
@test seed3 != seed2

Random.seed!(Random.GLOBAL_RNG)
seed4 = Random.GLOBAL_SEED
@test seed4 isa Vector{UInt64}
@test seed4 != seed3
for ii = 1:8
iseven(ii) ? Random.seed!(nothing) : Random.seed!()
push!(seeds, Random.GLOBAL_SEED)
@test Random.GLOBAL_SEED isa UInt128 # could change, but must not be nothing
end
@test allunique(seeds)
end

struct RandomStruct23964 end
Expand Down Expand Up @@ -800,9 +782,9 @@ end
end

@testset "GLOBAL_RNG" begin
@test VERSION < v"2" # deprecate this in v2 (GLOBAL_RNG must go)
local GLOBAL_RNG = Random.GLOBAL_RNG
local LOCAL_RNG = Random.default_rng()
@test VERSION < v"2" # deprecate this in v2

@test Random.seed!(GLOBAL_RNG, nothing) === LOCAL_RNG
@test Random.seed!(GLOBAL_RNG, UInt32[0]) === LOCAL_RNG
Expand Down
16 changes: 7 additions & 9 deletions stdlib/Test/src/Test.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1589,11 +1589,10 @@ function testset_beginend_call(args, tests, source)
# we reproduce the logic of guardseed, but this function
# cannot be used as it changes slightly the semantic of @testset,
# by wrapping the body in a function
local RNG = default_rng()
local oldrng = copy(RNG)
local oldrng = copy(default_rng())
local oldseed = Random.GLOBAL_SEED
try
# RNG is re-seeded with its own seed to ease reproduce a failed test
# default RNG is re-seeded with its own seed to ease reproduce a failed test
Random.seed!(Random.GLOBAL_SEED)
let
$(esc(tests))
Expand All @@ -1609,7 +1608,7 @@ function testset_beginend_call(args, tests, source)
record(ts, Error(:nontest_error, Expr(:tuple), err, Base.current_exceptions(), $(QuoteNode(source))))
end
finally
copy!(RNG, oldrng)
copy!(default_rng(), oldrng)
Random.set_global_seed!(oldseed)
pop_testset()
ret = finish(ts)
Expand Down Expand Up @@ -1677,7 +1676,7 @@ function testset_forloop(args, testloop, source)
finish_errored = false

# it's 1000 times faster to copy from tmprng rather than calling Random.seed!
copy!(RNG, tmprng)
copy!(default_rng(), tmprng)

end
ts = if ($testsettype === $DefaultTestSet) && $(isa(source, LineNumberNode))
Expand All @@ -1704,11 +1703,10 @@ function testset_forloop(args, testloop, source)
local first_iteration = true
local ts
local finish_errored = false
local RNG = default_rng()
local oldrng = copy(RNG)
local oldrng = copy(default_rng())
local oldseed = Random.GLOBAL_SEED
Random.seed!(Random.GLOBAL_SEED)
local tmprng = copy(RNG)
local tmprng = copy(default_rng())
try
let
$(Expr(:for, Expr(:block, [esc(v) for v in loopvars]...), blk))
Expand All @@ -1719,7 +1717,7 @@ function testset_forloop(args, testloop, source)
pop_testset()
push!(arr, finish(ts))
end
copy!(RNG, oldrng)
copy!(default_rng(), oldrng)
Random.set_global_seed!(oldseed)
end
arr
Expand Down
8 changes: 4 additions & 4 deletions stdlib/UUIDs/src/UUIDs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ Generates a version 1 (time-based) universally unique identifier (UUID), as spec
by RFC 4122. Note that the Node ID is randomly generated (does not identify the host)
according to section 4.5 of the RFC.
The default rng used by `uuid1` is not `GLOBAL_RNG` and every invocation of `uuid1()` without
The default rng used by `uuid1` is not `Random.default_rng()` and every invocation of `uuid1()` without
an argument should be expected to return a unique identifier. Importantly, the outputs of
`uuid1` do not repeat even when `Random.seed!(seed)` is called. Currently (as of Julia 1.6),
`uuid1` uses `Random.RandomDevice` as the default rng. However, this is an implementation
detail that may change in the future.
!!! compat "Julia 1.6"
The output of `uuid1` does not depend on `GLOBAL_RNG` as of Julia 1.6.
The output of `uuid1` does not depend on `Random.default_rng()` as of Julia 1.6.
# Examples
```jldoctest; filter = r"[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}"
Expand Down Expand Up @@ -90,14 +90,14 @@ end
Generates a version 4 (random or pseudo-random) universally unique identifier (UUID),
as specified by RFC 4122.
The default rng used by `uuid4` is not `GLOBAL_RNG` and every invocation of `uuid4()` without
The default rng used by `uuid4` is not `Random.default_rng()` and every invocation of `uuid4()` without
an argument should be expected to return a unique identifier. Importantly, the outputs of
`uuid4` do not repeat even when `Random.seed!(seed)` is called. Currently (as of Julia 1.6),
`uuid4` uses `Random.RandomDevice` as the default rng. However, this is an implementation
detail that may change in the future.
!!! compat "Julia 1.6"
The output of `uuid4` does not depend on `GLOBAL_RNG` as of Julia 1.6.
The output of `uuid4` does not depend on `Random.default_rng()` as of Julia 1.6.
# Examples
```jldoctest
Expand Down
4 changes: 2 additions & 2 deletions stdlib/UUIDs/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ for (init_uuid, next_uuid) in standard_namespace_uuids
end

# Issue 35860
Random.seed!(Random.GLOBAL_RNG, 10)
Random.seed!(Random.default_rng(), 10)
u1 = uuid1()
u4 = uuid4()
Random.seed!(Random.GLOBAL_RNG, 10)
Random.seed!(Random.default_rng(), 10)
@test u1 != uuid1()
@test u4 != uuid4()

Expand Down

0 comments on commit b74daf5

Please sign in to comment.