Multithreaded random: different behaviour in Julia >= 1.5 (not just different stream)

Hi, I just realised that my solution below for multithreaded random number generators works only in Julia >= 1.5. In particular the test of “CASE A” fails, i.e. the two repetitions issue the same number.
Is there a way to get the same framework working on Julia <= 1.4 ?

using Random, Test, Future, Statistics

FIXEDRNG = MersenneTwister(123)

function generateParallelRngs(rng::AbstractRNG, n::Integer)
    step = rand(rng,big(10)^20:big(10)^40) # making the step random too !
    rngs = Vector{Union{MersenneTwister,Random._GLOBAL_RNG}}(undef, n) # Vector{typeof(rng)}(undef, n)
    rngs[1] = copy(rng)
    for i = 2:n
        rngs[i] = Future.randjump(rngs[i-1], step)
    end
    return rngs
end


# ==================================
# New test
println("** Testing generateParallelRngs()...")
x = rand(100)

function innerFunction(bootstrappedx; rng=Random.GLOBAL_RNG)
     sum(bootstrappedx .* rand(rng) ./ 0.5)
end
function outerFunction(x;rng = Random.GLOBAL_RNG)
    rngs = generateParallelRngs(rng,Threads.nthreads()) # make new copy instances
    results = Array{Float64,1}(undef,30)
    Threads.@threads for i in 1:30
        tsrng = rngs[Threads.threadid()] # Thread safe random number generator
        toSample = rand(tsrng, 1:100,100)
        bootstrappedx = x[toSample]
        innerResult = innerFunction(bootstrappedx, rng=tsrng)
        results[i] = innerResult
    end
    overallResult = mean(results)
    return overallResult
end

# Case A - Different sequences..
@test outerFunction(x) != outerFunction(x)

# Case B - Different values, but same sequence..
mainRng = copy(FIXEDRNG)
a = outerFunction(x, rng=mainRng)
b = outerFunction(x, rng=mainRng)

mainRng = copy(FIXEDRNG)
A = outerFunction(x, rng=mainRng)
B = outerFunction(x, rng=mainRng)

@test a != b && a == A && b == B


# Case C - Same value at each call
a = outerFunction(x,rng=copy(FIXEDRNG))
b = outerFunction(x,rng=copy(FIXEDRNG))
@test a == b

Just as a note, the default RNG is now thread safe so there is no need to roll your own. You can just use rand() from different threads.

2 Likes

Thank you, but how can I obtain then the three behaviours I need: (1) completely random; (2) script fixed random (each time I run the script/sequence I obtain the same sequence of results) and (3) completely fixed: each time I call the function I obtain the same result.

I pass the functions a seed instead of the RNG ?

A faster, more readable, working with many RNG and Julia versions:

function generateParallelRngs1(rng::AbstractRNG, n::Integer)
    seeds = [rand(rng,1:18446744073709551615) for i in 1:n]
    rngs  = [copy(rng) for i in 1:n]
    return Random.seed!.(rngs,seeds)
end
3 Likes

https://github.com/JuliaRandom/StableRNGs.jl

1 Like

Yes, my second version works with StableRNGs.

To be clear, you obtain the “fixed” results also with MersenneTwister(123) (or with Random.seed!(123)). The only difference (performances apart) is that StableRNGs streams are guaranteed to remain invariant, while default RNG outputs could change even between minor Julia versions, so you make a certain test with a Julia vetsion and it’s fine, but then it may breack on newer versions just because the random stream has changed. This make StableRNGs perfect for your package tests where stochasticiity is involved.

FYI, How to do X in parallel? · FLoops discusses a couple of approaches for parallel RNG use cases, including the ones that can reproduce RNG outputs.