Every neuralsbi function that calls a simulator – npe(),
simulate_for_sbi(), npe_sequential(), sbc(), tarp(),
posterior_predictive() – runs it through one execution path. That path is
sequential by default and parallel as soon as you declare a future
plan:
library(future)
plan(multisession) # or multicore, cluster, batchtools, ...
fit <- npe(prior, simulator, n_simulations = 10000)Nothing else changes: no argument to set, no variant of the function to
call. With no plan (or plan(sequential)) the simulations run in the
current process, and neuralsbi mentions the two lines above once per
session. Silence that hint with
options(neuralsbi.parallel_hint = FALSE).
Chunking
The parameter matrix is split into row chunks and one chunk at a time is
handed to the simulator, so a vectorized simulator keeps most of its
vectorization while progress can still be reported and work spread across
workers. The number of chunks depends only on the number of simulations
(ceiling(n / chunk_size), with chunk_size chosen to give about 64
chunks), not on the number of workers – which is what makes results
reproducible across plans. Set it yourself with the chunk_size argument,
or globally with options(neuralsbi.chunks = ). Larger chunks mean less
overhead per call and a coarser progress bar; smaller chunks balance
uneven simulator run times better. Under a multi-process plan each chunk
ships the simulator – and everything its environment captures – to a
worker, so a simulator closing over a large object is a reason to raise
chunk_size.
Random numbers
Each chunk gets its own L'Ecuyer-CMRG random-number stream, derived from the
session's RNG state at the moment simulation starts. So set.seed(42)
before a fit (or npe(..., seed = 42)) gives the same simulations whether
you run sequentially or on 32 workers, and whatever the worker count. What
does change results is the chunking: fixing chunk_size fixes the draws.
Because the simulations are drawn from separate streams, the simulator no longer consumes the caller's RNG state directly; the state advances by one draw per simulation phase.
What is not parallelized
Training runs in the calling process. Torch tensors and modules are external
pointers that cannot be shipped to a worker, and libtorch already uses
multiple threads internally (torch::torch_set_num_threads()). The same
goes for the posterior-sampling loops in sbc() and tarp() – those call
the trained network, not the simulator, and are reported with their own
progress bar.
See also
nsbi_progress for controlling progress bars.
