Skip to content

Commit 8719151

Browse files
Andy-Jostclaude
andcommitted
cuda.core: define the error handling policy and report failures that cannot be raised
Write down how cuda.core handles CUDA failures (docs/source/error_handling.rst for users, a "Failure handling" section in AGENTS.md and _cpp/DESIGN.md for contributors) and bring the code into line with it: - Add cuda.core.CUDAWarning, emitted for CUDA errors that cannot be raised (destructors, CUDA callbacks, cleanup after an earlier failure). The C++ handle layer reports through one helper that uses the Python warnings machinery when the interpreter is usable, delivers an escalated warning as an unraisable exception, and falls back to stderr otherwise. CUDA_ERROR_DEINITIALIZED is not reported. - Wrap every destroy call made from a deleter (pw_*) so its failure is reported instead of discarded, including memory pools, green contexts, graphs, graph execs, graphics resources, the linker, user objects, the NVRTC/NVVM/nvJitLink handles and file descriptors; release the GIL around the compiler-handle destroys like the CUDA ones. - When the caller's context cannot be restored after a successful operation, undo the creation and raise a CUDAError that says which context is current; report the same failure as a warning in deleters; report a skipped context-sensitive undo instead of leaking silently. - Add context_get_device and graph_node_set_params so Stream_get_ctx_device and _set_definition_node_params stop hand-rolling cuCtxPush/Pop/SetCurrent. The node update now publishes its attachment before raising a restoration failure, closing a window that left the node referencing released owners. - Device.set_current(ctx) switches with a single cuCtxSetCurrent, so a failure leaves the previous context current and the call works without one. - Report failed cuStreamEndCapture in GraphBuilder.__dealloc__ and failed child-graph rollbacks; warn from _mr_dealloc_callback instead of printing. - Add a test hook that makes the next context restoration fail, tests for the policy, and release notes for 1.3.0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 83a3d46 commit 8719151

23 files changed

Lines changed: 1051 additions & 129 deletions

‎cuda_core/AGENTS.md‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,62 @@ and agents should flag violations.
101101
(kernel arguments, memcpy/memset operands, `dst_owner`/`src_owner`, and
102102
host-callback closures) inherit this contract.
103103

104+
## Failure handling
105+
106+
The user-facing contract lives in `docs/source/error_handling.rst`; the rules
107+
below are for contributors. Reviewers and agents should flag violations.
108+
109+
- **Raise by default**: any failure on a path where an exception can propagate
110+
raises. Driver statuses go through `HANDLE_RETURN` (Cython) or are returned as
111+
`CUresult` from the C++ handle layer and then `HANDLE_RETURN`ed; never
112+
replace a `CUresult` with a generic `RuntimeError`, and drain
113+
`get_last_error()` immediately after a handle constructor returns empty so a
114+
stale status cannot be misattributed later.
115+
- **Guarantees**: a call that creates a resource must create nothing when it
116+
raises (undo the creation if a later step fails). Every call except
117+
`Device.set_current` must leave the calling thread's current context as it
118+
found it. Do not hand-roll `cuCtxPush/Pop/SetCurrent` sequences in Cython; use
119+
the handle layer's scoped-context helpers (`invoke_in_context`,
120+
`invoke_in_context_or_undo`, `cleanup_in_context`, `context_get_device`,
121+
`graph_node_set_params`) so the failure handling exists in one place.
122+
- **Publish before you raise**: when a driver mutation has succeeded and a later
123+
step can still fail, commit whatever keeps that mutation memory-safe (for
124+
example the graph attachment that retains a node's new owners) before raising
125+
the later error. Rolling back the retention of a live mutation creates a
126+
dangling reference. When ownership cannot be established, retain the
127+
resources anyway (leak) rather than release them; a leak is always preferred
128+
to a use-after-free.
129+
- **Non-propagating paths never raise and never discard a status**: shared_ptr
130+
deleters, `__dealloc__`, CUDA callbacks and cleanup after a failure report
131+
through one channel, `report_cuda_error()` / `report_message()` in C++ (the
132+
`pw_*` wrappers) or `warnings.warn(..., CUDAWarning)` in Cython and Python,
133+
which emits `cuda.core.CUDAWarning`. No `print(file=sys.stderr)` and no
134+
`fprintf` outside that helper. `CUDA_ERROR_DEINITIALIZED` is filtered by the
135+
helper because it means the driver is shutting down.
136+
- **Rollback failure**: the original exception propagates; the failed rollback
137+
is reported out of band (or chained with `raise ... from` when a second
138+
exception must be raised). Bare `except:` is acceptable only for
139+
rollback-then-`raise` blocks.
140+
- **Finalization**: once `py_is_finalizing()` is true, do no Python work from
141+
destructors or callbacks and accept the leak (see
142+
`_cpp/resource_handles.hpp` and `_cpp/GRAPH_ATTACHMENTS.md`).
143+
- **Aborting**: `std::abort` (or any process termination) is reserved for an
144+
internal invariant violation where continuing could corrupt memory or produce
145+
silently wrong results *and* no leak-based fallback exists. A failed CUDA
146+
call, including a failed context restoration, never qualifies: raise or
147+
report instead. There is currently no such path; if one is ever needed it
148+
must go through a single helper that writes a diagnostic (call, CUDA error,
149+
invariant, "please report") to stderr before aborting, must never trigger
150+
during interpreter finalization or for driver-shutdown errors, and must be
151+
called out in the docs and release notes. An *implicit* abort (an exception
152+
escaping a `noexcept` function or a deleter, including `std::bad_alloc` from
153+
an allocation inside `noexcept` code) is a bug (#1489, #2417), not a policy
154+
choice: `noexcept` helpers must not allocate, or must catch what they call.
155+
- **Testing**: inject restoration failures with
156+
`cuda.core._resource_handles._set_context_restore_fault_for_testing`; assert
157+
reports with `pytest.warns(CUDAWarning)` or `warnings.catch_warnings`, never
158+
by matching stderr text.
159+
104160
## API design guidelines
105161

106162
These are some API design guidelines we try to follow when adding new APIs to

‎cuda_core/cuda/core/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,10 @@ class _PatchedProperty(metaclass=_PatchedPropMeta):
102102
from cuda.core._stream import __all__ as _stream_all
103103
from cuda.core._tensor_map import *
104104
from cuda.core._tensor_map import __all__ as _tensor_map_all
105+
from cuda.core._utils.cuda_utils import CUDAWarning
105106

106107
__all__ = [
108+
"CUDAWarning",
107109
*_context_all,
108110
*_device_all,
109111
*_device_resources_all,

‎cuda_core/cuda/core/_cpp/DESIGN.md‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,37 @@ Related functions:
275275
- `peek_last_error()`: Returns the error without clearing it
276276
- `clear_last_error()`: Clears the error state
277277

278+
Some functions return a `CUresult` directly instead of a handle (for example
279+
`context_synchronize`, `context_get_device`, `graph_node_set_params`). Their
280+
callers `HANDLE_RETURN` the value.
281+
282+
### Context-scoped operations
283+
284+
Operations that must run in a specific context use `invoke_in_context` /
285+
`invoke_in_context_or_undo` (propagating paths) and `cleanup_in_context`
286+
(deleters). They switch the current context, run the operation, and restore the
287+
caller's context. When restoration fails after the operation succeeded, the
288+
creation is undone and the restoration status is returned; the helper also
289+
records a thread-local detail (`take_last_error_detail()`) that the Cython error
290+
path appends to the raised `CUDAError`, so the user learns that the caller's
291+
context was not restored and which context is current. When both the operation
292+
and the restoration fail, the operation status is returned and the restoration
293+
failure is reported out of band. Tests inject restoration failures with
294+
`set_context_restore_fault_for_testing()`.
295+
296+
### Reporting from non-propagating paths
297+
298+
Deleters, CUDA callbacks and cleanup-after-failure cannot raise. They report
299+
through `report_cuda_error()` / `report_message()` (the `pw_*` wrappers
300+
decorate destroy calls with it), which emit a `cuda.core.CUDAWarning` through
301+
the Python warnings machinery when the interpreter is usable, deliver an
302+
escalated warning as an unraisable exception, and fall back to stderr when the
303+
GIL cannot be taken (for example during finalization). `CUDA_ERROR_DEINITIALIZED`
304+
is never reported because it means the driver is shutting down. No status is
305+
discarded silently anywhere in this layer, and nothing in this layer terminates
306+
the process; see `docs/source/error_handling.rst` and the "Failure handling"
307+
section of `AGENTS.md` for the policy.
308+
278309
## Usage from Cython
279310

280311
```cython

0 commit comments

Comments
 (0)