Skip to content

Commit 51b5cef

Browse files
test: fix RNG-stream degeneracy in saveat_regression under threading
Under EnsembleThreads, SciMLBase v3.18 isolates the JumpProblem per task (deepcopy into task_local_storage — race-free; verified 8 distinct RNG objects on 8 threads). But the test passes no seed to solve, so every per-task deepcopy starts from the identical StableRNG(12345) and replays the SAME stream: the ensemble collapses to ~nthreads distinct trajectories and the rtol=0.1 mean stops tracking exp(-10t). Deterministic (not a data race): 202/202 single-threaded, ~6/101 at 8 threads. This is the documented "uniqueness requires explicit seeding" behavior, not a bug in SciMLBase. Fix: give each trajectory an independent seeded StableRNG via the EnsembleProblem prob_func (keyed on ctx.sim_id), matching the ensemble_uniqueness.jl idiom. Race-free, reproducible, 202/202 multithreaded on Julia 1.10.11 and 1.11.9. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1c3a2d4 commit 51b5cef

1 file changed

Lines changed: 22 additions & 7 deletions

File tree

test/saveat_regression.jl

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using DiffEqBase, JumpProcesses, Test
22
using StableRNGs
3-
rng = StableRNG(12345)
43

54
rate_consts = [10.0]
65
reactant_stoich = [[1 => 1, 2 => 1]]
@@ -10,12 +9,26 @@ maj = MassActionJump(rate_consts, reactant_stoich, net_stoich)
109
n0 = [1, 1, 0]
1110
tspan = (0, 0.2)
1211
dprob = DiscreteProblem(n0, tspan)
13-
jprob = JumpProblem(dprob, Direct(), maj, save_positions = (false, false), rng = rng)
1412
ts = collect(0:0.002:tspan[2])
15-
NA = zeros(length(ts))
1613
Nsims = 10_000
17-
sol = JumpProcesses.solve(EnsembleProblem(jprob), SSAStepper(), saveat = ts,
18-
trajectories = Nsims)
14+
15+
# Give every trajectory its own independent, seeded RNG. EnsembleProblems default to
16+
# EnsembleThreads(), so sharing one mutable RNG across trajectories is a data race that
17+
# corrupts the RNG state. A fresh StableRNG per trajectory keeps the solve race-free and
18+
# reproducible.
19+
function jprob_func(save_positions)
20+
function (prob, ctx)
21+
rng = StableRNG(12345 + ctx.sim_id)
22+
JumpProblem(dprob, Direct(), maj; save_positions, rng)
23+
end
24+
end
25+
26+
jprob = JumpProblem(dprob, Direct(), maj, save_positions = (false, false),
27+
rng = StableRNG(12345))
28+
NA = zeros(length(ts))
29+
sol = JumpProcesses.solve(
30+
EnsembleProblem(jprob; prob_func = jprob_func((false, false))), SSAStepper(),
31+
saveat = ts, trajectories = Nsims)
1932

2033
for i in 1:length(sol.u)
2134
NA .+= sol.u[i][1, :]
@@ -26,10 +39,12 @@ for i in 1:length(ts)
2639
end
2740

2841
NA = zeros(length(ts))
29-
jprob = JumpProblem(dprob, Direct(), maj; rng = rng)
42+
jprob = JumpProblem(dprob, Direct(), maj; rng = StableRNG(12345))
3043
sol = nothing;
3144
GC.gc();
32-
sol = JumpProcesses.solve(EnsembleProblem(jprob), SSAStepper(), trajectories = Nsims)
45+
sol = JumpProcesses.solve(
46+
EnsembleProblem(jprob; prob_func = jprob_func((false, true))), SSAStepper(),
47+
trajectories = Nsims)
3348

3449
for i in 1:Nsims
3550
for n in 1:length(ts)

0 commit comments

Comments
 (0)