Skip to content

Handle std::bad_alloc in make_deallocation_stream / set_deallocation_stream - #3134

Merged
mdboom merged 5 commits into
NVIDIA:mainfrom
mdboom:handle-memory-exception
Nov 4, 2026
Merged

mdboom merged 5 commits into
NVIDIA:mainfrom
mdboom:handle-memory-exception

Conversation

@mdboom

@mdboom mdboom commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

I have been working on adding clang-tidy to pre-commit for cuda-python. There is exactly one (potentially) real bug it found (the rest is just helping to make the code clearer to prevent /future/ bugs). So with that said, this is a theoretical bug, but something that has been observed in practice. We can decide this isn't worth the effort and just suppress the clang-tidy observation about it instead.

My agent's summary is below:

Fix: std::bad_alloc aborts the process in make_deallocation_stream / set_deallocation_stream

Branch: handle-memory-exception (based on upstream/main at e957c5c).
Package: cuda_core. Nothing here is committed.

Summary

make_deallocation_stream() and set_deallocation_stream() in the C++ handle
layer (cuda_core/cuda/core/_cpp/rt/) were declared noexcept, but both can
allocate host memory. When that allocation fails, std::bad_alloc escapes a
noexcept function and the runtime calls std::terminate, which aborts the
whole Python process (SIGABRT). It should raise MemoryError.

The fix removes noexcept from both functions, declares the Cython binding of
set_deallocation_stream as except+ (so std::bad_alloc becomes
MemoryError), and handles the one caller that needs cleanup before the error
propagates. It is a robustness fix: it only matters when the host is out of
memory, but the failure mode (an unrecoverable abort) is exactly what the
project's own guidelines forbid.

How it was found

clang-tidy's bugprone-unhandled-exception-at-new flagged the bare new
in make_deallocation_stream:

stream.cpp: warning: missing exception handler for allocation failure at 'new'
            [bugprone-unhandled-exception-at-new]

The warning alone does not say why it matters, and it points at only one of the
two throwing sites. The rest of the analysis below was done by reading the code.

Why it is a bug

cuda_core/CLAUDE.md ("Failure handling", "Never terminate the process") says:

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.

make_deallocation_stream(h, out) noexcept (stream.cpp) binds a deallocation
stream to a stream handle. Real streams are copied unchanged, but a
default-stream token (nullptr, CU_STREAM_LEGACY, CU_STREAM_PER_THREAD)
with no embedded context takes this path:

  1. get_current_context() is called. It is not noexcept and allocates:
    it goes through create_context_handle_ref, which builds a
    std::shared_ptr (a control-block allocation).
  2. new StreamBox{...} and the std::shared_ptr that owns it allocate.

Either allocation can throw std::bad_alloc. Because the enclosing function is
noexcept, the exception cannot propagate and std::terminate runs.

set_deallocation_stream(h, h_stream) noexcept (memory.cpp) calls it, so it
inherited the problem. It is the C++ side of the public
Buffer.set_deallocation_stream(), and it is also called from
Buffer.from_handle() whenever the memory resource is device accessible.

The other callers of make_deallocation_stream (deviceptr_alloc_from_pool,
deviceptr_alloc_async, deviceptr_create_mapped_graphics,
deviceptr_import_ipc) are not noexcept, and their Cython declarations are
already except+, so they were already safe once the callee may throw.

The fix

C++ (cuda_core/cuda/core/_cpp/rt/):

  • stream.cpp, internal.hpp: make_deallocation_stream is no longer
    noexcept. The comment documents that it may throw std::bad_alloc.
  • memory.cpp, api.hpp: set_deallocation_stream is no longer noexcept.
    The header comment now says it may throw std::bad_alloc, matching how
    create_vmm_range documents the same thing.

Cython:

  • _rt.pyx, _rt.pxd: set_deallocation_stream is declared
    except+ nogil instead of noexcept nogil, like its sibling
    deviceptr_import_ipc. Cython converts std::bad_alloc into MemoryError.
  • _memory/_buffer.pyx:
    • _apply_deallocation_stream (used by Buffer.set_deallocation_stream)
      is declared except *, so the MemoryError propagates with no change.
    • Buffer._init (used by Buffer.from_handle) already resets self._h_ptr
      before raising on a failed status, because the DevicePtrHandle destructor
      calls _mr_dealloc_callback, whose except Exception handler can clear an
      in-flight error indicator. The new MemoryError path does the same reset,
      inside an except MemoryError: handler and then re-raises, so the same
      hazard cannot swallow the error.

Release note added to cuda_core/docs/source/release/1.3.0-notes.rst.

