Skip to content

Commit 876b1f8

Browse files
authored
Merge branch 'main' into fix_2183
2 parents 5d0dd2e + 0d22cb4 commit 876b1f8

13 files changed

Lines changed: 324 additions & 60 deletions

File tree

cuda_bindings/cuda/bindings/_lib/param_packer.h

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,28 @@
77
#include <functional>
88
#include <stdexcept>
99
#include <string>
10+
#include <climits>
11+
#include <cstdint>
12+
13+
// PyLong_AsInt entered the public/stable CPython API in 3.13. cuda.bindings
14+
// supports Python 3.10+, so provide a file-local backport for older builds.
15+
// This is a copy of the CPython implementation; it is `static` (unlike the
16+
// original) because this header is compiled into every extension module that
17+
// includes it, mirroring the other helpers below.
18+
#if PY_VERSION_HEX < 0x030D0000
19+
static int
20+
PyLong_AsInt(PyObject *obj)
21+
{
22+
int overflow;
23+
long result = PyLong_AsLongAndOverflow(obj, &overflow);
24+
if (overflow || result > INT_MAX || result < INT_MIN) {
25+
PyErr_SetString(PyExc_OverflowError,
26+
"Python int too large to convert to C int");
27+
return -1;
28+
}
29+
return (int)result;
30+
}
31+
#endif
1032

1133
static PyObject* ctypes_module = nullptr;
1234

@@ -69,7 +91,13 @@ static void populate_feeders(PyTypeObject* target_t, PyTypeObject* source_t)
6991
{
7092
m_feeders[{target_t,source_t}] = [](void* ptr, PyObject* value) -> int
7193
{
72-
*((int*)ptr) = (int)PyLong_AsLong(value);
94+
// PyLong_AsInt range-checks against the 32-bit int slot and raises
95+
// OverflowError itself, so an out-of-range value is rejected rather
96+
// than silently truncated.
97+
int v = PyLong_AsInt(value);
98+
if (v == -1 && PyErr_Occurred())
99+
return -1;
100+
*((int*)ptr) = v;
73101
return sizeof(int);
74102
};
75103
return;
@@ -89,7 +117,23 @@ static void populate_feeders(PyTypeObject* target_t, PyTypeObject* source_t)
89117
{
90118
m_feeders[{target_t,source_t}] = [](void* ptr, PyObject* value) -> int
91119
{
92-
*((int8_t*)ptr) = (int8_t)PyLong_AsLong(value);
120+
// c_byte is an 8-bit slot with no dedicated CPython converter, so
121+
// range-check explicitly against INT8_MIN/INT8_MAX. AsLongAndOverflow's
122+
// `overflow` only flags values outside `long` (64-bit on LP64), so a
123+
// value in that range would be silently truncated by (int8_t)v without
124+
// the explicit bounds check. When overflow!=0, v is the -1 sentinel
125+
// (not the real value), so that case must be caught before trusting v.
126+
int overflow = 0;
127+
long v = PyLong_AsLongAndOverflow(value, &overflow);
128+
if (overflow == 0 && v == -1 && PyErr_Occurred())
129+
return -1; // non-overflow conversion error; exception already set
130+
if (overflow != 0 || v < INT8_MIN || v > INT8_MAX)
131+
{
132+
PyErr_SetString(PyExc_OverflowError,
133+
"Python int is out of range for a c_byte (8-bit) kernel argument");
134+
return -1;
135+
}
136+
*((int8_t*)ptr) = (int8_t)v;
93137
return sizeof(int8_t);
94138
};
95139
return;

cuda_bindings/cuda/bindings/_lib/param_packer.pxd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
# Include "param_packer.h" so its contents get compiled into every
55
# Cython extension module that depends on param_packer.pxd.
66
cdef extern from "param_packer.h":
7-
int feed(void* ptr, object o, object ct)
7+
int feed(void* ptr, object o, object ct) except? -1

cuda_bindings/tests/test_kernelParams.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,3 +782,34 @@ def __init__(self, address, typestr):
782782
ASSERT_DRV(err)
783783
(err,) = cuda.cuModuleUnload(module)
784784
ASSERT_DRV(err)
785+
786+
787+
def test_kernelParams_c_int_out_of_range_raises(device):
788+
# #363: an out-of-range Python int for a c_int / c_byte kernel argument must
789+
# raise instead of being silently truncated to fit the declared width.
790+
kernelString = """\
791+
extern "C" __global__ void take_int(int i) {}
792+
"""
793+
module = common_nvrtc(kernelString, device)
794+
err, kernel = cuda.cuModuleGetFunction(module, b"take_int")
795+
ASSERT_DRV(err)
796+
err, stream = cuda.cuStreamCreate(0)
797+
ASSERT_DRV(err)
798+
799+
# An in-range value still packs and launches fine.
800+
(err,) = cuda.cuLaunchKernel(kernel, 1, 1, 1, 1, 1, 1, 0, stream, ((5,), (ctypes.c_int,)), 0)
801+
ASSERT_DRV(err)
802+
803+
# Out-of-range values now raise OverflowError during packing (previously the
804+
# high bits were silently dropped, so the kernel saw a different value).
805+
with pytest.raises(OverflowError):
806+
cuda.cuLaunchKernel(kernel, 1, 1, 1, 1, 1, 1, 0, stream, ((2**32 + 5,), (ctypes.c_int,)), 0)
807+
with pytest.raises(OverflowError):
808+
cuda.cuLaunchKernel(kernel, 1, 1, 1, 1, 1, 1, 0, stream, ((200,), (ctypes.c_byte,)), 0)
809+
810+
(err,) = cuda.cuStreamSynchronize(stream)
811+
ASSERT_DRV(err)
812+
(err,) = cuda.cuStreamDestroy(stream)
813+
ASSERT_DRV(err)
814+
(err,) = cuda.cuModuleUnload(module)
815+
ASSERT_DRV(err)

cuda_core/cuda/core/_device_resources.pxd

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5-
cimport cython
6-
75
from cuda.bindings cimport cydriver
86
from cuda.core._resource_handles cimport ContextHandle, GreenCtxHandle
97

@@ -17,7 +15,6 @@ cdef class SMResource:
1715
unsigned int _flags
1816
bint _is_usable
1917
object __weakref__
20-
cython.pymutex _split_mutex
2118

2219
@staticmethod
2320
cdef SMResource _from_dev_resource(cydriver.CUdevResource res, int device_id)

cuda_core/cuda/core/_device_resources.pyx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -520,11 +520,10 @@ cdef class SMResource:
520520
)
521521
_resolve_group_count(opts)
522522
_check_green_ctx_support()
523-
with self._split_mutex:
524-
if _can_use_structured_sm_split():
525-
return _split_with_general_api(self, opts, dry_run)
526-
# SplitByCount requires the same 12.4+ as green ctx support (already checked above)
527-
return _split_with_count_api(self, opts, dry_run)
523+
if _can_use_structured_sm_split():
524+
return _split_with_general_api(self, opts, dry_run)
525+
# SplitByCount requires the same 12.4+ as green ctx support (already checked above)
526+
return _split_with_count_api(self, opts, dry_run)
528527

529528

530529
cdef class WorkqueueResource:

cuda_core/cuda/core/_module.pyi

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,20 @@ class ObjectCode:
458458
459459
"""
460460

461+
def get_module(self) -> object:
462+
"""Return a context-dependent :obj:`~driver.CUmodule` for legacy interop.
463+
464+
Bridges the native :obj:`~driver.CUlibrary` (see :attr:`handle`) to a
465+
``CUmodule`` via ``cuLibraryGetModule``, for use with legacy driver APIs
466+
that only accept ``CUmodule``.
467+
468+
Returns
469+
-------
470+
:obj:`~driver.CUmodule`
471+
Module handle for the current CUDA context, suitable for legacy
472+
driver APIs that accept ``CUmodule``.
473+
"""
474+
461475
@property
462476
def code(self) -> CodeTypeT:
463477
"""Return the underlying code object."""
@@ -476,7 +490,10 @@ class ObjectCode:
476490

477491
@property
478492
def handle(self) -> object:
479-
"""Return the underlying handle object.
493+
"""Return the native, context-independent :obj:`~driver.CUlibrary` handle.
494+
495+
Used by ``cuda.core`` and newer driver library APIs. For legacy APIs
496+
that only accept a ``CUmodule``, use :meth:`get_module` instead.
480497
481498
.. caution::
482499

cuda_core/cuda/core/_module.pyx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ from __future__ import annotations
66

77
cimport cython
88
from libc.stddef cimport size_t
9+
from libc.stdint cimport intptr_t
910

1011
from collections import namedtuple
1112
from os import fsencode, fspath, PathLike
@@ -796,6 +797,25 @@ cdef class ObjectCode:
796797
HANDLE_RETURN(get_last_error())
797798
return Kernel._from_handle(h_kernel)
798799

800+
def get_module(self) -> object:
801+
"""Return a context-dependent :obj:`~driver.CUmodule` for legacy interop.
802+
803+
Bridges the native :obj:`~driver.CUlibrary` (see :attr:`handle`) to a
804+
``CUmodule`` via ``cuLibraryGetModule``, for use with legacy driver APIs
805+
that only accept ``CUmodule``.
806+
807+
Returns
808+
-------
809+
:obj:`~driver.CUmodule`
810+
Module handle for the current CUDA context, suitable for legacy
811+
driver APIs that accept ``CUmodule``.
812+
"""
813+
self._lazy_load_module()
814+
cdef cydriver.CUmodule mod
815+
with nogil:
816+
HANDLE_RETURN(cydriver.cuLibraryGetModule(&mod, as_cu(self._h_library)))
817+
return driver.CUmodule(<intptr_t>mod)
818+
799819
@property
800820
def code(self) -> CodeTypeT:
801821
"""Return the underlying code object."""
@@ -818,7 +838,10 @@ cdef class ObjectCode:
818838

819839
@property
820840
def handle(self) -> object:
821-
"""Return the underlying handle object.
841+
"""Return the native, context-independent :obj:`~driver.CUlibrary` handle.
842+
843+
Used by ``cuda.core`` and newer driver library APIs. For legacy APIs
844+
that only accept a ``CUmodule``, use :meth:`get_module` instead.
822845

823846
.. caution::
824847

cuda_core/tests/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,18 @@ def wrapper(*args, **kwargs):
131131
kwargs["mempool_device_x2"] = _mempool_device_impl(2)
132132
if "mempool_device_x3" in kwargs:
133133
kwargs["mempool_device_x3"] = _mempool_device_impl(3)
134+
135+
# These are used by test_green_context.py. The original fixtures include
136+
# pytest.skip() but that should have correctly fired by this time.
137+
if "sm_resource" in kwargs:
138+
kwargs["sm_resource"] = device.resources.sm
139+
if "wq_resource" in kwargs:
140+
kwargs["wq_resource"] = device.resources.workqueue
141+
if "green_ctx" in kwargs:
142+
from cuda.core import ContextOptions, SMResourceOptions
143+
144+
groups, _ = device.resources.sm.split(SMResourceOptions(count=None))
145+
kwargs["green_ctx"] = device.create_context(ContextOptions(resources=[groups[0]]))
134146
return func(*args, **kwargs)
135147

136148
wrapper._cuda_core_worker_cuda_wrapped = True

cuda_core/tests/graph/test_graph_definition_lifetime.py

Lines changed: 86 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -611,48 +611,97 @@ def test_user_object_cleanup_is_coalesced_on_python_thread(init_cuda):
611611

612612

613613
@pytest.mark.agent_authored(model="gpt-5.6")
614-
def test_pending_call_queue_saturation_preserves_cleanup(init_cuda):
614+
def test_pending_call_queue_saturation_preserves_cleanup(tmp_path):
615615
"""A full CPython queue neither strands nor mis-threads cleanup."""
616-
pending_callback_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p)
617-
add_pending_call = ctypes.pythonapi.Py_AddPendingCall
618-
add_pending_call.argtypes = [pending_callback_type, ctypes.c_void_p]
619-
add_pending_call.restype = ctypes.c_int
616+
code = f"timeout = {_FINALIZE_TIMEOUT!r}\n" + textwrap.dedent(
617+
"""
618+
import ctypes
619+
import gc
620+
import threading
621+
import time
622+
623+
from cuda.core import Device
624+
from cuda.core.graph import GraphDefinition
620625
621-
@pending_callback_type
622-
def noop_pending_call(_):
623-
return 0
626+
class ThreadRecordingCallback:
627+
def __init__(self, finalized_threads):
628+
self.finalized_threads = finalized_threads
624629
625-
finalized_threads = []
626-
main_thread = threading.get_ident()
627-
first_callback = _ThreadRecordingCallback(finalized_threads)
628-
first_graph = GraphDefinition()
629-
first_graph.callback(first_callback)
630-
graph_holder = [first_graph]
631-
worker_done = threading.Event()
632-
queue_was_full = []
630+
def __call__(self):
631+
pass
632+
633+
def __del__(self):
634+
self.finalized_threads.append(threading.get_ident())
635+
636+
def wait_until(predicate):
637+
deadline = time.monotonic() + timeout
638+
while True:
639+
gc.collect()
640+
if predicate():
641+
return
642+
if time.monotonic() >= deadline:
643+
break
644+
time.sleep(0)
645+
time.sleep(0.02)
646+
raise AssertionError(f"condition not satisfied within {timeout}s")
647+
648+
Device(0).set_current()
649+
650+
pending_callback_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p)
651+
add_pending_call = ctypes.pythonapi.Py_AddPendingCall
652+
add_pending_call.argtypes = [pending_callback_type, ctypes.c_void_p]
653+
add_pending_call.restype = ctypes.c_int
654+
make_pending_calls = ctypes.pythonapi.Py_MakePendingCalls
655+
make_pending_calls.argtypes = []
656+
make_pending_calls.restype = ctypes.c_int
657+
658+
@pending_callback_type
659+
def noop_pending_call(_):
660+
return 0
661+
662+
finalized_threads = []
663+
main_thread = threading.get_ident()
664+
first_callback = ThreadRecordingCallback(finalized_threads)
665+
first_graph = GraphDefinition()
666+
first_graph.callback(first_callback)
667+
graph_holder = [first_graph]
668+
worker_done = threading.Event()
669+
queue_was_full = []
670+
671+
del first_callback, first_graph
672+
673+
def fill_queue_and_destroy():
674+
while add_pending_call(noop_pending_call, None) == 0:
675+
pass
676+
queue_was_full.append(True)
677+
graph_holder.clear()
678+
worker_done.set()
633679
634-
del first_callback, first_graph
680+
worker = threading.Thread(target=fill_queue_and_destroy)
681+
worker.start()
682+
assert worker_done.wait(timeout=5)
683+
worker.join()
684+
assert queue_was_full == [True]
635685
636-
def fill_queue_and_destroy():
637-
while add_pending_call(noop_pending_call, None) == 0:
638-
pass
639-
queue_was_full.append(True)
640-
graph_holder.clear()
641-
worker_done.set()
642-
643-
worker = threading.Thread(target=fill_queue_and_destroy)
644-
worker.start()
645-
assert worker_done.wait(timeout=5)
646-
worker.join()
647-
assert queue_was_full == [True]
648-
649-
# A later safe cuda-core close retries after the main thread has had an
650-
# opportunity to drain the foreign pending calls.
651-
retry_builder = Device().create_graph_builder()
652-
retry_builder.close()
653-
654-
_wait_until(lambda: len(finalized_threads) == 1)
655-
assert set(finalized_threads) == {main_thread}
686+
# Free space before the cuda-core entry that retries cleanup scheduling.
687+
assert make_pending_calls() == 0
688+
689+
retry_builder = Device().create_graph_builder()
690+
retry_builder.close()
691+
692+
wait_until(lambda: len(finalized_threads) == 1)
693+
assert set(finalized_threads) == {main_thread}
694+
"""
695+
)
696+
result = subprocess.run( # noqa: S603 - controlled interpreter probe
697+
[sys.executable, "-c", code],
698+
capture_output=True,
699+
text=True,
700+
timeout=60,
701+
# Isolate the process-global pending-call queue from parallel tests.
702+
cwd=tmp_path,
703+
)
704+
assert result.returncode == 0, result.stderr
656705

657706

658707
@pytest.mark.agent_authored(model="gpt-5.6")

cuda_core/tests/test_green_context.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
# ---------------------------------------------------------------------------
4242
# Fixtures
4343
# ---------------------------------------------------------------------------
44+
# Note that the following fixtures (except fill_kernel) require per-thread setup
45+
# and are currently special cased to work with pytest-run-parallel in conftest.
4446

4547

4648
# Resource queries (dev.resources.sm, dev.resources.workqueue) can fail in

0 commit comments

Comments
 (0)