From ea5645031999a4b00dfc17aeeb759bf397bb5932 Mon Sep 17 00:00:00 2001 From: Benjamin Demaille Date: Thu, 30 Jul 2026 15:27:00 +0200 Subject: [PATCH 1/2] fix: --runThreadN 1 ran on every core, not on one The rayon global pool was configured only when `--runThreadN` was greater than 1. Skipping the build at 1 does not give one thread: it leaves rayon's default, which is one worker per logical core. So `--runThreadN 1` ran the whole machine. Measured on 200k reads, before: 2.44 s wall at **1300% CPU**. After: 26.23 s wall at 100% CPU. The old figure was not a fast single-threaded run, it was a sixteen-way run wearing the wrong flag. This matters beyond the flag reading falsely. A scheduler or a container given one CPU gets sixteen worker threads; on a shared machine the run oversubscribes every other job; and with a thread-caching allocator each of those threads keeps its own heap, which is the very cost the comment above this code says the pool sizing exists to avoid. It also means the project's thread-invariance checks were weaker than they read: the `--runThreadN 1` leg was not a one-thread leg. Verified now that it is one: records are byte-identical between 1 and 8 threads on 200k real reads, and byte-identical to the previous binary's output at `--runThreadN 1`. Only the `@PG` `CL:` line differs between thread counts, because it records the command line. Co-Authored-By: Claude Opus 5 (1M context) --- src/lib.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6fce173..8a07f5f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,11 +68,13 @@ pub fn run(params: &Parameters) -> anyhow::Result<()> { // RSS for nothing. `build_global` errors if called twice; we // ignore the error so the in-process tests that already // initialised the pool still work. - if usize::from(params.run_thread_n) > 1 { - let _ = rayon::ThreadPoolBuilder::new() - .num_threads(params.run_thread_n.into()) - .build_global(); - } + // + // Configured at every value, `1` included. Skipping the build at 1 does not + // yield one thread: it leaves rayon's default of one worker per logical + // core, so `--runThreadN 1` ran on the whole machine. + let _ = rayon::ThreadPoolBuilder::new() + .num_threads(params.run_thread_n.into()) + .build_global(); match params.run_mode { RunMode::GenomeGenerate => genome_generate(params), From b391032319f85b7daec37995250c38a769164717 Mon Sep 17 00:00:00 2001 From: Benjamin Demaille Date: Thu, 30 Jul 2026 20:12:03 +0200 Subject: [PATCH 2/2] docs(changelog): record the --runThreadN 1 fix Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6e4893..f103273 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,11 @@ Sections commonly used: Features, Bug fixes, Other changes. ### Bug fixes +- `--runThreadN 1` ran on every logical core instead of on one. The + rayon pool was configured only above 1, and skipping it leaves rayon's + default of one worker per core. Output is unchanged; the run now uses + the thread count asked for. + - **STARsolo `Gene` assignment now requires exon concordance**, matching STARsolo: a read counts toward a gene only when every aligned block lies within the gene's exons, rather than merely overlapping one. This