From 448c5cebc74f6b069969ea22233e31a6d3c81ea6 Mon Sep 17 00:00:00 2001 From: Rohit Goswami Date: Thu, 22 May 2025 13:51:10 -0600 Subject: [PATCH 1/7] MAINT: Baseline setup for free-threading --- .github/workflows/tests.yml | 2 +- README.md | 6 ++++++ pyproject.toml | 4 ++-- setup.py | 14 +++++++++++++- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8d50278..06b9e39 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,7 +24,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python_version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + python_version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.13t"] include: - os: windows-2019 python_version: "3.6" diff --git a/README.md b/README.md index 9e21837..3e2fd89 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,12 @@ pip install -U pip setuptools wheel pip install cymem ``` +### Free threading + +`cymem` has support for being built and run under free-threaded CPython. +Currently `Pool` is not thread safe when used from multiple threads at once; +**please avoid sharing a single** `Pool` instance between threads. + ## Example Use Case: An array of structs Let's say we want a sequence of sparse matrices. We need fast access, and a diff --git a/pyproject.toml b/pyproject.toml index ab36d78..f4f0898 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,9 +7,9 @@ build-backend = "setuptools.build_meta" [tool.cibuildwheel] build = "*" -skip = "pp* cp36* cp37* cp38*" +skip = "pp* cp36* cp37* cp38* *t*_i686" test-skip = "" -free-threaded-support = false +enable = ["cpython-freethreading"] archs = ["native"] diff --git a/setup.py b/setup.py index 1bb51bd..9a2a3fb 100755 --- a/setup.py +++ b/setup.py @@ -8,11 +8,21 @@ from setuptools.command.build_ext import build_ext from sysconfig import get_path from Cython.Build import cythonize +from Cython.Compiler.Version import version as cython_version +from packaging.version import Version PACKAGES = find_packages() MOD_NAMES = ["cymem.cymem"] +compiler_directives = dict() +compiler_tenv = dict() + +if Version(cython_version) >= Version("3.1.0"): + compiler_directives["freethreading_compatible"] = True + compiler_tenv["CYTHON_FREE_THREADING"] = True +else: + compiler_tenv["CYTHON_FREE_THREADING"] = False # By subclassing build_extensions we have the actual compiler that will be used which is really known only after finalize_options # http://stackoverflow.com/questions/724664/python-distutils-how-to-get-a-compiler-that-is-going-to-be-used @@ -100,7 +110,9 @@ def setup_package(): version=about["__version__"], url=about["__uri__"], license=about["__license__"], - ext_modules=cythonize(ext_modules, language_level=2), + ext_modules=cythonize(ext_modules, language_level=3, + compiler_directives=compiler_directives, + compile_time_env=compiler_tenv), setup_requires=["cython>=0.25"], classifiers=[ "Environment :: Console", From 40d94e861bfeda12e25d41ff0b722db6aaff27ea Mon Sep 17 00:00:00 2001 From: Rohit Goswami Date: Thu, 22 May 2025 14:09:14 -0600 Subject: [PATCH 2/7] DOC: Add details for building the readme example --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index 3e2fd89..d08e7b5 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,32 @@ pip install cymem Currently `Pool` is not thread safe when used from multiple threads at once; **please avoid sharing a single** `Pool` instance between threads. +Also remember to declare `freethreading_compatible` for the example case below: + +``` python +from setuptools import setup +from Cython.Build import cythonize + +from Cython.Compiler.Version import version as cython_version +from packaging.version import Version + + +compiler_directives = dict() +compiler_tenv = dict() + +if Version(cython_version) >= Version("3.1.0"): + compiler_directives["freethreading_compatible"] = True + compiler_tenv["CYTHON_FREE_THREADING"] = True +else: + compiler_tenv["CYTHON_FREE_THREADING"] = False + +setup( + ext_modules = cythonize("*.pyx", language_level=3, + compiler_directives=compiler_directives, + compile_time_env=compiler_tenv) +) +``` + ## Example Use Case: An array of structs Let's say we want a sequence of sparse matrices. We need fast access, and a From 13fd129bf4ff386b7aaa50dc726f3edadde228a4 Mon Sep 17 00:00:00 2001 From: Rohit Goswami Date: Fri, 16 May 2025 19:48:55 +0200 Subject: [PATCH 3/7] DOC: Update to fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d08e7b5..1f8ff9f 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ cdef class MatrixArray: self.length = len(py_matrices) self.matrices = self.mem.alloc(self.length, sizeof(SparseMatrix*)) for i, py_matrix in enumerate(py_matrices): - self.matrices[i] = sparse_matrix_init(self.mem, py_matrix) + self.matrices[i] = sparse_matrix_init_cymem(self.mem, py_matrix) cdef SparseMatrix* sparse_matrix_init_cymem(Pool mem, list py_matrix) except NULL: sm = mem.alloc(1, sizeof(SparseMatrix)) From 9b53e14c379a7334a469889a692b20d23770abab Mon Sep 17 00:00:00 2001 From: Rohit Goswami Date: Thu, 22 May 2025 14:19:00 -0600 Subject: [PATCH 4/7] MAINT: Cleanup unused Part of the next PR... --- README.md | 7 +------ setup.py | 9 ++------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 1f8ff9f..d2864b7 100644 --- a/README.md +++ b/README.md @@ -59,18 +59,13 @@ from packaging.version import Version compiler_directives = dict() -compiler_tenv = dict() if Version(cython_version) >= Version("3.1.0"): compiler_directives["freethreading_compatible"] = True - compiler_tenv["CYTHON_FREE_THREADING"] = True -else: - compiler_tenv["CYTHON_FREE_THREADING"] = False setup( ext_modules = cythonize("*.pyx", language_level=3, - compiler_directives=compiler_directives, - compile_time_env=compiler_tenv) + compiler_directives=compiler_directives) ) ``` diff --git a/setup.py b/setup.py index 9a2a3fb..5b0ae1f 100755 --- a/setup.py +++ b/setup.py @@ -16,13 +16,9 @@ MOD_NAMES = ["cymem.cymem"] compiler_directives = dict() -compiler_tenv = dict() if Version(cython_version) >= Version("3.1.0"): - compiler_directives["freethreading_compatible"] = True - compiler_tenv["CYTHON_FREE_THREADING"] = True -else: - compiler_tenv["CYTHON_FREE_THREADING"] = False + compiler_directives["freethreading_compatible"] = rue # By subclassing build_extensions we have the actual compiler that will be used which is really known only after finalize_options # http://stackoverflow.com/questions/724664/python-distutils-how-to-get-a-compiler-that-is-going-to-be-used @@ -111,8 +107,7 @@ def setup_package(): url=about["__uri__"], license=about["__license__"], ext_modules=cythonize(ext_modules, language_level=3, - compiler_directives=compiler_directives, - compile_time_env=compiler_tenv), + compiler_directives=compiler_directives), setup_requires=["cython>=0.25"], classifiers=[ "Environment :: Console", From 7fc4205838f383e167353de5f9fdf99f43497fc1 Mon Sep 17 00:00:00 2001 From: Rohit Goswami Date: Thu, 22 May 2025 14:21:49 -0600 Subject: [PATCH 5/7] MAINT: Start adding directives and conditionals --- README.md | 7 ++++++- setup.py | 9 +++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d2864b7..1f8ff9f 100644 --- a/README.md +++ b/README.md @@ -59,13 +59,18 @@ from packaging.version import Version compiler_directives = dict() +compiler_tenv = dict() if Version(cython_version) >= Version("3.1.0"): compiler_directives["freethreading_compatible"] = True + compiler_tenv["CYTHON_FREE_THREADING"] = True +else: + compiler_tenv["CYTHON_FREE_THREADING"] = False setup( ext_modules = cythonize("*.pyx", language_level=3, - compiler_directives=compiler_directives) + compiler_directives=compiler_directives, + compile_time_env=compiler_tenv) ) ``` diff --git a/setup.py b/setup.py index 5b0ae1f..9a2a3fb 100755 --- a/setup.py +++ b/setup.py @@ -16,9 +16,13 @@ MOD_NAMES = ["cymem.cymem"] compiler_directives = dict() +compiler_tenv = dict() if Version(cython_version) >= Version("3.1.0"): - compiler_directives["freethreading_compatible"] = rue + compiler_directives["freethreading_compatible"] = True + compiler_tenv["CYTHON_FREE_THREADING"] = True +else: + compiler_tenv["CYTHON_FREE_THREADING"] = False # By subclassing build_extensions we have the actual compiler that will be used which is really known only after finalize_options # http://stackoverflow.com/questions/724664/python-distutils-how-to-get-a-compiler-that-is-going-to-be-used @@ -107,7 +111,8 @@ def setup_package(): url=about["__uri__"], license=about["__license__"], ext_modules=cythonize(ext_modules, language_level=3, - compiler_directives=compiler_directives), + compiler_directives=compiler_directives, + compile_time_env=compiler_tenv), setup_requires=["cython>=0.25"], classifiers=[ "Environment :: Console", From 52ee6de0aefb46deecfb0bd35541a781d44ac834 Mon Sep 17 00:00:00 2001 From: Rohit Goswami Date: Thu, 22 May 2025 15:12:59 -0600 Subject: [PATCH 6/7] TMP: Threading updates MAINT: Try using standard memory allocators --- cymem/cymem.pxd | 2 ++ cymem/cymem.pyx | 46 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/cymem/cymem.pxd b/cymem/cymem.pxd index dbcbd95..d9e6bc2 100644 --- a/cymem/cymem.pxd +++ b/cymem/cymem.pxd @@ -1,4 +1,6 @@ ctypedef void* (*malloc_t)(size_t n) +ctypedef void* (*calloc_t)(size_t n) +ctypedef void* (*realloc_t)(size_t n) ctypedef void (*free_t)(void *p) cdef class PyMalloc: diff --git a/cymem/cymem.pyx b/cymem/cymem.pyx index e2470fb..c4ad460 100644 --- a/cymem/cymem.pyx +++ b/cymem/cymem.pyx @@ -8,6 +8,12 @@ import warnings WARN_ZERO_ALLOC = False +cdef bint CYTHON_FREE_THREADING + +IF CYTHON_FREE_THREADING: + from libcpp.mutex cimport once_flag, call_once + cdef once_flag _lazy_import_lock + cdef class PyMalloc: cdef void _set(self, malloc_t malloc): self.malloc = malloc @@ -17,6 +23,24 @@ cdef PyMalloc WrapMalloc(malloc_t malloc): o._set(malloc) return o +cdef class PyCalloc: + cdef void _set(self, calloc_t calloc): + self.calloc = calloc + +cdef PyCalloc WrapCalloc(calloc_t calloc): + cdef PyCalloc o = PyCalloc() + o._set(calloc) + return o + +cdef class PyRealloc: + cdef void _set(self, realloc_t realloc): + self.realloc = realloc + +cdef PyRealloc WrapRealloc(realloc_t realloc): + cdef PyRealloc o = PyRealloc() + o._set(realloc) + return o + cdef class PyFree: cdef void _set(self, free_t free): self.free = free @@ -27,6 +51,8 @@ cdef PyFree WrapFree(free_t free): return o Default_Malloc = WrapMalloc(PyMem_Malloc) +Default_Calloc = WrapCalloc(PyMem_Calloc) +Default_Realloc = WrapRealloc(PyMem_Realloc) Default_Free = WrapFree(PyMem_Free) cdef class Pool: @@ -43,15 +69,21 @@ cdef class Pool: size (size_t): The current size (in bytes) allocated by the pool. addresses (dict): The currently allocated addresses and their sizes. Read-only. pymalloc (PyMalloc): The allocator to use (default uses PyMem_Malloc). + pycalloc (PyCalloc): The allocator to use (default uses PyMem_Calloc). + pyrealloc (PyRealloc): The allocator to use (default uses PyMem_Realloc). pyfree (PyFree): The free to use (default uses PyMem_Free). """ def __cinit__(self, PyMalloc pymalloc=Default_Malloc, + PyCalloc pycalloc = Default_Calloc, + PyRealloc pyrealloc = Default_Realloc, PyFree pyfree=Default_Free): self.size = 0 self.addresses = {} self.refs = [] self.pymalloc = pymalloc + self.pycalloc = pycalloc + self.pyrealloc = pyrealloc self.pyfree = pyfree def __dealloc__(self): @@ -69,10 +101,9 @@ cdef class Pool: """ if WARN_ZERO_ALLOC and (number == 0 or elem_size == 0): warnings.warn("Allocating zero bytes") - cdef void* p = self.pymalloc.malloc(number * elem_size) + cdef void* p = self.pycalloc.calloc(number * elem_size) if p == NULL: raise MemoryError("Error assigning %d bytes" % (number * elem_size)) - memset(p, 0, number * elem_size) self.addresses[p] = number * elem_size self.size += number * elem_size return p @@ -89,13 +120,12 @@ cdef class Pool: if new_size == 0: raise ValueError("Realloc requires new_size > 0") assert new_size > self.addresses[p] - cdef void* new_ptr = self.alloc(1, new_size) - if new_ptr == NULL: + self.realloc(p, new_size) + if p == NULL: raise MemoryError("Error reallocating to %d bytes" % new_size) - memcpy(new_ptr, p, self.addresses[p]) - self.free(p) - self.addresses[new_ptr] = new_size - return new_ptr + # Resize in place + self.addresses[p] = new_size + return p cdef void free(self, void* p) except *: """Frees the memory block pointed to by p, which must have been returned From 17eb68a896ee00a9476c30b2e3c67f6431056cdd Mon Sep 17 00:00:00 2001 From: Rohit Goswami Date: Mon, 26 May 2025 06:51:44 +0200 Subject: [PATCH 7/7] MAINT: Consider that realloc can return a pointer --- cymem/cymem.pyx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/cymem/cymem.pyx b/cymem/cymem.pyx index c4ad460..287656a 100644 --- a/cymem/cymem.pyx +++ b/cymem/cymem.pyx @@ -115,17 +115,21 @@ cdef class Pool: If p is not in the Pool or new_size is 0, a MemoryError is raised. """ + cdef void* p_new if p not in self.addresses: raise ValueError("Pointer %d not found in Pool %s" % (p, self.addresses)) if new_size == 0: raise ValueError("Realloc requires new_size > 0") assert new_size > self.addresses[p] - self.realloc(p, new_size) - if p == NULL: + old_size_val = self.addresses[p] + p_new = self.pyrealloc.realloc(p, new_size) + if p_new == NULL: raise MemoryError("Error reallocating to %d bytes" % new_size) # Resize in place - self.addresses[p] = new_size - return p + self.addresses.pop(p_old) + self.addresses[p_new] = new_size + self.size = (self.size - old_size_val) + new_size + return p_new cdef void free(self, void* p) except *: """Frees the memory block pointed to by p, which must have been returned