Add support for free-threading#1
Conversation
133dff1 to
2f0d487
Compare
2f0d487 to
11ceaec
Compare
Implement thread-safety using Cython 3.1+ critical sections to protect Pool operations (alloc, free, realloc) from race conditions. Update dependencies and CI to support free-threaded builds.
11ceaec to
2f9a29a
Compare
|
Unfortunately, because the # cython: freethreading_compatible=True
from collections import deque
import threading
from cymem.cymem cimport Pool
def test_concurrent_pool_operations():
cdef Pool pool = Pool()
cdef int num_threads = 10
cdef int ops_per_thread = 100
errors = []
def worker(thread_id):
cdef Pool p = pool
cdef void* ptr
cdef void* old_ptr
cdef void* new_ptr
cdef int i
cdef size_t addr
try:
ptrs = deque()
for _ in range(5):
ptr = p.alloc(1, 64)
ptrs.append(<size_t>ptr)
for i in range(ops_per_thread):
op = i % 3
if op == 0: # alloc
ptr = p.alloc(1, 64)
ptrs.append(<size_t>ptr)
elif op == 1 and ptrs: # free
addr = <size_t>ptrs.popleft()
ptr = <void*>addr
p.free(ptr)
elif op == 2 and ptrs: # realloc
addr = <size_t>ptrs.popleft()
old_ptr = <void*>addr
new_ptr = p.realloc(old_ptr, 128)
ptrs.append(<size_t>new_ptr)
# Cleanup remaining allocations
for addr_obj in ptrs:
addr = <size_t>addr_obj
ptr = <void*>addr
p.free(ptr)
except Exception as e:
errors.append((thread_id, e))
threads = [threading.Thread(target=worker, args=(i,)) for i in range(num_threads)]
for t in threads:
t.start()
for t in threads:
t.join()
assert not errors, f"Thread errors: {errors}"
assert pool.size == 0, f"Pool size should be 0, got {pool.size}"
assert len(pool.addresses) == 0, f"Pool addresses should be empty, got {len(pool.addresses)}"And then in a command line: cythonize -i test_threadsafety.pyx
python -c "from cymem.tests.test_threadsafety import test_concurrent_pool_operations; test_concurrent_pool_operations()" |
ngoldbaum
left a comment
There was a problem hiding this comment.
If it's part of the public API for these Pool objects to access mutable state, then I think you should add new reader methods that read the state under a critical section and document that third party libraries accessing the C state directly without holding a critical section on the Pool isn't thread-safe.
It'd also be nice to package your test script as a git repo with a meson build configuration. That will also make it easier to run the test under TSan.
|
I just went over preshed which depends on cymem. It looks like preshed only uses Let's hold off on adding any new API until we find that we need it. Once this PR is merged I'll go ahead and make my fork of preshed depend on your fork of cymem. From there we'll need to do a breadth-search reverse search up the spacy dependency tree, setting up CI running against forks that have had a thread safety pass. I don't think we need to go to the trouble of doing pypi releases and actually forking the packages, we'll just muddle through by hacking the CI. |
|
Here's my WIP work for preshed: ngoldbaum/preshed#1 |
Co-authored-by: Guido Imperiale <crusaderky@gmail.com>
Co-authored-by: Guido Imperiale <crusaderky@gmail.com>
|
Thanks for the reviews, folks! I've addressed all of the feedback.
I'd propose to not merge these PRs on our local forks and just depend on the branch in CI. That makes it easier to upstream the PRs.
|
What do you think about working in a |
Strongly in favour |
|
Awesome! I think this actually solves most of the "testing Cython is hard" worries the cymem maintainers had about testing Cython code. This would probably be a nice pattern to follow to add more tests. When we eventually speak with the Explosion folks I might propose adding your test as a blueprint for more tests. I probably would have skipped including I guess this is an issue with the cymem packaging, but when I try to run your test code, I get a build error: It looks like you did specify your fork of cymem as a build dependency, not sure what's going wrong there. If I install your branch of That avoids this error: After doing that I'm able to build and install your test using a TSAN build of Python 3.14-dev and don't see any race reports. Woohoo! |
|
Folks, I've addressed all feedback here and I think this is now in a good state. If you could give it another review, that'd be great! |
| if <size_t>p not in self.addresses: | ||
| self.pyfree.free(new_ptr) | ||
| raise ValueError("Pointer %d not found in Pool %s" % (<size_t>p, self.addresses)) | ||
|
|
||
| old_size = self.addresses[<size_t>p] |
There was a problem hiding this comment.
| if <size_t>p not in self.addresses: | |
| self.pyfree.free(new_ptr) | |
| raise ValueError("Pointer %d not found in Pool %s" % (<size_t>p, self.addresses)) | |
| old_size = self.addresses[<size_t>p] | |
| old_size = self.addresses.pop(<size_t>p, 0) | |
| if old_size == 0: | |
| self.pyfree.free(new_ptr) | |
| raise ValueError("Pointer %d not found in Pool %s" % (<size_t>p, self.addresses)) |
There was a problem hiding this comment.
I did a
try:
self.addresses.pop(...)
except KeyError:
raise ValueError()
Theoretically, malloc(0) can succeed, which means that 0 can also be a valid value in the dict. I know we could instead default to -1 or something, but the try/except feels clearer here.
|
|
||
| with cython.critical_section(self, self.addresses): | ||
| if <size_t>p not in self.addresses: | ||
| self.pyfree.free(new_ptr) |
There was a problem hiding this comment.
PyFree could, in theory, release the critical section.
In practice, this is an exception condition that most times in user code corresponds to a bug, so I'm not overly concerned.
Co-authored-by: Guido Imperiale <crusaderky@gmail.com>
|
It looks like everything is working over in preshed if I make it depend on this branch of cymem and the master branch of murmurhash: Commenting about that mostly for visibility to you two, I'm going to self-merge it. The thread-safety work over in preshed looks similar to what happened here (although thankfully it looks like state is stored outside the interpreter so it might be simpler). I'll probably ping both of you for review, or just Lysandros if it slips into next week. |
|
Closing this since upstream PR is now open: explosion#53 |
Implement thread-safety using Cython 3.1+ critical sections to protect Pool operations (alloc, free, realloc) from race conditions. Update dependencies and CI to support free-threaded builds.