Alternatives considered

  • Catch std::bad_alloc in C++ and return CUDA_ERROR_OUT_OF_MEMORY. This
    would leave the Cython signature untouched, but it reports host exhaustion as
    a device error, and it hides the real exception type. MemoryError is the
    accurate Python exception and is what the sibling functions already raise.
  • Keep noexcept and wrap the body in try/catch. This still needs some
    channel to report the failure, so it ends up as one of the two options above.

Testing

New file cuda_core/tests/test_allocation_failure.py. A tiny shared library
(compiled with the system C++ compiler during the test) replaces
operator new so that the Nth C++ allocation on the arming thread throws
std::bad_alloc. It is preloaded with LD_PRELOAD into a child process, so
an abort shows up as a non-zero exit status instead of killing the test run.
Python allocates through its own allocators, so only C++ allocations are
affected. The tests are Linux-only and skip if no C++ compiler is available.

  • test_set_deallocation_stream_raises_memory_error_instead_of_terminating:
    fails the first C++ allocation inside Buffer.set_deallocation_stream with a
    default-stream token and asserts a clean MemoryError.
  • test_buffer_from_handle_survives_allocation_failure: fails each of the
    first eight allocations on the way through Buffer.from_handle and asserts the
    child exits normally and at least once saw a MemoryError.

Results (GPU run on this machine):

Build test_allocation_failure.py
Without the fix both fail: child exits with signal 6 and terminate called after throwing an instance of 'std::bad_alloc'
With the fix both pass

I confirmed the "without" row by reverting only the source changes, rebuilding,
and running the new tests.

Wider run with the fix: pytest cuda_core/tests --ignore=cuda_core/tests/cython
gave 4081 passed, 201 skipped, 6 xfailed, 1 failed. The one failure,
test_program_cache.py::test_filestream_write_over_cap_skips_vanished_files, is
unrelated and flaky: three consecutive runs of that single test gave pass, fail,
pass. The repo's pre-commit hooks pass on the changed files.

Limits and follow-ups

  • Only these two functions were fixed. A scan of every noexcept function body
    under _cpp/rt/ for new, make_shared, containers and std::string found
    no other direct allocation. Transitive cases (a noexcept function calling a
    throwing function) are not caught by any stock clang-tidy check, so there may
    be others. The fault-injection shim used here would find them if run across
    the whole suite (fail the Nth allocation for many N).
  • Cases where a driver resource is created and a later host allocation
    throws (a possible leak under memory exhaustion) were not audited. This fix
    does not change them.
  • The tests depend on a Linux-style preload; other platforms skip them.

@mdboom
mdboom requested a review from Andy-Jost October 16, 2026 15:13
@mdboom mdboom self-assigned this Oct 16, 2026
@mdboom mdboom added bug Something isn't working cuda.core Everything related to the cuda.core module labels Oct 16, 2026
@mdboom mdboom changed the title Handle memory exhaustion exceptions Handle std::bad_alloc in make_deallocation_stream / set_deallocation_stream Oct 16, 2026
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Summary

Summary by CodeRabbit

  • Bug Fixes
    • Buffer operations that bind a default stream now raise MemoryError if the host runs out of memory, instead of terminating the process.
    • If stream setup fails with a memory error during buffer initialization, the newly created device-pointer handle is reset before the error is raised.

The deallocation-stream functions can now propagate allocation exceptions through C++ and Cython. Buffer._init resets its device-pointer handle and re-raises MemoryError. The release notes describe the resulting error behavior.

Changes: Default-stream allocation errors

Layer / File(s) Summary
C++ exception propagation and pointer cleanup
cuda_core/cuda/core/_cpp/rt/api.hpp, cuda_core/cuda/core/_cpp/rt/internal.hpp, cuda_core/cuda/core/_cpp/rt/stream.cpp, cuda_core/cuda/core/_cpp/rt/memory.cpp
The deallocation-stream functions no longer declare noexcept. Binding a default-stream token may throw std::bad_alloc.
Python error propagation and validation
cuda_core/cuda/core/_rt.pxd, cuda_core/cuda/core/_rt.pyx, cuda_core/cuda/core/_memory/_buffer.pyx, cuda_core/docs/source/release/1.3.0-notes.rst
Cython translates C++ exceptions. Buffer._init resets its device-pointer handle when MemoryError is raised. The release notes describe this outcome.

Change: Bug fix

Merge Risk: Moderate · up to 10f93

On host-memory exhaustion, CUDA resources can remain allocated or mapped after the new exception propagation. Add rollback handling before merging to prevent resource leaks and later availability problems.

- [ ] Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts ### CodeRabbit Verify
  • Run CodeRabbit Verify to test the changes in this pull request.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments: 1
cuda_core/tests/test_allocation_failure.py: 1

165-167: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

