Repository navigation
Keep the GIL in span reads of chunks that can run Python codecs or filters - #775
Open
FabianIsensee wants to merge 2 commits into
Open
FabianIsensee wants to merge 2 commits into
FabianIsensee wants to merge 2 commits into
Conversation
destroy() waited only for the NDArray slice reads that run without the GIL before blosc2_destroy() frees the global context and the thread pools. The calls that set_releasegil(True) runs without the GIL (compress(), compress2(), decompress2(), vlcompress(), the SChunk writes and to_cframe()) were not counted, so a daemon thread inside blosc2.compress() at exit kept compressing with the freed global context and crashed (SIGSEGV in 4 of 5 runs with 4 MiB blocks). Count all of them with one pair of helpers, _nogil_enter() and _nogil_exit(), so that destroy(), register_codec() and register_filter() wait for every one and keep new ones from releasing the GIL meanwhile. The counter still only changes with the GIL held. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiPFztdpnKMJkzrjT36BuP
NDArray.get_1d_span_numpy() and the native span readers of graph executions (both from the improved-miniexpr merge) decompress without the GIL whatever codec or filter the chunk uses. For a codec or filter registered from Python, the callbacks then run Python without the GIL and the interpreter crashes (SIGSEGV), while a slice read of the same chunk keeps the GIL and works. Keep the GIL while anything is registered from Python, as _decompress_ctx() does, and decompress such spans with one thread: C-Blosc2's worker threads never hold the GIL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiPFztdpnKMJkzrjT36BuP
This was referenced Nov 6, 2026
Release the input buffer when SChunk(data=...) fails; test the prefilter writes without the GIL
#777
Open
This branch has not been deployed
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 Francesc,
a small follow-up to the improved-miniexpr merge (#741). It is based on #772 (it changes lines that #772 touches), so please look at it after #772 (only the last commit, 3c1f4c3, is new here; the first one is #772); I will rebase it once #772 is merged.
NDArray.get_1d_span_numpy()(used by the indexing planner and the storage readers) and the native span readers of graph executions (NativeGraphSchedule.execute_sources(..., native_spans=True)) decompress without the GIL, whatever codec or filter the chunk uses. With a codec or filter registered from Python, its callbacks then run Python without the GIL and the interpreter crashes:The same happens with a filter registered from Python, with
nthreads=4for the span, and in a graph execution with native span readers over such an array (all SIGSEGV on main 5ff9823 and on #772). A slice read of the same chunk keeps the GIL (_can_release_gil()) and works.What this PR does (
blosc2_ext.pyx): while any codec or filter is registered from Python (the C flag_python_ids_registered, the same check thatdecompress2()uses),get_1d_span_numpy()decompresses with the GIL and with one thread (C-Blosc2's worker threads never hold the GIL), and graph executions with native span readers keep the GIL. Without registrations nothing changes. I did not use_can_release_gil(): it also keeps the GIL for postfilters, which take the GIL themselves since #741, also on worker threads when a span uses several threads, so those threads would wait for a GIL that the calling thread holds.Tests (
tests/test_span_reads_python_callbacks.py, each case in a child interpreter, skipped on WASM): span reads of a Python codec and of a Python filter with 1 and 4 threads, and graph executions with native span readers over both (skipped where the build has no native graph readers). All 6 fail (crash) on main and on #772 and pass here. Full suite: 11005 passed (main 10997), same 2 failures as on main (b2view http tests that need aiohttp).How this relates to #774: #774 keeps operands whose cparams use a Python codec or filter away from the paths that decode them on C-Blosc2's worker threads (the miniexpr, matmul and Menudet prefilters and the parallel native-graph route, via
_reads_need_python()), where holding the GIL in the caller would not help. This PR covers the two entry points that decode on the calling thread,get_1d_span_numpy()(also used by the storage readers of the remaining graph routes) andexecute_sources(..., native_spans=True), by keeping the GIL there. The two merge cleanly and do not overlap: the tests of this PR still crash with #774 alone, and #774's lazy-expression cases pass without this PR. The check here is registry-wide, as in_decompress_ctx(); once #774 (or #769, with its chunk-header test_chunk_runs_python()) is in, it could become per array or per chunk, so that registering a Python codec does not serialize span reads of unrelated arrays.Perf: no change without registrations. 1024-item span reads, wheels built with GCC 14, 6 interleaved rounds: 1.597 vs 1.628 us (1 thread) and 4.075 vs 4.125 us per read (4 Python threads) on main, within the per-round spread.
I used Claude Code (Anthropic) to find and reproduce the crash, write the change and the tests, and run the benchmarks; I reviewed the result.
Best,
Fabian
🤖 Generated with Claude Code
https://claude.ai/code/session_01DiPFztdpnKMJkzrjT36BuP