Repository navigation
bamRemoveDuplicates: stream the BAM cluster by cluster (peak RSS 6.4 GB to 10 MB on 7.6M records, identical bytes) - #363
Open
BenjaminDEMAILLE wants to merge 1 commit into
Open
BenjaminDEMAILLE wants to merge 1 commit into
BenjaminDEMAILLE wants to merge 1 commit into
Conversation
bam_remove_duplicates read the whole BAM into memory before collapsing anything. It now writes a cluster's records as soon as the next record falls outside it, so memory follows the largest cluster. The output is byte-identical. The fields of DedupRec that only served the old second pass (pos, nh, mate_pos) are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 2, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #354 (item 25).
Not stacked; based on
main.Problem
bam_remove_duplicatesread every record of the input into memory, then grouped clusters, then wrote everything. On a 7.6 million record BAM (175 MB) that is 6.4 GB of RSS, 36 times the file and about four times what STAR uses on the same input.Change
The first loop now does all of it. When a record falls outside the current cluster (another reference, or past the cluster's last mate),
write_clustercollapses that cluster, writes all its records in input order and clears the buffers. The tests for a boundary are the same as before, against the same values, applied to the record as it arrives instead of in a second pass, and every record since the cluster began is written at that point, multimappers included. So the bytes do not change.collapse_groupis untouched.DedupReclosespos,nhandmate_pos, which only the second pass read.What changes for a user
NHorAStag is still a fatal error with the same message, and exit status is still non-zero. ButProcessed.out.bamnow holds the clusters that ended before the bad record, and it looks finished: on a test BAM with a bad 11th record,mainleft 0 bytes and this branch leaves 8 records that passsamtools quickcheck, both with exit status 1. A pipeline that ignores the exit status would take it for a complete file. Removing the file on error would be a small change, if you prefer that; I kept this PR to what was asked. A file with several bad records can report a different one first:NHused to be checked on the whole file before anyAS.Measured
Same machine, same input, no
--runThreadN. macOS arm64,/usr/bin/time -l, maximum resident set size. The machine was loaded (load average 65 to 120), so wall-clock numbers are only a rough guide; I make no claim about speed.mainand branch are binaries built from32fbbc1and from this branch.Peak RSS
mainThe numbers are for
UniqueIdentical;UniqueIdenticalNotMultidiffers by less than 0.4 MB on every paired-end row. Wall clock on the 7.6M row was 13.2 s against 12.8 s (UniqueIdentical) and 20.1 s against 18.7 s (UniqueIdenticalNotMulti).Byte-identical to
mainsamtools view -h --no-PG | md5.--no-PGbecause without it samtools adds its own command line, with the file path, to the header. Thefile md5column is the md5 ofProcessed.out.bamitself, which is identical too, compression included.mainUniqueIdentical125a2a220682125a2a220682UniqueIdenticalNotMulti9199a8cdba8b9199a8cdba8bUniqueIdenticalfb291205bbe8fb291205bbe8UniqueIdenticalNotMultic7f5962c7777c7f5962c7777UniqueIdenticalfd4b061a3090fd4b061a3090UniqueIdenticalNotMulti732ef6625816732ef6625816UniqueIdenticale74c065bb5ece74c065bb5ecUniqueIdenticalNotMultia7ee2b9a88d0a7ee2b9a88d0UniqueIdentical,--bamRemoveDuplicatesMate2basesN 2071581f5b4d0071581f5b4d00UniqueIdentical1dfa11a087411dfa11a08741Against STAR 2.7.11b (
--runMode inputAlignmentsFromBAM)Observed output, source not read. Homebrew's STAR. This is the same on
main: this PR changes none of it.UniqueIdenticalview -hmd5125a2a220682UniqueIdenticalNotMulti9199a8cdba8bUniqueIdenticalUniqueIdenticalNotMultiUniqueIdenticalUniqueIdenticalNotMultiUniqueIdenticalUniqueIdenticalNotMultiUniqueIdentical,--bamRemoveDuplicatesMate2basesN 20The duplicate counts are always equal, and the differences are of one kind. I grouped the pairs by the dedup key (both mates' flag, soft-clip-folded position and CIGAR) and compared the pair each tool kept: in every group that differs, in the four sets checked (seed 2 and seed 3 with
UniqueIdentical, STAR-aligned with both types), the two kept pairs have the sameAS. So rustar-aligner and STAR agree on what is a duplicate and on the best score, and differ on which of several equally scored pairs survives. rustar-aligner takes the first in a stable sort. STAR's choice looks like an artefact of its sort, but that is a guess from the output; I have not read the source and did not find out what order it uses. This is an existing parity gap and is not part of this PR. The mate-2 row was not put through the grouping check, because that key would need the mate's SEQ as well.The STAR-aligned BAM: 8,000 simulated templates (1 to 3 copies each, 20 % of reads with one changed base) on a random 20 kb genome, aligned by STAR with
--outSAMtype BAM SortedByCoordinate --outFilterMismatchNmax 3: 25,498 records, 8 of them multimappers.Reproducing the generated BAMs
bam_gen.py: coordinate-sorted paired-end SAM with PCR duplicates, soft clips, splices and multimappersThe single-end input is this with seed 4 and 1,000,000 templates, keeping lines without the
128flag bit and settingFLAGto 0 or 16,RNEXTto*, andPNEXTandTLENto 0.Tests
In
src/bam_dedup.rs. There was no test ofbam_remove_duplicatesas a whole before this.clusters_are_collapsed_and_written_in_input_order: an in-memory BAM with two duplicate pairs and a multimapper in one cluster, a later cluster, and the same coordinates on another reference. It checks each record's flag and the order, withmark_multion and off.a_missing_tag_leaves_the_earlier_clusters_written: the error is returned, and the output holds the cluster that had ended and not the one in progress. This is also the test that fails if the function goes back to reading everything first.cargo test --release(815 passed),cargo clippy --all-targetsandcargo fmt --checkare clean on macOS.Not checked
test/parity_suite.shwas not run. It does not cover this run mode as far as I can tell fromgrep: nothing undertests/ortest/passes--bamRemoveDuplicatesType.--inputBAMfile -) was not run end to end. It calls the same function.🤖 Generated with Claude Code