suggestion: Assert a failure inside default-stream binding.

The first Buffer.from_handle attempt can raise MemoryError while creating its pointer handle. That result alone satisfies this assertion, even if no attempt reaches the new cleanup in Buffer._init. Identify the allocation that fails during set_deallocation_stream, or instrument the shim to confirm that call was reached, and assert that outcome separately.

As per path instructions, check that “public behavior changes have corresponding tests.”

Source: Path instructions


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository: NVIDIA/cuda-python/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Enterprise
  • Run ID: be0d2d91-33c8-454f-b0cd-38822486029d
📥 Commits

Reviewing files that changed from the base of the PR and between e957c5c and 52ef259.

📒 Files selected for processing: 9
  • cuda_core/cuda/core/_cpp/rt/api.hpp
  • cuda_core/cuda/core/_cpp/rt/internal.hpp
  • cuda_core/cuda/core/_cpp/rt/memory.cpp
  • cuda_core/cuda/core/_cpp/rt/stream.cpp
  • cuda_core/cuda/core/_memory/_buffer.pyx
  • cuda_core/cuda/core/_rt.pxd
  • cuda_core/cuda/core/_rt.pyx
  • cuda_core/docs/source/release/1.3.0-notes.rst
  • cuda_core/tests/test_allocation_failure.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 10 remain after this review.

Comment thread cuda_core/cuda/core/_cpp/rt/stream.cpp Resolved
@mdboom mdboom added this to the cuda.core next milestone Oct 16, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments: 1
cuda_core/tests/test_allocation_failure.py: 1

98-115: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

suggestion: Extend the allocation-failure harness to cover make_deviceptr_box.

test_allocation_failure.py does not call deviceptr_alloc_from_pool, deviceptr_alloc_async, or deviceptr_import_ipc. Add one focused scenario that forces new DevicePtrBox to throw after the CUDA allocation, then asserts MemoryError, a zero child exit status, and resource cleanup. This covers the shared cleanup branch without testing every allocation count.


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository: NVIDIA/cuda-python/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Enterprise
  • Run ID: 21d485aa-b15d-4e0e-a9c8-affbb174901e
📥 Commits

Reviewing files that changed from the base of the PR and between 52ef259 and 8fb56cb.

📒 Files selected for processing: 1
  • cuda_core/cuda/core/_cpp/rt/memory.cpp
🚧 Files skipped from review as they are similar to previous changes: 1
  • cuda_core/cuda/core/_cpp/rt/memory.cpp

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 10 remain after this review.

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor
Doc Preview CI
Preview removed because the pull request was closed or merged.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · important: Unmap the graphics resource when handle creation throws. · memory.cpp:345-349

cuda_core/cuda/core/_cpp/rt/memory.cpp:345-349
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

important: Unmap the graphics resource when handle creation throws.

cuGraphicsMapResources succeeds before make_deallocation_stream can throw std::bad_alloc. The unmap-owning handle is not constructed, and _mapped_buffer remains unset. GraphicsResource.close() therefore cannot unmap the resource. A later map() call can fail because the resource is still mapped.

Suggested fix
     DeallocationStream ds;
