Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
151b415
Add free-threading (no-GIL) support for Python 3.13t+
bodono Apr 4, 2026
48815c5
Fix free-threading CI: create venv before installing packages
bodono Apr 4, 2026
8601ecf
Fix free-threading CI: install build deps for --no-build-isolation
bodono Apr 4, 2026
c2db4a4
Fix free-threading CI: use build isolation instead of --no-build-isol…
bodono Apr 4, 2026
b4dc028
Fix free-threading CI: install pytest-timeout for --timeout flag
bodono Apr 4, 2026
ecda1a6
Address free-threading review feedback and make all tests thread-safe
bodono Apr 7, 2026
29880b0
Fix thread-safe signal handling and link pthreads
bodono Apr 7, 2026
ead672e
Set OPENBLAS_NUM_THREADS=1 for TSan CI
bodono Apr 7, 2026
26e53ce
Add ASan CI job via sanitizer matrix
bodono Apr 7, 2026
3e110c9
Set sanitizer env vars before build step
bodono Apr 7, 2026
2e4b379
Move sanitizer suppressions to test/, add LSan suppressions
bodono Apr 8, 2026
ddf02d5
Stabilize Accelerate backend tests
bodono Apr 10, 2026
796382e
Keep Accelerate random-problem tests deterministic
bodono Apr 10, 2026
027c3e0
Add faulthandler_exit_on_timeout and simplify CI pytest flags
bodono Apr 10, 2026
845986e
Address review: update FT classifier, drop cibuildwheel enable, cleanup
bodono Apr 17, 2026
dc30748
Drop re-init concurrency test, document construction as thread-local
bodono Apr 17, 2026
e53abfa
Use LinearSolver enum in test_concurrent_direct_and_indirect
bodono Apr 17, 2026
0061bfc
Mark test_resolve_auto_* as thread_unsafe
bodono Apr 17, 2026
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
128 changes: 128 additions & 0 deletions .github/workflows/freethreading_tests.yml
Comment thread
bodono marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
name: Free-threading tests

on:
pull_request:
push:
branches:
- master

jobs:
test_freethreading:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.13t", "3.14t"]

steps:
- uses: actions/checkout@v6
with:
submodules: recursive

- name: Install uv
uses: astral-sh/setup-uv@v6

- name: Set up free-threaded Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}

- name: Install OpenBLAS
run: |
sudo apt-get update
sudo apt-get install -y libopenblas-dev

- name: Create venv and install dependencies
run: |
uv venv --python ${{ matrix.python-version }} .venv
source .venv/bin/activate
uv pip install numpy scipy pytest pytest-run-parallel

- name: Build and install scs
run: |
source .venv/bin/activate
uv pip install -v .

- name: Run tests
run: |
source .venv/bin/activate
python -m pytest test/ -v

- name: Run tests in parallel threads (race detection)
run: |
source .venv/bin/activate
python -m pytest test/ -v --parallel-threads=4 --iterations=3

test_sanitizers:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- sanitizer: tsan
cpython_configure_flag: "--with-thread-sanitizer"
cflags: "-fsanitize=thread -g"
- sanitizer: asan
cpython_configure_flag: "--with-address-sanitizer"
cflags: "-fsanitize=address -g"

steps:
- uses: actions/checkout@v6
with:
submodules: recursive

- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y clang libopenblas-dev libssl-dev zlib1g-dev \
libbz2-dev libreadline-dev libsqlite3-dev libncurses5-dev \
libncursesw5-dev xz-utils libffi-dev liblzma-dev

- name: Cache CPython ${{ matrix.sanitizer }} build
id: cache-cpython
uses: actions/cache@v4
with:
path: cpython-${{ matrix.sanitizer }}
key: cpython-${{ matrix.sanitizer }}-3.14-${{ runner.os }}-v1

- name: Build CPython 3.14t with ${{ matrix.sanitizer }}
if: steps.cache-cpython.outputs.cache-hit != 'true'
run: |
git clone --depth 1 https://github.com/python/cpython.git -b 3.14 cpython-src
cd cpython-src
CC=clang CXX=clang++ ./configure --disable-gil ${{ matrix.cpython_configure_flag }} \
--prefix $GITHUB_WORKSPACE/cpython-${{ matrix.sanitizer }}
make -j$(nproc)
make install

