Skip to content

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

Open
mdboom wants to merge 3 commits into
NVIDIA:mainfrom
mdboom:handle-memory-exception
Open

mdboom wants to merge 3 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

    • Host-memory allocation failures while binding a deallocation stream now raise MemoryError instead of terminating the process.
    • Buffer initialization cleans up its newly created handle before propagating the error.
    • Buffer.from_handle also propagates allocation failures as MemoryError.
  • Documentation

    • Clarified that binding a default stream without a current CUDA context reports CUDA_ERROR_INVALID_CONTEXT.
  • Tests

    • Added coverage for allocation failures during deallocation-stream binding and buffer creation.

C++ deallocation-stream functions can now propagate allocation failures. Cython translates those failures to MemoryError, and Buffer._init resets the device-pointer handle before re-raising. Subprocess tests cover allocation failures, and release notes describe the 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
set_deallocation_stream and make_deallocation_stream no longer declare noexcept. Pointer-box creation invokes cleanup actions on failure and callers rethrow captured exceptions in allocation and IPC paths.
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/tests/test_allocation_failure.py, cuda_core/docs/source/release/1.3.0-notes.rst
Cython declarations use except+ nogil. Buffer._init resets its handle and re-raises MemoryError. Subprocess tests inject allocation failures in stream binding and Buffer.from_handle; release notes describe the behavior.

Priority: ➖ Normal

Change: Bug fix

Merge Risk: 🔵 Low · up to c07e4

A host allocation failure while mapping a graphics resource can leave it mapped and break a later map attempt; the tests also do not specifically exercise the new Buffer initialization cleanup. These are narrow, localized risks, so the PR remains mergeable with follow-up.

✨ Finishing Touches: 💡 2
⚔️ Resolve merge conflicts: 💡
  • Resolve merge conflict in branch handle-memory-exception
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@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
@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

Copy link
Copy Markdown
Contributor

@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.

This branch has not been deployed

No deployments
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant