Repository navigation
BUG: Release the Python GIL during pipeline updates - #7002
hjmjohnson merged 1 commit into
Conversation
[High impact] The change affects pipeline updates across ITK’s Python wrappers. Fix the unchecked float requirement in Findings
|
|
The Linux failure looks separate from this fix: filtering succeeds, then the median test’s type-hint check raises All three GIL-related tests passed in that job. The float-wrapping review finding is fixed in 2d8272e; those tests also pass locally. |
There was a problem hiding this comment.
@sdiebolt Thanks, this is a careful fix, and I reproduced it locally (results below). The setswitchinterval probe is a real improvement: at the merge base the old overlap test passes while your new test fails with Update did not release the GIL.
Requested before merge:
-
Squash
2d8272e742candd0a09029620into0f6c5a10a70. Both fix something the first commit introduced: it switchesPyImageFilterTest.pytoitk.F, and releases the GIL inUpdate()whilePyCommandstill callsPyCallable_Checkwithout it. One commit keeps every revision bisectable. -
Two comments still describe the wider release (on lines this PR doesn't touch, so noted here):
Wrapping/Generators/Python/CMakeLists.txt:339: "release the GIL during ITK operations"Wrapping/Generators/Python/Tests/test_gil_release_safety.py:27: "SWIG wraps C++ calls in SaveThread/RestoreThread"
Only
UpdateandUpdateLargestPossibleRegionrelease it now. -
The GIL tests hard-code
itk.F(inline). Either guard them withITK_WRAP_floatin CMake or select a mandatory wrapped type (ITK.F can be disabled) i.e. do the test with uchar. -
The unset-callable
PyCommandblock is an error-path test, not a regression test for the GIL fix (inline).
The END_BLOCK and duplicated-callback notes inline are optional cleanup.
Local verification (macOS arm64, CPython 3.13.9, NumPy 2.3.4, SWIG 4.4.1, abi3)
Minimal wrapped Release build (ITKSmoothing, ITKBridgeNumPy, ITK_PYTHON_RELEASE_GIL=ON), running this PR's three test files against each revision:
| Revision | test_gil_release.py |
test_gil_release_safety.py |
PyImageFilterTest.py |
|---|---|---|---|
merge base 5e1bd43 |
FAIL: Update did not release the GIL (the old overlap test passes here) |
16/16 | pass |
2d8272e742c (before the PyCommand fix) |
pass | 16/16 | pass |
d0a09029620 (head) |
pass, 20/20 repeats on a loaded host | 16/16 | pass |
Not run: a build with ITK_WRAP_float=OFF, and a build with ITK_PYTHON_RELEASE_GIL=OFF.
Checked against the code
igenerator.py:1565-1570dropsthreads="1", keeps the global%feature("nothreadallow"), and adds%threadallow Update;and%threadallow UpdateLargestPossibleRegion;. That answers #7000's question aboutnothreadallowinteracting with-threads.ITK_PYTHON_RELEASE_GIL=OFFnow really turns the release off.- C++ exceptions retake the GIL before SWIG converts them to Python errors: SWIG's thread-allow object lives inside
$action, within thetryinpyBase.i. itk.imread,itk.imwrite, and the functional filter calls all go through the patched methods.
Not requested here: UpdateOutputInformation, ImageFileWriter::Write(), and optimizer StartOptimization() still hold the GIL, which matches #7000 and the migration guide. Wider coverage belongs with the thread-safety audit in #6757.
| itkExceptionStringMacro("There was an error executing the CommandCallable."); | ||
| } | ||
| } | ||
| SWIG_PYTHON_THREAD_END_BLOCK; |
There was a problem hiding this comment.
The SWIG_PYTHON_THREAD_END_BLOCK lines here (and at 206, 237, 272) are redundant. SWIG_PYTHON_THREAD_BEGIN_BLOCK declares a SWIG_Python_Thread_Block object whose destructor calls end(), and END_BLOCK only calls end() early. Each one is the last statement before the closing brace, so the GIL is released at the same point either way. The throw paths skip END_BLOCK already and rely on the destructor, so dropping all four changes nothing.
These four methods now repeat the same "call, decref, PyErr_Print, throw" sequence. A private helper would remove the duplication and give each caller a single GIL scope:
template <class TInputImage, class TOutputImage>
void
PyImageFilter<TInputImage, TOutputImage>::CallPython(PyObject * callable, PyObject * args) const
{
PyObject * result = PyObject_Call(callable, args, nullptr);
SWIG_Py_DECREF(args);
if (result == nullptr)
{
PyErr_Print();
itkExceptionStringMacro("There was an error executing the CommandCallable.");
}
SWIG_Py_DECREF(result);
}
template <class TInputImage, class TOutputImage>
void
PyImageFilter<TInputImage, TOutputImage>::GenerateOutputInformation()
{
Superclass::GenerateOutputInformation();
SWIG_PYTHON_THREAD_BEGIN_BLOCK;
if (PyCallable_Check(this->m_GenerateOutputInformationCallable))
{
this->CallPython(this->m_GenerateOutputInformationCallable, PyTuple_Pack(1, this->m_Self));
}
}In EnlargeOutputRequestedRegion, decref pyDataObject right after PyTuple_Pack(2, ...) (the tuple holds its own reference), so the throw path doesn't leak it.
There was a problem hiding this comment.
Indeed, removed all four!
| median.AddObserver(itk.StartEvent(), itk.PyCommand.New()) | ||
| try: | ||
| median.Update() | ||
| check("unset observer callable rejected", False, "should have raised") | ||
| except RuntimeError as error: | ||
| check("unset observer callable rejected", "CommandCallable" in str(error)) |
There was a problem hiding this comment.
This is a good error-path check, but it can't detect the defect d0a09029620 fixes. With no callable set, m_Object is NULL, and PyCallable_Check(NULL) returns 0 without touching interpreter state, so the block passes whether or not the GIL is held. I confirmed this locally: at 2d8272e742c (GIL released, before this fix) the safety script passes 16/16, including this block.
I don't think a release-build test can catch a missing GIL before PyCallable_Check; the fix is clearest by reading the code. Could the commit message and PR body describe this block as an error-path test rather than a regression test for the GIL fix?
There was a problem hiding this comment.
Indeed, I hadn't tested that properly. Fixed the PR message.
| # itk.imread on a non-existent file throws RuntimeError through the | ||
| # SWIG wrapper. With -threads, the GIL is released before the C++ call | ||
| # and must be re-acquired before setting the Python exception. | ||
| image_type = itk.Image[itk.F, 2] |
There was a problem hiding this comment.
Nothing in these GIL tests needs float: they run a median filter on a constant image and check an exception for a missing input. Any wrapped scalar pixel type works. make_image() (line 101) and test_gil_release.py:8 also hard-code itk.F, so builds with ITK_WRAP_float=OFF can't run them, even though PyImageFilterTest.py was fixed for the same reason in 2d8272e742c.
Either fix is fine:
- Register the GIL tests only when float is wrapped, as
PythonGeometryProtocolTestalready does (Tests/CMakeLists.txt:190,if(ITK_WRAP_float AND WRAP_3)). This is the smallest change. - Reuse the pixel-type selection from
PyImageFilterTest.pyin both GIL test files.
There was a problem hiding this comment.
Thanks for catching this! Both tests are now guarded by ITK_WRAP_float. All four CMake registration cases pass.
| COMMAND | ||
| ${CMAKE_CURRENT_SOURCE_DIR}/test_gil_release.py | ||
| ) | ||
| if(ITK_PYTHON_RELEASE_GIL) |
There was a problem hiding this comment.
If you take the CMake route for the float dependency, one block can guard both GIL tests. The safety test then needs ITK_WRAP_float only, since it is meant to pass with ITK_PYTHON_RELEASE_GIL on or off:
if(ITK_WRAP_float)
if(ITK_PYTHON_RELEASE_GIL)
itk_python_add_test(NAME PythonGILReleaseTest ...)
endif()
itk_python_add_test(NAME PythonGILReleaseSafetyTest ...)
endif()There was a problem hiding this comment.
Done, using your suggested nesting. The safety test still runs with GIL release on or off, as long as float wrapping is enabled.
d0a0902 to
1f0ae06
Compare
|
Thanks for the careful review and local reproduction! All required changes are included in the single commit 1f0ae06. |
dzenanz
left a comment
There was a problem hiding this comment.
Thanks @sdiebolt, and thanks @hjmjohnson for the thorough first pass.
All of Hans's requested items are addressed in 1f0ae063adf (single
commit, comments updated, float guard, error-path wording). LGTM after
a rebase; local Windows results, including ITK_PYTHON_RELEASE_GIL=OFF,
are below.
Requested:
- Rebase on
main. TheITK.Linux.Python/ITK.macOS.Python
failures arePythonMedianImageFilterTestraising
NameError: name 'xr' is not defined, already fixed onmainby
c4a04cb ("Resolve optional array types in MedianImageFilterTest
type hints"). The GIL tests pass in both jobs.
Optional:
EnlargeOutputRequestedRegionstill leakspyDataObjectwhen the
callback fails (pre-existing). Decref it right afterPyTuple_Pack.
Local verification (Windows 11, MSVC 19.44, Python 3.13, SWIG/CastXML from pixi)
Minimal wrapped Release build (ITKSmoothing, ITKBridgeNumPy) of 1f0ae063adf:
| Test | ITK_PYTHON_RELEASE_GIL=ON |
ITK_PYTHON_RELEASE_GIL=OFF |
|---|---|---|
PythonGILReleaseTest |
pass, 10/10 repeats | not registered (as intended) |
PythonGILReleaseSafetyTest |
pass (signal test skipped: no SIGUSR1 on Windows) |
pass |
PyImageFilterTest |
pass | pass |
itkPyCommandPythonTest |
pass | pass |
PythonMedianImageFilterTest |
pass | pass |
Running test_gil_release.py by hand against the OFF build fails with
Update did not release the GIL, so OFF really keeps the GIL and the
test tells the two modes apart. The OFF build compiles cleanly; in that
mode SWIG_PYTHON_THREAD_BEGIN_BLOCK expands to nothing.
This covers the two configurations not yet tested: Windows/MSVC, and
ITK_PYTHON_RELEASE_GIL=OFF.
|
Rebased onto main in 242edca, including the type-hint fix. Thanks for testing both GIL modes on Windows! |
242edca to
d1a66ec
Compare
Thank you for the review and guidance! 😊 I've tested this locally in a multi-frame registration use-case (registering each frame of a series to the first) and it speeds up parallel calls of ITK python by a big margin (when using threads)! |
The global SWIG nothreadallow feature suppresses native GIL release. Enable it for Update and UpdateLargestPossibleRegion, with callback GIL guards and a regression test that excludes Python-side setup. Keep observer error-path coverage separate from GIL ownership checks. Fixes InsightSoftwareConsortium#7000
d1a66ec to
1254ee2
Compare
|
Rebased on main (plain rebase, patch unchanged) to pick up #7028, which fixes the |
|
@hjmjohnson Have you taken another look? Should we merge this? |
hjmjohnson
left a comment
There was a problem hiding this comment.
May want to consider backporting?? Good as is.
Let Python threads run during ITK pipeline updates, and make the regression test check filtering rather than setup. Closes #7000.
Why the GIL stayed locked
The 2019 GIL-release change enabled SWIG threading but also set
nothreadallow, citing this SWIG discussion. That advice served a different goal: protecting director callbacks while deliberately avoiding GIL release around native calls. The combination therefore did not provide the concurrency described by the commit.The later
-threadsaddition in #5776/#6022 left that override in place. Its regression test accepted overlapping setup/filtering intervals as evidence of GIL release. Measured intervals were about 0.81 seconds, but only 0.002–0.003 seconds were insideUpdate().What this fixes
Update()andUpdateLargestPossibleRegion(), retaining it for unrelated wrappers and respectingITK_PYTHON_RELEASE_GIL.PyImageFiltercallbacks and beforePyCommandvalidates observer callables.Update()calls, alongside the waiting-thread GIL probe. Both checks suppress periodic Python thread switching.Update()exceptions instead of errors that could arise in Python-side file-reading setup.ITK_WRAP_float.Validation
Linux x86_64, CPython 3.13.9, NumPy 2.3.4, Release build:
Update()appears to hold the GIL despite GIL-release support #7000's ticker workload: 913–927 ticks during 0.97–0.98 seconds of filtering.PyCommandfix.PyCallable_Check()without the GIL before that fix and passes afterward; the final safety script also passes with this instrumentation.The initial Linux CI run also passed all three targeted tests. Its separate median type-hint failure is described in the PR comments.
Full local suite, Python 3.14 wheel, and full GIL-disabled native build validation were not run. CI must validate the latest revision.
PR Checklist
AI assistance
AI assistance supported diagnosis, implementation, and local testing.