-    if (!make_deallocation_stream(h_stream, ds)) {
-        return {};
+    try {
+        if (!make_deallocation_stream(h_stream, ds)) {
+            return {};
+        }
+    } catch (...) {
+        GILReleaseGuard gil;
+        CUgraphicsResource resource = as_cu(h_resource);
+        cleanup_in_context(
+            {}, "cuGraphicsUnmapResources", handle_bits(resource),
+            [&]() noexcept {
+                return DRIVER_CALL(cuGraphicsUnmapResources,
+                    1, &resource, as_cu(h_stream));
+            });
+        throw;
     }

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository: NVIDIA/cuda-python/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Enterprise
  • Run ID: 205354a6-7984-4b2a-8508-0b311fb6c6b3
📥 Commits

Reviewing files that changed from the base of the PR and between 8fb56cb and c07e400.

📒 Files selected for processing: 1
  • cuda_core/tests/test_allocation_failure.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 9 remain after this review.

@mdboom mdboom modified the milestones: cuda.core next, cuda Oct 20, 2026
Comment thread cuda_core/cuda/core/_cpp/rt/memory.cpp Outdated
// once it holds no lock (undo may not report under a lock, see
// deviceptr_import_ipc). Never throws.
template <typename Undo>
static DevicePtrBox* make_deviceptr_box(

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.

Please drop this helper and restore the four call sites (deviceptr_alloc_from_pool, deviceptr_alloc_async, and both branches of deviceptr_import_ipc) to their form on main.

No owning creator in _cpp/rt/ rolls back the driver resource when new Box throws after the driver call. create_stream_handle leaks the stream, and deviceptr_alloc_from_pool on main leaks the allocation. The one try/catch in a creation path, in graph_prepare_attachment, protects a host object, not a driver resource. The accepted outcome for a host allocation failure is a MemoryError and a leaked driver resource. The same call was made in #3028 (thread). A rollback here would be the first of more than twenty, one per owning creator.

The two new tests do not go through these call sites, so they are unaffected.


import pytest

pytestmark = pytest.mark.skipif(

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.

Optional. My preference is to drop this file. It compiles a shim at test time, runs on Linux only, and guards one regression: a bare new inside a noexcept function. The clang-tidy check from your pre-commit work, bugprone-unhandled-exception-at-new, catches that case at lint time.

mdboom and others added 2 commits November 3, 2026 15:09
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@mdboom
mdboom requested a review from Andy-Jost November 3, 2026 20:14
@mdboom mdboom added this to the cuda.core next milestone Nov 3, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Follow-up Comments

Outside diff important: Roll back the graphics mapping when stream binding raises.

347-373: Major · Stability & Availability · Quick win

important: Roll back the graphics mapping when stream binding raises.

GraphicsResource.map maps the resource before deviceptr_create_mapped_graphics binds the deallocation stream. If make_deallocation_stream raises std::bad_alloc, the DevicePtrBox owner does not exist, so cuGraphicsUnmapResources is not called and the resource remains mapped.

Catch the helper failure, unmap with cy_stream, and attach any rollback failure while preserving the original exception. This is a fifth site in addition to the four allocation/import paths.

Suggested fix
 from cuda.core._rt cimport (
     create_graphics_resource_handle,
     deviceptr_create_mapped_graphics,
+    DevicePtrHandle,
+    attach_rollback_failure,
     as_cu,
     as_intptr,
 )
@@
         cdef Stream s_obj = Stream_accept(stream)
         cdef cydriver.CUgraphicsResource raw = as_cu(self._handle)
         cdef cydriver.CUstream cy_stream = as_cu(s_obj._h_stream)
+        cdef DevicePtrHandle h_ptr
+        cdef cydriver.CUresult rollback_status
         with nogil:
             HANDLE_RETURN(
                 cydriver.cuGraphicsMapResources(1, &raw, cy_stream)
@@
                 cydriver.cuGraphicsResourceGetMappedPointer(&dev_ptr, &size, raw)
             )
-        cdef Buffer buf = Buffer_from_deviceptr_handle(
-            deviceptr_create_mapped_graphics(dev_ptr, self._handle, s_obj._h_stream),
-            size,
-            None,
-            None,
-        )
+        try:
+            h_ptr = deviceptr_create_mapped_graphics(
+                dev_ptr, self._handle, s_obj._h_stream)
+        except:
+            with nogil:
+                rollback_status = cydriver.cuGraphicsUnmapResources(
+                    1, &raw, cy_stream)
+            if rollback_status != cydriver.CUDA_SUCCESS:
+                attach_rollback_failure(
+                    b"cuGraphicsUnmapResources",
+                    rollback_status,
+                    b"failed while rolling back a graphics mapping")
+            raise
+        cdef Buffer buf = Buffer_from_deviceptr_handle(h_ptr, size, None, None)

Open the source location

Review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between c07e400 and 10f93cd.

Files selected for processing: 7
  • cuda_core/cuda/core/_cpp/rt/api.hpp
  • cuda_core/cuda/core/_cpp/rt/internal.hpp
  • cuda_core/cuda/core/_cpp/rt/memory.cpp
  • cuda_core/cuda/core/_cpp/rt/stream.cpp
  • cuda_core/cuda/core/_rt.pxd
  • cuda_core/cuda/core/_rt.pyx
  • cuda_core/docs/source/release/1.3.0-notes.rst
Files skipped from review as they are similar to previous changes: 1
  • cuda_core/docs/source/release/1.3.0-notes.rst

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 10 remain after this review.

@Andy-Jost Andy-Jost left a comment

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.

LGTM! Thanks for taking care of this.

@Andy-Jost Andy-Jost added the P1 Medium priority - Should do label Nov 4, 2026
@mdboom
mdboom enabled auto-merge (squash) November 4, 2026 19:39
@mdboom
mdboom merged commit 7b315e7 into NVIDIA:main Nov 4, 2026
218 of 225 checks passed
github-actions Bot pushed a commit that referenced this pull request Nov 5, 2026
Removed preview folders for the following PRs:
- PR #3134
- PR #3240
- PR #3272
- PR #3326
- PR #3349
- PR #3352
- PR #3354
- PR #3360
- PR #3362
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working cuda.core Everything related to the cuda.core module P1 Medium priority - Should do

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants