Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .github/workflows/build-and-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-22.04, macos-14, windows-2022 ]
python-version: [ "3.11", "3.12", "3.13", "3.14" ]
# 3.14t is the free-threaded build. No 3.13t: NumPy ships no cp313t
# wheels from 2.5 on, so it would compile NumPy from source.
python-version: [ "3.11", "3.12", "3.13", "3.14", "3.14t" ]

env:
PYTHON_VERSION: ${{ matrix.python-version }}
Expand Down Expand Up @@ -60,6 +62,7 @@ jobs:
- name: Build wheels
uses: pypa/cibuildwheel@v3.3.0
env:
# cp314t needs no CIBW_ENABLE; cibuildwheel gates only cp313t.
CIBW_BUILD: "cp3${{ env.PYTHON_SUBVERSION }}-*"
CIBW_SKIP: "*-win32 *-manylinux_i686 *-musllinux*"
CIBW_ARCHS_MACOS: "x86_64 universal2"
Expand All @@ -69,12 +72,13 @@ jobs:
CIBW_ENVIRONMENT_WINDOWS: "CMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake"
# Bundle openblas.dll into the wheel (auditwheel/delocate handle this on Linux/macOS)
CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: "delvewheel repair -w {dest_dir} {wheel} --add-path C:/vcpkg/installed/x64-windows/bin"
CIBW_TEST_COMMAND: python -c "import sparsediffpy"
# On free-threaded builds, assert the import did not re-enable the GIL.
CIBW_TEST_COMMAND: python -c "import sys, sysconfig, sparsediffpy; ft = bool(sysconfig.get_config_var('Py_GIL_DISABLED')); assert not ft or not sys._is_gil_enabled(), 'GIL re-enabled on import'; print('freethreaded_build=', ft)"
- name: Check wheels
shell: bash
run: |
pip install --upgrade twine
twine check wheelhouse/*
python -m pip install --upgrade twine
python -m twine check wheelhouse/*

- uses: actions/upload-artifact@v4
with:
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,25 @@ pip install sparsediffpy
from sparsediffpy import _sparsediffengine
```

## Free-threaded Python

Wheels are published for free-threaded CPython 3.14 (`cp314t`) as well as the
default builds. There is no `cp313t` wheel because NumPy ships none from 2.5
onwards, so that build would have to compile NumPy from source.

The extension declares that it does not need the GIL, so importing it leaves
free threading enabled. Distinct problems can be built and evaluated
concurrently from different threads: the engine keeps no mutable global state,
every problem and expression owns its own buffers, and all inputs and outputs
are copied at the boundary. This assumes a default build, since
`SP_TRACK_MEMORY` adds global allocation counters that are not thread-safe.

A single problem or expression capsule is **not** thread-safe. Do not call into
the same problem from two threads at once, and do not share expression capsules
between problems that are evaluated concurrently. Note that `expr::refcount` is
a plain `int`, so breaking this rule corrupts the heap rather than merely
interleaving calls, as it would under the GIL.

## License

Apache License 2.0
2 changes: 1 addition & 1 deletion SparseDiffEngine
Submodule SparseDiffEngine updated 136 files
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ file = "README.md"
content-type = "text/markdown"

[tool.scikit-build]
cmake.version = ">=3.15"
# 3.30.3+ so FindPython3 detects the free-threaded ABI on all platforms.
cmake.version = ">=3.30.3"
cmake.build-type = "Release"
wheel.packages = ["sparsediffpy"]
# Persistent build dir: avoids scikit-build-core's TemporaryDirectory, whose cleanup
Expand Down
12 changes: 11 additions & 1 deletion sparsediffpy/_bindings/bindings.c
Original file line number Diff line number Diff line change
Expand Up @@ -187,5 +187,15 @@ static struct PyModuleDef sparsediffpy_module = {
PyMODINIT_FUNC PyInit__sparsediffengine(void)
{
if (ensure_numpy() < 0) return NULL;
return PyModule_Create(&sparsediffpy_module);
PyObject *module = PyModule_Create(&sparsediffpy_module);
if (!module) return NULL;
#ifdef Py_GIL_DISABLED
/* Requires an engine built without SP_TRACK_MEMORY (the default), which
leaves no mutable global state, so distinct problems are independent.
A single problem or expression capsule is still single-threaded:
expr::refcount is a plain int, so sharing one across threads corrupts
the heap. See README.md. */
PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED);
#endif
return module;
}
Loading