Support multi-chain sampling via future.apply mapping - #139
Open
jennajiali wants to merge 7 commits into
Open
jennajiali wants to merge 7 commits into
jennajiali wants to merge 7 commits into
Conversation
- Add `chain_index` as the first argument of `sample_chain()` to seamlessly support `future.apply::future_lapply()`. - Allow `initial_state` to be a generator function (e.g., `stats::rnorm`) by adding a `dimension` field to target distribution helpers. - Route progress updates through the `progressr` package when available to ensure parallel-safe progress reporting. - Introduce `combine_chain_results()` to invert the nested list output of parallel execution into a single list keyed by result-field name.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #139 +/- ##
===========================================
- Coverage 100.00% 98.81% -1.19%
===========================================
Files 11 11
Lines 762 842 +80
===========================================
+ Hits 762 832 +70
- Misses 0 10 +10 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This was
linked to
issues
Aug 28, 2026
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
This PR resolves the immediate goals of issue #18. It targets the in-scope subset agreed upon in the meeting: N independent chains sampling the same target distribution, running the full warm-up and main procedure on N processors, so wall-clock time buys N× samples. Parallel-tempering-style inter-chain communication is explicitly out of scope for this change.
Design decisions & implementation
Following the guidance in the issue,
sample_chain()is kept as a single-chain function and made pleasant tofuture_lapplyover, rather than wrapping the parallel loop internally.chain_indexargument: a new leading argumentchain_indexaccepts an optional integer identifier for the chain. Placingchain_indexfirst makesfuture.apply::future_lapply(seq_len(n_chain), sample_chain, ...)work out of the box, sincefuture_lapplypasses each iteration variable as the first positional argument to the function. When non-NULL, it is prefixed onto progress-bar labels and fallback progress messages, so output from several chains running in parallel remains distinguishable.initial_stateas a generator:initial_statenow additionally accepts a generator function such asstats::rnormorstats::runif. The function is called withtarget_distribution$dimensionto produce a different random starting position for each chain.target_distribution_from_log_density_formula()andtarget_distribution_from_stan_model()now populate adimensionfield on the returned list, unlocking the generator-function form ofinitial_statewithout further user input.progressrpackage when it is installed, keeping progress reporting well-behaved under parallel futures. The existingprogresspackage andmessage()-based fallbacks continue to work whenprogressris unavailable.combine_chain_results()function, which inverts the list-of-lists returned byfuture_lapplyinto a single list keyed by result-field name, with each entry a per-chain list. This makes downstream diagnostic workflows (e.g. with theposteriorpackage) simpler to express.Usage examples
1. Sampling four chains in parallel with
future.apply:2. Sequential multi-chain run without a
futurebackend (useful for debugging before scaling out toplan(multisession)):plain base-R
lapplyinstead offuture_lapply— no parallelism, everything runs one chain after another in your current R session. This is the "debug it here first" version: if something's wrong with your target distribution or adapter setup, you get a normal R traceback instead of a confusing error from inside a background worker. Once this works, swappinglapply→future_lapply(plusplan(multisession)) is the only change needed to parallelize it.3. Distinguishing chains in progress output, e.g. when running a single chain manually or outside of
future_lapply:This isn't really about parallelism — it shows that chain_index is just an optional label you can set by hand, even for a single ad-hoc call. The only visible effect is the progress output.
vs. without chain_index, you'd just see
Warm-up (stage 1/1): 20% done ...with no "Chain 3 |" prefix. Useful if you're eyeballing interleaved log output from several chains and need to tell which line came from which.4. Parallel-safe progress reporting with
progressr(recommended when running many chains underfuture.apply, since a localprogress::bar isn't meaningful across worker processes):We have each worker report progress events back to the main process, which then renders one unified bar/message locally. Two pieces:
handlers(global = TRUE)— registers how progress should be displayed (default handler prints text updates to console).with_progress({ ... })— wraps the code whose progress you want tracked. Inside chain_loop(), rmcmc checks whether it's running inside a with_progress() block and, if so, calls progressr::progressor() instead of progress::progress_bar$new().You don't have to change anything about the sample_chain() calls themselves — just wrap them in with_progress() and install the progressr package. If you don't do this and just run Example 1 as-is, it still works fine, you just won't see live progress from the workers.
5. Combining chains that also trace warm-up (
combine_chain_results()preserves the extrawarm_up_traces/warm_up_statisticsfields whentrace_warm_up = TRUE):Note the
function(i) { sample_chain(chain_index = i, ...) }wrapper here instead of passingsample_chaindirectly to lapply. That's because we want to passispecifically into the named chain_index argument (to also demonstrate setting it), rather than relying on positional matching like Example 1 did. Both approaches work — this is just the more explicit.