Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 103 additions & 122 deletions cuda_bindings/cuda/bindings/_lib/param_packer.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

#include <map>
#include <functional>
#include <stdexcept>
#include <string>
#include <climits>
#include <cstdint>

Expand All @@ -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;
Expand All @@ -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::pair<PyTypeObject*,PyTypeObject*>, std::function<int(void*, PyObject*)>> 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::pair<PyTypeObject*,PyTypeObject*>, std::function<int(void*, PyObject*)>> 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just a thought, but I think we can add noexcept here (even if std::function will erase it). That'll remind us of it and maybe also warn us if we should change it.
(I guess feed itself maybe cannot be marked, because of the std::function type erasure.)

(I honestly don't see much of a reason to not do except -1* on the Python side, but as these are all trivial it's probably OK as is.)

{
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);
Expand Down
8 changes: 6 additions & 2 deletions cuda_bindings/cuda/bindings/_lib/param_packer.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions cuda_bindings/cuda/bindings/_lib/utils.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading