diff --git a/CHANGELOG.md b/CHANGELOG.md index 833a3fb5e2d..4cbf75bdaf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -122,6 +122,7 @@ This release is compatible with NumPy 2.5. * 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 `dpnp.__all__` missing `ndarray` and `__array_namespace_info__` and listing `mean`, `median` and `min` twice [#3100](https://github.com/IntelPython/dpnp/pull/3100) * Fixed `dpnp.repeat` and `dpnp.tensor.repeat` silently returning wrong results for `uint64` repeats above the `int64` maximum [#3115](https://github.com/IntelPython/dpnp/pull/3115) +* 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"