Skip to content

Commit f396e52

Browse files
authored
Merge pull request #5776 from InsightSoftwareConsortium/copilot/release-gil-itk-operations
ENH: Release Python GIL during ITK operations
2 parents c78ee79 + f8b120f commit f396e52

File tree

5 files changed

+191
-2
lines changed

5 files changed

+191
-2
lines changed

Documentation/docs/migration_guides/itk_6_migration_guide.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,53 @@ target_link_libraries(MyTest
219219

220220
These names were deprecated in CMake 3.20 and removed in CMake 4.1.0. Additionally, the GoogleTest project itself uses the lowercase target names (`GTest::gtest` and `GTest::gtest_main`), meaning the old ITK-specific aliases were not compatible when using GoogleTest directly from its upstream repository. ITK 6 adopts the standard lowercase target names to ensure compatibility with modern CMake versions, the GoogleTest project, and consistency with other projects.
221221

222+
Python Global Interpreter Lock (GIL) Release
223+
---------------------------------------------
224+
225+
ITK now releases the Python Global Interpreter Lock (GIL) during C++ operations by default,
226+
allowing for true multi-threaded execution of ITK operations from Python. This enables
227+
parallel filter invocation when using ITK in parallel computing frameworks like Dask, Ray, or
228+
Python's standard `threading` module.
229+
230+
### Key Changes
231+
232+
**New CMake Option:**
233+
- `ITK_PYTHON_RELEASE_GIL` (default: `ON`) - Controls whether the GIL is released during ITK operations
234+
- When enabled, the `-threads` flag is passed to SWIG to generate thread-safe wrappers
235+
236+
**Benefits:**
237+
- Multiple Python threads can execute ITK operations concurrently
238+
- Improves performance in parallel computing scenarios
239+
- Prevents thread blocking when using frameworks like Dask
240+
241+
**Example:**
242+
243+
```python
244+
import itk
245+
import threading
246+
247+
image_paths = ["image1.mha", "image2.mha"]
248+
249+
def process_image(image_path):
250+
# GIL is released during ITK operations
251+
image = itk.imread(image_path)
252+
smoothed = itk.median_image_filter(image, radius=5)
253+
return smoothed
254+
255+
# Multiple threads can now execute ITK operations concurrently
256+
threads = [
257+
threading.Thread(target=process_image, args=(path,))
258+
for path in image_paths
259+
]
260+
for thread in threads:
261+
thread.start()
262+
for thread in threads:
263+
thread.join()
264+
```
265+
266+
**Note:** ITK callbacks and event monitoring may be affected by GIL release. If you encounter
267+
issues with callbacks, you can disable GIL release by setting `-DITK_PYTHON_RELEASE_GIL=OFF`
268+
when building ITK.
222269

223270
Modern CMake Interface Libraries
224271
---------------------------------

Wrapping/Generators/Python/CMakeLists.txt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,13 +329,20 @@ macro(
329329
)
330330
endif()
331331

332+
# Conditionally add -threads flag to release the GIL during ITK operations
333+
set(_swig_threads_flag "")
334+
if(ITK_PYTHON_RELEASE_GIL)
335+
set(_swig_threads_flag "-threads")
336+
endif()
337+
332338
add_custom_command(
333339
OUTPUT
334340
${cpp_file}
335341
${python_file}
336342
COMMAND
337-
${swig_command} -c++ -python -fastdispatch -fvirtual -features autodoc=2
338-
-doxygen -Werror -w302 # Identifier 'name' redefined (ignored)
343+
${swig_command} -c++ -python ${_swig_threads_flag} -fastdispatch -fvirtual
344+
-features autodoc=2 -doxygen -Werror
345+
-w302 # Identifier 'name' redefined (ignored)
339346
-w303 # %extend defined for an undeclared class 'name' (to avoid warning about customization in pyBase.i)
340347
-w312 # Unnamed nested class not currently supported (ignored)
341348
-w314 # 'identifier' is a lang keyword
@@ -366,6 +373,7 @@ macro(
366373

367374
unset(dependencies)
368375
unset(swig_command)
376+
unset(_swig_threads_flag)
369377
endmacro()
370378

371379
macro(itk_end_wrap_submodule_python group_name)

Wrapping/Generators/Python/Tests/CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,3 +261,10 @@ itk_python_add_test(
261261
58
262262
5
263263
)
264+
265+
# Test GIL release during ITK operations
266+
itk_python_add_test(
267+
NAME PythonGILReleaseTest
268+
COMMAND
269+
${CMAKE_CURRENT_SOURCE_DIR}/test_gil_release.py
270+
)
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""
2+
Test that the Python Global Interpreter Lock (GIL) is released during ITK operations.
3+
4+
This test verifies that when ITK_PYTHON_RELEASE_GIL is enabled, multiple Python threads
5+
can execute ITK operations concurrently.
6+
"""
7+
8+
import sys
9+
import threading
10+
import time
11+
12+
# Threshold for determining if parallel execution is significantly faster than sequential
13+
# With 4 outer threads and 1 ITK thread per filter, we expect at least 2x speedup
14+
# A value of 0.5 means parallel execution should be at most 50% of sequential time
15+
# This accounts for threading overhead and ensures GIL is being released
16+
PARALLEL_SPEEDUP_THRESHOLD = 0.5
17+
18+
19+
def test_gil_release():
20+
"""Test that GIL is released during ITK operations."""
21+
try:
22+
import itk
23+
except ImportError:
24+
print("ITK not available, skipping GIL release test")
25+
sys.exit(0)
26+
27+
# Create a simple test image
28+
image_type = itk.Image[itk.F, 2]
29+
size = [100, 100]
30+
31+
# Shared counter to track concurrent execution
32+
execution_times = []
33+
lock = threading.Lock()
34+
35+
def run_filter():
36+
"""Run an ITK filter operation that should release the GIL."""
37+
# Create an image
38+
image = itk.Image[itk.F, 2].New()
39+
region = itk.ImageRegion[2]()
40+
region.SetSize(size)
41+
image.SetRegions(region)
42+
image.Allocate()
43+
image.FillBuffer(1.0)
44+
45+
start_time = time.time()
46+
47+
# Run a computationally intensive filter
48+
# MedianImageFilter is a good test as it performs actual computation
49+
median_filter = itk.MedianImageFilter[image_type, image_type].New()
50+
median_filter.SetInput(image)
51+
median_filter.SetRadius(5)
52+
# Limit ITK internal threads to 1 to make the test more reliable
53+
median_filter.SetNumberOfWorkUnits(1)
54+
median_filter.Update()
55+
56+
end_time = time.time()
57+
58+
with lock:
59+
execution_times.append((start_time, end_time))
60+
61+
# Run multiple threads
62+
num_threads = 4
63+
threads = []
64+
65+
overall_start = time.time()
66+
67+
for _ in range(num_threads):
68+
thread = threading.Thread(target=run_filter)
69+
thread.start()
70+
threads.append(thread)
71+
72+
for thread in threads:
73+
thread.join()
74+
75+
overall_end = time.time()
76+
77+
# If GIL is properly released, the threads should have overlapping execution times
78+
# and the total time should be less than the sum of individual execution times
79+
80+
total_sequential_time = sum(end - start for start, end in execution_times)
81+
total_parallel_time = overall_end - overall_start
82+
83+
print(f"Total sequential time if run serially: {total_sequential_time:.3f}s")
84+
print(f"Total parallel time: {total_parallel_time:.3f}s")
85+
86+
# Check for overlap in execution times
87+
has_overlap = False
88+
if len(execution_times) >= 2:
89+
for i in range(len(execution_times)):
90+
for j in range(i + 1, len(execution_times)):
91+
start1, end1 = execution_times[i]
92+
start2, end2 = execution_times[j]
93+
# Check if there's any overlap
94+
if (start1 <= start2 < end1) or (start2 <= start1 < end2):
95+
has_overlap = True
96+
break
97+
if has_overlap:
98+
break
99+
100+
if has_overlap:
101+
print("SUCCESS: Thread execution times overlap - GIL appears to be released")
102+
return 0
103+
else:
104+
# Even without overlap, if parallel time is significantly less than sequential,
105+
# it suggests concurrent execution
106+
if total_parallel_time < total_sequential_time * PARALLEL_SPEEDUP_THRESHOLD:
107+
print("SUCCESS: Parallel execution is faster - GIL appears to be released")
108+
return 0
109+
else:
110+
print("FAILURE: No clear evidence of concurrent execution")
111+
print("This indicates that GIL is not being released properly")
112+
print(
113+
f"Expected parallel time < {total_sequential_time * PARALLEL_SPEEDUP_THRESHOLD:.3f}s, got {total_parallel_time:.3f}s"
114+
)
115+
return 1
116+
117+
118+
if __name__ == "__main__":
119+
sys.exit(test_gil_release())

Wrapping/WrappingOptions.cmake

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ else()
1313
set(ITK_WRAPPING OFF CACHE INTERNAL "Build external languages support" FORCE)
1414
endif()
1515

16+
cmake_dependent_option(
17+
ITK_PYTHON_RELEASE_GIL
18+
"Release Python Global Interpreter Lock (GIL) during ITK operations"
19+
ON
20+
"ITK_WRAP_PYTHON"
21+
OFF
22+
)
23+
1624
cmake_dependent_option(
1725
ITK_WRAP_unsigned_char
1826
"Wrap unsigned char type"

0 commit comments

Comments
 (0)