Skip to content

Add NDArray.read_into(): decompress a selection into an existing array - #762

Merged
FrancescAlted merged 2 commits into
Blosc:mainfrom
FabianIsensee:read-into
Nov 5, 2026
Merged

FrancescAlted merged 2 commits into
Blosc:mainfrom
FabianIsensee:read-into

Conversation

@FabianIsensee

@FabianIsensee FabianIsensee commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Hi Francesc,

Depends on: b2nd_get_slice_cbuffer_into() from the c-blosc2 PRs Blosc/c-blosc2#844 and Blosc/c-blosc2#845, both merged. The last commit bundles c-blosc2 main e3ff64f2 until the next c-blosc2 release and raises BLOSC2_MIN_VERSION to 3.5.1, since a system c-blosc2 3.5.0 lacks the function. Rebased onto main after #743 and the other merges of today.

arr[key] always returns a new array. Code that needs the data in an existing array pays for a temporary, its page faults and one more copy: the interior of a padded patch (CNN training on images smaller than the patch), one item of a batch, a reused or pinned buffer for GPU transfers. NumPy has out=, h5py has Dataset.read_direct(), zarr has get_basic_selection(..., out=); python-blosc2 only has the undocumented get_slice_numpy(), which needs a C-contiguous buffer of exactly the slice's shape.

NDArray.read_into(out, key=None, casting="same_kind") decompresses arr[key] into the NumPy array out:

  • For basic keys (integers, slices with step 1, Ellipsis), out of the array's dtype, and out whose last axis is contiguous and whose other strides are whole multiples of the next one, the data goes straight into out through b2nd_get_slice_cbuffer_into(). That covers C-contiguous arrays and every box of one, also with steps in the outer axes. Nothing outside out is touched. The GIL is released meanwhile, as in the other slice reads.
  • Any other layout (Fortran order, transposed, steps along the last axis), key (steps, integer or boolean arrays) or dtype (casting as in numpy.copyto) goes through arr[key] and one copy, so any writeable NumPy array works.
  • out must have the selection's shape (no broadcasting). It returns out.

It is documented in the NDArray reference next to __getitem__, with examples.

Benchmarks

Ryzen 9 7950X, warm, on disk, median of 15: a (4, 132, 162, 133) float32 case into the interior of a (4, 160, 192, 160) patch (fresh per call), against patch[interior] = a[...]: 16.1 -> 12.2 ms with 1 thread, 16.1 -> 11.8 ms with 8. Into one item of a batch or into a reused buffer it is not faster than batch[j] = a[key] (0.85-1.0x with 1 thread, 1.0x with 8): there the temporary is cheap, as malloc reuses it and it stays in cache; the gain is in avoiding it where it is not. Per call it costs no more than a[key]: an 8x8x8 crop into a reused buffer takes 12.4 us, against 13.0 us for buf[...] = a[key]. With nnU-Net's data loader on BraTS (cases smaller than the patch), a prototype cut the time per sample by 22% and its CPU time by 23%.

Tests

tests/ndarray/test_read_into.py: in memory, contiguous and sparse frames on disk, and mmap; six dtypes and eleven keys into contiguous out (checked to bypass __getitem__); interiors of padded arrays, batch items, channels and strided views with nothing outside out changed; Fortran-order, transposed, last-axis-step and negative-stride out; other keys (steps, index arrays also as blosc2 arrays, masks, fields) and dtypes, casting; empty and 0-d selections; errors; shape changes made by another handle (SWMR), also with an out of the old shape; threads with one handle each; and 240 random keys into random views of larger arrays against NumPy. Full suite after the rebase: 10634 passed; the 2 b2view HTTP tests fail without aiohttp, as on main.

AI disclosure: I used Claude Code (Anthropic) for the code, the tests and a first draft of this description, and Claude Code opened this PR on my behalf. I reviewed and tested everything.

Best,
Fabian

🤖 Generated with Claude Code

arr[key] always returns a new array, so code that needs the data in an
existing array (the interior of a padded patch, one item of a batch, a
pinned or reused buffer) pays for a temporary, its page faults and one more
copy. read_into(out, key) decompresses arr[key] straight into `out`, a
NumPy array of the selection's shape. For basic keys (integers, slices
with step 1, Ellipsis) and `out` of the array's dtype whose last axis is
contiguous and whose other strides are whole multiples of the next one, i.e.
any C-contiguous array or box of one, also with steps in the outer axes,
the data goes straight to `out` through b2nd_get_slice_cbuffer_into() and
nothing outside `out` is touched; the GIL is released meanwhile as in the
other slice reads. Any other layout, key or dtype (`casting` as in
numpy.copyto) goes through arr[key], so every writeable NumPy array works.

As in the fast path of __getitem__, basic keys are resolved in Cython
against the cached shape, and the read is retried if it finds that another
handle changed the shape (SWMR), so small reads cost no more per call than
arr[key].

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SwMZj7wsgYdoCR5uvYPwEo
Comment thread CMakeLists.txt Outdated
set(BLOSC2_BUNDLED_VERSION v3.5.0)
# TEMPORARY: build against the companion c-blosc2 PR branch until it is merged and released
set(BLOSC2_BUNDLED_REPOSITORY https://github.com/FabianIsensee/c-blosc2)
set(BLOSC2_BUNDLED_VERSION 64a3aa6f9a54082ba7df0d79291953610aa2e482)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This has been merged already in c-blosc2 repo. Feel free to update this at your convenience.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks! Done: the pin now points to c-blosc2 main e3ff64f2 (#844, #845 and the short-rows copy), and BLOSC2_MIN_VERSION is 3.5.1, because a system c-blosc2 3.5.0 lacks b2nd_get_slice_cbuffer_into() (main reports 3.5.1.dev, which pkg-config accepts). Once the next c-blosc2 release is out, the pin can become its tag.

read_into() needs b2nd_get_slice_cbuffer_into() (c-blosc2 #845), which is
on c-blosc2 main but not in a release yet. Pin the bundled c-blosc2 to the
current main commit until the next release, and require at least 3.5.1
when building against a system c-blosc2 (3.5.0 lacks the function; main
reports 3.5.1.dev, which pkg-config accepts).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@FrancescAlted

Copy link
Copy Markdown
Member

LGTM. Thanks @FabianIsensee !

@FrancescAlted
FrancescAlted merged commit d3bca9e into Blosc:main Nov 5, 2026
37 checks passed
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