From f876cbd77711300544b32119b6e2ed2c88dd54f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 21:49:26 +0000 Subject: [PATCH] ENH: Add ITK_PYTHON_RELEASE_GIL option and SWIG -threads flag - Added cmake option ITK_PYTHON_RELEASE_GIL (default ON) in WrappingOptions.cmake - Modified Python CMakeLists.txt to add -threads flag to SWIG when option is enabled - Created test_gil_release.py to verify GIL is released during ITK operations - Added test to CMakeLists.txt Co-authored-by: Matt McCormick Co-authored-by: dzenanz <1792121+dzenanz@users.noreply.github.com> --- Wrapping/Generators/Python/CMakeLists.txt | 10 +- .../Generators/Python/Tests/CMakeLists.txt | 24 ++-- .../Python/Tests/test_gil_release.py | 119 ++++++++++++++++++ Wrapping/WrappingOptions.cmake | 8 ++ 4 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 Wrapping/Generators/Python/Tests/test_gil_release.py diff --git a/Wrapping/Generators/Python/CMakeLists.txt b/Wrapping/Generators/Python/CMakeLists.txt index 69662147eff..6d1341623af 100644 --- a/Wrapping/Generators/Python/CMakeLists.txt +++ b/Wrapping/Generators/Python/CMakeLists.txt @@ -275,10 +275,17 @@ macro( "${doc_file}") endif() + # Conditionally add -threads flag to release the GIL during ITK operations + set(_swig_threads_flag "") + if(ITK_PYTHON_RELEASE_GIL) + set(_swig_threads_flag "-threads") + endif() + add_custom_command( OUTPUT ${cpp_file} ${python_file} COMMAND - ${swig_command} -c++ -python -fastdispatch -fvirtual -features autodoc=2 -doxygen -Werror + ${swig_command} -c++ -python ${_swig_threads_flag} -fastdispatch -fvirtual + -features autodoc=2 -doxygen -Werror -w302 # Identifier 'name' redefined (ignored) -w303 # %extend defined for an undeclared class 'name' (to avoid warning about customization in pyBase.i) -w312 # Unnamed nested class not currently supported (ignored) @@ -306,6 +313,7 @@ macro( unset(dependencies) unset(swig_command) + unset(_swig_threads_flag) endmacro() macro(itk_end_wrap_submodule_python group_name) diff --git a/Wrapping/Generators/Python/Tests/CMakeLists.txt b/Wrapping/Generators/Python/Tests/CMakeLists.txt index faec1571af3..dd7fecb6aec 100644 --- a/Wrapping/Generators/Python/Tests/CMakeLists.txt +++ b/Wrapping/Generators/Python/Tests/CMakeLists.txt @@ -249,10 +249,20 @@ itk_python_add_test( ${ITK_TEST_OUTPUT_DIR}/TestVLV.seg.nrrd DATA{${WrapITK_SOURCE_DIR}/images/TestVLV.seg.nrrd} COMMAND - ${CMAKE_CURRENT_SOURCE_DIR}/readWriteVLV.py - DATA{${WrapITK_SOURCE_DIR}/images/TestVLV.seg.nrrd} - ${ITK_TEST_OUTPUT_DIR}/TestVLV.seg.nrrd - 59 - 85 - 58 - 5) + ${CMAKE_CURRENT_SOURCE_DIR}/readWriteVLV.py + DATA{${WrapITK_SOURCE_DIR}/images/TestVLV.seg.nrrd} + ${ITK_TEST_OUTPUT_DIR}/TestVLV.seg.nrrd + 59 + 85 + 58 + 5 +) + +if(ITK_PYTHON_RELEASE_GIL) + # Test GIL release during ITK operations + itk_python_add_test( + NAME PythonGILReleaseTest + COMMAND + ${CMAKE_CURRENT_SOURCE_DIR}/test_gil_release.py + ) +endif() diff --git a/Wrapping/Generators/Python/Tests/test_gil_release.py b/Wrapping/Generators/Python/Tests/test_gil_release.py new file mode 100644 index 00000000000..3aab977e88f --- /dev/null +++ b/Wrapping/Generators/Python/Tests/test_gil_release.py @@ -0,0 +1,119 @@ +""" +Test that the Python Global Interpreter Lock (GIL) is released during ITK operations. + +This test verifies that when ITK_PYTHON_RELEASE_GIL is enabled, multiple Python threads +can execute ITK operations concurrently. +""" + +import sys +import threading +import time + +# Threshold for determining if parallel execution is significantly faster than sequential +# With 4 outer threads and 1 ITK thread per filter, we expect at least 2x speedup +# A value of 0.5 means parallel execution should be at most 50% of sequential time +# This accounts for threading overhead and ensures GIL is being released +PARALLEL_SPEEDUP_THRESHOLD = 0.5 + + +def test_gil_release(): + """Test that GIL is released during ITK operations.""" + try: + import itk + except ImportError: + print("ITK not available, skipping GIL release test") + sys.exit(0) + + # Create a simple test image + image_type = itk.Image[itk.F, 2] + size = [100, 100] + + # Shared counter to track concurrent execution + execution_times = [] + lock = threading.Lock() + + def run_filter(): + """Run an ITK filter operation that should release the GIL.""" + # Create an image + image = itk.Image[itk.F, 2].New() + region = itk.ImageRegion[2]() + region.SetSize(size) + image.SetRegions(region) + image.Allocate() + image.FillBuffer(1.0) + + start_time = time.time() + + # Run a computationally intensive filter + # MedianImageFilter is a good test as it performs actual computation + median_filter = itk.MedianImageFilter[image_type, image_type].New() + median_filter.SetInput(image) + median_filter.SetRadius(5) + # Limit ITK internal threads to 1 to make the test more reliable + median_filter.SetNumberOfWorkUnits(1) + median_filter.Update() + + end_time = time.time() + + with lock: + execution_times.append((start_time, end_time)) + + # Run multiple threads + num_threads = 4 + threads = [] + + overall_start = time.time() + + for _ in range(num_threads): + thread = threading.Thread(target=run_filter) + thread.start() + threads.append(thread) + + for thread in threads: + thread.join() + + overall_end = time.time() + + # If GIL is properly released, the threads should have overlapping execution times + # and the total time should be less than the sum of individual execution times + + total_sequential_time = sum(end - start for start, end in execution_times) + total_parallel_time = overall_end - overall_start + + print(f"Total sequential time if run serially: {total_sequential_time:.3f}s") + print(f"Total parallel time: {total_parallel_time:.3f}s") + + # Check for overlap in execution times + has_overlap = False + if len(execution_times) >= 2: + for i in range(len(execution_times)): + for j in range(i + 1, len(execution_times)): + start1, end1 = execution_times[i] + start2, end2 = execution_times[j] + # Check if there's any overlap + if (start1 <= start2 < end1) or (start2 <= start1 < end2): + has_overlap = True + break + if has_overlap: + break + + if has_overlap: + print("SUCCESS: Thread execution times overlap - GIL appears to be released") + return 0 + else: + # Even without overlap, if parallel time is significantly less than sequential, + # it suggests concurrent execution + if total_parallel_time < total_sequential_time * PARALLEL_SPEEDUP_THRESHOLD: + print("SUCCESS: Parallel execution is faster - GIL appears to be released") + return 0 + else: + print("FAILURE: No clear evidence of concurrent execution") + print("This indicates that GIL is not being released properly") + print( + f"Expected parallel time < {total_sequential_time * PARALLEL_SPEEDUP_THRESHOLD:.3f}s, got {total_parallel_time:.3f}s" + ) + return 1 + + +if __name__ == "__main__": + sys.exit(test_gil_release()) diff --git a/Wrapping/WrappingOptions.cmake b/Wrapping/WrappingOptions.cmake index 6a629508a85..c32f406efa6 100644 --- a/Wrapping/WrappingOptions.cmake +++ b/Wrapping/WrappingOptions.cmake @@ -17,6 +17,14 @@ else() CACHE INTERNAL "Build external languages support" FORCE) endif() +cmake_dependent_option( + ITK_PYTHON_RELEASE_GIL + "Release Python Global Interpreter Lock (GIL) during ITK operations" + ON + "ITK_WRAP_PYTHON" + OFF +) + cmake_dependent_option( ITK_WRAP_unsigned_char "Wrap unsigned char type"