Repository navigation
Release the GIL in NDArray slice reads - #743
Merged
Merged
Conversation
Contributor
Author
Member
|
Please, rebase. |
FabianIsensee
force-pushed
the
release-gil-slice-reads
branch
from
November 3, 2026 09:07
078cd62 to
58328e7
Compare
Contributor
Author
|
done :-) |
This was referenced Nov 3, 2026
Merged
NDArray.get_slice_numpy() decompressed with the GIL held, so threads reading separate arrays, or separate handles of one file, ran one at a time; on NFS this included the wait for the server. Release the GIL around b2nd_get_slice_cbuffer(). The GIL was also what kept two threads from using one super-chunk at the same time. Replace the per-NDArray read_lock with one reentrant lock per super-chunk, shared by the SChunk wrapper, the NDArray, its views and vlmeta, and hold it in every entry point into C. Keep the GIL when a Python postfilter, codec or filter could run during the read, and on wasm32. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01SwMZj7Wi7Sc8HtQpELyNXB
Hold the operands' handle locks while the miniexpr prefilter of a lazy expression reads their super-chunks from C. Count the reads that run without the GIL, so that exit and the registration of Python codecs and filters wait for them, and free handle locks that other threads held at a fork in the child. Lock the metalayer counters too. Skip the new tests on wasm32, shorten them, retry the GIL check on busy machines, and add tests for lazy expressions and fork. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01SwMZj7Wi7Sc8HtQpELyNXB
FabianIsensee
force-pushed
the
release-gil-slice-reads
branch
from
November 4, 2026 13:30
58328e7 to
ddd70de
Compare
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.
Hi there,
we use blosc2 in nnU-Net to read preprocessed data during training (the write side is in Blosc/c-blosc2#821). Our data loader reads random crops from .b2nd files with threads, and we noticed that throughput barely goes up with more threads, while processes scale fine. I looked into this together with Claude Opus 5.5. The reason is that
NDArray.get_slice_numpy()(behind everyarr[...]) callsb2nd_get_slice_cbuffer()with the GIL held, so threads decompress one after the other, even when each thread has its own array. On NFS it is worse because the GIL is also held while waiting for the server.Fix
Just wrapping the call in
with nogil:does not work: right now the GIL is the only thing that keeps two threads from using the same super-chunk at the same time. When I tried that I gotRuntimeErrors (read + partial write on the same array), errors fromarr.schunkchunk access and segfaults with Python postfilters. So:_handle_lock), shared by theSChunk, theNDArray, its views and vlmeta. It replaces theread_lockfrom Fix two NDArray thread-safety bugs blocking free-threaded support (#555) #713@_handle_locked).concatand the miniexpr prefilter of lazy expressions also take the locks of their operands, always in the same orderget_slice_numpyreleases the GIL while holding the lock. It keeps the GIL if a Python callback could run (postfilter, codecs/filters registered from Python) and on wasm32atexitwaits up to 1s for them beforeblosc2_destroy(), and a forked child resets locks that were held by other threadsThreads sharing one array handle are still serialized, as before. Separate handles now read in parallel.
Benchmarks
Random 512x512x3 uint8 crops (one crop per chunk, LZ4HC), one handle per thread, MB/s, main -> this PR.
Local disk, page cache warm (Ryzen 9 7950X, 16 cores):
NFS, cold (client cache dropped so every read goes to the server; 2x Xeon Gold 6330):
We only tested NFS, but nothing in the fix is NFS specific. With cold data the gain comes from threads overlapping their waits for the storage instead of holding the GIL while waiting. So I would expect the same for other network filesystems (SMB, Lustre, GPFS, sshfs, ...) and slow disks. How much it helps probably depends on how many requests the client can have in flight (our NFS mount uses
nconnect=16).The lock costs +0.15 to +0.4 µs per method call (e.g.
schunk.get_chunk0.10 -> 0.25 µs) andblosc2.open()goes from 45 to 48 µs. Reading and writing actual data and lazy expressions are unchanged within noise.Gotchas
locking=Truefixes itread_lockfrom Fix two NDArray thread-safety bugs blocking free-threaded support (#555) #713 had the same issueget_slice_numpyreleases the GIL. Other read paths (decompress_chunk, orthogonal/sparse selections, ...) are locked but keep the GIL. Easy to extend if you want thatshape,nchunks& co are not locked and can be stale for a moment if the file is changed through another handleTests
tests/ndarray/test_concurrent_access.py(12 tests, ~5s, skipped on wasm32): one handle with a reader plus a writer / chunk access / vlmeta / dparams / resize in another thread, Python postfilters, lazy expressions,concatin both orders, fork while another thread holds a lock. Most of them fail or crash with a plainwith nogil:b2viewHTTP tests that also fail on main (missing optional module)AI disclosure: I used Claude Code (Anthropic) for the investigation, the code, the tests and a first draft of this description.
Best,
Fabian