#multithreading #for-loop #random #julia
Вопрос:
Я хочу генерировать случайные числа в Julia, используя многопоточность. Я использую Threads.@threads
макрос для этого. Тем не менее, я изо всех сил пытаюсь зафиксировать количество семян, чтобы получать один и тот же результат каждый раз, когда я запускаю код. Вот мое испытание:
Random.seed!(1234)
a = [Float64[] for _ in 1:10]
Threads.@threads for i = 1:10
push!(a[Threads.threadid()],rand())
end
sum(reduce(vcat, a))
Приведенный выше сценарий дает разные результаты каждый раз, когда я его запускаю. Напротив, я получаю те же результаты, если использую простой цикл for:
Random.seed!(12445)
b = []
for i = 1:10
push!(b,rand())
end
sum(b)
У меня сложилось впечатление, что решение этого вопроса должно быть легким. И все же я не мог его найти. Мы очень ценим любую помощь.
Спасибо.
Ответ №1:
Вам нужно сгенерировать отдельный случайный поток для каждого потока. Самый простой способ-иметь генератор случайных чисел с другим начальным значением:
using Random
rngs = [MersenneTwister(i) for i in 1: Threads.nthreads()];
Threads.@threads for i = 1:10
val = rand(rngs[Threads.threadid()])
# do something with val
end
Если вы не хотите рисковать корреляцией для разных семян случайных чисел, вы можете фактически обойти один генератор чисел:
julia> rngs2 = Future.randjump.(Ref(MersenneTwister(0)), big(10)^20 .* (1:Threads.nthreads()))
4-element Vector{MersenneTwister}:
MersenneTwister(0, (200000000000000000000, 0))
MersenneTwister(0, (400000000000000000000, 0))
MersenneTwister(0, (600000000000000000000, 0))
MersenneTwister(0, (800000000000000000000, 0))
Комментарии:
1. Спасибо! Симпатичный. Я выбираю комментарий Богумила, потому что он несколько проще, но ваше решение не менее велико.
Ответ №2:
Ciao Fabrizio. В BetaML я решил эту проблему с помощью:
"""
generateParallelRngs(rng::AbstractRNG, n::Integer;reSeed=false)
For multi-threaded models, return n independent random number generators (one per thread) to be used in threaded computations.
Note that each ring is a _copy_ of the original random ring. This means that code that _use_ these RNGs will not change the original RNG state.
Use it with `rngs = generateParallelRngs(rng,Threads.nthreads())` to have a separate rng per thread.
By default the function doesn't re-seed the RNG, as you may want to have a loop index based re-seeding strategy rather than a threadid-based one (to guarantee the same result independently of the number of threads).
If you prefer, you can instead re-seed the RNG here (using the parameter `reSeed=true`), such that each thread has a different seed. Be aware however that the stream of number generated will depend from the number of threads at run time.
"""
function generateParallelRngs(rng::AbstractRNG, n::Integer;reSeed=false)
if reSeed
seeds = [rand(rng,100:18446744073709551615) for i in 1:n] # some RNGs have issues with too small seed
rngs = [deepcopy(rng) for i in 1:n]
return Random.seed!.(rngs,seeds)
else
return [deepcopy(rng) for i in 1:n]
end
end
Приведенная выше функция дает те же результаты, также независимо от количества потоков, используемых в Julia, и затем может быть использована, например, как здесь:
using Test
TESTRNG = MersenneTwister(123)
println("** Testing generateParallelRngs()...")
x = rand(copy(TESTRNG),100)
function innerFunction(bootstrappedx; rng=Random.GLOBAL_RNG)
sum(bootstrappedx .* rand(rng) ./ 0.5)
end
function outerFunction(x;rng = Random.GLOBAL_RNG)
masterSeed = rand(rng,100:9999999999999) # important: with some RNG it is important to do this before the generateParallelRngs to guarantee independance from number of threads
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: one RNG per thread
Random.seed!(tsrng,masterSeed i*10) # But the seeding depends on the i of the loop not the thread: we get same results indipendently of the number of threads
toSample = rand(tsrng, 1:100,100)
bootstrappedx = x[toSample]
innerResult = innerFunction(bootstrappedx, rng=tsrng)
results[i] = innerResult
end
overallResult = mean(results)
return overallResult
end
# Different sequences..
@test outerFunction(x) != outerFunction(x)
# Different values, but same sequence
mainRng = copy(TESTRNG)
a = outerFunction(x, rng=mainRng)
b = outerFunction(x, rng=mainRng)
mainRng = copy(TESTRNG)
A = outerFunction(x, rng=mainRng)
B = outerFunction(x, rng=mainRng)
@test a != b amp;amp; a == A amp;amp; b == B
# Same value at each call
a = outerFunction(x,rng=copy(TESTRNG))
b = outerFunction(x,rng=copy(TESTRNG))
@test a == b
Комментарии:
1. Спасибо! Симпатичный. Я выбираю комментарий Богумила, потому что он несколько проще, но ваше решение не менее велико.
Ответ №3:
Предполагая, что вы находитесь на Julia 1.6, вы можете сделать, например, следующее:
julia> using Random
julia> foreach(i -> Random.seed!(Random.default_rng(i), i), 1:Threads.nthreads())
Дело в том, что в настоящее время у Джулии уже есть отдельный генератор случайных чисел для каждого потока, поэтому вам не нужно создавать свой собственный (конечно, вы могли бы сделать это, как в других ответах, но вам это не нужно).
Также обратите внимание, что в будущих версиях Джулии:
Threads.@threads for i = 1:10
push!(a[Threads.threadid()],rand())
end
часть не гарантирует получения воспроизводимых результатов. В Julia 1.6 Threads.@threads
используется статическое планирование, но, как вы можете прочитать в его строке документа, оно может быть изменено.