diff --git a/cuda_bindings/cuda/bindings/_lib/param_packer.h b/cuda_bindings/cuda/bindings/_lib/param_packer.h index 8d4833bb200..b11c26dfb92 100644 --- a/cuda_bindings/cuda/bindings/_lib/param_packer.h +++ b/cuda_bindings/cuda/bindings/_lib/param_packer.h @@ -5,8 +5,6 @@ #include #include -#include -#include #include #include @@ -30,7 +28,10 @@ PyLong_AsInt(PyObject *obj) } #endif -static PyObject* ctypes_module = nullptr; +// Statics must be initialized at Python import time via init_param_packer() +// which happens when including utils.pxi. +// This includes the m_feeders maps as it must not be mutated from threads. +static bool param_packer_initialized = false; static PyTypeObject* ctypes_c_char = nullptr; static PyTypeObject* ctypes_c_bool = nullptr; @@ -50,138 +51,118 @@ static PyTypeObject* ctypes_c_float = nullptr; static PyTypeObject* ctypes_c_double = nullptr; static PyTypeObject* ctypes_c_void_p = nullptr; -static void fetch_ctypes() +// (target type, source type) +static std::map, std::function> m_feeders; + +// Helper to fetch a strong reference of the ctypes type. +static PyTypeObject* fetch_ctypes_type(PyObject* ctypes_module, const char* name) { - ctypes_module = PyImport_ImportModule("ctypes"); - if (ctypes_module == nullptr) - throw std::runtime_error("Cannot import ctypes module"); - // get method addressof - PyObject* ctypes_dict = PyModule_GetDict(ctypes_module); - if (ctypes_dict == nullptr) - throw std::runtime_error(std::string("FAILURE @ ") + std::string(__FILE__) + " : " + std::to_string(__LINE__)); - // supportedtypes - ctypes_c_char = (PyTypeObject*) PyDict_GetItemString(ctypes_dict, "c_char"); - ctypes_c_bool = (PyTypeObject*) PyDict_GetItemString(ctypes_dict, "c_bool"); - ctypes_c_wchar = (PyTypeObject*) PyDict_GetItemString(ctypes_dict, "c_wchar"); - ctypes_c_byte = (PyTypeObject*) PyDict_GetItemString(ctypes_dict, "c_byte"); - ctypes_c_ubyte = (PyTypeObject*) PyDict_GetItemString(ctypes_dict, "c_ubyte"); - ctypes_c_short = (PyTypeObject*) PyDict_GetItemString(ctypes_dict, "c_short"); - ctypes_c_ushort = (PyTypeObject*) PyDict_GetItemString(ctypes_dict, "c_ushort"); - ctypes_c_int = (PyTypeObject*) PyDict_GetItemString(ctypes_dict, "c_int"); - ctypes_c_uint = (PyTypeObject*) PyDict_GetItemString(ctypes_dict, "c_uint"); - ctypes_c_long = (PyTypeObject*) PyDict_GetItemString(ctypes_dict, "c_long"); - ctypes_c_ulong = (PyTypeObject*) PyDict_GetItemString(ctypes_dict, "c_ulong"); - ctypes_c_longlong = (PyTypeObject*) PyDict_GetItemString(ctypes_dict, "c_longlong"); - ctypes_c_ulonglong = (PyTypeObject*) PyDict_GetItemString(ctypes_dict, "c_ulonglong"); - ctypes_c_size_t = (PyTypeObject*) PyDict_GetItemString(ctypes_dict, "c_size_t"); - ctypes_c_float = (PyTypeObject*) PyDict_GetItemString(ctypes_dict, "c_float"); - ctypes_c_double = (PyTypeObject*) PyDict_GetItemString(ctypes_dict, "c_double"); - ctypes_c_void_p = (PyTypeObject*) PyDict_GetItemString(ctypes_dict, "c_void_p"); // == c_voidp + return (PyTypeObject*)PyObject_GetAttrString(ctypes_module, name); } +static bool fetch_ctypes() +{ + PyObject* ctypes_module = PyImport_ImportModule("ctypes"); + if (ctypes_module == nullptr) return false; + // Parenthesize each assignment: `=` binds looser than `&&`. + bool success = ( + (ctypes_c_char = fetch_ctypes_type(ctypes_module, "c_char")) && + (ctypes_c_bool = fetch_ctypes_type(ctypes_module, "c_bool")) && + (ctypes_c_wchar = fetch_ctypes_type(ctypes_module, "c_wchar")) && + (ctypes_c_byte = fetch_ctypes_type(ctypes_module, "c_byte")) && + (ctypes_c_ubyte = fetch_ctypes_type(ctypes_module, "c_ubyte")) && + (ctypes_c_short = fetch_ctypes_type(ctypes_module, "c_short")) && + (ctypes_c_ushort = fetch_ctypes_type(ctypes_module, "c_ushort")) && + (ctypes_c_int = fetch_ctypes_type(ctypes_module, "c_int")) && + (ctypes_c_uint = fetch_ctypes_type(ctypes_module, "c_uint")) && + (ctypes_c_long = fetch_ctypes_type(ctypes_module, "c_long")) && + (ctypes_c_ulong = fetch_ctypes_type(ctypes_module, "c_ulong")) && + (ctypes_c_longlong = fetch_ctypes_type(ctypes_module, "c_longlong")) && + (ctypes_c_ulonglong = fetch_ctypes_type(ctypes_module, "c_ulonglong")) && + (ctypes_c_size_t = fetch_ctypes_type(ctypes_module, "c_size_t")) && + (ctypes_c_float = fetch_ctypes_type(ctypes_module, "c_float")) && + (ctypes_c_double = fetch_ctypes_type(ctypes_module, "c_double")) && + (ctypes_c_void_p = fetch_ctypes_type(ctypes_module, "c_void_p")) // == c_voidp + ); + Py_DECREF(ctypes_module); + return success; +} -// (target type, source type) -static std::map, std::function> m_feeders; -static void populate_feeders(PyTypeObject* target_t, PyTypeObject* source_t) +// Initialize common (target_type, Python type) pairs for fast argument feeding. +static void populate_feeders() { - if (target_t == ctypes_c_int) + m_feeders[{ctypes_c_int, &PyLong_Type}] = [](void* ptr, PyObject* value) -> int { - if (source_t == &PyLong_Type) - { - m_feeders[{target_t,source_t}] = [](void* ptr, PyObject* value) -> int - { - // PyLong_AsInt range-checks against the 32-bit int slot and raises - // OverflowError itself, so an out-of-range value is rejected rather - // than silently truncated. - int v = PyLong_AsInt(value); - if (v == -1 && PyErr_Occurred()) - return -1; - *((int*)ptr) = v; - return sizeof(int); - }; - return; - } - } else if (target_t == ctypes_c_bool) { - if (source_t == &PyBool_Type) - { - m_feeders[{target_t,source_t}] = [](void* ptr, PyObject* value) -> int - { - *((bool*)ptr) = (value == Py_True); - return sizeof(bool); - }; - return; - } - } else if (target_t == ctypes_c_byte) { - if (source_t == &PyLong_Type) - { - m_feeders[{target_t,source_t}] = [](void* ptr, PyObject* value) -> int - { - // c_byte is an 8-bit slot with no dedicated CPython converter, so - // range-check explicitly against INT8_MIN/INT8_MAX. AsLongAndOverflow's - // `overflow` only flags values outside `long` (64-bit on LP64), so a - // value in that range would be silently truncated by (int8_t)v without - // the explicit bounds check. When overflow!=0, v is the -1 sentinel - // (not the real value), so that case must be caught before trusting v. - int overflow = 0; - long v = PyLong_AsLongAndOverflow(value, &overflow); - if (overflow == 0 && v == -1 && PyErr_Occurred()) - return -1; // non-overflow conversion error; exception already set - if (overflow != 0 || v < INT8_MIN || v > INT8_MAX) - { - PyErr_SetString(PyExc_OverflowError, - "Python int is out of range for a c_byte (8-bit) kernel argument"); - return -1; - } - *((int8_t*)ptr) = (int8_t)v; - return sizeof(int8_t); - }; - return; - } - } else if (target_t == ctypes_c_double) { - if (source_t == &PyFloat_Type) - { - m_feeders[{target_t,source_t}] = [](void* ptr, PyObject* value) -> int - { - *((double*)ptr) = (double)PyFloat_AsDouble(value); - return sizeof(double); - }; - return; - } - } else if (target_t == ctypes_c_float) { - if (source_t == &PyFloat_Type) - { - m_feeders[{target_t,source_t}] = [](void* ptr, PyObject* value) -> int - { - *((float*)ptr) = (float)PyFloat_AsDouble(value); - return sizeof(float); - }; - return; - } - } else if (target_t == ctypes_c_longlong) { - if (source_t == &PyLong_Type) + // PyLong_AsInt range-checks against the 32-bit int slot and raises + // OverflowError itself, so an out-of-range value is rejected rather + // than silently truncated. + int v = PyLong_AsInt(value); + if (v == -1 && PyErr_Occurred()) + return -1; + *((int*)ptr) = v; + return sizeof(int); + }; + m_feeders[{ctypes_c_bool, &PyBool_Type}] = [](void* ptr, PyObject* value) -> int + { + *((bool*)ptr) = (value == Py_True); + return sizeof(bool); + }; + m_feeders[{ctypes_c_byte, &PyLong_Type}] = [](void* ptr, PyObject* value) -> int + { + // c_byte is an 8-bit slot with no dedicated CPython converter, so + // range-check explicitly against INT8_MIN/INT8_MAX. AsLongAndOverflow's + // `overflow` only flags values outside `long` (64-bit on LP64), so a + // value in that range would be silently truncated by (int8_t)v without + // the explicit bounds check. When overflow!=0, v is the -1 sentinel + // (not the real value), so that case must be caught before trusting v. + int overflow = 0; + long v = PyLong_AsLongAndOverflow(value, &overflow); + if (overflow == 0 && v == -1 && PyErr_Occurred()) + return -1; // non-overflow conversion error; exception already set + if (overflow != 0 || v < INT8_MIN || v > INT8_MAX) { - m_feeders[{target_t,source_t}] = [](void* ptr, PyObject* value) -> int - { - *((long long*)ptr) = (long long)PyLong_AsLongLong(value); - return sizeof(long long); - }; - return; + PyErr_SetString(PyExc_OverflowError, + "Python int is out of range for a c_byte (8-bit) kernel argument"); + return -1; } - } + *((int8_t*)ptr) = (int8_t)v; + return sizeof(int8_t); + }; + m_feeders[{ctypes_c_double, &PyFloat_Type}] = [](void* ptr, PyObject* value) -> int + { + *((double*)ptr) = (double)PyFloat_AsDouble(value); + return sizeof(double); + }; + m_feeders[{ctypes_c_float, &PyFloat_Type}] = [](void* ptr, PyObject* value) -> int + { + *((float*)ptr) = (float)PyFloat_AsDouble(value); + return sizeof(float); + }; + m_feeders[{ctypes_c_longlong, &PyLong_Type}] = [](void* ptr, PyObject* value) -> int + { + long long v = PyLong_AsLongLong(value); + if (v == -1 && PyErr_Occurred()) + return -1; + *((long long*)ptr) = v; + return sizeof(long long); + }; } +// Call once from each consuming module body (import, single-threaded). +static void init_param_packer() +{ + if (param_packer_initialized) + return; + if (!fetch_ctypes()) return; + populate_feeders(); + param_packer_initialized = true; +} + +// Never-mutated lookup. 0 -> ctypes fallback; -1 -> exception already set. static int feed(void* ptr, PyObject* value, PyObject* type) { - PyTypeObject* pto = (PyTypeObject*)type; - if (ctypes_c_int == nullptr) - fetch_ctypes(); - auto found = m_feeders.find({pto,value->ob_type}); - if (found == m_feeders.end()) - { - populate_feeders(pto, value->ob_type); - found = m_feeders.find({pto,value->ob_type}); - } + auto found = m_feeders.find({(PyTypeObject*)type, Py_TYPE(value)}); if (found != m_feeders.end()) { return found->second(ptr, value); diff --git a/cuda_bindings/cuda/bindings/_lib/param_packer.pxd b/cuda_bindings/cuda/bindings/_lib/param_packer.pxd index d1f84059db1..cf0c37be768 100644 --- a/cuda_bindings/cuda/bindings/_lib/param_packer.pxd +++ b/cuda_bindings/cuda/bindings/_lib/param_packer.pxd @@ -2,6 +2,10 @@ # SPDX-License-Identifier: Apache-2.0 # Include "param_packer.h" so its contents get compiled into every -# Cython extension module that depends on param_packer.pxd. +# Cython extension module that depends on param_packer.pxd. Each such module +# owns a copy of the header statics and must call init_param_packer(). cdef extern from "param_packer.h": - int feed(void* ptr, object o, object ct) except? -1 + # except +* so a C++ throw or pending ImportError become Python exceptions. + void init_param_packer() except +* + # -1 is a feeder rejection; 0 means no feeder (ctypes fallback). + int feed(void* ptr, object o, object ct) except -1 diff --git a/cuda_bindings/cuda/bindings/_lib/utils.pxi b/cuda_bindings/cuda/bindings/_lib/utils.pxi index 2796f910798..7783afed97c 100644 --- a/cuda_bindings/cuda/bindings/_lib/utils.pxi +++ b/cuda_bindings/cuda/bindings/_lib/utils.pxi @@ -11,6 +11,9 @@ import ctypes as _ctypes cimport cuda.bindings.cydriver as cydriver cimport cuda.bindings._lib.param_packer as param_packer +# Import-time init so feed() is a pure read under free threading. +param_packer.init_param_packer() + cdef void* _callocWrapper(length, size): cdef void* out = calloc(length, size) if out is NULL: