Repository navigation
Replace Blosc2 with zstd-1 and decode chunks in parallel on read - #68
Merged
Merged
Conversation
Blosc2+LZ4 compresses the IVCSR packed_indices stream by ~1.00x on real files (the delta+varint bytes have no repeats for an LZ codec to find), while zstd's entropy coding gets ~1.15-1.5x. zstd at level 1 is now the default for every numeric array written to .h5ad and zarr. HDF5 runs a dataset's filter pipeline one chunk at a time on one thread, so reads are decode-bound. read_array() looks up every chunk's file offset in one chunk_iter pass, preads the compressed chunks, and decodes them with numcodecs.Zstd on a thread pool sized by numba's thread count. VCS/IVCS arrays read through the anndata registry and the rapid loader both use it. Anything else -- Blosc2 files from earlier versions, uncompressed or contiguous datasets, unwritten chunks, files open for writing or on non-sec2 drivers -- falls back to an ordinary h5py read. read_h5ad now imports hdf5plugin itself: in a fresh process it previously failed on compressed obs/var columns, which are read before X. Claude-Session: https://claude.ai/code/session_01QBeTZy6BKKGKGJ7fwvkWgM
This was referenced Oct 14, 2026
Merging main into this branch kept this branch's _rapid_load.py, which re-added load_and_normalize, _read_ivcsr_group and _validate_min_cells after #66 removed them -- without the numba kernels and imports they call, so ruff flagged 19 undefined names and calling it would raise NameError. Take main's version. load_packed already reads X through the anndata registry, which uses the parallel reader, so the only change this branch needs here is the hdf5plugin comment. Claude-Session: https://claude.ai/code/session_01QBeTZy6BKKGKGJ7fwvkWgM
h5py's automatic chunking gave the VCS arrays ~64-330 KiB chunks, and the parallel reader pays a fixed, GIL-holding Python cost per chunk, so on lupus (1.26M cells) reading packed_indices took 0.33 s instead of the ~0.08 s the zstd decode itself needs. numeric_only_compression now gives compressed 1-D HDF5 arrays larger than one chunk ~1 MiB chunks (unless the caller picked chunks), and read_array hands each thread one contiguous run of chunks and decodes full chunks straight into the output. On lupus: packed_indices 1.21 s (Blosc2) -> 0.095 s, value_ptr 0.10 s -> 0.031 s, load_packed 3.74 s -> 2.76 s, at the same zstd ratio. Claude-Session: https://claude.ai/code/session_01QBeTZy6BKKGKGJ7fwvkWgM
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.
Summary
zstd level 1 replaces Blosc2+LZ4 as the default compression for every numeric array written by
write_h5ad(viahdf5plugin.Zstd) andwrite_zarr(zarr's/numcodecs' zstd codec). There is no option to switch back;dataset_kwargsstill overrides it as before..h5adreads now decode in parallel. HDF5 runs a dataset's filter pipeline one chunk at a time on a single thread. The new_compression.read_array():chunk_iterpass;os.pread;numcodecs.Zstd, which releases the GIL, on a thread pool sized bynumba.get_num_threads(), giving each thread one contiguous run of chunks.Every VCS/IVCS array read through the anndata registry (
read_h5ad,load_packed,anndata.io.read_elem) uses it.Large arrays are written in 1 MiB chunks.
numeric_only_compressionnow gives compressed 1-D HDF5 arrays larger than one chunk ~1 MiB chunks, unless the caller picked chunks. h5py's automatic 64–330 KiB chunks made the reader's per-chunk Python overhead dominate. The zstd ratio is unchanged from 64 KiB to 4 MiB chunks.Blosc2 files still read. Anything the parallel path doesn't handle falls back to an ordinary h5py read: Blosc2 files from earlier versions, uncompressed or contiguous datasets, zstd combined with another filter, chunks that were never written, files open for writing, and non-
sec2drivers.Fixes
read_h5adin a fresh process. It never importedhdf5plugin, so reading compressedobs/varcolumns (which happens beforeX) failed unless something else had already loaded the plugin.numcodecsis now a direct dependency. It was already installed via zarr.Results
Recompressed with this branch, all 15 files in
/opt/IVCSCwent from 18.96 GB to 14.99 GB (1.26×). Onpacked_indices, Blosc2/LZ4 got ~1.00× on every file, while zstd-1 gets 1.1–1.5× through entropy coding of the mostly single-byte varints. Higher zstd levels gain nothing.Warm-cache reads, Blosc2 file → zstd-1 file:
packed_indicesreadload_packedWhat's left of
load_packedis mostly anndata parsingobs/obsmplus index decoding, which this PR doesn't touch. Subsetting cells during decode is planned in #67, which also closes the design question in #24.Merge fix
Merging
main(#66) into this branch kept this branch's_rapid_load.py, re-addingload_and_normalizeand its helpers without the kernels and imports they call: 19 undefined names underruff, and aNameErrorif called. cca3f93 restoresmain's file. The only change this PR needs there is the plugin-import comment, sinceload_packedalready readsXthrough the registry.Test plan
tests/test_parallel_read.py:core) files, and zarr arrays;write_h5ad→read_h5adtaking the parallel path;read_h5adin a fresh subprocess (fails without thehdf5pluginimport fix);tests/test_compression.py: 1 MiB chunking for large 1-D arrays; small arrays, explicit chunks and 2-D arrays are left alone; zstd filter/codec defaults.ruff check,ruff format --check,codespellandty checkare clean./opt/IVCSCfile was verified against its Blosc2 original, before and after being overwritten in place. The check covers every group, attribute, dataset, shape, dtype and value, and the new files are read throughread_array.https://claude.ai/code/session_01QBeTZy6BKKGKGJ7fwvkWgM