Skip to content

Conversation

HaoZeke
Copy link
Contributor

@HaoZeke HaoZeke commented Jun 1, 2025

Closes #130.

Local tests via:

uv venv --python 3.13t .ftci
source .ftci/bin/activate
uv pip install numpy scipy pytest-timeout pytest-durations pytest-run-parallel
uv pip install -v .
uv run --no-project python -m pytest --parallel-threads=2 --iterations=2 -v -s --timeout=600 --durations=10 -m "not thread_unsafe" test

Basically follows the porting extensions advice:

  • Vendored pythoncapi-compat
  • Didn't rework Get* calls to use Get*Ref here since they're uses of argument parsing (xref C-API documentation)
  • Locks access to the C library, using an instance lock and a global lock (for memory allocations)

EDIT: After the split into #132 and #133 to test this for now one needs to manually setup the pythoncapi-compat bit.

@HaoZeke HaoZeke force-pushed the freethreading branch 7 times, most recently from 7a29b98 to 0f46bb7 Compare June 1, 2025 12:12
@HaoZeke
Copy link
Contributor Author

HaoZeke commented Jun 1, 2025

@HaoZeke HaoZeke force-pushed the freethreading branch 6 times, most recently from 06fb4a8 to 0f46bb7 Compare June 1, 2025 13:18
Copy link

@ngoldbaum ngoldbaum left a comment

Choose a reason for hiding this comment

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

Some comments inline - I think you probably want to use the critical section API rather than adding a mutex to the wrapper object since the wrapper object extends PyObject and you need to call into the C API while working with it. Otherwise you're going to need to refactor things so that you don't have to call into the C API while holding the per-object lock.

Copy link

@ngoldbaum ngoldbaum left a comment

Choose a reason for hiding this comment

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

Comments inline. I didn't read over the new tests since I'm not familiar with this library.

@@ -563,10 +568,16 @@ static int SCS_init(SCS *self, PyObject *args, PyObject *kwargs) {
self->sol->s = (scs_float *)scs_calloc(self->m, sizeof(scs_float));

/* release the GIL */
#ifdef Py_GIL_DISABLED

Choose a reason for hiding this comment

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

Does the GIL protect against the race you're worried about here? If not then you should lock on the GIL-enabled build too. It'd be nice to have an explicit test case for this fix if you don't already.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The general idea here was to lock before Py_BEGIN_ALLOW_THREADS.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since its the initialization call, yes, it is covered by tests :)

Choose a reason for hiding this comment

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

Oh oops, I completely missed that this is happening inside SCS_init. In that case locking probably isn't necessary, unless it's somehow possible for two threads to simultaneously see the same SCS object before SCS_init returns an initialized object.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm still a bit unclear about this. The guarantee with the lock is that there can be no two python threads trying to call scs_init (the external library) at the same time...

This is the general pattern of locks around calls to scs.

Under the free threading build without the lock isn't it possible for two threads to try to create scs objects at the same time thereby simultaneously calling into the external library? Or is the point that since the lock is on the object which hasn't been initialized yet it is meaningless anyway (or needs a global lock)?

Copy link

@ngoldbaum ngoldbaum Jun 14, 2025

Choose a reason for hiding this comment

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

So we need to make a distinction between scs_init and SCS_init. The latter is the tp_init slot for SCS_Type - it's the function that we're inside of in this comment thread.

A Python object is effectively thread-local while it's under construction. Said another way, there's no way for another thread to have a handle on the SCS instance before SCS_init completes.

@bodono
Copy link
Owner

bodono commented Jun 13, 2025

Thanks for taking this on! Let me know when you think it is ready to merge!

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.

ENH: Free-Threading (No-GIL) Support for Python 3.13+
3 participants