Skip to content

Ensemble rng redesign - #1252

Merged
ChrisRackauckas merged 19 commits into
SciML:v3from
isaacsas:ensemble_rng_redesign
Apr 8, 2026
Merged

Ensemble rng redesign#1252
ChrisRackauckas merged 19 commits into
SciML:v3from
isaacsas:ensemble_rng_redesign

Conversation

@isaacsas

@isaacsas isaacsas commented Feb 25, 2026

Copy link
Copy Markdown
Member

Ensemble-level RNG management for reproducible per-trajectory seeding

This PR adds deterministic, thread-count-independent reproducibility to ensemble solves. Passing seed or rng to an ensemble solve pre-generates per-trajectory seeds from a master RNG, then seeds each trajectory's TaskLocalRNG before solving. Results are identical regardless of thread count, batch size, or ensemble algorithm.

New kwargs on ensemble solve

  • seed: Master seed (integer). Pre-generates deterministic per-trajectory seeds.
  • rng: Master RNG (e.g. Xoshiro). Takes priority over seed.
  • rng_func: Custom per-trajectory RNG factory (ctx::EnsembleContext) -> AbstractRNG. Defaults to default_rng_func which seeds the TaskLocalRNG.

New exported type

  • EnsembleContext{S, R}: Passed to rng_func and the optional 5-arg prob_func. Fields: global_trajectory_id, worker_id, trajectory_seed, master_rng.

Usage examples

Basic reproducible ensemble solve:

using OrdinaryDiffEq

prob = ODEProblem((u, p, t) -> 1.01u, 0.5, (0.0, 1.0))
eprob = EnsembleProblem(prob;
    prob_func = (prob, i, repeat) -> remake(prob; u0 = rand() * 0.1 + 0.5))

# Same seed → identical results, regardless of thread count or ensemble algorithm
sim1 = solve(eprob, Tsit5(), EnsembleThreads(); seed = UInt64(42), trajectories = 100)
sim2 = solve(eprob, Tsit5(), EnsembleSerial();  seed = UInt64(42), trajectories = 100)
@assert [sol[end] for sol in sim1] == [sol[end] for sol in sim2]

5-arg prob_func with explicit RNG access:

eprob = EnsembleProblem(prob;
    prob_func = (prob, i, repeat, rng, ctx) -> remake(prob; u0 = rand(rng) * 0.1 + 0.5))

sim = solve(eprob, Tsit5(); seed = UInt64(42), trajectories = 50)

Custom rng_func with StableRNG:

using StableRNGs

eprob = EnsembleProblem(prob;
    prob_func = (prob, i, repeat, rng, ctx) -> remake(prob; u0 = rand(rng) * 0.1 + 0.5))

sim = solve(eprob, Tsit5();
    seed = UInt64(42),
    rng_func = ctx -> StableRNG(ctx.trajectory_seed),
    trajectories = 50)

Master RNG instead of seed:

using Random

sim = solve(eprob, Tsit5(); rng = Xoshiro(99), trajectories = 50)

Implementation details

  • Should be fully non-breaking in all non-broken cases (i.e. outside of JumpProblems with safety copies disabled). I don't think this impacts DiffEqGPU but that should be reviewed (i.e. DiffEqGPU would just not support the new kwargs).
  • Ensemble layer is the single source of truth for RNG: rng_func is always called to seed each trajectory. The rng kwarg is forwarded to inner solve only when supports_solve_rng(prob, alg) returns true. Non-DE solvers that don't accept rng still benefit from the already-seeded TaskLocalRNG.
  • 5-arg prob_func detection: When any method of prob.prob_func has ≥5 arguments (detected via numargs), the form (prob, i, repeat, rng, ctx) is used.
  • Per-task JumpProblem isolation: Replaces the vestigial threadid()-indexed deepcopy array with task_local_storage()-based per-task copies, which is safe under Julia's M:N threading model. Only active when safetycopy = false resulting in one JumpProblem deepcopy per thread.
  • Distributed master_rng sanitization: master_rng is set to nothing before pmap captures the closure in EnsembleDistributed and EnsembleSplitThreads, avoiding serialization of potentially non-serializable RNG objects.
  • worker_id propagation: EnsembleDistributed and EnsembleSplitThreads pass worker_id = myid() through solve_batch to batch_func, so ctx.worker_id correctly reflects the distributed worker pid.

