diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d6b8161ef8..755a7bb9a7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -118,6 +118,7 @@ This release is compatible with NumPy 2.5. * Fixed incorrect results of `dpnp.tensor.vecdot` in some cases with strided outputs and of `dpnp.tensor` reductions, `dpnp.tensor.vecdot` and `dpnp.tensor.matmul` on large inputs with some data types [#3082](https://github.com/IntelPython/dpnp/pull/3082) * Fixed `dpnp.median` and `dpnp.nanmedian` raising a `ValueError` for a tuple `axis` when a kept dimension has size 0 [#3081](https://github.com/IntelPython/dpnp/pull/3081) * Fixed a `RuntimeError` raised by `dpnp.tensor` kernels launched over more than `INT_MAX` work-items when built with DPC++ compiler 2026.2 or newer, by also passing `-fno-sycl-id-queries-fit-in-int` to the linker [#3089](https://github.com/IntelPython/dpnp/pull/3089) +* Fixed importing `dpnp.scipy.sparse` re-enabling the GIL on free-threaded Python builds, because its pybind11 extension did not declare free-threading support [#3101](https://github.com/IntelPython/dpnp/pull/3101) ### Security diff --git a/dpnp/backend/extensions/sparse/sparse_py.cpp b/dpnp/backend/extensions/sparse/sparse_py.cpp index 96c414238de..09f0d77e213 100644 --- a/dpnp/backend/extensions/sparse/sparse_py.cpp +++ b/dpnp/backend/extensions/sparse/sparse_py.cpp @@ -43,7 +43,7 @@ using dpnp::extensions::sparse::sparse_gemv_compute; using dpnp::extensions::sparse::sparse_gemv_init; using dpnp::extensions::sparse::sparse_gemv_release; -PYBIND11_MODULE(_sparse_impl, m) +PYBIND11_MODULE(_sparse_impl, m, py::mod_gil_not_used()) { init_sparse_gemv_dispatch_tables(); diff --git a/dpnp/tests/test_free_threading.py b/dpnp/tests/test_free_threading.py new file mode 100644 index 00000000000..c032e3b0c3c --- /dev/null +++ b/dpnp/tests/test_free_threading.py @@ -0,0 +1,49 @@ +import os +import subprocess +import sys +import sysconfig + +import pytest + +# Prints the first dpnp module whose import re-enables the GIL, +# "deps" if a dependency already re-enabled it, or "ok". +_IMPORT_ALL = """ +import importlib, importlib.machinery, pathlib, sys +import dpctl, numpy +if sys._is_gil_enabled(): + print("deps") + sys.exit() +import dpnp +if sys._is_gil_enabled(): + print("dpnp") + sys.exit() +root = pathlib.Path(dpnp.__file__).parent +suffix = importlib.machinery.EXTENSION_SUFFIXES[0] +for path in sorted(root.rglob("*" + suffix)): + rel = path.relative_to(root.parent) + name = ".".join(rel.parts[:-1] + (path.name[: -len(suffix)],)) + importlib.import_module(name) + if sys._is_gil_enabled(): + print(name) + sys.exit() +print("ok") +""" + + +@pytest.mark.skipif( + not sysconfig.get_config_var("Py_GIL_DISABLED"), + reason="requires a free-threaded Python build", +) +def test_extensions_keep_gil_disabled(): + env = {k: v for k, v in os.environ.items() if k != "PYTHON_GIL"} + res = subprocess.run( + [sys.executable, "-c", _IMPORT_ALL], + capture_output=True, + text=True, + env=env, + ) + assert res.returncode == 0, res.stderr + out = res.stdout.strip() + if out == "deps": + pytest.skip("a dependency re-enabled the GIL") + assert out == "ok", f"importing {out} re-enabled the GIL"