Commit 3c84c8e
Add free-threading (no-GIL) support for Python 3.13t+ (#175)
* Add free-threading (no-GIL) support for Python 3.13t+
Enables scs to run with the GIL disabled in free-threaded CPython builds.
C extension changes:
- Add PyMutex per-instance lock (only in Py_GIL_DISABLED builds) to serialize
concurrent access to the SCS workspace from multiple threads
- Lock around scs_solve, scs_update, and scs_finish (pure C calls safe to
hold across Py_BEGIN_ALLOW_THREADS)
- No locking in SCS_init (object is thread-local during construction)
- Declare module as GIL-not-used via PyUnstable_Module_SetGIL
Build/packaging:
- Enable cpython-freethreading in cibuildwheel to ship 3.13t+ wheels
- Add free-threading PyPI classifier
CI:
- Add free-threading test workflow (3.13t + 3.14t) alongside existing build.yml
Tests:
- 15 new concurrency tests covering: independent instances, shared instances,
solve+update sequences, warm starts, legacy API, stress tests, result isolation
Closes #130
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix free-threading CI: create venv before installing packages
uv pip install requires a virtual environment or --system flag.
Use uv venv to create a proper venv for the free-threaded Python.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix free-threading CI: install build deps for --no-build-isolation
meson-python and meson must be in the venv when using --no-build-isolation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix free-threading CI: use build isolation instead of --no-build-isolation
Let the build system handle its own dependencies (meson-python, numpy)
via pyproject.toml build-system.requires, rather than manually installing
them and using --no-build-isolation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix free-threading CI: install pytest-timeout for --timeout flag
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Address free-threading review feedback and make all tests thread-safe
Correctness fixes from @ngoldbaum's review:
- PyDict_GetItemString -> PyDict_GetItemStringRef (strong refs)
- PyList_GetItem -> PyList_GetItemRef (strong refs)
- Proper Py_DECREF and scs_free on all error paths
- Move self->work NULL check under lock (TOCTOU fix) in SCS_solve/SCS_update
- Check PyThread_acquire_lock return value (PY_LOCK_ACQUIRED)
- Add comment to SCS_finish explaining unchecked lock acquire in dealloc path
- Fix SCS_update lock release ordering with comment explaining why it
differs from SCS_solve (avoid holding instance lock while waiting for GIL)
Thread-safe RNG in all test files:
- Replace all np.random.seed() + global RNG with local RandomState instances
- Each test file gets unique seeds so tests exercise different random data
- Backend-variant tests (direct/dense/mkl/cudss) share seeds intentionally
since they test the same problem with different linear system solvers
Testing infrastructure:
- 27 new threading tests covering borrowed refs, TOCTOU races, concurrent
solve+update, re-init races, error path lock release, stress tests
- GIL re-enable detection in conftest.py (modeled after NumPy)
- pytest-run-parallel in CI (--parallel-threads=4 --iterations=3)
- TSan CI job with cached CPython 3.14t build
- faulthandler_timeout and thread_unsafe markers in pyproject.toml
- tsan-suppressions.txt for known CPython-internal races
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix thread-safe signal handling and link pthreads
Update scs_source submodule to thread-safe-ctrlc branch which adds
mutex-protected reference counting for signal handler registration,
fixing TSan-detected data races when multiple threads solve concurrently.
Add pthreads dependency to meson.build for the ctrlc mutex.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Set OPENBLAS_NUM_THREADS=1 for TSan CI
OpenBLAS spawns internal threads that are not instrumented with TSan
annotations, causing false positive data race reports (e.g. in dsyrk
worker threads racing with the caller after the BLAS call returns).
Forcing single-threaded BLAS under TSan is standard practice.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add ASan CI job via sanitizer matrix
Refactor the TSan CI job into a matrix over {tsan, asan}. Both build
CPython 3.14t from source with the corresponding sanitizer flag. Runtime
options (TSAN_OPTIONS, ASAN_OPTIONS, OPENBLAS_NUM_THREADS) are set via
GITHUB_ENV in a shared setup step.
ASan uses detect_leaks=0 to avoid false positives from CPython's
internal memory allocator.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Set sanitizer env vars before build step
ASan's LeakSanitizer was killing the build because ASAN_OPTIONS was only
set after the build. Move the env setup step before pip install so
detect_leaks=0 applies during compilation too.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Move sanitizer suppressions to test/, add LSan suppressions
Move tsan-suppressions.txt to test/ and add lsan-suppressions.txt for
CPython-internal leaks. Use LSan suppressions instead of detect_leaks=0
so that real SCS leaks are still caught.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Stabilize Accelerate backend tests
* Keep Accelerate random-problem tests deterministic
* Add faulthandler_exit_on_timeout and simplify CI pytest flags
Set faulthandler_exit_on_timeout = true in pyproject.toml so hanging
tests actually terminate in CI instead of just dumping tracebacks.
Remove redundant -p no:faulthandler -o faulthandler_timeout=600 flags
from the free-threading CI since the config is now in pyproject.toml.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Address review: update FT classifier, drop cibuildwheel enable, cleanup
- Fix Free Threading trove classifier: drop extraneous "Implementation ::"
prefix, bump "1 - Unstable" to "3 - Stable" per PEP 779 and reviewer
feedback (free-threading is no longer experimental in 3.14).
- Drop `enable = ["cpython-freethreading"]`: deprecated in cibuildwheel
3.4.1, 3.14t wheels build without it. Side effect: we no longer ship
3.13t wheels, which aligns with upstream's migration push to 3.14t.
- Drop `thread_unsafe_fixtures = ["capsys", "capfd"]`: pytest-run-parallel
marks these automatically.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Drop re-init concurrency test, document construction as thread-local
Per review discussion: the `SCS_init` guard against concurrent re-init of
a live instance is not standard practice for CPython extensions — NumPy,
SciPy, and similar libraries assume object construction is thread-local
and do not lock `__init__`. The `test_reinit_while_solving` test also
swallowed exceptions broadly, which hid rather than detected any real
race on the `self->work` field.
Remove the test and add a docstring note to `SCS.__init__` making the
thread-local-construction assumption explicit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Use LinearSolver enum in test_concurrent_direct_and_indirect
Missed during the rebase onto master: this test still passed the old
`use_indirect=` boolean kwarg, which was removed in #189 in favor of
the `linear_solver=scs.LinearSolver.*` enum. This broke the full CI
matrix (wheel builds, accelerate builds, free-threading tests,
sanitizers) on a single shared failure.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Mark test_resolve_auto_* as thread_unsafe
These tests patch scs.sys and scs._load_module — module-level state
that leaks to other tests running in parallel threads under
pytest-run-parallel, causing MagicMock to be substituted for the real
scs.SCS class in unrelated tests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>1 parent f6b7dc6 commit 3c84c8e
23 files changed
Lines changed: 1485 additions & 171 deletions
File tree
- .github/workflows
- scs
- py
- test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
132 | 132 | | |
133 | 133 | | |
134 | 134 | | |
| 135 | + | |
135 | 136 | | |
136 | 137 | | |
137 | 138 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
| 26 | + | |
26 | 27 | | |
27 | 28 | | |
28 | 29 | | |
| |||
96 | 97 | | |
97 | 98 | | |
98 | 99 | | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
95 | 95 | | |
96 | 96 | | |
97 | 97 | | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
98 | 103 | | |
99 | 104 | | |
100 | 105 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
67 | 71 | | |
68 | 72 | | |
69 | 73 | | |
| |||
0 commit comments