Prerequisite PRs (should be merged and released in order before this PR for testing purposes)

  1. SciMLBase, NON-BREAKING — Add supports_solve_rng trait for ensemble RNG forwarding guard #1250
  2. OrdinaryDiffEq, NON-BREAKING — Add supports_solve_rng trait for ODE/DAE problem+algorithm paths OrdinaryDiffEq.jl#3081, requires release of 1
  3. StochasticDiffEq, NON-BREAKING — Add integrator-level RNG support (Phase 1, Step 3) StochasticDiffEq.jl#681, requires release of 1
  4. JumpProcesses, BREAKING — use integrator rng API JumpProcesses.jl#557, requires release of 1-3
  5. This PR (SciMLBase ensemble_rng_redesign), NON-BREAKING: Merge after steps 2–4 are released.

isaacsas and others added 2 commits February 25, 2026 17:11
…ding

Introduce EnsembleContext, generate_trajectory_seeds, and default_rng_func
to support deterministic, thread-count-independent ensemble solves via
seed/rng/rng_func kwargs on solve(). Per-trajectory seeds are pre-generated
from a master RNG/seed and forwarded to solvers guarded by the
supports_solve_rng trait. Includes 5-arg prob_func support, per-task
JumpProblem isolation via task_local_storage(), master_rng sanitization
for distributed modes, and worker_id propagation through solve_batch.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Run runic formatter on basic_ensemble_solve.jl. Add SDE+Jump problem
definition and K2 test section covering safetycopy=false with SDE, SSA,
ODE+Jump, and SDE+Jump across EnsembleThreads and EnsembleDistributed.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@isaacsas

Copy link
Copy Markdown
Member Author

This is currently marked WIP as I need to carefully review all the new tests still, and because it shouldn't be merged till the listed prerequisite PRs are handled with package releases.

isaacsas and others added 9 commits February 25, 2026 18:17
Add SDE+Jump to SOLVER_PAIRS (test A) and serial/threaded equivalence
(test C) for complete 5-pathway coverage matching the v4 test plan.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add prob_func-level distinct stream tests (StableRNG end-to-end, all 4 algorithms)
- Add solver-level distinct stream tests (jump first-times, SDE endpoints)
- Add EnsembleSplitThreads and EnsembleDistributed to cross-algorithm equivalence
- Expand different-seeds test to all 5 problem types
- Restructure and renumber tests for logical flow (15 sections, 86 tests)
- Remove unused STOCHASTIC_PAIRS constant

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix @inferred regression: use Val-dispatch + function barrier so
  Core.Compiler.return_type sees concrete types in batch_func closure.
  Val(::Bool) infers as abstract Val; explicit if/else with Val literals
  gives Union{Val{true},Val{false}} enabling union splitting.
- Add 3-mode solve dispatch (:rng, :seed, :none) for JumpProblem
  explicit-RNG compatibility: when supports_solve_rng is false but
  prob is AbstractJumpProblem, pass seed kwarg to enable per-trajectory
  reseeding via JP v9's resetted_jump_problem.
- Add type assertion for task_local_storage access in EnsembleThreads
  to preserve inference through tmap's Core.Compiler.return_type.
- Add test section 5b for explicit JumpProblem RNG (Xoshiro).
- Document prob_func type preservation constraint in docstring.
- Fix 3 runic lambda indentation issues in test file.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
… 5-arg examples

