Skip to content

bismark/align: intra-instance -p threading (replace --multicore fork model for Bowtie 2 + HISAT2)#12385

Open
FelixKrueger wants to merge 6 commits into
nf-core:masterfrom
FelixKrueger:bismark-align-p-threading
Open

bismark/align: intra-instance -p threading (replace --multicore fork model for Bowtie 2 + HISAT2)#12385
FelixKrueger wants to merge 6 commits into
nf-core:masterfrom
FelixKrueger:bismark-align-p-threading

Conversation

@FelixKrueger

Copy link
Copy Markdown
Contributor

What & why

bismark/align currently auto-computes --multicore ccore (ccore = task.cpus / 3 directional / / 5 non-directional). For Bowtie 2, --multicore N forks N file-level workers, and each worker runs the 2 (directional/pbat) or 4 (non-directional) concurrent per-strand Bowtie 2 instances — so the job holds N × (2 or 4) copies of the converted index in RAM and re-decodes the input per fork. It also under-subscribes the reservation (the /3,/5 divisor leaves cores idle).

This replaces that with intra-instance threading: -p = task.cpus / n_instances, where n_instances = 2 (directional/pbat), 4 (non-directional), 1 (--combined_index; 2 for --combined_index_parallel --non_directional). The job then holds only 2/4 index copies total and uses all task.cpus.

  • Bowtie 2 output is byte-identical-p N --reorder is thread-invariant.
  • HISAT2 shares the same 2/4-instance model and honours -p too, so it gets the divisor. HISAT2 is not strictly thread-invariant, but the effect is negligible (see evidence) and HISAT2 was already task.cpus-dependent under --multicore.
  • minimap2/rammap are unchanged — bismark hard-codes minimap2 -t 2 and auto-scales rammap, so the module can't route threads to them; they keep the existing --multicore auto-compute verbatim. (Unifying their threading is tracked separately, upstream in Bismark.)
  • Escape hatch preserved: an explicit --multicore/--parallel/-p in ext.args short-circuits the auto-compute. -p is emitted only when task.cpus / n_instances >= 2, else omitted (faithful single-thread-per-instance).

Evidence (benchmark)

GRCh38, 2 M paired-end reads, bismark 3.0.0/3.1.0 (resource model is byte-identical across the two), process-tree peak-RSS sampler, records-only 3-way byte-identity. Median of ≥3 reps; OLD = --multicore ccore, NEW = -p (cpus/n):

mode cpus wall OLD→NEW peak RSS OLD→NEW
directional 12 217.8→122.8 s (−43.6 %) 29.1→9.8 GB (−66 %)
directional 24 126.2→68.0 s (−46.1 %) 55.2→10.1 GB (−82 %)
non-directional 12 390.0→242.9 s (−37.7 %) 29.2→16.3 GB (−44 %)
non-directional 24 215.5→128.4 s (−40.4 %) 55.4→16.6 GB (−70 %)

Bowtie 2 records byte-identical OLD vs NEW vs single-core oracle (both file-order and order-normalised md5), directional + non-directional. HISAT2 (separate spot-check): no RAM change (it never forked), wall −8 to −41 %, output re-baseline negligible (~2–4 records differ per 3.245 M, 99.9999 % identical).

Testing

nf-test 9/9 pass on native amd64 with the bismark 3.1.0 container:

  • The 5 existing tests are unchanged — at the module test default cpus=2, task.cpus / n < 2 so no -p is emitted for any path → commands byte-identical → no snapshots regenerated.
  • 4 new tests assert the emitted -p via the alignment report's options echo (-p N --reorder), at cpus=8 (dedicated config): Bowtie 2 directional (-p 4), Bowtie 2 non-directional (-p 2), HISAT2 directional (-p 4), and --combined_index (-p 8, n=1).

Notes for reviewers

  • No snapshot changes — deliberate (see above). The new -p behaviour is verified by report-echo assertions, not snapshots, because the alignment BAM is thread-invariant (Bowtie 2) or near-invariant (HISAT2), so a snapshot wouldn't distinguish it.
  • Consumers: default Bowtie 2 alignments stay byte-identical. HISAT2 consumers running at high cpus will re-baseline by a handful of reads (thread-count-driven; already cpus-dependent). MultiQC parses metric counts (not the settings-echo line), so its output is unaffected for Bowtie 2.
  • Pre-existing, out of scope: the minimap2/rammap legacy branch guards --multicore but not its alias --parallel; --minimap2 --parallel N at high cpu would crash on duplicate args. This predates this PR (the old single --multicore block had the same gap) and is left untouched here to keep minimap2/rammap byte-identical; it's folded into the upstream Bismark aligner-threading unification work.

