Skip to content

Store lossless qualities in rANS blocks by default - #115

Merged
mvassilev merged 1 commit into
developfrom
feature/rans-qualities
Oct 18, 2026
Merged

mvassilev merged 1 commit into
developfrom
feature/rans-qualities

Conversation

@mvassilev

Copy link
Copy Markdown
Collaborator

Lossless qualities were a string column compressed by zstd, 109.3 MiB of the 478.2 MB file on chr20 of NA12878 at 30x; -fqzcomp put them into the blocks of 10,000 records instead, compressed with fqzcomp. On this NovaSeq data, whose qualities are binned to four values, fqzcomp saves 9.1% of the file but takes three times the CPU to read the qualities, half of it in building its 68 MB model for every block. CRAM 3.1's default profile does not use fqzcomp; it compresses its quality series with rANS Nx16 at order 1. A probe over the same blocks gave 72.4 MB for rANS order 1 with RLE and PACK, against 71.6 MB for fqzcomp and 72.7 MB for CRAM 3.1's quality series, and decoded 25 times faster than fqzcomp.

Lossless qualities now go into the blocks whatever the quality policy, compressed with rANS order 1 with RLE and PACK. The first byte of a block says its codec; a rANS block holds the lengths of its qualities, as varints compressed the same way (0.03 MB on chr20), then the qualities. -fqzcomp passes the new kFqzcomp in the policy and gets fqzcomp blocks, as before plus the codec byte; kQualInBlock now only says that a record's quality is in its block. The format version becomes 8.

On all of chr20 the file goes from 478.2 to 435.7 MB from SAM and from 477.7 to 435.3 MB from BAM, against develop acb8904 built the same way. The md5 of ramdump -h is the input's for SAM and develop's for BAM, and flagstat and idxstats print the same. On the 2 million record subset, medians of five alternating runs on a loaded machine, conversion takes 25.1 against 28.3 s of CPU on one thread, 29.3 against 31.2 s with -threads 4 and 34.5 against 37.4 s from BAM, at the same peak memory. Reading every quality costs CPU in the rANS decoder, built without SIMD: ramdump -h takes 8.0 against 6.9 s of CPU and 6.3 against 5.8 s wall on the subset, and 44.7 against 36.6 s of CPU and 32.5 against 30.0 s wall on chr20. Regions of 500 to 2,000 bp take 0.39 to 0.50 s on both sides, and ramdump -c and flagstat take the same time.

Lossless qualities were a string column compressed by zstd, 109.3 MiB of the
478.2 MB file on chr20 of NA12878 at 30x; -fqzcomp put them into the blocks of
10,000 records instead, compressed with fqzcomp. On this NovaSeq data, whose
qualities are binned to four values, fqzcomp saves 9.1% of the file but takes
three times the CPU to read the qualities, half of it in building its 68 MB
model for every block. CRAM 3.1's default profile does not use fqzcomp; it
compresses its quality series with rANS Nx16 at order 1. A probe over the same
blocks gave 72.4 MB for rANS order 1 with RLE and PACK, against 71.6 MB for
fqzcomp and 72.7 MB for CRAM 3.1's quality series, and decoded 25 times faster
than fqzcomp.

Lossless qualities now go into the blocks whatever the quality policy,
compressed with rANS order 1 with RLE and PACK. The first byte of a block says
its codec; a rANS block holds the lengths of its qualities, as varints
compressed the same way (0.03 MB on chr20), then the qualities. -fqzcomp passes
the new kFqzcomp in the policy and gets fqzcomp blocks, as before plus the
codec byte; kQualInBlock now only says that a record's quality is in its
block. The format version becomes 8.

On all of chr20 the file goes from 478.2 to 435.7 MB from SAM and from 477.7
to 435.3 MB from BAM, against develop acb8904 built the same way. The md5 of
ramdump -h is the input's for SAM and develop's for BAM, and flagstat and
idxstats print the same. On the 2 million record subset, medians of five
alternating runs on a loaded machine, conversion takes 25.1 against 28.3 s of
CPU on one thread, 29.3 against 31.2 s with -threads 4 and 34.5 against 37.4 s
from BAM, at the same peak memory. Reading every quality costs CPU in the rANS
decoder, built without SIMD: ramdump -h takes 8.0 against 6.9 s of CPU and 6.3
against 5.8 s wall on the subset, and 44.7 against 36.6 s of CPU and 32.5
against 30.0 s wall on chr20. Regions of 500 to 2,000 bp take 0.39 to 0.50 s
on both sides, and ramdump -c and flagstat take the same time.
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.65432% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.69%. Comparing base (acb8904) to head (2a0094e).