- Rename _prob_func_has_rng → _prob_func_is_5arg for clarity (Val version too)
- Fix comment referencing _solve_rng_mode in __solve (variable moved to dispatch)
- Expand _invoke_prob_func / _invoke_solve comments with dispatch details
- Add 5-arg prob_func example to EnsembleProblem docstring
- Fix misleading "one source of truth" comment in batch_func

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Move closing parens to own lines for named tuples and function calls
- Rephrase comment to avoid typos-flagged word

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The deprecated EnsembleProblem(::Vector{<:Problem}) stores a Vector as
prob.prob, not an AbstractSciMLProblem. Add a general fallback so
supports_solve_rng does not error on non-problem types.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Tests that prob_func + remake on JumpProblems correctly propagates
parameter changes and does not cause race conditions from aliased
fields on EnsembleThreads. Covers both safetycopy=true and false.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@isaacsas isaacsas changed the title WIP: Ensemble rng redesign Ensemble rng redesign Mar 1, 2026
…tness

Forward seed to inner solves for deprecated EnsembleProblem(::Vector) path
by treating AbstractVector like AbstractJumpProblem for RNG mode selection.
Fix 5-arg prob_func docstring to clarify it works without rng/seed too.
Replace integer endpoint != checks with continuous-valued time comparisons
to avoid potential spurious collisions in SSA tests. Fix stale comment
about JumpProblem u0 in make_eprob.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@isaacsas
isaacsas requested a review from ChrisRackauckas March 1, 2026 15:06
@isaacsas

isaacsas commented Mar 1, 2026

Copy link
Copy Markdown
Member Author

@ChrisRackauckas assuming tests pass now this is good with me. I think it would be better to merge and release this before making the JumpProcesses v10 release. This should be setup to work correctly/safely with JumpProcesses v9 too.

Comment thread src/SciMLBase.jl
Comment thread src/ensemble/ensemble_problems.jl Outdated
@ChrisRackauckas

Copy link
Copy Markdown
Member

DiffEqGPU.jl should be aligned with this as well, since it hooks into the non-public API part here so I think this'll break it.

DiffEqGPU CPU offload path calls SciMLBase.solve_batch with 5 positional
args (without ensemble_rng_state). Add fallback methods for all four ensemble
algorithm types that forward to the 6-arg version with a no-op RNG state.
Uses Returns(nothing) for rng_func to avoid calling Random.default_rng().

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@isaacsas

isaacsas commented Mar 1, 2026

Copy link
Copy Markdown
Member Author

I tried to add a shim to keep the old behavior for DiffEqGPU. I don't have any real GPU coding knowledge, so wouldn't feel comfortable that I could handle that ok at this point (I would have to essentially just trust an AI, which I don't think is a good approach here). I will open a PR to DiffEqGPU to see if I can get it to run CI vs. this branch to confirm its tests still pass with it.

Comment thread src/ensemble/basic_ensemble_solve.jl Outdated
isaacsas and others added 3 commits March 16, 2026 16:27
Breaking change: prob_func(prob, i, repeat) → prob_func(prob, ctx) and
output_func(sol, i) → output_func(sol, ctx) where ctx::EnsembleContext.

Redesign EnsembleContext{S,R,M} to include sim_id, repeat, rng, sim_seed,
worker_id, and master_rng fields. Eliminate numargs-based arity detection
that blocked trim and static compilation. Use Accessors.jl @set for
type-stable immutable struct updates in batch_func.

Renamed: global_trajectory_id → sim_id, trajectory_seed → sim_seed,
generate_trajectory_seeds → generate_sim_seeds. Removed _invoke_prob_func
and _prob_func_is_5arg entirely.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@isaacsas

Copy link
Copy Markdown
Member Author

@ChrisRackauckas how does this look now?

@isaacsas

Copy link
Copy Markdown
Member Author

I can followup to have DiffEqGPU support the new prob_func and output_func signatures once this gets released.

@isaacsas
isaacsas changed the base branch from master to v3 April 7, 2026 10:26
isaacsas and others added 2 commits April 7, 2026 12:13
Resolve conflict in ensemble_problems.jl by accepting v3 removal of
the deprecated vector-of-problems EnsembleProblem constructor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@isaacsas

isaacsas commented Apr 7, 2026

Copy link
Copy Markdown
Member Author

@ChrisRackauckas should be set to merge to v3.

@ChrisRackauckas
ChrisRackauckas merged commit 9790e8e into SciML:v3 Apr 8, 2026
44 of 46 checks passed
@ChrisRackauckas ChrisRackauckas mentioned this pull request Apr 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants