Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion ci/tools/tests/test_compute_ci_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def test_ignored_paths_select_no_work(self) -> None:
"cuda_core/tests/fixtures/pixi.toml",
"benchmarks/cuda_bindings/pixi.toml",
"benchmarks/cuda_bindings/AGENTS.md",
"cuda_core/cuda/core/_cpp/DESIGN.md",
"cuda_core/cuda/core/_cpp/rt/DESIGN.md",
"cuda_bindings/README.md",
"cuda_core/README.md",
"new-area/pixi.toml",
Expand Down
67 changes: 64 additions & 3 deletions cuda_core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ This file describes `cuda_core`, the high-level Pythonic CUDA subpackage in the
- system-level APIs: `cuda/core/system/`
- compile/link path: `_program.pyx`, `_linker.pyx`, `_module.pyx`
- execution path: `_launcher.pyx`, `_launch_config.pyx`, `_stream.pyx`
- **C++ helpers**: module-specific C++ implementations live under
`cuda/core/_cpp/`.
- **C++ helpers**: module-specific C++ lives under `cuda/core/_cpp/`, either as
one `_cpp/<name>.cpp` or as a directory `_cpp/<name>/` whose sources all
compile into the `_<name>` extension (`_cpp/rt/` for `_rt`).
- **Build backend**: `build_hooks.py` handles Cython extension setup and build
dependency wiring.

Expand Down Expand Up @@ -90,7 +91,7 @@ and agents should flag violations.
objects that are not meant to be shared (e.g., the thread-local `Device`) do not
need such guards (see #2321). Reference-count integrity is guaranteed; cache
value-identity/idempotency is not.
- **Entry points assume the GIL is held**: the helpers in `_cpp/resource_handles.*`
- **Entry points assume the GIL is held**: the helpers in `_cpp/rt/`
are called from Cython with the GIL held and do not re-acquire it. Driver and
destructor callbacks run at arbitrary times, so they take the GIL (`with gil`)
and probe for interpreter shutdown before touching Python objects.
Expand All @@ -101,6 +102,66 @@ and agents should flag violations.
(kernel arguments, memcpy/memset operands, `dst_owner`/`src_owner`, and
host-callback closures) inherit this contract.

## Failure handling

The user-facing contract lives in `docs/source/error_handling.rst`; the rules
below are for contributors. Reviewers and agents should flag violations.

- **Raise by default**: any failure on a path where an exception can propagate
raises. Driver statuses go through `HANDLE_RETURN` (Cython) or are returned as
`CUresult` from the C++ handle layer and then `HANDLE_RETURN`ed; never
replace a `CUresult` with a generic `RuntimeError`, and drain
`get_last_error()` immediately after a handle constructor returns empty so a
stale status cannot be misattributed later.
- **Guarantees**: a call that creates a resource must create nothing when it
raises (undo the creation if a later step fails). Every call except
`Device.set_current` must leave the calling thread's current context as it
found it. Do not hand-roll `cuCtxPush/Pop/SetCurrent` sequences in Cython; use
the handle layer's scoped-context helpers (`invoke_in_context`,
`invoke_in_context_or_undo`, `cleanup_in_context`, `context_get_device`,
`graph_node_set_params`) so the failure handling exists in one place.
- **Publish before you raise**: when a driver mutation has succeeded and a later
step can still fail, commit whatever keeps that mutation memory-safe (for
example the graph attachment that retains a node's new owners) before raising
the later error. Rolling back the retention of a live mutation creates a
dangling reference. When ownership cannot be established, retain the
resources anyway (leak) rather than release them; a leak is always preferred
to a use-after-free.
- **Non-propagating paths never raise and never discard a status**: shared_ptr
deleters, `__dealloc__` and CUDA callbacks report through one channel, `report_cuda_error()` / `report_message()` in C++ (the
`pw_*` wrappers) or `warnings.warn(..., CUDAWarning)` in Cython and Python,
which emits `cuda.core.CUDAWarning`. No `print(file=sys.stderr)` and no
`fprintf` outside that helper. `CUDA_ERROR_DEINITIALIZED` is filtered by the
helper because it means the driver is shutting down.
- **Rollback failure**: the original exception propagates; the failed rollback
is attached to it with `note_or_report_cuda_error()` (a PEP 678 note on
Python 3.11+, reported out-of-band on 3.10), or chained with
`raise ... from` when a second exception must be raised. Catching everything
(bare `except:` or `except BaseException:`) is acceptable only for
rollback-then-`raise` blocks, where the rollback must also run for
`KeyboardInterrupt`.
- **Finalization**: once `py_is_finalizing()` is true, do no Python work from
destructors or callbacks and accept the leak (see
`_cpp/rt/py.hpp` and `_cpp/rt/GRAPH_ATTACHMENTS.md`).
- **Never terminate the process**: no `std::abort`, `std::terminate`, `exit`,
`Py_FatalError`, or `assert` that survives into a release build, anywhere in
`cuda.core`. A failed CUDA call, including a failed context restoration, is
raised or reported. An internal invariant violation is handled the same way:
raise a `RuntimeError` that says "internal cuda.core error, please report"
where an exception can propagate, report through the channel above where it
cannot, and leak the affected resource rather than touch state that may be
inconsistent. Users who want fail-fast behavior get it with
`warnings.filterwarnings("error", category=CUDAWarning)` and
`PYTHONFAULTHANDLER`; the library does not make that choice for them. An
*implicit* abort (an exception escaping a `noexcept` function or a deleter,
including `std::bad_alloc` from an allocation inside `noexcept` code) is a
bug (#1489, #2417), not a policy choice: `noexcept` helpers must not
allocate, or must catch what they call.
- **Testing**: inject restoration failures with
`cuda.core._rt._set_context_restore_fault_for_testing`; assert
reports with `pytest.warns(CUDAWarning)` or `warnings.catch_warnings`, never
by matching stderr text.

## API design guidelines

These are some API design guidelines we try to follow when adding new APIs to
Expand Down
29 changes: 29 additions & 0 deletions cuda_core/build_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,29 @@ def _extension_sources(mod_name):
return sources


def _extension_depends():
"""Headers whose edits must rebuild an extension: every header under a
directory-form module's cuda/core/_cpp/<stem>/ (a single-file module has
none).

The same list serves every extension. A module that cimports a
directory-form module compiles against the header its .pxd names, and
cythonize copies each `depends` entry into its build directory before
compiling, so the copied header finds its sibling includes beside it
(quoted includes resolve next to the copy, not in the source tree).
Listing the whole directory keeps the rule free of include parsing; the
cost is that every extension rebuilds when any of these headers changes,
exactly as editing the one monolithic header did before the split."""
cpp = Path("cuda", "core", "_cpp")

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.

This will break if building from anywhere but cuda_core.

Suggested change
cpp = Path("cuda", "core", "_cpp")
cpp = Path(__file__).parent / "cuda" / "core" / "_cpp"

return sorted(
str(path)
for module_dir in cpp.iterdir()
if module_dir.is_dir()
for path in module_dir.rglob("*")
if path.suffix in (".h", ".hpp")
)


def _build_cuda_core(debug=False):
# Customizing the build hooks is needed because we must defer cythonization until cuda-bindings,
# now a required build-time dependency that's dynamically installed via the other hook below,
Expand Down Expand Up @@ -265,10 +288,12 @@ def module_names():
# related to free-threading builds.
extra_compile_args += ["-DCYTHON_TRACE_NOGIL=1", "-DCYTHON_USE_SYS_MONITORING=0"]

depends = _extension_depends()
ext_modules = tuple(
Extension(
f"cuda.core.{mod.replace(os.path.sep, '.')}",
sources=_extension_sources(mod),
depends=depends,
include_dirs=[
"cuda/core/_include",
"cuda/core/_cpp",
Expand Down Expand Up @@ -299,6 +324,10 @@ def module_names():
# CUDA_PYTHON_COVERAGE deliberately generates in-tree so the sources can
# be packaged; every other build gets its own per-configuration cache,
# anchored alongside the stamp so both resolve the same from any cwd.
# Cython also copies each extension's extern headers and `depends` under
# this directory and compiles against the copies. Copies are refreshed by
# mtime and never deleted, so remove build/ after renaming or deleting a
# header under _cpp/.
build_dir="." if COMPILE_FOR_COVERAGE else str(_BUILD_DIR / "cython" / f"cu{cuda_major}"),
nthreads=nthreads,
compiler_directives=compiler_directives,
Expand Down
2 changes: 2 additions & 0 deletions cuda_core/cuda/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,10 @@ class _PatchedProperty(metaclass=_PatchedPropMeta):
from cuda.core._stream import __all__ as _stream_all
from cuda.core._tensor_map import *
from cuda.core._tensor_map import __all__ as _tensor_map_all
from cuda.core._utils.cuda_utils import CUDAWarning

__all__ = [
"CUDAWarning",
*_context_all,
*_device_all,
*_device_resources_all,
Expand Down
2 changes: 1 addition & 1 deletion cuda_core/cuda/core/_context.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# SPDX-License-Identifier: Apache-2.0

from cuda.core._resource_handles cimport ContextHandle, GreenCtxHandle
from cuda.core._rt cimport ContextHandle, GreenCtxHandle

cdef class Context:
"""Cython declaration for Context class.
Expand Down
2 changes: 1 addition & 1 deletion cuda_core/cuda/core/_context.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import cython
from cuda.bindings cimport cydriver
from cuda.core._device_resources cimport DeviceResources, SMResource, WorkqueueResource
from cuda.core._device_resources import SMResource, WorkqueueResource
from cuda.core._resource_handles cimport (
from cuda.core._rt cimport (
ContextHandle,
GreenCtxHandle,
as_cu,
Expand Down
Loading