- name: Create venv and install dependencies
run: |
$GITHUB_WORKSPACE/cpython-${{ matrix.sanitizer }}/bin/python3.14t -m venv .san-venv
source .san-venv/bin/activate
pip install numpy scipy pytest meson-python meson ninja

- name: Set sanitizer runtime options
run: |
if [ "${{ matrix.sanitizer }}" = "tsan" ]; then
echo "TSAN_OPTIONS=halt_on_error=1 allocator_may_return_null=1 suppressions=${{ github.workspace }}/test/tsan-suppressions.txt" >> $GITHUB_ENV
echo "OPENBLAS_NUM_THREADS=1" >> $GITHUB_ENV
elif [ "${{ matrix.sanitizer }}" = "asan" ]; then
echo "ASAN_OPTIONS=halt_on_error=1 allocator_may_return_null=1" >> $GITHUB_ENV
echo "LSAN_OPTIONS=suppressions=${{ github.workspace }}/test/lsan-suppressions.txt" >> $GITHUB_ENV
fi

- name: Build and install scs
run: |
source .san-venv/bin/activate
pip install -v . --no-build-isolation
env:
CC: clang
CFLAGS: ${{ matrix.cflags }}

- name: Run tests under ${{ matrix.sanitizer }}
run: |
source .san-venv/bin/activate
python -m pytest test/ -v -s

- name: Run threading stress tests under ${{ matrix.sanitizer }}
run: |
source .san-venv/bin/activate
python -m pytest test/test_free_threading.py test/test_thread_safety.py -v -s
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ if get_option('native_arch')
endif

_deps = [blas_deps]
_deps += dependency('threads')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

why the pthreads dependency?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

It's for the upstream thread-safe ctrl-c fix (cvxgrp/scs#375). scs_source/src/ctrlc.c now uses a pthread_mutex_t to protect the ref-counted signal-handler registration, so the extension needs to link against the pthread library on platforms where libc doesn't pull it in by default. dependency('threads') is the meson-idiomatic, portable way to ask for that (it resolves to -lpthread on glibc, nothing on musl/macOS, the right Windows shim, etc.).

if get_option('use_openmp')
_deps += dependency('openmp')
endif
Expand Down
9 changes: 9 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ classifiers = [
'Programming Language :: Python :: 3.13',
'Programming Language :: Python :: 3.14',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: Free Threading :: 3 - Stable',
'Programming Language :: Python :: Implementation :: CPython',
'Operating System :: Microsoft :: Windows',
'Operating System :: POSIX',
Expand Down Expand Up @@ -96,3 +97,11 @@ before-all = [
testpaths = [
"test",
]
faulthandler_timeout = 600
faulthandler_exit_on_timeout = true
markers = [
"thread_unsafe: mark test as unsafe to run in parallel threads (pytest-run-parallel)",
]
# pytest-run-parallel: functions that are not thread-safe
# (capsys/capfd are detected automatically by pytest-run-parallel)
thread_unsafe_functions = ["numpy.random.seed"]
5 changes: 5 additions & 0 deletions scs/py/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ def __init__(self, data, cone, **settings):
@param data Dictionary containing keys `P`, `A`, `b`, `c`.
@param cone Dictionary containing cone information.
@param settings Settings as kwargs, see docs.

Thread safety: construction is assumed to be thread-local. Calling
`__init__` on a live SCS instance from another thread (i.e. while
`solve` or `update` may be running on it) is undefined behavior.
Use a fresh `SCS(...)` instance instead.
"""
self._settings = settings
if not data or not cone:
Expand Down
4 changes: 4 additions & 0 deletions scs/scsmodule.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ static PyObject *moduleinit(void) {
return NULL;
}

#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif

/* Initialize SCS_Type */
SCS_Type.tp_new = PyType_GenericNew;
if (PyType_Ready(&SCS_Type) < 0)
Expand Down
Loading
Loading