Skip to content

Add LinearSolver enum, remove boolean solver-selection flags - #189

Merged
bodono merged 8 commits into
masterfrom
linear-solver-enum
Apr 11, 2026
Merged

bodono merged 8 commits into
masterfrom
linear-solver-enum

Conversation

@bodono

@bodono bodono commented Apr 11, 2026 •

Copy link
Copy Markdown
Owner

Summary

Replaces the boolean flags (gpu, mkl, apple_ldl, dense, cudss,
use_indirect) with a single linear_solver parameter that takes a
scs.LinearSolver enum value (also accepts strings).

Available solvers: AUTO, QDLDL, INDIRECT, MKL, ACCELERATE, DENSE,
GPU, CUDSS.

The default is AUTO, which auto-detects the best available direct solver:

  • macOS: Apple Accelerate if built, otherwise QDLDL
  • Linux/Windows: MKL Pardiso if built, otherwise QDLDL

Usage

import scs

# Auto-detect best backend (default)
solver = scs.SCS(data, cone)

# Explicitly select a solver
solver = scs.SCS(data, cone, linear_solver=scs.LinearSolver.MKL)

# String values also work
solver = scs.SCS(data, cone, linear_solver="indirect")

Implementation

Module selection uses dict dispatch for clean, extensible mapping:

_SOLVER_DISPATCH = {
    LinearSolver.AUTO: _resolve_auto,
    LinearSolver.QDLDL: lambda: _scs_direct,
    LinearSolver.INDIRECT: lambda: _load_module("_scs_indirect"),
    LinearSolver.MKL: lambda: _load_module("_scs_mkl"),
    ...
}

def _select_scs_module(stgs):
    linear_solver = stgs.pop("linear_solver", LinearSolver.AUTO)
    if isinstance(linear_solver, str):
        linear_solver = LinearSolver(linear_solver)
    return _SOLVER_DISPATCH[linear_solver]()

This is a breaking API change: the old boolean flags are removed, not deprecated.

Test plan

  • All 250 tests pass with zero warnings
  • Updated all 16 test files to use new linear_solver= API
  • Tests for each LinearSolver enum value (QDLDL, INDIRECT, MKL, ACCELERATE, DENSE, GPU, CUDSS)
  • Test string values work (linear_solver="qdldl")
  • Test AUTO default works when no solver is specified
  • Test invalid string raises ValueError

🤖 Generated with Claude Code

bodono and others added 2 commits April 11, 2026 15:05
Introduces a `linear_solver=scs.LinearSolver.<SOLVER>` parameter as the
single way to select the linear system solver backend. The default is
`AUTO`, which picks the best available solver for the platform:
- macOS: Apple Accelerate if available, otherwise QDLDL
- Linux/Windows: MKL Pardiso if available, otherwise QDLDL

The old boolean flags (gpu, mkl, apple_ldl, dense, cudss, use_indirect)
still work but now emit a DeprecationWarning. Combining old flags with
the new `linear_solver` parameter raises a ValueError.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces the boolean flags (gpu, mkl, apple_ldl, dense, cudss,
use_indirect) with a single `linear_solver=scs.LinearSolver.<SOLVER>`
parameter using dict dispatch.

Available solvers: AUTO, QDLDL, INDIRECT, MKL, ACCELERATE, DENSE, GPU,
CUDSS. The default is AUTO, which picks the best available solver for
the platform:
- macOS: Apple Accelerate if available, otherwise QDLDL
- Linux/Windows: MKL Pardiso if available, otherwise QDLDL

Also accepts string values, e.g. linear_solver="mkl".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bodono bodono changed the title Add LinearSolver enum to replace boolean solver-selection flags Add LinearSolver enum, remove boolean solver-selection flags Apr 11, 2026
bodono and others added 3 commits April 11, 2026 15:31
- Add LinearSolver.AUTO to all parametrized solver tests (tolerances,
  QP, exp cone, SDP)
- Add explicit tests for AUTO via enum and string
- Add mock-based unit tests for _resolve_auto: verifies macOS tries
  Accelerate, Linux/Windows try MKL, and all platforms fall back to
  QDLDL when the preferred module is missing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- README: document linear_solver parameter, AUTO defaults, and
  available enum values
- scs_source docs: replace old boolean flags with LinearSolver enum,
  add table of all solver values with descriptions and examples,
  describe AUTO behavior for each platform

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
bodono and others added 3 commits April 11, 2026 15:56
The docs changes are in a separate PR (cvxgrp/scs#381).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bodono
bodono merged commit 78758c8 into master Apr 11, 2026
24 checks passed
@bodono
bodono deleted the linear-solver-enum branch April 11, 2026 16:27
bodono added a commit that referenced this pull request Apr 17, 2026
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>
bodono added a commit that referenced this pull request Apr 17, 2026
* 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>
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.

1 participant