Skip to content

Rust backend: parallelism fixes for bigWig writer, alignmentSieve, computeMatrix - #1445

Closed
adRn-s wants to merge 5 commits into
4.0.0from
feature/rust-backend-parallelism-tuning
Closed

Rust backend: parallelism fixes for bigWig writer, alignmentSieve, computeMatrix#1445
adRn-s wants to merge 5 commits into
4.0.0from
feature/rust-backend-parallelism-tuning

Conversation

@adRn-s

@adRn-s adRn-s commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Welcome to deepTools GitHub repository! Please check the following regarding
your pull request :

  • Does the PR contain new feature?
  • Does the PR contain bugfix?
  • Does the PR contain documentation changes?
  • Does the PR contain changes to the galaxy wrapper?

Summary

Three bounded parallelism fixes in the Rust backend, found while checking why bamCoverage/bamCompare's bigWig writer, alignmentSieve, and computeMatrix don't scale as well as expected with -p. Each commit is independently buildable and testable. Rebased onto current 4.0.0 (0f07abf3, which independently fixed computeMatrix's hashmap-based memory footprint) to keep the comparison fair.

  1. 4f483b94 bigwig writer: use nproc for the tokio runtime, skip the multi-thread runtime entirely at -p 1 (mirrors bigtools' own bedgraphtobigwig CLI behavior).
  2. 03a525b5 alignmentSieve: share one htslib thread pool between the main and --filteredOutReads BAM writers, instead of two independent set_threads() calls that were oversubscribing cores (up to 2 * (nproc - 2) threads competing for nproc reserved cores).
  3. ee7d9e8c computeMatrix: add a region-chunk axis to matrix-building parallelism, so core usage isn't bounded by min(nproc, bw_files.len()) on single- or few-sample runs.

A fourth commit (07aca1fa, reading the bigwig writer's input from a merged, indexed bedgraph instead of a sequential one) is also in the branch history but reverted (5291287b): benchmarking showed it wrote a temp file to disk at -p > 1 that the pre-change baseline never touched, which made it strictly worse than baseline above roughly 200MB of output, i.e. most chip/wgs samples. Kept the bwintervals slice-signature widening it introduced, since commit 3 needs it.

Root causes for items 1-3 were confirmed against the actual pinned library source, not just documentation: rust-htslib v1.0.1 (cloned at that tag) for item 2, and bigtools 0.5.8 (cloned at HEAD, one commit past our pinned tag) for item 1.

Testing

  • Full pytest suite passes after every commit (231/232 locally, same one pre-existing unrelated failure as 4.0.0).
  • Manual smoke tests: byte-identical or diff-identical output across -p 1/4/12 for the affected tools.
  • Benchmarked on raven (24-thread SLURM profile, 10 reps per sample, human + triticum references). Summary, full numbers and mechanism writeups are in the PR comments below:
    • bamCoverage/bamCompare/alignmentSieve/multibamSummary: at parity with baseline (~1.0x, ~1.0x mem) everywhere except bamCoverage on low-depth/large-genome samples (triticum chip/rna), where the bigwig writer's per-chromosome parallel encode gives a real 1.5-3.6x speedup, no memory cost. WGS samples see no gain in either species since read-depth-bound coverage computation dominates wall time there, not the write stage.
    • computeMatrix: flat parity (1.0x) when the input already has enough bigwig files to saturate -p 24 on the file axis alone (15 files here). With few files (3 here), the region-chunking commit still gives a real ~1.7x speedup, down from an earlier (invalid, stale-baseline) reading of up to 2.23x now that both sides share 4.0.0's hashmap-to-vector fix. That speedup comes with a real memory cost on the largest genome (triticum, 1.19x baseline RSS), not the near-free win it first looked like.

cc colleagues for input before this merges.

@adRn-s

This comment has been minimized.

@adRn-s

adRn-s commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

Four commits, three "confirmed against pinned source" citations, a spec doc with a section literally titled "Memory tradeoff of the combined change set," and not one single em dash anywhere in sight. Either this was written by someone who's spent way too long reading bigtools internals for fun, or it's the most thoroughly laundered AI slop you've ever reviewed. Benchmarks on raven will settle which... 👍🏽 maybe it's both

@adRn-s

adRn-s commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed one more commit (349c594): gates the parallel indexed-read path (item 1b) behind a size threshold on the merged bedgraph.

This was flagged in the spec as the expected fallback if benchmarks disagreed on item 1, and raven's 24-thread run confirmed it does: bamCoverage/bamCompare on WGS-scale samples regressed to 0.78-0.85x wall time under the always-on parallel path, while chip/rna-scale samples sped up up to 3.8x, with memory flat in both cases. Full numbers and rationale are in the commit message.

Set up a proper A/B benchmark Snakefile (4.0.0 pre-branch vs this branch, side by side, two pixi environments so both binaries coexist) on https://github.com/deeptools/deeptools25/tree/dt4-ab-benchmark, benchmark/dt4_ab.smk. Will use that to re-verify the threshold fix above and for any further tuning here.

@adRn-s
adRn-s force-pushed the feature/rust-backend-parallelism-tuning branch from 349c594 to 117f41e Compare August 13, 2026 11:22
@adRn-s

adRn-s commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Status update, replacing the outdated spec above (minimized, kept for history).

Branch is now rebased onto latest 4.0.0 (117f41e), which picked up two upstream commits: exhaustive testing (#1443) and gz support (#1446). Final 4 commits:

  • 0a97c23a bigwig writer: use nproc for the tokio runtime, skip it entirely at -p 1
  • abf04e41 alignmentSieve: share one htslib thread pool between the two BAM writers
  • 126867b4 computeMatrix: add a region-chunk axis to matrix-building parallelism
  • 117f41e5 bigwig writer: parallel indexed read at nproc>1, gated by output size

Worth calling out: the rebase surfaced that upstream's gz-support PR had already restructured write_covfile to feed bamCoverage/bamCompare's in-memory iterator straight into bigtools (BedParserStreamingIterator::wrap_infallible_iter), no temp file, no text round-trip. That's a cleaner version of what the original spec's "Item 1a" was chasing, and it landed independently on 4.0.0 while this branch was in flight. Reconciled by keeping that as the nproc <= 1 baseline (zero disk I/O, matches upstream exactly) and only spooling to a merged temp file at nproc > 1, where it's needed to build bigtools' index for the size-gated parallel reader from the last commit.

232 tests passing (up from 153 pre-rebase, thanks to #1443's exhaustive suite).

Re-running the raven A/B benchmark (4.0.0 pre-branch vs this branch) now that the rebase is in, still draft until those numbers are in.

@adRn-s
adRn-s force-pushed the feature/rust-backend-parallelism-tuning branch from 31a9852 to 052bdf5 Compare August 18, 2026 10:35
@adRn-s

adRn-s commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

Re-ran the A/B benchmark after rebasing on 117f41e5 and found the "size-gated" fix wasn't actually fixing the regression. The gate only chose how to read back a merged temp bedgraph; it still wrote that temp file first at nproc>1, when the pre-branch baseline never touched disk at all. Above the 200MB threshold this made things strictly worse than baseline, and most outputs cross 200MB easily in bedgraph text form, so the regression hit chip/wgs samples across the board, not just wgs as originally reported.

Reverted 117f41e5 (052bdf5a), keeping only the unrelated bwintervals slice-signature widening it introduced (needed by the computeMatrix chunking commit). Reran bamCoverage/bamCompare, 10 reps, raven 24-thread profile:

sample speedup mem ratio
bcov_human_chip 1.11x 1.00
bcov_human_rna 1.02x 0.99
bcov_human_wgs 0.98x 0.97
bcov_triticum_chip 1.54x 1.05
bcov_triticum_rna 3.68x 1.00
bcov_triticum_wgs 0.98x 1.01
bcom_human_chip 1.00x 0.98
bcom_triticum_chip 0.99x 0.96

Back at parity with baseline everywhere. The two outliers above 1.5x run identical code to orig now, so that's cluster noise, not a real effect.

Still working through computeMatrix/alignmentSieve/multibamSummary numbers before deciding if this is ready to leave draft.

adRn-s added 5 commits August 19, 2026 11:57
write_covfile hardcoded worker_threads(1), starving bigtools' own
per-chromosome zoom/encode parallelism regardless of -p. Pass nproc
through from bamCoverage/bamCompare instead.

At nproc == 1, mirror bigtools' own bedgraphtobigwig CLI and use a
current_thread runtime (channel_size = 0) rather than paying for a
multi-thread runtime that will only ever have one consumer.

Verified byte-identical bigWig output across worker counts (1/4/12)
on single- and multi-chromosome fixtures; full pytest suite passes.
set_threads() spins up a private htslib thread pool per call. With
--filteredOutReads set, both obam and ofilterbam independently
requested writerthreads threads, so total BGZF compression threads
could reach 2 * writerthreads against a writerthreads (or nproc)
budget. Build one ThreadPool and share it via set_thread_pool()
instead, per rust-htslib's own documented pattern for multiple
concurrent readers/writers (bam/mod.rs tests, v1.0.1).

Verified byte-identical main and filtered-out BAM output across -p
1/4/12 with both writers active; full pytest suite passes.
Matrix building only parallelized across bw_files.par_iter(); each
task then processed every region for that file in a strictly
sequential loop (bwintervals reuses one BigWigRead reader, since
get_interval takes &mut self and can't be shared across threads). So
core utilization was bounded by min(nproc, bw_files.len()), leaving
most of a reserved node idle on typical single- or few-sample runs.

Widen bwintervals to take region/slopregion slices instead of &Vec,
and parallelize over (bigwig file, region chunk) pairs instead of
just bigwig file, opening one reader per task and reusing it across
that task's chunk of regions, mirroring the pattern bam_pileup
already uses for BAM reading. Chunk count targets roughly nproc total
tasks (nproc / bw_files.len(), rounded up to 1) so a single-track run
still fans out across the whole reservation.

rayon's par_iter().collect() into a Vec is order-preserving for an
indexed source, so results[k] always corresponds to tasks[k]
regardless of completion order; the merge step is a plain sequential
walk rather than needing its own reordering logic.

Verified the raw --outFileNameMatrix output is identical across -p
1/4/12 with two bigwig tracks, and matches the pre-change baseline
exactly. Full pytest suite passes.
wrap_infallible_iter already drives BigWigWrite directly off the
in-memory iterator with no disk round-trip; that stays the path at
nproc <= 1; skip building a multi-thread runtime there too, since it
would only ever have one consumer (mirrors bigtools' own
bedgraphtobigwig CLI at nthreads == 1).

At nproc > 1, spool to one merged bedgraph temp file and hand it to
bigtools' own indexed parallel reader (BedParserParallelStreamingIterator),
the same path bigtools' own bedgraphtobigwig CLI uses by default. That
reads and encodes multiple chromosomes concurrently straight off disk,
instead of the single sequential stream wrap_infallible_iter produces.
This unavoidably costs one extra write pass versus the nproc <= 1 path,
since index_chroms() needs a real sorted file on disk to build its
byte-offset index.

Gate that parallel read behind a size threshold on the merged file,
inverted from bigtools' own ~200MB auto-enable-above heuristic. Raven
24-thread benchmarks (this branch vs 4.0.0 pre-branch, 10 reps) showed
it as a real win on chip/rna-scale outputs (up to ~3.8x faster) but a
regression on whole-genome-scale ones (bamCoverage/bamCompare on WGS
samples dropped to 0.78-0.85x), with memory flat in both cases. Likely
cause: index_chroms() does one full linear scan of the merged bedgraph
before BedParserParallelStreamingIterator does its own per-chromosome
reads; cheap next to the parse time it saves on small/medium files,
but dominant on multi-hundred-MB WGS ones. Data-driven, not a hunch:
this exact fallback was flagged in the parallelism spec's "alternative
if benchmarks disagree" section for this item before benchmarking.

Full pytest suite passes.
…put size"

This reverts commit 117f41e.

The size-threshold gate this commit added doesn't fix the WGS regression
it was meant to fix, and it regresses chip/rna-scale outputs it claimed
to speed up. Root cause: at nproc>1, the reverted code always spools
`lines` to a merged temp bedgraph file before deciding anything - the
gate only chooses between an indexed parallel *read* of that file
(under threshold) or a plain sequential *read* of it (over threshold).
Either way the disk round-trip (format to text, write, reopen, reparse)
happens unconditionally, on top of whatever the read path costs. The
pre-regression baseline never touched disk here at all: it streamed
`lines` straight into BigWigWrite in memory.

Fresh raven 24-thread A/B benchmark (10 reps, this branch rebased onto
4.0.0 117f41e's parent vs pre-branch 4.0.0) confirms this end to end -
bamCoverage/bamCompare regressed across every chip/wgs sample tested,
not just WGS as the original commit's benchmark suggested:

  bcov_human_wgs       0.68x   bcom_human_chip      0.74x
  bcov_triticum_wgs    0.58x   bcom_triticum_chip   0.73x
  bcov_human_chip      0.67x   bcov_triticum_chip   0.95x

Deciding the fast path correctly would require knowing the output size
before consuming `lines` (a single-pass streaming iterator), which
isn't available cheaply for bamCoverage/bamCompare's use case. Given
real deepTools output sizes appear to exceed the 200MB threshold
routinely (bedgraph text is far less compact than the bigwig it
produces), the disk-based parallel-read path doesn't pay for itself in
practice. Reverting restores the proven-correct in-memory streaming
write for nproc>1, matching 4.0.0 baseline exactly.
@adRn-s
adRn-s force-pushed the feature/rust-backend-parallelism-tuning branch from 052bdf5 to 5291287 Compare August 19, 2026 09:58
@adRn-s

adRn-s commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

Quick disclaimer up front: computeMatrix numbers below are stale and a rerun is in progress, see the note at the end before reading too much into them.

Full A/B table after the bigwig-writer revert (10 reps, raven 24-thread profile), for the record:

tool sample speedup mem ratio
alignmentsieve human_chip 1.03x 1.00
alignmentsieve human_rna 1.03x 1.03
alignmentsieve human_wgs 1.04x 1.00
alignmentsieve triticum_chip 1.01x 0.99
alignmentsieve triticum_rna 0.98x 1.00
alignmentsieve triticum_wgs 1.02x 0.99
bamcompare human_chip 1.00x 0.98
bamcompare triticum_chip 0.99x 0.96
bamcoverage human_chip 1.10x 1.01
bamcoverage human_rna 1.02x 0.99
bamcoverage human_wgs 0.98x 0.97
bamcoverage triticum_chip 1.53x 1.03
bamcoverage triticum_rna 3.59x 1.00
bamcoverage triticum_wgs 0.98x 1.03
computematrix human_15 1.01x 0.82
computematrix human_3 2.23x 1.20
computematrix triticum_15 1.03x 0.85
computematrix triticum_3 1.54x 1.03
multibamsummary human_15 0.98x 1.00
multibamsummary human_3 1.00x 1.00
multibamsummary triticum_15 1.00x 1.01
multibamsummary triticum_3 0.99x 1.00

alignmentSieve, bamCoverage/bamCompare and multibamSummary are all clean, matching the expected effect of each commit. multibamSummary is untouched by any of this branch's commits and sits flat at ~1.00x, which is a good sanity check that the rest isn't noise.

The computeMatrix row is where it gets complicated. Our orig baseline pin (76ba44b5) turned out to be stale: it predates a rebase/force-push on 4.0.0 and isn't reachable from the current tip anymore. More importantly, current 4.0.0 picked up 0f07abf3 (drop hashmap for dense vec+indexing in bwintervals), a real fix to the same function our region-chunking commit widened the signature of. The two changes aren't redundant: 0f07abf3 changes the per-call memory representation, ours changes how work is split across threads. But the baseline our computeMatrix numbers above were measured against no longer reflects current 4.0.0, so those speedup/mem numbers can't be trusted as-is.

Rebased the branch onto current 4.0.0 (clean, no conflicts, builds and passes tests locally), repinned both benchmark environments, and a computeMatrix-only rerun against the fair baseline is in progress now. Will post the corrected numbers once that's done.

@adRn-s

adRn-s commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

Dug into why bamCoverage's triticum_chip (1.53x) and triticum_rna (3.59x) stand out from the rest of the table, which is otherwise close to parity. Short version: it's real, not noise, and it comes from genome size.

write_covfile parallelizes the bigwig write itself per chromosome (bigtools spawns one async encode/zoom task per chromosome; our commit switched the tokio runtime from a hardcoded worker_threads(1) to worker_threads(nproc), baseline was always serial here regardless of thread count). How much that helps depends on how much per-chromosome work there actually is to split across threads.

Checked the two reference genomes used in the benchmark:

total size contigs avg chrom size largest chrom
triticum (hexaploid wheat) 14.5 Gb 22 661 Mb 831 Mb
human 3.1 Gb 194 (only 25 >1Mb) 16 Mb 249 Mb

Triticum is about 4.7x bigger genome-wide, in 22 uniformly large chromosomes. Human has more contigs on paper, but 169 of them are unplaced/alt scaffolds that finish almost instantly. The real work is in ~25 chromosomes, the largest of which is a third the size of triticum's largest. So parallelizing the same write stage has far more raw work to divide across threads for triticum.

That also explains why WGS shows no gain in either species (0.98x both) while chip/rna do. WGS read depth is high enough that the upstream BAM coverage computation dominates total wall time, so a faster write stage barely moves the total. Chip/rna have much lower depth, so the write stage (which scales with genome length, not depth) is a bigger share of runtime in both species, and that's where triticum's heavier per-chromosome write cost, now finally parallelized instead of serial, shows up.

computeMatrix numbers are still pending the rerun against the corrected baseline mentioned above, will follow up separately once that's done.

@adRn-s

adRn-s commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

Updated the PR description to match. Corrected computeMatrix numbers, rerun against the fixed baseline (current 4.0.0 tip, both sides now include the hashmap→vector bwintervals fix):

sample speedup mem ratio
cm_human_15 1.00x 0.91
cm_human_3 1.68x 1.02
cm_triticum_15 1.00x 1.02
cm_triticum_3 1.69x 1.19

For reference, the old (stale-baseline) numbers were human_15 1.01x/0.82, human_3 2.23x/1.20, triticum_15 1.03x/0.85, triticum_3 1.54x/1.03.

The _15 samples (15 bigwig files, enough on their own to saturate 24 threads without any region chunking) now sit at flat parity, which makes sense: the baseline's file-axis parallelism was already using all the threads there, so the region-chunking commit had nothing to add.

The _3 samples (3 bigwig files, so file-axis parallelism alone leaves most of the 24 threads idle) still show a real ~1.7x speedup. Smaller than the earlier 2.23x, since some of that number really did come from the now-fixed hashmap, but the region-chunking commit is still splitting the 3 files' regions across threads instead of leaving 21 of them idle.

One thing to call out: cm_triticum_3's memory ratio moved the wrong way, 1.19x versus baseline, worse than the old 1.03x reading. Region chunking keeps more region buffers in flight at once, and that cost is more visible now that both sides use the leaner vector-based bwintervals instead of a hashmap.

So the chunking commit still earns its complexity on the low-bigwig-count case, but it's a real speed/memory tradeoff rather than a free win. The PR description should say so instead of leading with the old 2.23x number.

@adRn-s adRn-s closed this Aug 19, 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.

1 participant