Skip to content

[WIP] Build free-threaded (cp313t/cp314t) wheels and declare GIL-free support - #24

Open
dance858 wants to merge 2 commits into
mainfrom
free-threading-wheels
Open

[WIP] Build free-threaded (cp313t/cp314t) wheels and declare GIL-free support#24
dance858 wants to merge 2 commits into
mainfrom
free-threading-wheels

Conversation

@dance858

@dance858 dance858 commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Closes #18.

What

  • bindings.c: on free-threaded CPython, call PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED) after module creation so importing _sparsediffengine no longer re-enables the GIL. No-op on regular builds.
  • CI: add an abi matrix axis ("" / "t"), build cp314t wheels, set CIBW_ENABLE=cpython-freethreading, and extend the wheel test to assert sys._is_gil_enabled() is still False after import on the t build. Artifact names get the t suffix.
  • CI: run pip/twine via python -m in the wheel-check step (one macOS job failed there with pip: command not found).
  • README: document the thread-safety contract.

No 3.13t wheel. NumPy >= 2.5 ships no cp313t wheels on any platform, so a 3.13t build has to compile NumPy from source in the isolated build env. In the first CI run that failed on macOS, took 6 minutes on Windows, and was still running after an hour on Linux aarch64 under QEMU. Free-threading users on 3.13 would hit the same NumPy problem at install time anyway.

Why it is safe to declare GIL-free

Audit of the engine (submodule at d8cb9b8) and the bindings:

  • nm on libdnlp_diff.a shows no writable data/bss symbols. The only globals in the engine source are the SP_TRACK_MEMORY byte counters, which the wheel build does not enable. No function-local statics, no non-reentrant libc calls.
  • All refresh/version counters (values_version, jacobian_csc_seen, csc_seen, transpose_seen) live inside the owning matrix or expr node.
  • Every problem and expr owns its buffers. variable forward, problem_update_params, new_parameter and new_problem memcpy their inputs; every wrapper copies results into fresh NumPy arrays. No Python memory is aliased across calls.
  • The only static state in the bindings is the NumPy-init flag and the module def, written once at import under the import lock.
  • OpenBLAS is safe for concurrent calls.

Caveat, documented in the README: expr refcounts are plain ints, so one expr/problem capsule must not be used from two threads at once. That is the same contract as under the GIL, which only serialized individual calls and never protected against interleaved use of one object.

Verification

  • Both code paths compile cleanly with -Wall -Wextra (regular headers, and with Py_GIL_DISABLED defined).
  • Built the cp314t extension against Homebrew python3.14t 3.14.7: import keeps the GIL disabled (the exact check CI now runs).
  • 16 threads x 200 evaluations on distinct problems (forward, jacobian, hessian over exp/sin/log/sum) are bit-identical to a single-threaded reference, across 5 runs, on both the free-threaded and the regular 3.14 build.
  • First CI run: the cp314t wheels built and passed the GIL check on Linux x86_64, Windows, and macOS x86_64 + arm64 (universal2).

🤖 Generated with Claude Code

https://claude.ai/code/session_013aQs1u37EG6XTHFrcaBRkA

Closes #18.

The extension now calls PyUnstable_Module_SetGIL(Py_MOD_GIL_NOT_USED) on
free-threaded CPython, so importing it no longer re-enables the GIL. The
build matrix gains an `abi` axis ("" / "t") for 3.13 and 3.14, opts in via
CIBW_ENABLE=cpython-freethreading, and the wheel test asserts the GIL stays
disabled after import on the t builds.

Audit backing the declaration (engine d8cb9b8 + these bindings):
- nm on libdnlp_diff.a shows no writable data/bss symbols; the only globals
  in the engine are the SP_TRACK_MEMORY byte counters, which the wheel
  build does not enable. No function-local statics, no non-reentrant libc.
- All version counters (values_version, *_seen) are per-matrix/per-expr.
- Every problem/expr owns its buffers; forward, update_params and the
  problem constructor memcpy their inputs, and every wrapper copies results
  into fresh NumPy arrays, so no Python memory is aliased across calls.
- The only static state in the bindings is the NumPy-init flag and the
  module def, both written once at import under the import lock.
- expr refcounts are plain ints, so a single expr/problem capsule must not
  be used from two threads at once -- the same contract as with the GIL.

Verified locally on Homebrew python3.14t: import keeps the GIL disabled and
16 threads x 200 evaluations on distinct problems are bit-identical to a
single-threaded reference (forward, jacobian, hessian).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013aQs1u37EG6XTHFrcaBRkA
@dance858 dance858 changed the title Build free-threaded (cp313t/cp314t) wheels and declare GIL-free support [WIP] Build free-threaded (cp313t/cp314t) wheels and declare GIL-free support Sep 5, 2026
NumPy >= 2.5 ships no cp313t wheels on any platform, so the 3.13t builds
had to compile NumPy from source: macOS failed outright, Windows took six
minutes for the NumPy build alone, and Linux aarch64 under QEMU was still
running after an hour. Only 3.14t is built now.

The macOS 3.14t job built and tested both wheels fine (GIL stays disabled
on x86_64 and arm64) but then failed in the twine step with
"pip: command not found". Call pip and twine via python -m so the step
does not depend on a pip launcher being on PATH.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013aQs1u37EG6XTHFrcaBRkA
Comment on lines +35 to +45
# "" 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"

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for free-threading

2 participants