Files with missing lines Patch % Lines
src/ramcore/RecordBlocks.cxx 86.20% 5 Missing and 7 partials ⚠️
test/record_blocks_test.cxx 95.00% 2 Missing and 1 partial ⚠️
tools/samtoramntuple.cxx 0.00% 2 Missing ⚠️
test/parallel_conversion_test.cxx 80.00% 0 Missing and 1 partial ⚠️
test/ramcoretests.cxx 85.71% 0 Missing and 1 partial ⚠️
tools/bamtoramntuple.cxx 0.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop     #115      +/-   ##
===========================================
- Coverage    88.69%   88.69%   -0.01%     
===========================================
  Files           25       25              
  Lines         3115     3220     +105     
  Branches      1050     1103      +53     
===========================================
+ Hits          2763     2856      +93     
- Misses         188      194       +6     
- Partials       164      170       +6     
Flag Coverage Δ
unittests 88.69% <87.65%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
inc/ramcore/RecordBlocks.h 100.00% <ø> (ø)
inc/rntuple/RAMNTupleRecord.h 100.00% <ø> (ø)
test/parallel_conversion_test.cxx 98.06% <80.00%> (-0.63%) ⬇️
test/ramcoretests.cxx 99.80% <85.71%> (-0.20%) ⬇️
tools/bamtoramntuple.cxx 44.87% <0.00%> (ø)
tools/samtoramntuple.cxx 43.47% <0.00%> (ø)
test/record_blocks_test.cxx 95.53% <95.00%> (-2.03%) ⬇️
src/ramcore/RecordBlocks.cxx 90.07% <86.20%> (+0.29%) ⬆️
Files with missing lines Coverage Δ
inc/ramcore/RecordBlocks.h 100.00% <ø> (ø)
inc/rntuple/RAMNTupleRecord.h 100.00% <ø> (ø)
test/parallel_conversion_test.cxx 98.06% <80.00%> (-0.63%) ⬇️
test/ramcoretests.cxx 99.80% <85.71%> (-0.20%) ⬇️
tools/bamtoramntuple.cxx 44.87% <0.00%> (ø)
tools/samtoramntuple.cxx 43.47% <0.00%> (ø)
test/record_blocks_test.cxx 95.53% <95.00%> (-2.03%) ⬇️
src/ramcore/RecordBlocks.cxx 90.07% <86.20%> (+0.29%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions github-actions 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.

clang-tidy made some suggestions

// fqzcomp and the name tokeniser take char, rANS unsigned char.
unsigned char *AsBytes(char *p)
{
return static_cast<unsigned char *>(static_cast<void *>(p));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: do not cast 'char *' to 'unsigned char *' through 'void *' [bugprone-casting-through-void]

   return static_cast<unsigned char *>(static_cast<void *>(p));
                                       ^

@mvassilev
mvassilev merged commit 5b61106 into develop Oct 18, 2026
4 checks passed
@mvassilev
mvassilev deleted the feature/rans-qualities branch October 18, 2026 08:48
mvassilev added a commit that referenced this pull request Oct 18, 2026
The sizes and timings in CHANGELOG.md were measured with scripts that lived
on one desktop, on data made by hand, so nobody else could check them or
measure them on another machine.

bench/ramtools-bench.py does it all from a clone. fetch reads chr20 of NA12878
from the 1000 Genomes 30x CRAM at EBI and chr20 of the GRCh38 reference,
checks them against the M5 of chr20 and the md5 of the 17,705,654 records, and
writes the SAM, BAM and CRAM 3.1 files and subsets of 50,000 and 2 million
records, without @pg lines, so every machine gets the same files. run builds
two git revisions, or the working tree, the same way, and measures conversion
from SAM on one thread and with -threads, conversion from BAM, the whole file,
a large and three small regions, a count, flagstat and idxstats, in
alternating order, with wall time, CPU time and peak memory. It does the same
reads with samtools on the BAM and the CRAM as the yardstick, checks every
output against the input or samtools, and saves the report as Markdown and
JSON. --pin keeps a run on one NUMA node. bench/README.md says what the script
needs, how to run it and how to read the results.

Tried on macOS: check; fetch of an 8 Mb slice of chr20 in 1 min 47 s; and
run on 50,000 records, where all seven checks hold for develop before and
after #115.
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.

2 participants