Checklist

  • nf-core modules lint bismark/align (CI)
  • nf-test test green in CI (native amd64)
  • modules.json unaffected (no version bump)

Replace the --multicore fork resource model with an intra-instance -p
divisor (-p = task.cpus / n_instances; n = 2 directional/pbat, 4
non-directional, 1 combined-index, 2 combined_index_parallel+non_dir)
for Bowtie 2 and HISAT2. minimap2/rammap keep the legacy --multicore
auto-compute unchanged.

Lower peak RAM (one index load per strand instance instead of M forks
each re-loading the index), fuller CPU use, and byte-identical Bowtie 2
output (-p N --reorder is thread-invariant). Emits -p only when
task.cpus/n >= 2, else omits it (faithful single-thread-per-instance).

Adds 4 nf-tests asserting -p is emitted (bowtie2 dir/non-dir, hisat2,
combined-index) via a dedicated cpus=8 config; existing tests unchanged
(cpus=2 emits no -p -> byte-identical, snapshots unchanged).
The -p-threading tests requested cpus=8 to force the divisor to emit -p,
but nf-core CI runners have 4 cpus and nextflow.enable.strict=true rejects
a request exceeding available cores (req 8 > avail 4) before the process
runs. Drop to cpus=4 (the runner ceiling): directional -> -p 2, HISAT2
directional -> -p 2, combined (n=1) -> -p 4. The non-directional case at
4/4=1 now asserts -p is OMITTED, exercising the omit-below-2 boundary.
Verified 9/9 nf-test pass on native amd64 with the 3.1.0 container.
@FelixKrueger
FelixKrueger requested a review from ewels July 20, 2026 14:53
Comment thread modules/nf-core/bismark/align/tests/main.nf.test
Comment thread modules/nf-core/bismark/align/main.nf
FelixKrueger added a commit to FelixKrueger/Bismark that referenced this pull request Jul 20, 2026
nf-core/modules#12385 review (mashehu) asks for the long-name flag. bismark's
-p was short-only (unlike bowtie2, which accepts -p/--threads); add the
--threads alias so downstream (nf-core module) can use the long name once a
bismark release with it ships. Behaviour-neutral: -p stays valid, no output
change (existing tests + perl-oracle unaffected).
@FelixKrueger

Copy link
Copy Markdown
Contributor Author

Thanks @mashehu! Two notes on these:

  1. Long name — good call. The catch is that the module invokes bismark (not bowtie2 directly), and bismark's CLI currently defines -p as short-only — no --threads alias, unlike bowtie2 itself — so bismark --threads N would fail to parse against the shipped 3.1.0 container. I've added a --threads alias to bismark (aligner: route -p to minimap2/rammap -t (thread-invariant, default -t 2) FelixKrueger/Bismark#1075); once a bismark release carrying it lands and this module bumps to that container, we can switch the flag here. Keeping -p for now so the module works against the current bismark:3.1.0.

  2. Test assertion — I've kept -p 2 --reorder rather than --threads 2: the assertion checks the alignment report, which echoes the underlying bowtie2 command. bismark always constructs that with -p internally regardless of how threads are passed to bismark, so --threads never appears in the report — the assertion has to stay -p.

FelixKrueger added a commit to FelixKrueger/Bismark that referenced this pull request Jul 20, 2026
… 2) (#1075)

* aligner: route -p to minimap2/rammap -t; kill 5-Base -t mangling (#1074)

minimap2_options renders -t from cli.bowtie_threads (Bismark -p), defaulting to
the faithful -t 2 when -p is absent — spike-confirmed minimap2 -t is thread-
invariant (byte-identical + order-preserving), so a bare --minimap2 stays
byte-identical to Perl. rammap-subprocess shares this clean-slate path; the
rammap in-process pool (inprocess_rammap_threads) now honors -p too
(precedence: -p > --multicore > capped available_parallelism).

The 5-Base path's fragile `.replace("-t 2", …)` — a SUBSTRING replace that
mangled `-t 20`→`-t 200` once the base carried a resolved `-t {p}` — is replaced
by a token-safe rewrite of the value after `-t`.

Closes #1074. Tests: 1427 lib tests pass incl. new -p-lift + C2 token-safe guard
+ rammap-pool -p cases; cargo fmt + clippy clean.

* aligner: add --threads long alias for -p (nf-core long-name convention)

nf-core/modules#12385 review (mashehu) asks for the long-name flag. bismark's
-p was short-only (unlike bowtie2, which accepts -p/--threads); add the
--threads alias so downstream (nf-core module) can use the long name once a
bismark release with it ships. Behaviour-neutral: -p stays valid, no output
change (existing tests + perl-oracle unaffected).
Comment thread modules/nf-core/bismark/align/tests/nextflow_pthreads.config
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