Skip to content

fix(group): cap metric memory with per-thread accumulators (#285) - #287

Merged
nh13 merged 1 commit into
mainfrom
nh/group-oom
Apr 18, 2026
Merged

nh13 merged 1 commit into
mainfrom
nh/group-oom

Conversation

@nh13

@nh13 nh13 commented Apr 17, 2026

Copy link
Copy Markdown
Member

Summary

  • Replaces the Arc<SegQueue<CollectedMetrics>> in fgumi group with Arc<Vec<Mutex<GroupMetricsAccumulator>>>, sized to num_threads.
  • Each worker claims a slot via a TLS-cached index from a process-wide atomic counter and merges per-position-group counts into its slot immediately, so retained memory is O(threads × distinct sizes) instead of O(position groups).
  • Eliminates the unbounded ~1 KB-per-position-group AHashMap::with_capacity(50) allocation that was the dominant consumer at ~10 GB on a 500 M-pair input (issue group OOMs at ~30 GB RSS on ~500 M-pair real data, regardless of --queue-memory #285).

Why

On a 500 M-pair CODEC BAM (~38 GB) on a 30 GiB box, fgumi group --strategy adjacency OOMed at ~30 GB RSS after ~170–180 M records regardless of --queue-memory or --threads settings. Static analysis pinned the dominant consumer to the SegQueue of CollectedMetrics, which held one AHashMap<usize, u64> (~1 KB backing table even when empty) per position group and was only drained at pipeline end. Downstream write_all_metrics only needs the aggregated counters — the per-group objects were pure scratch.

The single-threaded path already merges incrementally; this change brings the pipeline path in line with that.

What's next

  • Heap profile at ~150 M records is pending to confirm peak RSS is flat (±working set) across input sizes, not just "no longer OOMs at 180 M".
  • Same pattern (SegQueue<CollectedXxxMetrics>) exists in simplex, duplex, codec, filter, clip, correct. Stacked follow-up PR will cover the tractable cases; the consensus commands additionally buffer rejected records as pipeline output, which needs a careful migration.

Test plan

  • cargo ci-fmt
  • cargo ci-lint
  • cargo nextest run — all 225 group-touching tests pass
  • Heap profile (dhat/heaptrack) on real 500 M-pair CODEC data at 150 M records — confirm CollectedMetrics no longer dominates
  • End-to-end run at full scale — confirm peak RSS is flat across 50 M / 150 M / 400 M records on the same input
  • fgbio GroupReadsByUmi byte-identity check on a shared-scale input

@nh13
nh13 temporarily deployed to github-actions April 17, 2026 20:12 — with GitHub Actions Inactive
@codecov

codecov Bot commented Apr 17, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.03%. Comparing base (05e89b8) to head (68c17b7).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #287      +/-   ##
==========================================
- Coverage   90.03%   90.03%   -0.01%     
==========================================
  Files         122      122              
  Lines       61791    61811      +20     
==========================================
+ Hits        55632    55650      +18     
- Misses       6159     6161       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

nh13 added a commit that referenced this pull request Apr 17, 2026
…group

Introduces `crate::per_thread_accumulator::PerThreadAccumulator<A>`, a
sharded accumulator backed by a Vec of mutex-protected slots. Workers claim
a slot lazily via TLS-cached indices from a process-wide atomic counter,
then merge per-group results into the slot immediately. After the pipeline
completes, callers fold the slots into the final aggregate.

This generalizes the inline accumulator introduced for `fgumi group` (#285,
#287) so the same pattern can replace `SegQueue<CollectedXxxMetrics>` in
the other pipeline commands without re-implementing the slot-assignment
machinery each time.

Migrates `fgumi group` to the shared helper.
@nh13
nh13 marked this pull request as ready for review April 17, 2026 21:04
@coderabbitai

coderabbitai Bot commented Apr 17, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@nh13 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 41 minutes and 2 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 41 minutes and 2 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6d75ab11-4fa8-4937-b9db-e971956e5931

📥 Commits

Reviewing files that changed from the base of the PR and between 7b092d8 and 68c17b7.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • src/lib/commands/group.rs
📝 Walkthrough

Walkthrough

The PR changes metrics aggregation in the UMI grouping command from a global lock-free queue of per-position-group hashmaps to per-thread accumulator slots protected by mutexes. Each worker serializes directly into a stable thread-assigned accumulator; final metrics are produced by merging all accumulator slots. A changelog entry documents the memory-usage fix.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: replacing a shared queue with per-thread accumulators to cap memory usage in the group command, matching the changeset.
Description check ✅ Passed The description provides detailed context on the memory issue, the solution approach, and testing plan, directly relating to the changeset modifications.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch nh/group-oom

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nh13

nh13 commented Apr 17, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 17, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
src/lib/commands/group.rs (1)

1414-1420: Add a threaded metrics regression test.

This is the new correctness path, but the shown --metrics tests use the single-threaded fast path. Please add or parameterize a ThreadingOptions::new(...) metrics test that asserts family sizes, grouping metrics, and position-group sizes match the single-threaded output.

Also applies to: 1549-1573

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/commands/group.rs` around lines 1414 - 1420, Add a threaded
regression test variant that uses ThreadingOptions::new(...) to exercise the
multi-threaded metrics path (the code using group_thread_slot,
accumulators_clone, and the record_group call that consumes
processed.family_sizes and processed.filter_metrics) and assert that family
sizes, grouping metrics, and position-group sizes equal the existing
single-threaded (--metrics) output; implement this by parameterizing the
existing metrics test to run both single-threaded and threaded configurations
and compare their outputs, and apply the same parameterization/update for the
other metrics test block referenced around the second region (the logic near
lines 1549-1573) so both test cases validate parity between threading modes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/lib/commands/group.rs`:
- Around line 1414-1420: Add a threaded regression test variant that uses
ThreadingOptions::new(...) to exercise the multi-threaded metrics path (the code
using group_thread_slot, accumulators_clone, and the record_group call that
consumes processed.family_sizes and processed.filter_metrics) and assert that
family sizes, grouping metrics, and position-group sizes equal the existing
single-threaded (--metrics) output; implement this by parameterizing the
existing metrics test to run both single-threaded and threaded configurations
and compare their outputs, and apply the same parameterization/update for the
other metrics test block referenced around the second region (the logic near
lines 1549-1573) so both test cases validate parity between threading modes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4b39da25-ed1f-4873-875e-0190ff40244e

📥 Commits

Reviewing files that changed from the base of the PR and between 05e89b8 and a2c7b90.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • src/lib/commands/group.rs

@nh13 nh13 added enhancement New feature or request fgumi group labels Apr 18, 2026
@nh13
nh13 temporarily deployed to github-actions April 18, 2026 01:24 — with GitHub Actions Inactive
@nh13

nh13 commented Apr 18, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
src/lib/commands/group.rs (1)

2644-2770: Add one discarded template to this parity test.

This test would still pass if GroupMetricsAccumulator::record_group() stopped merging filter_metrics, because all discard counters are expected to be zero. Add a low-MAPQ or N-containing UMI case and assert the non-zero discard counter across threading modes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/commands/group.rs` around lines 2644 - 2770, The test
test_metrics_parity_across_threading_modes currently has all discard counters
asserted as zero, so it would not catch regressions in
GroupMetricsAccumulator::record_group merging of filter_metrics; modify the test
to add one extra template that will be discarded (e.g., create an additional
pair with either a low MAPQ or a UMI containing 'N') before
create_test_bam(records) and then update the grouping assertions to expect that
this template increments the appropriate discard counter (reference
grouping[0].discarded_poor_alignment or grouping[0].discarded_ns_in_umi), adjust
total_records and accepted_records accordingly while leaving total_families
unchanged, so the parity check across threading modes includes a non-zero
discard metric.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/lib/commands/group.rs`:
- Around line 2644-2770: The test test_metrics_parity_across_threading_modes
currently has all discard counters asserted as zero, so it would not catch
regressions in GroupMetricsAccumulator::record_group merging of filter_metrics;
modify the test to add one extra template that will be discarded (e.g., create
an additional pair with either a low MAPQ or a UMI containing 'N') before
create_test_bam(records) and then update the grouping assertions to expect that
this template increments the appropriate discard counter (reference
grouping[0].discarded_poor_alignment or grouping[0].discarded_ns_in_umi), adjust
total_records and accepted_records accordingly while leaving total_families
unchanged, so the parity check across threading modes includes a non-zero
discard metric.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 57859605-633f-4354-944a-fe90b8fc9397

📥 Commits

Reviewing files that changed from the base of the PR and between a2c7b90 and 7b092d8.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • src/lib/commands/group.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

The multi-threaded `group` pipeline accumulated one `CollectedMetrics`
struct per position group in a shared `SegQueue`, drained only after the
pipeline completed. Each entry held an `AHashMap<usize, u64>` with
`with_capacity(50)` (~1 KB even when empty), so a 500 M-pair run
accumulated ~10 GB just for metrics before OOMing.

Replace the `SegQueue` with an `Arc<Vec<Mutex<GroupMetricsAccumulator>>>`
sized to `num_threads`. Each worker claims a slot via a TLS-cached index
from a process-wide atomic counter and merges per-position-group counts
into that slot immediately, so retained memory is now O(threads ×
distinct family/position-group sizes) rather than O(position groups).

Closes #285.
@nh13
nh13 temporarily deployed to github-actions April 18, 2026 02:15 — with GitHub Actions Inactive
@nh13
nh13 merged commit 9bb0dd6 into main Apr 18, 2026
8 of 9 checks passed
@nh13
nh13 deleted the nh/group-oom branch April 18, 2026 02:19
@nh13 nh13 mentioned this pull request Apr 18, 2026
nh13 added a commit that referenced this pull request Apr 18, 2026
…group

Introduces `crate::per_thread_accumulator::PerThreadAccumulator<A>`, a
sharded accumulator backed by a Vec of mutex-protected slots. Workers claim
a slot lazily via TLS-cached indices from a process-wide atomic counter,
then merge per-group results into the slot immediately. After the pipeline
completes, callers fold the slots into the final aggregate.

This generalizes the inline accumulator introduced for `fgumi group` (#285,
#287) so the same pattern can replace `SegQueue<CollectedXxxMetrics>` in
the other pipeline commands without re-implementing the slot-assignment
machinery each time.

Migrates `fgumi group` to the shared helper.
nh13 added a commit that referenced this pull request Apr 18, 2026
…group

Introduces `crate::per_thread_accumulator::PerThreadAccumulator<A>`, a
sharded accumulator backed by a Vec of mutex-protected slots. Workers claim
a slot lazily via TLS-cached indices from a process-wide atomic counter,
then merge per-group results into the slot immediately. After the pipeline
completes, callers fold the slots into the final aggregate.

This generalizes the inline accumulator introduced for `fgumi group` (#285,
#287) so the same pattern can replace `SegQueue<CollectedXxxMetrics>` in
the other pipeline commands without re-implementing the slot-assignment
machinery each time.

Migrates `fgumi group` to the shared helper.
nh13 added a commit that referenced this pull request Apr 18, 2026
…lection (#290)

* refactor(pipeline): extract PerThreadAccumulator helper and adopt in group

Introduces `crate::per_thread_accumulator::PerThreadAccumulator<A>`, a
sharded accumulator backed by a Vec of mutex-protected slots. Workers claim
a slot lazily via TLS-cached indices from a process-wide atomic counter,
then merge per-group results into the slot immediately. After the pipeline
completes, callers fold the slots into the final aggregate.

This generalizes the inline accumulator introduced for `fgumi group` (#285,
#287) so the same pattern can replace `SegQueue<CollectedXxxMetrics>` in
the other pipeline commands without re-implementing the slot-assignment
machinery each time.

Migrates `fgumi group` to the shared helper.

* fix(filter): cap metric memory with PerThreadAccumulator

Replaces `Arc<SegQueue<CollectedFilterMetrics>>` with a
`PerThreadAccumulator` sized to the pipeline's worker count. Each worker
merges per-batch counts into its slot directly, eliminating the unbounded
queue that previously retained one metrics struct per batch for the life
of the run (see issue #285).

* fix(clip): cap metric memory with PerThreadAccumulator

Replaces `Arc<SegQueue<CollectedClipMetrics>>` with a
`PerThreadAccumulator`. See issue #285 for the shared pattern.

* fix(correct): cap metric memory with PerThreadAccumulator

Replaces `Arc<SegQueue<CollectedCorrectMetrics>>` with a
`PerThreadAccumulator`. Each worker merges per-batch template counts,
per-UMI match counts, and rejected records into its own slot. Rejected
records are drained by-move from the slot at pipeline end, preserving the
existing write-all-rejects-after-pipeline semantics. See issue #285.

* fix(simplex): cap metric memory with PerThreadAccumulator

Replaces `Arc<SegQueue<CollectedSimplexMetrics>>` with a
`PerThreadAccumulator`. Each worker merges per-batch consensus stats,
overlapping-correction stats, group counts, and raw reject bytes into its
own slot. Rejects buffering semantics are preserved; streaming rejects to
disk during the pipeline is tracked as a separate follow-up. See #285.

* fix(duplex): cap metric memory with PerThreadAccumulator

Mirrors the simplex change: replaces `Arc<SegQueue<CollectedDuplexMetrics>>`
with a `PerThreadAccumulator`. Rejects buffering semantics preserved.
See #285.

* fix(codec): cap metric memory with PerThreadAccumulator

Mirrors the simplex/duplex changes: replaces
`Arc<SegQueue<CollectedCodecMetrics>>` with a `PerThreadAccumulator`.
Rejects buffering semantics preserved. See #285.

* docs(changelog): note PerThreadAccumulator rollout across pipeline commands
@nh13 nh13 mentioned this pull request Apr 22, 2026

This branch was previously deployed

1 inactive deployment
github-actions — 68c17b7a Deployed Apr 18, 2026 by nh13 via coverage #1191
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request fgumi group

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant