From 00468907dca964b94115014360904b5688efbb57 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 15 Sep 2026 14:26:17 -0400 Subject: [PATCH 01/34] os.path.exists("/proc/self/maps") is very expensive, maybe because it needs to create and then check a long list of things in /proc/self? --- threadpoolctl.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/threadpoolctl.py b/threadpoolctl.py index b8b4448d..acbfe245 100644 --- a/threadpoolctl.py +++ b/threadpoolctl.py @@ -56,6 +56,8 @@ _SYSTEM_UINT = ctypes.c_uint64 if sys.maxsize > 2**32 else ctypes.c_uint32 _SYSTEM_UINT_HALF = ctypes.c_uint32 if sys.maxsize > 2**32 else ctypes.c_uint16 +_PROCFS_EXISTS = sys.platform == "linux" and os.path.exists("/proc/self") + class _dl_phdr_info(ctypes.Structure): _fields_ = [ @@ -1137,7 +1139,7 @@ def _load_libraries(self): # CPython before 3.14 does not provide dll inspection. dllist = None - if sys.platform == "linux" and os.path.exists("/proc/self/maps"): + if _PROCFS_EXISTS: # On glibc, dl_iterate_phdr has an internal lock, and that plus # calling back into Python and the need to (re)acquire the GIL # results in deadlocks. To avoid that, use a Linux-specific From 735a373602c5a5c9083a97aac74cad63c4949873 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 15 Sep 2026 14:30:10 -0400 Subject: [PATCH 02/34] Don't check the existence of the same file repeatedly. --- threadpoolctl.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/threadpoolctl.py b/threadpoolctl.py index acbfe245..6ae2e594 100644 --- a/threadpoolctl.py +++ b/threadpoolctl.py @@ -1175,6 +1175,8 @@ def _find_libraries_with_linux(self): if start_index == -1 or ".so" not in line: continue filepath = line[start_index:] + if filepath in filepaths: + continue if os.path.exists(filepath): filepaths.add(filepath) From 561b60afe35619a912d8c09e52f47e6f709d5bf8 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 15 Sep 2026 14:41:33 -0400 Subject: [PATCH 03/34] Use a regex --- threadpoolctl.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/threadpoolctl.py b/threadpoolctl.py index 6ae2e594..38d5179b 100644 --- a/threadpoolctl.py +++ b/threadpoolctl.py @@ -1161,6 +1161,8 @@ def _load_libraries(self): # Non-Linux Unix platforms. self._find_libraries_with_dl_iterate_phdr() + _PATH_RE = re.compile(r" (/[^\n]+\.so[^\n]*)\n", re.MULTILINE) + def _find_libraries_with_linux(self): """Loop through loaded libraries and return binders on supported ones @@ -1169,19 +1171,10 @@ def _find_libraries_with_linux(self): """ with open("/proc/self/maps") as f: maps = f.read() - filepaths = set() - for line in maps.splitlines(): - start_index = line.find("/") - if start_index == -1 or ".so" not in line: - continue - filepath = line[start_index:] - if filepath in filepaths: - continue - if os.path.exists(filepath): - filepaths.add(filepath) - + filepaths = set(self._PATH_RE.findall(maps)) for filepath in filepaths: - self._make_controller_from_path(filepath) + if os.path.exists(filepath): + self._make_controller_from_path(filepath) def _find_libraries_with_python(self, dllist): """Loop through loaded libraries and return binders on supported ones From b08b5acc531b7af26a3d88857ff08c12115640c7 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 15 Sep 2026 14:42:07 -0400 Subject: [PATCH 04/34] Bytes regex is slightly faster. --- threadpoolctl.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/threadpoolctl.py b/threadpoolctl.py index 38d5179b..23a9782d 100644 --- a/threadpoolctl.py +++ b/threadpoolctl.py @@ -1161,7 +1161,7 @@ def _load_libraries(self): # Non-Linux Unix platforms. self._find_libraries_with_dl_iterate_phdr() - _PATH_RE = re.compile(r" (/[^\n]+\.so[^\n]*)\n", re.MULTILINE) + _PATH_RE = re.compile(rb" (/[^\n]+\.so[^\n]*)\n", re.MULTILINE) def _find_libraries_with_linux(self): """Loop through loaded libraries and return binders on supported ones @@ -1169,10 +1169,11 @@ def _find_libraries_with_linux(self): Uses a Linux-specific mechanism: https://man7.org/linux/man-pages/man5/proc_pid_maps.5.html """ - with open("/proc/self/maps") as f: + with open("/proc/self/maps", "rb") as f: maps = f.read() filepaths = set(self._PATH_RE.findall(maps)) for filepath in filepaths: + filepath = filepath.decode("utf-8") if os.path.exists(filepath): self._make_controller_from_path(filepath) From 2f29ebf3db5deeaf8a33e7de7ff86d954116a79d Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 15 Sep 2026 15:51:27 -0400 Subject: [PATCH 05/34] More robust regex --- threadpoolctl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/threadpoolctl.py b/threadpoolctl.py index 23a9782d..a8b12ab4 100644 --- a/threadpoolctl.py +++ b/threadpoolctl.py @@ -1161,7 +1161,7 @@ def _load_libraries(self): # Non-Linux Unix platforms. self._find_libraries_with_dl_iterate_phdr() - _PATH_RE = re.compile(rb" (/[^\n]+\.so[^\n]*)\n", re.MULTILINE) + _PATH_RE = re.compile(rb" (/[^\n]+\.so[^\n^/]*)\n", re.MULTILINE) def _find_libraries_with_linux(self): """Loop through loaded libraries and return binders on supported ones From b574c5a8f75b1a7a08bb8ab6ec28c1a33a8fb031 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 15 Sep 2026 15:52:33 -0400 Subject: [PATCH 06/34] Other methods don't check for existence, add safety net elsewhere --- threadpoolctl.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/threadpoolctl.py b/threadpoolctl.py index a8b12ab4..b9cd1a0a 100644 --- a/threadpoolctl.py +++ b/threadpoolctl.py @@ -1174,8 +1174,7 @@ def _find_libraries_with_linux(self): filepaths = set(self._PATH_RE.findall(maps)) for filepath in filepaths: filepath = filepath.decode("utf-8") - if os.path.exists(filepath): - self._make_controller_from_path(filepath) + self._make_controller_from_path(filepath) def _find_libraries_with_python(self, dllist): """Loop through loaded libraries and return binders on supported ones @@ -1597,9 +1596,13 @@ def _make_controller_from_path(self, filepath): # expected library (e.g. a library having a common prefix with one of the # our supported libraries). Otherwise, create and store the library # controller. - lib_controller = controller_class( - filepath=filepath, prefix=prefix, parent=self - ) + try: + lib_controller = controller_class( + filepath=filepath, prefix=prefix, parent=self + ) + except OSError: + # Probably because we couldn't load the filepath as a CDLL. + continue if filepath in (lib.filepath for lib in self.lib_controllers): # We already have a controller for this library. From e31a6a8a6b048193f18eac87a485e36f374f605d Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 15 Sep 2026 16:22:44 -0400 Subject: [PATCH 07/34] Avoid /proc if there are safe alternatives. --- tests/test_threadpoolctl.py | 2 +- threadpoolctl.py | 33 +++++++++++++++++++++++++-------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/tests/test_threadpoolctl.py b/tests/test_threadpoolctl.py index 35350110..57331282 100644 --- a/tests/test_threadpoolctl.py +++ b/tests/test_threadpoolctl.py @@ -1146,7 +1146,7 @@ def create_controllers(): backtrace_nogil(buf, 20) threads = [] - for _ in range(os.cpu_count() * 4): + for _ in range(os.cpu_count()): t = Thread(target=create_controllers) threads.append(t) t.start() diff --git a/threadpoolctl.py b/threadpoolctl.py index b9cd1a0a..3cdbd149 100644 --- a/threadpoolctl.py +++ b/threadpoolctl.py @@ -56,7 +56,28 @@ _SYSTEM_UINT = ctypes.c_uint64 if sys.maxsize > 2**32 else ctypes.c_uint32 _SYSTEM_UINT_HALF = ctypes.c_uint32 if sys.maxsize > 2**32 else ctypes.c_uint16 -_PROCFS_EXISTS = sys.platform == "linux" and os.path.exists("/proc/self") +# On glibc, dl_iterate_phdr has an internal lock, and that plus calling back +# into Python and the need to (re)acquire the GIL can result in deadlocks. To +# avoid that, listing shared libraries can use a Linux-specific mechanism that +# doesn't have these issues (/proc/self/maaps). Since it's the Linux kernel, +# musl works fine too. +# +# The downside is this mechanism is slower, so avoid it when safe alternatives +# are available (Python 3.14+ with free-threading has dllist(), and no GIL to +# cause deadlocks). +_USE_PROCFS = ( + # Only available on Linux: + sys.platform == "linux" + # Make sure /proc is mounted: + and os.path.exists("/proc/self") + # If dllist() is available, and there is no GIL, no need to use /proc. It's + # possible dllist() might work even if there is a GIL, but that's harder to + # prove. See https://github.com/python/cpython/issues/157573 + and not ( + sys.version_info[:2] >= (3, 14) + and not getattr(sys, "_is_gil_enabled", lambda: True)() + ) +) class _dl_phdr_info(ctypes.Structure): @@ -1132,19 +1153,15 @@ def _load_libraries(self): # libffi builds (#225). dllist also uses dl_iterate_phdr internally # (#239), which we already avoid on Linux via /proc/self/maps. dllist = None - if sys.platform not in ("linux", "emscripten"): + if sys.platform != "emscripten": try: from ctypes.util import dllist except ImportError: # CPython before 3.14 does not provide dll inspection. dllist = None - if _PROCFS_EXISTS: - # On glibc, dl_iterate_phdr has an internal lock, and that plus - # calling back into Python and the need to (re)acquire the GIL - # results in deadlocks. To avoid that, use a Linux-specific - # mechanism that doesn't have these issues; since it's Linux, musl - # works fine too. + if _USE_PROCFS: + # See comment on _USE_PROCFS. self._find_libraries_with_linux() elif dllist is not None: # On Python 3.14+, this functionality is built-in. Once Python 3.13 From 713ed718c1a6dee7eed49bd407c91da7e844ff95 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 15 Sep 2026 16:51:07 -0400 Subject: [PATCH 08/34] Note glibc 2.40 --- tests/test_threadpoolctl.py | 3 +++ threadpoolctl.py | 15 ++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/test_threadpoolctl.py b/tests/test_threadpoolctl.py index 57331282..8b9db0e2 100644 --- a/tests/test_threadpoolctl.py +++ b/tests/test_threadpoolctl.py @@ -1142,6 +1142,9 @@ def create_controllers(): buf = (ctypes.c_void_p * 20)() for _ in range(100): limiter = threadpool_limits() + # On glibc 2.39, backtrace() calling dl_iterate_phdr() is enough to + # cause deadlocks. On 2.40 and later, they switched to a read-write + # lock, so this won't deadlock at all... backtrace_gil(buf, 20) backtrace_nogil(buf, 20) diff --git a/threadpoolctl.py b/threadpoolctl.py index 3cdbd149..3246ea07 100644 --- a/threadpoolctl.py +++ b/threadpoolctl.py @@ -56,15 +56,20 @@ _SYSTEM_UINT = ctypes.c_uint64 if sys.maxsize > 2**32 else ctypes.c_uint32 _SYSTEM_UINT_HALF = ctypes.c_uint32 if sys.maxsize > 2**32 else ctypes.c_uint16 -# On glibc, dl_iterate_phdr has an internal lock, and that plus calling back -# into Python and the need to (re)acquire the GIL can result in deadlocks. To -# avoid that, listing shared libraries can use a Linux-specific mechanism that -# doesn't have these issues (/proc/self/maaps). Since it's the Linux kernel, -# musl works fine too. +# On glibc 2.39 and earlier, dl_iterate_phdr has an internal lock, and that +# plus calling back into Python and the need to (re)acquire the GIL can result +# in deadlocks. To avoid that, listing shared libraries can use a +# Linux-specific mechanism that doesn't have these issues (/proc/self/maaps). +# Since it's the Linux kernel, musl works fine too. # # The downside is this mechanism is slower, so avoid it when safe alternatives # are available (Python 3.14+ with free-threading has dllist(), and no GIL to # cause deadlocks). +# +# On glibc 2.40 and later, dl has a read-write lock, and dl_iterate_phdr should +# only use the read version since it's not modifying anything. So you no longer +# get deadlocks purely from dl_iterate_phdr. You can still deadlock with +# dlopen() though. _USE_PROCFS = ( # Only available on Linux: sys.platform == "linux" From 6c798d46e39023ff98497066d14b58557b542c38 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Wed, 16 Sep 2026 11:00:40 -0400 Subject: [PATCH 09/34] Make sure extensions don't prevent free threading --- tests/_openmp_test_helper/nested_prange_blas.pyx | 2 ++ tests/_openmp_test_helper/nested_prange_blas_custom.pyx | 2 ++ tests/_openmp_test_helper/openmp_helpers_inner.pyx | 2 ++ tests/_openmp_test_helper/openmp_helpers_outer.pyx | 2 ++ 4 files changed, 8 insertions(+) diff --git a/tests/_openmp_test_helper/nested_prange_blas.pyx b/tests/_openmp_test_helper/nested_prange_blas.pyx index af2a6693..ad3ef185 100644 --- a/tests/_openmp_test_helper/nested_prange_blas.pyx +++ b/tests/_openmp_test_helper/nested_prange_blas.pyx @@ -1,3 +1,5 @@ +# cython: freethreading_compatible = True + cimport openmp from cython.parallel import parallel, prange diff --git a/tests/_openmp_test_helper/nested_prange_blas_custom.pyx b/tests/_openmp_test_helper/nested_prange_blas_custom.pyx index 6c0a9771..d76291e7 100644 --- a/tests/_openmp_test_helper/nested_prange_blas_custom.pyx +++ b/tests/_openmp_test_helper/nested_prange_blas_custom.pyx @@ -1,3 +1,5 @@ +# cython: freethreading_compatible = True + cimport openmp from cython.parallel import parallel, prange diff --git a/tests/_openmp_test_helper/openmp_helpers_inner.pyx b/tests/_openmp_test_helper/openmp_helpers_inner.pyx index e7928d2f..566e7f4c 100644 --- a/tests/_openmp_test_helper/openmp_helpers_inner.pyx +++ b/tests/_openmp_test_helper/openmp_helpers_inner.pyx @@ -1,3 +1,5 @@ +# cython: freethreading_compatible = True + cimport openmp from cython.parallel import prange diff --git a/tests/_openmp_test_helper/openmp_helpers_outer.pyx b/tests/_openmp_test_helper/openmp_helpers_outer.pyx index 2c8a383c..9e668f5c 100644 --- a/tests/_openmp_test_helper/openmp_helpers_outer.pyx +++ b/tests/_openmp_test_helper/openmp_helpers_outer.pyx @@ -1,3 +1,5 @@ +# cython: freethreading_compatible = True + cimport openmp from cython.parallel import prange from openmp_helpers_inner cimport inner_openmp_loop From 944d1f515821c21ea2e8afd9548d4b01844c0c24 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Wed, 16 Sep 2026 11:18:19 -0400 Subject: [PATCH 10/34] More reliable deadlocks, including newer glibc --- tests/test_threadpoolctl.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/tests/test_threadpoolctl.py b/tests/test_threadpoolctl.py index 8b9db0e2..24857895 100644 --- a/tests/test_threadpoolctl.py +++ b/tests/test_threadpoolctl.py @@ -1,6 +1,7 @@ from __future__ import annotations import ctypes +import gc import json import os import pytest @@ -1131,25 +1132,23 @@ def test_controller_parallelism_no_deadlocks(): if sys.platform != "linux" or not hasattr(ctypes.PyDLL(None), "backtrace"): pytest.skip("Testing glibc on Linux") - # Internally, backtrace() calls dl_iterate_phdr which can result in - # deadlocks if threadpoolctl is also using dl_iterate_phdr. - backtrace_gil = ctypes.PyDLL(None).backtrace - backtrace_gil.argtypes = [ctypes.c_void_p, ctypes.c_int] - backtrace_nogil = ctypes.CDLL(None).backtrace - backtrace_nogil.argtypes = [ctypes.c_void_p, ctypes.c_int] + done = [] def create_controllers(): buf = (ctypes.c_void_p * 20)() for _ in range(100): + # May use dl_iterate_phdr() on Linux: limiter = threadpool_limits() - # On glibc 2.39, backtrace() calling dl_iterate_phdr() is enough to - # cause deadlocks. On 2.40 and later, they switched to a read-write - # lock, so this won't deadlock at all... - backtrace_gil(buf, 20) - backtrace_nogil(buf, 20) + # dlopen(): + try: + dll = ctypes.CDLL("libncurses.so.6") + del dll + except OSError: + done.append(False) + done.append(True) threads = [] - for _ in range(os.cpu_count()): + for _ in range(os.cpu_count() * 4): t = Thread(target=create_controllers) threads.append(t) t.start() @@ -1157,6 +1156,11 @@ def create_controllers(): for t in threads: t.join() + if False in done: + pytest.skip("libncurses.so.6 not available") + + assert len(done) == os.cpu_count() * 4 + @pytest.mark.skipif( not sys.platform.startswith("linux"), From 2cc8f692d4c320dbae7e18de349300f7eb828e8d Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Wed, 16 Sep 2026 11:31:24 -0400 Subject: [PATCH 11/34] Update with latest understanding --- threadpoolctl.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/threadpoolctl.py b/threadpoolctl.py index 3246ea07..7df4fb5b 100644 --- a/threadpoolctl.py +++ b/threadpoolctl.py @@ -58,26 +58,30 @@ # On glibc 2.39 and earlier, dl_iterate_phdr has an internal lock, and that # plus calling back into Python and the need to (re)acquire the GIL can result -# in deadlocks. To avoid that, listing shared libraries can use a -# Linux-specific mechanism that doesn't have these issues (/proc/self/maaps). -# Since it's the Linux kernel, musl works fine too. -# -# The downside is this mechanism is slower, so avoid it when safe alternatives -# are available (Python 3.14+ with free-threading has dllist(), and no GIL to -# cause deadlocks). +# in deadlocks. # # On glibc 2.40 and later, dl has a read-write lock, and dl_iterate_phdr should # only use the read version since it's not modifying anything. So you no longer # get deadlocks purely from dl_iterate_phdr. You can still deadlock with # dlopen() though. +# +# To avoid that deadlock, listing shared libraries can use a Linux-specific +# mechanism that doesn't have these issues (/proc/self/maps). Since it's the +# Linux kernel, musl works fine too. +# +# The downside is this mechanism is slower, so avoid it when safe alternatives +# are available. _USE_PROCFS = ( # Only available on Linux: sys.platform == "linux" # Make sure /proc is mounted: and os.path.exists("/proc/self") - # If dllist() is available, and there is no GIL, no need to use /proc. It's - # possible dllist() might work even if there is a GIL, but that's harder to - # prove. See https://github.com/python/cpython/issues/157573 + # If dllist() is available (3.14+), and there is no GIL, no need to use + # /proc. It's possible dllist() might work in GIL builds too starting in + # Python 3.15, where it is written in C instead of being equivalent to + # threadpoolctl's implementation. But see + # https://github.com/python/cpython/issues/157573. We should consider + # enabling it on GIL Python too once threadpoolctl supports 3.15. and not ( sys.version_info[:2] >= (3, 14) and not getattr(sys, "_is_gil_enabled", lambda: True)() From 8dd87e37ddf95d2c47a8a26370cf668e6b7a9d5f Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Wed, 16 Sep 2026 11:45:15 -0400 Subject: [PATCH 12/34] Cleanups --- tests/test_threadpoolctl.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_threadpoolctl.py b/tests/test_threadpoolctl.py index 24857895..0aebee0c 100644 --- a/tests/test_threadpoolctl.py +++ b/tests/test_threadpoolctl.py @@ -1,7 +1,6 @@ from __future__ import annotations import ctypes -import gc import json import os import pytest @@ -1141,10 +1140,14 @@ def create_controllers(): limiter = threadpool_limits() # dlopen(): try: + # Should be available in most Linux, and importantly isn't + # loaded by default into Python: dll = ctypes.CDLL("libncurses.so.6") del dll except OSError: done.append(False) + return + del limiter done.append(True) threads = [] From b43e0562d27c147bd5d9b994abfebd7198980881 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Wed, 16 Sep 2026 11:45:20 -0400 Subject: [PATCH 13/34] dllist() is ok on Python 3.15 --- threadpoolctl.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/threadpoolctl.py b/threadpoolctl.py index 7df4fb5b..f47bc2cd 100644 --- a/threadpoolctl.py +++ b/threadpoolctl.py @@ -76,14 +76,13 @@ sys.platform == "linux" # Make sure /proc is mounted: and os.path.exists("/proc/self") - # If dllist() is available (3.14+), and there is no GIL, no need to use - # /proc. It's possible dllist() might work in GIL builds too starting in - # Python 3.15, where it is written in C instead of being equivalent to - # threadpoolctl's implementation. But see - # https://github.com/python/cpython/issues/157573. We should consider + # If dllist() is available (3.14+), written in C so no risk of GC part way + # (3.15+), and there is no GIL, no need to use /proc, since dllist() is + # faster. It's possible dllist() might work in GIL builds too but see + # https://github.com/python/cpython/issues/157573. So we should consider # enabling it on GIL Python too once threadpoolctl supports 3.15. and not ( - sys.version_info[:2] >= (3, 14) + sys.version_info[:2] >= (3, 15) and not getattr(sys, "_is_gil_enabled", lambda: True)() ) ) @@ -1162,7 +1161,10 @@ def _load_libraries(self): # libffi builds (#225). dllist also uses dl_iterate_phdr internally # (#239), which we already avoid on Linux via /proc/self/maps. dllist = None - if sys.platform != "emscripten": + if sys.platform != "emscripten" or ( + # Python 3.15 doesn't have the CFUNCTYPE anymore: + sys.platform == "linux" and sys.version_info[:2] >= (3, 15) + ): try: from ctypes.util import dllist except ImportError: From e87c8fedd1fad0fd3a466a06f179c25eb9ff7fd8 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Wed, 16 Sep 2026 11:56:05 -0400 Subject: [PATCH 14/34] Changelog entry --- CHANGES.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index a23ce6e0..2529e3bc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,10 @@ 3.8.0 (under development) ========================= -TODO: update me. +- Run faster on Linux, where the new `/proc/self/maps` mechanism in 3.7.0 added + quite a bit of overhead. + https://github.com/joblib/threadpoolctl/pull/250 + 3.7.0 (2026-09-15) ================== From f7d625e9b210bdef88d214dd122076982d33f771 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Wed, 16 Sep 2026 11:57:32 -0400 Subject: [PATCH 15/34] Reformat with black to match CI --- threadpoolctl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/threadpoolctl.py b/threadpoolctl.py index f47bc2cd..bb963c26 100644 --- a/threadpoolctl.py +++ b/threadpoolctl.py @@ -1163,7 +1163,8 @@ def _load_libraries(self): dllist = None if sys.platform != "emscripten" or ( # Python 3.15 doesn't have the CFUNCTYPE anymore: - sys.platform == "linux" and sys.version_info[:2] >= (3, 15) + sys.platform == "linux" + and sys.version_info[:2] >= (3, 15) ): try: from ctypes.util import dllist From c75c9f774c50e90841acba2d67899929dd274abe Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Wed, 16 Sep 2026 12:02:06 -0400 Subject: [PATCH 16/34] Correct ctypes.util logic on Linux --- tests/test_threadpoolctl.py | 6 +++++- threadpoolctl.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_threadpoolctl.py b/tests/test_threadpoolctl.py index 0aebee0c..f13f9c99 100644 --- a/tests/test_threadpoolctl.py +++ b/tests/test_threadpoolctl.py @@ -1167,7 +1167,11 @@ def create_controllers(): @pytest.mark.skipif( not sys.platform.startswith("linux"), - reason="ctypes.util is only avoided on Linux (#225)", + reason="ctypes.util is only avoided on Linux (#225) in Python 3.14", +) +@pytest.mark.skipif( + sys.version_info[:2] >= (3, 15), + reason="Python 3.15 shouldn't have the issue in #225", ) def test_linux_does_not_import_ctypes_util(): # ctypes.util on CPython 3.14 Linux allocates a process-lifetime CFUNCTYPE diff --git a/threadpoolctl.py b/threadpoolctl.py index bb963c26..41bb5994 100644 --- a/threadpoolctl.py +++ b/threadpoolctl.py @@ -1161,7 +1161,7 @@ def _load_libraries(self): # libffi builds (#225). dllist also uses dl_iterate_phdr internally # (#239), which we already avoid on Linux via /proc/self/maps. dllist = None - if sys.platform != "emscripten" or ( + if sys.platform != "emscripten" and ( # Python 3.15 doesn't have the CFUNCTYPE anymore: sys.platform == "linux" and sys.version_info[:2] >= (3, 15) From c0abb0a7fdc027a2018ccae0f5dd2e0bde85f744 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 17 Sep 2026 11:48:31 -0400 Subject: [PATCH 17/34] Apply batched suggestions from code review, removing dead code Co-authored-by: Olivier Grisel --- tests/test_threadpoolctl.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_threadpoolctl.py b/tests/test_threadpoolctl.py index f13f9c99..654c3fdd 100644 --- a/tests/test_threadpoolctl.py +++ b/tests/test_threadpoolctl.py @@ -1128,13 +1128,12 @@ def test_controller_parallelism_no_deadlocks(): Lacking the fixes from PR #243, this deadlocks on Conda environments, at least, but possibly not on PyPI with Python from a Linux distro. """ - if sys.platform != "linux" or not hasattr(ctypes.PyDLL(None), "backtrace"): + if sys.platform != "linux": pytest.skip("Testing glibc on Linux") done = [] def create_controllers(): - buf = (ctypes.c_void_p * 20)() for _ in range(100): # May use dl_iterate_phdr() on Linux: limiter = threadpool_limits() From 3d2e5f56e61388e8a02bb29c82aae0c9ae34ac27 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 17 Sep 2026 11:57:21 -0400 Subject: [PATCH 18/34] Additional runners --- .github/workflows/test.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 64304eae..b58bba6e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -221,6 +221,39 @@ jobs: CC_OUTER_LOOP: "gcc" CC_INNER_LOOP: "gcc" + # Python 3.15, with GIL + - name: py315_gil_pip_openblas + os: ubuntu-latest + PACKAGER: "pip" + PYTHON_VERSION: "3.15-dev" + CC_OUTER_LOOP: "gcc" + CC_INNER_LOOP: "gcc" + + # Python 3.15, free-threaded + - name: py315_freethreaded_pip_openblas + os: ubuntu-latest + PACKAGER: "pip" + PYTHON_VERSION: "3.15t-dev" + CC_OUTER_LOOP: "gcc" + CC_INNER_LOOP: "gcc" + + # Python 3.14, free-threaded + - name: py315_freethreaded_pip_openblas + os: ubuntu-latest + PACKAGER: "pip" + PYTHON_VERSION: "3.14t" + CC_OUTER_LOOP: "gcc" + CC_INNER_LOOP: "gcc" + + # Ubuntu 24.04, which has glibc with different dl_iterate_phdr than + # latest Ubuntus. + - name: ubuntu24_04 + os: ubuntu-24.04 + PACKAGER: "pip" + PYTHON_VERSION: "3.14" + CC_OUTER_LOOP: "gcc" + CC_INNER_LOOP: "gcc" + env: ${{ matrix }} runs-on: ${{ matrix.os }} From 8755aeebc3c2e0042ad25e2c52d1db6abf7a2617 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 17 Sep 2026 12:27:32 -0400 Subject: [PATCH 19/34] Much more aggressive (and therefore robust) test for deadlocks. --- conftest.py | 1 + tests/_dl_iterate_phdr_deadlock.py | 69 ++++++++++++++++++++++++++++++ tests/test_threadpoolctl.py | 50 +++++++--------------- 3 files changed, 85 insertions(+), 35 deletions(-) create mode 100644 tests/_dl_iterate_phdr_deadlock.py diff --git a/conftest.py b/conftest.py index 53baddff..ad3e8ad3 100644 --- a/conftest.py +++ b/conftest.py @@ -2,4 +2,5 @@ "tests/empirical_scope_observation.py", "tests/_openmp_test_helper.py", "tests/_limit_blas.py", + "tests/_dl_iterate_phdr_deadlock.py" ] diff --git a/tests/_dl_iterate_phdr_deadlock.py b/tests/_dl_iterate_phdr_deadlock.py new file mode 100644 index 00000000..ac98c36a --- /dev/null +++ b/tests/_dl_iterate_phdr_deadlock.py @@ -0,0 +1,69 @@ +"""Try to trigger deadlock via dl_iterate_phdr.""" + +import ctypes +import os +import sys +from threading import Thread + +from threadpoolctl import threadpool_limits + + +def create_controllers(done): + for _ in range(100): + # May use dl_iterate_phdr() on Linux: + limiter = threadpool_limits() + # dlopen() of some shared libraries like to be installed: + loaded = False + # Common libraries on Ubuntu: + for name in [ + "libncurses.so.6", + "libncursesw.so.6", + "libgmp.so.10", + "libssl.so.3", + "libcrypt.so.1", + ]: + try: + dll = ctypes.CDLL(name) + del dll + loaded = True + except OSError: + pass + if not loaded: + # Couldn't do an dlopen()s. + os._exit(7) + + # Imports, which also do dlopen(): + import numpy # also gets us BLAS + import _pickle + + try: + import tests._openmp_test_helper.nested_prange_blas + except ImportError: + pass + + del limiter + + done.append(True) + + +def main(): + threads = [] + done = [] + + for _ in range(os.cpu_count() * 4): + t = Thread(target=create_controllers, args=(done,)) + threads.append(t) + t.start() + + for t in threads: + t.join() + + if len(done) != os.cpu_count() * 4: + sys.exit(1) + + # Special success exist code: + sys.exit(17) + + +if __name__ == "__main__": + main() diff --git a/tests/test_threadpoolctl.py b/tests/test_threadpoolctl.py index 654c3fdd..75a6ac9a 100644 --- a/tests/test_threadpoolctl.py +++ b/tests/test_threadpoolctl.py @@ -1121,47 +1121,27 @@ def test_conda_blas_detection_after_import(module): def test_controller_parallelism_no_deadlocks(): - """Creating a controller in parallel to itself does not cause deadlocks. + """ + Creating a controller in parallel to itself and other operations loading + shared libraries does not cause deadlocks. Non-regression test for https://github.com/joblib/threadpoolctl/issues/239 - - Lacking the fixes from PR #243, this deadlocks on Conda environments, at - least, but possibly not on PyPI with Python from a Linux distro. """ if sys.platform != "linux": pytest.skip("Testing glibc on Linux") - done = [] - - def create_controllers(): - for _ in range(100): - # May use dl_iterate_phdr() on Linux: - limiter = threadpool_limits() - # dlopen(): - try: - # Should be available in most Linux, and importantly isn't - # loaded by default into Python: - dll = ctypes.CDLL("libncurses.so.6") - del dll - except OSError: - done.append(False) - return - del limiter - done.append(True) - - threads = [] - for _ in range(os.cpu_count() * 4): - t = Thread(target=create_controllers) - threads.append(t) - t.start() - - for t in threads: - t.join() - - if False in done: - pytest.skip("libncurses.so.6 not available") - - assert len(done) == os.cpu_count() * 4 + # Deadlock isn't always reliable, so run multiple times: + for _ in range(10): + process = subprocess.run( + [sys.executable, "-m", "tests._dl_iterate_phdr_deadlock"], timeout=10 + ) + + if process.returncode == 7: + # Special code indicating it couldn't load any shared libraries. + pytest.skip("Couldn't find any of the exected shared libraries") + + # Special code indicating success: + assert process.returncode == 17 @pytest.mark.skipif( From e6b88711b43d1f7c80681d387f8a3266c565e06d Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 17 Sep 2026 12:32:39 -0400 Subject: [PATCH 20/34] Maybe correct freethreading installations --- .github/workflows/test.yml | 16 +++++++++------- continuous_integration/install.sh | 4 +++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b58bba6e..f0ed7ac8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -224,24 +224,26 @@ jobs: # Python 3.15, with GIL - name: py315_gil_pip_openblas os: ubuntu-latest - PACKAGER: "pip" - PYTHON_VERSION: "3.15-dev" + PACKAGER: "conda-forge" + PYTHON_VERSION: "3.15" CC_OUTER_LOOP: "gcc" CC_INNER_LOOP: "gcc" # Python 3.15, free-threaded - name: py315_freethreaded_pip_openblas os: ubuntu-latest - PACKAGER: "pip" - PYTHON_VERSION: "3.15t-dev" + PACKAGER: "conda-forge" + PYTHON_VERSION: "3.15" + FREETHREADING: "1" CC_OUTER_LOOP: "gcc" CC_INNER_LOOP: "gcc" # Python 3.14, free-threaded - name: py315_freethreaded_pip_openblas os: ubuntu-latest - PACKAGER: "pip" - PYTHON_VERSION: "3.14t" + PACKAGER: "conda-forge" + PYTHON_VERSION: "3.14" + FREETHREADING: "1" CC_OUTER_LOOP: "gcc" CC_INNER_LOOP: "gcc" @@ -249,7 +251,7 @@ jobs: # latest Ubuntus. - name: ubuntu24_04 os: ubuntu-24.04 - PACKAGER: "pip" + PACKAGER: "conda-forge" PYTHON_VERSION: "3.14" CC_OUTER_LOOP: "gcc" CC_INNER_LOOP: "gcc" diff --git a/continuous_integration/install.sh b/continuous_integration/install.sh index cd9fc2d2..b6841e3d 100755 --- a/continuous_integration/install.sh +++ b/continuous_integration/install.sh @@ -41,7 +41,9 @@ make_conda() { fi fi - if [[ "$PYTHON_VERSION" == "*" ]]; then + if [[ "$FREETHREADING" == "1"]]; then + TO_INSTALL="$TO_INSTALL python-freethreading" + elif [[ "$PYTHON_VERSION" == "*" ]]; then # Avoid installing free-threaded python TO_INSTALL="$TO_INSTALL python-gil" fi From 0dc7249bc26702111d4e554c319e3df5c6410d85 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 17 Sep 2026 12:34:10 -0400 Subject: [PATCH 21/34] Reformat --- conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conftest.py b/conftest.py index ad3e8ad3..11855652 100644 --- a/conftest.py +++ b/conftest.py @@ -2,5 +2,5 @@ "tests/empirical_scope_observation.py", "tests/_openmp_test_helper.py", "tests/_limit_blas.py", - "tests/_dl_iterate_phdr_deadlock.py" + "tests/_dl_iterate_phdr_deadlock.py", ] From a31a0ab73359006ab2492c4f44e5ff31556f7dad Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 17 Sep 2026 12:38:52 -0400 Subject: [PATCH 22/34] Check for GIL on every call since imports can change. Also move some checks out to save a little time. --- threadpoolctl.py | 84 ++++++++++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/threadpoolctl.py b/threadpoolctl.py index 41bb5994..9690121e 100644 --- a/threadpoolctl.py +++ b/threadpoolctl.py @@ -24,6 +24,22 @@ from functools import lru_cache from contextlib import ContextDecorator +# ctypes.util is not imported on Linux: on CPython 3.14 it allocates a +# process-lifetime CFUNCTYPE callback that is not fork-safe with some libffi +# builds (#225). dllist also uses dl_iterate_phdr internally (#239), which we +# already avoid on Linux via /proc/self/maps on older versions of Python. +dllist = None +if sys.platform != "emscripten" and ( + # Python 3.15 doesn't have the CFUNCTYPE anymore: + sys.platform == "linux" and sys.version_info[:2] >= (3, 15) +): + try: + from ctypes.util import dllist + except ImportError: + # CPython before 3.14 does not provide dll inspection. + dllist = None + + __version__ = "3.8.0.dev0" __all__ = [ "threadpool_limits", @@ -56,35 +72,12 @@ _SYSTEM_UINT = ctypes.c_uint64 if sys.maxsize > 2**32 else ctypes.c_uint32 _SYSTEM_UINT_HALF = ctypes.c_uint32 if sys.maxsize > 2**32 else ctypes.c_uint16 -# On glibc 2.39 and earlier, dl_iterate_phdr has an internal lock, and that -# plus calling back into Python and the need to (re)acquire the GIL can result -# in deadlocks. -# -# On glibc 2.40 and later, dl has a read-write lock, and dl_iterate_phdr should -# only use the read version since it's not modifying anything. So you no longer -# get deadlocks purely from dl_iterate_phdr. You can still deadlock with -# dlopen() though. -# -# To avoid that deadlock, listing shared libraries can use a Linux-specific -# mechanism that doesn't have these issues (/proc/self/maps). Since it's the -# Linux kernel, musl works fine too. -# -# The downside is this mechanism is slower, so avoid it when safe alternatives -# are available. -_USE_PROCFS = ( + +_HAS_PROCFS = ( # Only available on Linux: sys.platform == "linux" # Make sure /proc is mounted: and os.path.exists("/proc/self") - # If dllist() is available (3.14+), written in C so no risk of GC part way - # (3.15+), and there is no GIL, no need to use /proc, since dllist() is - # faster. It's possible dllist() might work in GIL builds too but see - # https://github.com/python/cpython/issues/157573. So we should consider - # enabling it on GIL Python too once threadpoolctl supports 3.15. - and not ( - sys.version_info[:2] >= (3, 15) - and not getattr(sys, "_is_gil_enabled", lambda: True)() - ) ) @@ -1156,24 +1149,31 @@ def __len__(self): def _load_libraries(self): """Loop through loaded shared libraries and store the supported ones""" - # ctypes.util is not imported on Linux: on CPython 3.14 it allocates a - # process-lifetime CFUNCTYPE callback that is not fork-safe with some - # libffi builds (#225). dllist also uses dl_iterate_phdr internally - # (#239), which we already avoid on Linux via /proc/self/maps. - dllist = None - if sys.platform != "emscripten" and ( - # Python 3.15 doesn't have the CFUNCTYPE anymore: - sys.platform == "linux" - and sys.version_info[:2] >= (3, 15) + # On glibc 2.39 and earlier, dl_iterate_phdr has an internal lock, and + # that plus calling back into Python and the need to (re)acquire the + # GIL can result in deadlocks. + # + # On glibc 2.40 and later, dl has a read-write lock, and + # dl_iterate_phdr should only use the read version since it's not + # modifying anything. So you no longer get deadlocks purely from + # dl_iterate_phdr. You can still deadlock with dlopen() though. + # + # To avoid that deadlock, listing shared libraries can use a + # Linux-specific mechanism that doesn't have these issues + # (/proc/self/maps). Since it's the Linux kernel, musl works fine too. + # + # The downside is this mechanism is slower, so avoid it when safe + # alternatives are available. In particular, if dllist() is available + # (3.14+), written in C so no risk of GC part way (3.15+), and there is + # no GIL, no need to use /proc, since dllist() is faster. It's possible + # dllist() might work in GIL builds too but see + # https://github.com/python/cpython/issues/157573. So we should + # consider enabling it on GIL Python too once threadpoolctl supports + # 3.15. + if _HAS_PROCFS and not ( + sys.version_info[:2] >= (3, 15) + and not getattr(sys, "_is_gil_enabled", lambda: True)() ): - try: - from ctypes.util import dllist - except ImportError: - # CPython before 3.14 does not provide dll inspection. - dllist = None - - if _USE_PROCFS: - # See comment on _USE_PROCFS. self._find_libraries_with_linux() elif dllist is not None: # On Python 3.14+, this functionality is built-in. Once Python 3.13 From 59c8a637cf266cda849ccc5c54203c1a31f361d4 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 17 Sep 2026 12:42:47 -0400 Subject: [PATCH 23/34] Reformat harder --- threadpoolctl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/threadpoolctl.py b/threadpoolctl.py index 9690121e..177bb244 100644 --- a/threadpoolctl.py +++ b/threadpoolctl.py @@ -31,7 +31,8 @@ dllist = None if sys.platform != "emscripten" and ( # Python 3.15 doesn't have the CFUNCTYPE anymore: - sys.platform == "linux" and sys.version_info[:2] >= (3, 15) + sys.platform == "linux" + and sys.version_info[:2] >= (3, 15) ): try: from ctypes.util import dllist From 82d130cce0e4b2953e8f7bf95e4a229f4779f41a Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 17 Sep 2026 12:44:39 -0400 Subject: [PATCH 24/34] I love bash --- continuous_integration/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/continuous_integration/install.sh b/continuous_integration/install.sh index b6841e3d..42c551ba 100755 --- a/continuous_integration/install.sh +++ b/continuous_integration/install.sh @@ -41,7 +41,7 @@ make_conda() { fi fi - if [[ "$FREETHREADING" == "1"]]; then + if [[ "$FREETHREADING" == "1" ]]; then TO_INSTALL="$TO_INSTALL python-freethreading" elif [[ "$PYTHON_VERSION" == "*" ]]; then # Avoid installing free-threaded python From 152f1db0bf522c068ee1e855d05b64f149ad65a7 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 17 Sep 2026 12:47:24 -0400 Subject: [PATCH 25/34] Tell it which blas --- .github/workflows/test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f0ed7ac8..94e77743 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -225,6 +225,7 @@ jobs: - name: py315_gil_pip_openblas os: ubuntu-latest PACKAGER: "conda-forge" + BLAS: "openblas" PYTHON_VERSION: "3.15" CC_OUTER_LOOP: "gcc" CC_INNER_LOOP: "gcc" @@ -233,6 +234,7 @@ jobs: - name: py315_freethreaded_pip_openblas os: ubuntu-latest PACKAGER: "conda-forge" + BLAS: "openblas" PYTHON_VERSION: "3.15" FREETHREADING: "1" CC_OUTER_LOOP: "gcc" @@ -242,6 +244,7 @@ jobs: - name: py315_freethreaded_pip_openblas os: ubuntu-latest PACKAGER: "conda-forge" + BLAS: "openblas" PYTHON_VERSION: "3.14" FREETHREADING: "1" CC_OUTER_LOOP: "gcc" @@ -252,6 +255,7 @@ jobs: - name: ubuntu24_04 os: ubuntu-24.04 PACKAGER: "conda-forge" + BLAS: "openblas" PYTHON_VERSION: "3.14" CC_OUTER_LOOP: "gcc" CC_INNER_LOOP: "gcc" From 0883c2afc20c767263af05adde88429ae9cc6100 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 17 Sep 2026 12:50:03 -0400 Subject: [PATCH 26/34] Not always available --- tests/_dl_iterate_phdr_deadlock.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/_dl_iterate_phdr_deadlock.py b/tests/_dl_iterate_phdr_deadlock.py index ac98c36a..f55af53e 100644 --- a/tests/_dl_iterate_phdr_deadlock.py +++ b/tests/_dl_iterate_phdr_deadlock.py @@ -33,7 +33,10 @@ def create_controllers(done): os._exit(7) # Imports, which also do dlopen(): - import numpy # also gets us BLAS + try: + import numpy # also gets us BLAS + except ImportError: + pass import _pickle try: From cbd587df6affd47c3c94b92091cc61be59053dc1 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 17 Sep 2026 12:52:17 -0400 Subject: [PATCH 27/34] Add a channel for rcs --- continuous_integration/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/continuous_integration/install.sh b/continuous_integration/install.sh index 42c551ba..72a84248 100755 --- a/continuous_integration/install.sh +++ b/continuous_integration/install.sh @@ -83,7 +83,7 @@ elif [[ "$PACKAGER" == "conda-forge" ]]; then if [[ "$INSTALL_OPENCV" == "true" ]]; then TO_INSTALL="$TO_INSTALL opencv" fi - make_conda "conda-forge" "$TO_INSTALL" + make_conda "conda-forge" "$TO_INSTALL -c conda-forge/label/python_rc" elif [[ "$PACKAGER" == "pip" ]]; then # Use conda to build an empty python env and then use pip to install From 5090cbb5f7cd462d56849513565d22c5975f3d08 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 17 Sep 2026 12:56:07 -0400 Subject: [PATCH 28/34] A better way --- continuous_integration/install.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/continuous_integration/install.sh b/continuous_integration/install.sh index 72a84248..64060675 100755 --- a/continuous_integration/install.sh +++ b/continuous_integration/install.sh @@ -53,6 +53,10 @@ make_conda() { # prevent mixing conda channels conda config --set channel_priority strict conda config --add channels $CHANNEL + if [[ "$CHANNEL" == "conda-forge" ]]; then + # Support Python release candidates + conda config --add channels conda-forge/label/python_rc + fi conda update -n base conda conda-libmamba-solver -q --yes conda config --set solver libmamba @@ -83,7 +87,7 @@ elif [[ "$PACKAGER" == "conda-forge" ]]; then if [[ "$INSTALL_OPENCV" == "true" ]]; then TO_INSTALL="$TO_INSTALL opencv" fi - make_conda "conda-forge" "$TO_INSTALL -c conda-forge/label/python_rc" + make_conda "conda-forge" "$TO_INSTALL" elif [[ "$PACKAGER" == "pip" ]]; then # Use conda to build an empty python env and then use pip to install From 83ecfb363fb814dc2fc2c93ff7b5bf0c1e548f6b Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 17 Sep 2026 13:02:12 -0400 Subject: [PATCH 29/34] Try a different approach --- continuous_integration/install.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/continuous_integration/install.sh b/continuous_integration/install.sh index 64060675..e91ed12e 100755 --- a/continuous_integration/install.sh +++ b/continuous_integration/install.sh @@ -50,10 +50,9 @@ make_conda() { # Need to source conda.sh before first conda command source "$CONDA/etc/profile.d/conda.sh" - # prevent mixing conda channels - conda config --set channel_priority strict - conda config --add channels $CHANNEL if [[ "$CHANNEL" == "conda-forge" ]]; then + conda config --add channels conda-forge + conda config --remove channels defaults # Support Python release candidates conda config --add channels conda-forge/label/python_rc fi From b5d9071d4ee7722d1ad14465d7c53313905c4787 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 17 Sep 2026 13:03:09 -0400 Subject: [PATCH 30/34] Better order --- continuous_integration/install.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/continuous_integration/install.sh b/continuous_integration/install.sh index e91ed12e..ed97bc11 100755 --- a/continuous_integration/install.sh +++ b/continuous_integration/install.sh @@ -50,6 +50,9 @@ make_conda() { # Need to source conda.sh before first conda command source "$CONDA/etc/profile.d/conda.sh" + conda update -n base conda conda-libmamba-solver -q --yes + conda config --set solver libmamba + if [[ "$CHANNEL" == "conda-forge" ]]; then conda config --add channels conda-forge conda config --remove channels defaults @@ -57,9 +60,6 @@ make_conda() { conda config --add channels conda-forge/label/python_rc fi - conda update -n base conda conda-libmamba-solver -q --yes - conda config --set solver libmamba - conda create -n testenv -q --yes python=$PYTHON_VERSION $TO_INSTALL conda activate testenv } From 4a0b442fbdbe38fcc40d06a7216b7762e2528668 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 17 Sep 2026 13:06:59 -0400 Subject: [PATCH 31/34] Conda claims no defaults? --- continuous_integration/install.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/continuous_integration/install.sh b/continuous_integration/install.sh index ed97bc11..3e492c43 100755 --- a/continuous_integration/install.sh +++ b/continuous_integration/install.sh @@ -53,9 +53,10 @@ make_conda() { conda update -n base conda conda-libmamba-solver -q --yes conda config --set solver libmamba + conda config --show-sources + if [[ "$CHANNEL" == "conda-forge" ]]; then conda config --add channels conda-forge - conda config --remove channels defaults # Support Python release candidates conda config --add channels conda-forge/label/python_rc fi From 2ca058e73add2e93d95baf692e860f2983136c97 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 17 Sep 2026 13:10:23 -0400 Subject: [PATCH 32/34] Maybe this'll fix it --- continuous_integration/install.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/continuous_integration/install.sh b/continuous_integration/install.sh index 3e492c43..a1e5b2f1 100755 --- a/continuous_integration/install.sh +++ b/continuous_integration/install.sh @@ -59,6 +59,7 @@ make_conda() { conda config --add channels conda-forge # Support Python release candidates conda config --add channels conda-forge/label/python_rc + conda config --add channels conda-forge/label/numpy_rc fi conda create -n testenv -q --yes python=$PYTHON_VERSION $TO_INSTALL From d3d322ead46024d96f0fb72d9a9217af63810601 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 17 Sep 2026 13:14:45 -0400 Subject: [PATCH 33/34] Drop Python 3.15 for now --- .github/workflows/test.yml | 19 ------------------- continuous_integration/install.sh | 13 ++++--------- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 94e77743..046a2d0f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -221,25 +221,6 @@ jobs: CC_OUTER_LOOP: "gcc" CC_INNER_LOOP: "gcc" - # Python 3.15, with GIL - - name: py315_gil_pip_openblas - os: ubuntu-latest - PACKAGER: "conda-forge" - BLAS: "openblas" - PYTHON_VERSION: "3.15" - CC_OUTER_LOOP: "gcc" - CC_INNER_LOOP: "gcc" - - # Python 3.15, free-threaded - - name: py315_freethreaded_pip_openblas - os: ubuntu-latest - PACKAGER: "conda-forge" - BLAS: "openblas" - PYTHON_VERSION: "3.15" - FREETHREADING: "1" - CC_OUTER_LOOP: "gcc" - CC_INNER_LOOP: "gcc" - # Python 3.14, free-threaded - name: py315_freethreaded_pip_openblas os: ubuntu-latest diff --git a/continuous_integration/install.sh b/continuous_integration/install.sh index a1e5b2f1..42c551ba 100755 --- a/continuous_integration/install.sh +++ b/continuous_integration/install.sh @@ -50,18 +50,13 @@ make_conda() { # Need to source conda.sh before first conda command source "$CONDA/etc/profile.d/conda.sh" + # prevent mixing conda channels + conda config --set channel_priority strict + conda config --add channels $CHANNEL + conda update -n base conda conda-libmamba-solver -q --yes conda config --set solver libmamba - conda config --show-sources - - if [[ "$CHANNEL" == "conda-forge" ]]; then - conda config --add channels conda-forge - # Support Python release candidates - conda config --add channels conda-forge/label/python_rc - conda config --add channels conda-forge/label/numpy_rc - fi - conda create -n testenv -q --yes python=$PYTHON_VERSION $TO_INSTALL conda activate testenv } From 59f9b327c57c90358f41cb7813ea11b7206ebc77 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 17 Sep 2026 13:18:12 -0400 Subject: [PATCH 34/34] Correct the name --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 046a2d0f..a77b1c5f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -222,7 +222,7 @@ jobs: CC_INNER_LOOP: "gcc" # Python 3.14, free-threaded - - name: py315_freethreaded_pip_openblas + - name: py314_freethreaded_pip_openblas os: ubuntu-latest PACKAGER: "conda-forge" BLAS: "openblas"