feat(bam): --outBAMsortingBinsN, spilling the coordinate sort to disk bins - #263
feat(bam): --outBAMsortingBinsN, spilling the coordinate sort to disk bins#263BenjaminDEMAILLE wants to merge 6 commits into
Conversation
`--limitBAMsortRAM` was a threshold the sort died on, not a bound it respected:
every record stayed resident until `finish()`, and exceeding the limit aborted
the run with a message telling the user to raise it or give up on sorting.
Now, when a bound is set and the buffer exceeds it, records are partitioned by
reference sequence into `--outBAMsortingBinsN` bins written to temporary BAMs,
then read back one bin at a time, sorted and appended. Because the bins are
coordinate-disjoint and already in order relative to one another, no k-way
merge is needed. Only one bin is resident during the sort, so peak usage is the
largest bin rather than the whole run. Unmapped records sort last and get their
own bin.
Spilling is off unless it buys something: it needs both a non-zero
`--outBAMsortingBinsN` and a `--limitBAMsortRAM` that the estimate actually
exceeds. Without a bound there is nothing to respect and temporary files would
be pure cost. `--outBAMsortingBinsN 0` disables it outright.
The bin files are written uncompressed: they are read back immediately, so
compressing them would cost time and save nothing.
Verified on 1161 records that the binned and in-memory paths produce
byte-identical decoded BAM:
in-memory b52a23436e3e939e98160dffed89c1448c5da4da
binned b52a23436e3e939e98160dffed89c1448c5da4da
An earlier version of this partitioned in memory, which bounded the sort's
working set but not residency — every bucket was live at once. The comment
claiming reduced peak usage would have been false, so it spills for real.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The test asserted the old contract: exceeding the bound was fatal. With binning it is not, because the sort can now respect the bound instead of dying on it. Both halves are covered: with bins available the sort succeeds, and with --outBAMsortingBinsN 0 there is no way to honour the bound, so the run still stops rather than quietly using more memory than it was allowed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The binned path may frame BGZF blocks differently; what was measured and what holds is that the decoded records are identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…r the rebase main gained an accepted-but-inert copy of the flag in the CLI-parity work; this branch implements it, so the inert declaration and its ACCEPTED_BUT_INERT note both go.
`--outBAMsortingBinsN` spilled the coordinate sort to disk bins, but only from `finish()`. `write_batch` accumulated every record with no bound check, so by the time the first bin was written the whole run was already resident and the peak had been reached. The bound was respected during the sort and nowhere else, which is not where the memory goes. Measured on 560k records (yeast, 17 references, `--limitBAMsortRAM 100000000`), peak RSS: in-memory sort 1980 MB, spill-at-finish 1967 MB. The feature was buying nothing. Open the bins on the first crossing of the bound and push the buffer out to them as batches arrive, so what stays resident is one buffer rather than the run. `finish` then closes the bins it already has instead of building them from a full buffer. Peak RSS, same flags: | records | spill at finish | incremental | |---|---|---| | 560k | 1967 MB | 1744 MB | | 1.3M | 2925 MB | 2254 MB | The gap widens with record count, which is the property that was missing: the buffer no longer scales with the run. Peak is not flat, because the index, the alignment pipeline's own batches and the allocator's retained pages all sit underneath it, and none of those are affected by this. Output is unchanged: 561,312 decoded records identical to the in-memory sort, and the no-bins default path is untouched. Tests pass, 0 clippy warnings, fmt clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Closing this as a duplicate of #214, which I should have found before opening it. #214 has been open since 2026-08-06 and solves the same problem better:
The measurement I took here is the one useful thing to carry over: the existing Also worth noting for #214: Sorry for the noise. |
Six commits. The first five are the existing
bd/bam-sort-binswork, which was finished and pushed but had no PR open. The sixth fixes a gap I found while validating it.The feature
--limitBAMsortRAMwas a threshold the sort died on rather than a bound it respected: every record stayed resident untilfinish(), and exceeding the limit aborted the run telling the user to raise it or give up on sorting. This partitions records by reference sequence into--outBAMsortingBinsNtemporary BAMs, then reads them back one bin at a time. The bins are coordinate-disjoint and already ordered relative to each other, so no k-way merge is needed.The gap, and the fix
Spilling only happened from
finish().write_batchaccumulated every record with no bound check, so by the time the first bin was written the whole run was already resident and the peak had been reached. The bound was respected during the sort and nowhere else, which is not where the memory goes.Measured, 560k records, yeast (17 references),
--limitBAMsortRAM 100000000:The feature was buying nothing. The last commit opens the bins on the first crossing of the bound and pushes the buffer out as batches arrive:
The gap widens with record count, which is the property that was missing: the buffer no longer scales with the run. Peak is not flat, and the commit says so: the index, the alignment pipeline's own batches, and the allocator's retained pages sit underneath it and are untouched by this.
Caveat worth stating
Binning is by reference sequence, so the benefit scales with reference count. On a single-contig genome every mapped record lands in one bin and this buys nothing (that is how I first mis-measured it, on a 1-chromosome test index). Human, with its 24+ references, is the case it is for.
Correctness
--limitBAMsortRAM 100000000for this input; this completes.cargo fmt --checkclean.Independent of #256 / #257 / #261; touches only
src/io/bam.rsandsrc/params/mod.rs.🤖 Generated with Claude Code