Skip to content

BiocParallel for parallel loops: workers started once per call, one BLAS/OpenMP thread per worker - #18

Merged
ellispatrick merged 5 commits into
develfrom
biocparallel-threads
Oct 14, 2026
Merged

ellispatrick merged 5 commits into
develfrom
biocparallel-threads

Conversation

@ellispatrick

Copy link
Copy Markdown
Member

Statial's parallel loops (Kontextual(), relabelKontextual(), getDistances(), getAbundances(), calcStateChanges()) use one started-once BiocParallel pool per call, with one BLAS and OpenMP thread per worker. Version 1.15.4.

Why. Bioconductor asks for BiocParallel rather than the parallel package. Two problems also made parallel runs slow on our servers:

  1. BLAS threads. OpenBLAS (openblas-pthread) starts one thread per core in every R process, which is 128 on verona, and forked workers keep them. With 4 workers that was 513 threads, and a matrix-heavy job took 14.0 s instead of 1.6 s. Setting OPENBLAS_NUM_THREADS in .Renviron does not help, because OpenBLAS reads it before .Renviron is loaded.
  2. Forking per chunk. An unstarted MulticoreParam forks a new process for every chunk of tasks, and each forked process copies much of the session's memory when R collects garbage.

The helper (.par_lapply in R/utilities.R):

  • Honours cores as a number or a BiocParallel param object.
  • Sets BLAS and OpenMP to one thread in the main session just before the workers start, and restores them on exit. Neither is changed if it already runs one thread. Calling blas_set_num_threads(1) inside a worker would rebuild all 128 threads first. Workers forked after the session has used several OpenMP threads can hang.
  • Puts the task function and X in a package environment, calls bpstart() once, sends only task numbers in about 8 chunks per worker, then calls bpstop().
  • Socket workers are given OPENBLAS_NUM_THREADS=1 and OMP_NUM_THREADS=1 before they start.

Known fixed cost. MulticoreParam() and bpstart() together run 2–3 full gc() calls (.snowCoresMax() calls showConnections(), which calls gc()). That is about 0.3 s each in a light session and 0.9 s each with SpatialExperiment data loaded. The cost per call does not depend on the size of the job.

Changes beyond the helper:

  • Kontextual() used to fork five times per call. It now makes one pass over the images (point pattern, close pairs and edge correction), plus one pass for the core statistic, with a single param shared by both.
  • Behaviour change: relabelKontextual() used to ignore set.seed() (MulticoreParam without an RNGseed), and its results changed with the number of cores. It now draws its RNGseed from the session, so relabelled images are reproducible and the same for any number of cores. Its randomised rows therefore differ from older runs. Its "original" rows are identical.
  • The old .generateBPParam used .Random.seed[1] (the RNG kind code) as its seed. It is removed.

Results. Old and new are identical() at 1 and 8 cores (kerenSCE) for Kontextual() (8000 rows; also with edgeCorrect = FALSE and inhom = TRUE), getDistances, getAbundances, calcStateChanges and the original rows of relabelKontextual. All tests pass.

Timing (8 cores): Kontextual() is about the same (33.3 s against 31.2 s). relabelKontextual(nSim = 20) takes 326 s old and 178 s new.

RhpcBLASctl added to Imports; the BiocParallel importFroms are replaced by BiocParallel:: calls.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y1PuRkfDQ3ApzuNoe5ovxW

ellispatrick and others added 4 commits October 14, 2026 10:43
…worker; reproducible relabelKontextual

Parallel loops in Kontextual(), relabelKontextual(), getDistances(), getAbundances() and calcStateChanges()
go through .par_lapply()/.par_mapply() (the spicyR helper): forked workers start once per call after the
task exists and receive only task numbers, tasks go out in about eight chunks per worker, socket workers on
Windows, and OpenBLAS runs one thread per worker during the call (RhpcBLASctl, set in the main session).
Kontextual() builds point patterns, close pairs and edge correction in one pass over the images and uses
one param for both parallel steps. relabelKontextual() seeds its random number streams from the session,
so set.seed() makes it reproducible for any number of cores. Other results are identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y1PuRkfDQ3ApzuNoe5ovxW
Restoring several BLAS threads after forking restarts every OpenBLAS thread, and the new threads spin briefly
(about 14 CPU-seconds per call with 128 threads). BLAS threads are now changed only when there is more than one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y1PuRkfDQ3ApzuNoe5ovxW
Forked workers can hang when the main session has used several OpenMP threads before forking (seen with
randomForestSRC in ClassifyR). OpenMP threads are set to one for the call, as BLAS threads are, and restored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y1PuRkfDQ3ApzuNoe5ovxW
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y1PuRkfDQ3ApzuNoe5ovxW

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Already-running worker pools bypass worker-side BLAS/OpenMP limits, undermining the central resource-control guarantee.

2 open findings
What changed in this PR

Introduces shared BiocParallel helpers to improve parallel-loop performance and control worker thread usage.

Changes:

  • Adds parallel apply helpers with RNG and thread management.
  • Migrates spatial calculations and relabelling to the helpers.
  • Adds dependency, release notes, and parallel-behavior tests.
File Description
R/​utilities.R Implements BiocParallel helpers and thread controls.
R/​Statial.R Migrates distance, abundance, and state calculations.
R/​relabelKonditional.R Adds reproducible parallel relabelling.
R/​Konditional.R Consolidates image preprocessing and parallel calculations.
tests/​testthat/​test-utilities.R Tests results, RNG behavior, and thread restoration.
NEWS.txt Documents parallelization changes.
NAMESPACE Removes obsolete imports.
DESCRIPTION Bumps version and adds RhpcBLASctl.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread R/utilities.R Outdated
Comment on lines +70 to +75
if (BiocParallel::bpisup(BPPARAM) || !is(BPPARAM, "MulticoreParam")) {
# Workers that are already running, or are new R sessions, are sent the task with each chunk.
threadVars <- Sys.getenv(c("OPENBLAS_NUM_THREADS", "OMP_NUM_THREADS"), unset = NA)
Sys.setenv(OPENBLAS_NUM_THREADS = 1L, OMP_NUM_THREADS = 1L)
on.exit(.restore_env(threadVars), add = TRUE)
return(BiocParallel::bplapply(X, task, BPPARAM = BPPARAM))
Comment thread NEWS.txt Outdated
task numbers, and tasks are handed out in about eight chunks per worker. Socket workers are used on
Windows. BLAS and OpenMP run one thread in each worker during the call (RhpcBLASctl, now imported);
OpenBLAS otherwise starts one thread per core in every worker. Kontextual() builds each image's point pattern,
close pairs and edge correction in one pass over the images. Results are unchanged.
…per socket worker too

Workers of a param that was already running set one BLAS and OpenMP thread themselves before their first task
(settings changed in this session can't reach them). The eight-chunks-per-worker default now also applies to
socket workers.
NEWS no longer says all results are unchanged: relabelKontextual()'s relabelled images differ.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y1PuRkfDQ3ApzuNoe5ovxW
@ellispatrick
ellispatrick merged commit 3d04158 into devel Oct 14, 2026
1 check failed
@ellispatrick
ellispatrick deleted the biocparallel-threads branch October 14, 2026 02:31
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