Skip to content
Open
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
26 changes: 21 additions & 5 deletions .github/workflows/build-and-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ jobs:
matrix:
os: [ ubuntu-22.04, macos-14, windows-2022 ]
python-version: [ "3.11", "3.12", "3.13", "3.14" ]
# "" builds the default (GIL) interpreter, "t" the free-threaded one.
# Only 3.14t: NumPy >= 2.5 ships no cp313t wheels on any platform, so a
# 3.13t build would have to compile NumPy from source.
abi: [ "", "t" ]
exclude:
- python-version: "3.11"
abi: "t"
- python-version: "3.12"
abi: "t"
- python-version: "3.13"
abi: "t"
Comment on lines +35 to +45

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of this, we should maybe just add 3.14t?


env:
PYTHON_VERSION: ${{ matrix.python-version }}
Expand Down Expand Up @@ -60,7 +71,10 @@ jobs:
- name: Build wheels
uses: pypa/cibuildwheel@v3.3.0
env:
CIBW_BUILD: "cp3${{ env.PYTHON_SUBVERSION }}-*"
CIBW_BUILD: "cp3${{ env.PYTHON_SUBVERSION }}${{ matrix.abi }}-*"
# Opt in to cp3XXt build identifiers; harmless for the GIL builds since
# CIBW_BUILD already selects a single ABI.
CIBW_ENABLE: "cpython-freethreading"
CIBW_SKIP: "*-win32 *-manylinux_i686 *-musllinux*"
CIBW_ARCHS_MACOS: "x86_64 universal2"
CIBW_ARCHS_LINUX: "auto aarch64"
Expand All @@ -69,16 +83,18 @@ 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 also assert the import did not re-enable the GIL,
# i.e. the extension really declares Py_MOD_GIL_NOT_USED.
CIBW_TEST_COMMAND: python -c "import sys, sysconfig, sparsediffpy; ft = sysconfig.get_config_var('Py_GIL_DISABLED') == 1; assert not ft or not sys._is_gil_enabled(), 'GIL re-enabled on import'"
- 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:
name: wheels-${{ matrix.os }}-${{ matrix.python-version }}
name: wheels-${{ matrix.os }}-${{ matrix.python-version }}${{ matrix.abi }}
path: ./wheelhouse/*.whl

build_sdist:
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ 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 3.13t wheel because NumPy 2.5 and later ship no
`cp313t` wheels. The extension declares that it does not need the GIL:
the engine keeps no global mutable state, every problem and expression owns its
own buffers, and all inputs and outputs are copied at the boundary. Distinct
problems can therefore be built and evaluated concurrently from different
threads.

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. This is the same contract as
under the GIL, which only serialized individual calls and never protected
against interleaved use of one object.

## License

Apache License 2.0
14 changes: 13 additions & 1 deletion sparsediffpy/_bindings/bindings.c
Original file line number Diff line number Diff line change
Expand Up @@ -187,5 +187,17 @@ 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
/* Free-threaded CPython (3.13t+): declare that this module does not need
the GIL. The engine keeps no global mutable state -- every problem and
expression owns its own buffers, and the wrappers copy inputs and
outputs -- so distinct problems may be used concurrently from different
threads. A single problem or expression capsule is not thread-safe and
must not be used from two threads at once (same contract as with the
GIL, which never protected against interleaved calls on one object). */
PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED);
#endif
return module;
}
Loading