Skip to content

Stop ignoring a failed cudaSetDevice in CudaAllocator - #22093

Merged
shoumikhin merged 2 commits into
mainfrom
fix-cuda-setdevice-unchecked
Aug 24, 2026
Merged

Stop ignoring a failed cudaSetDevice in CudaAllocator#22093
shoumikhin merged 2 commits into
mainfrom
fix-cuda-setdevice-unchecked

Conversation

@shoumikhin

@shoumikhin shoumikhin commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

CudaAllocator switches the current CUDA device before it allocates, frees or
copies, and switches back afterwards. The result of that switch was thrown away
in all three places:

(void)cudaSetDevice(index);

When the switch fails, for example because the requested device index does not
exist on this machine, the work still went ahead on whatever device happened to
be current. allocate then returned success with a pointer that lives on a
different device than the caller asked for. The caller stores the requested
index next to that pointer, so the pointer and its recorded device disagree
from then on, and the mistake surfaces much later as a wrong result or an
unrelated CUDA error somewhere else.

On a machine with one GPU:

auto r = CudaAllocator::instance().allocate(1024, /*index=*/1);
// before: r.ok() == true, and the pointer is really on device 0
// after:  r.ok() == false, r.error() == Error::Internal

What changes:

  • allocate logs the CUDA error and returns Error::Internal instead of
    allocating on the wrong device. A device that cannot be selected is not an
    out-of-memory condition, and Error::Internal is what Exception.h maps
    every CUDA error to and what cuda_mutable_state.cpp already returns for
    this same call.
  • The host to device and device to host copy helpers log the CUDA error and
    return Error::Internal instead of copying against the wrong device.
  • All three paths also fail when cudaGetDevice fails, instead of carrying on
    without switching. Without the current device there is no way to switch to the
    requested one or restore afterwards, so the work would run against whatever
    device happened to be current. This is the branch the tests cannot reach: on a
    working CUDA host cudaGetDevice does not fail, so it is verified by
    inspection rather than by the new tests.
  • deallocate returns void, and cudaFree accepts a pointer from any device
    under unified addressing, so it logs the error and still frees the pointer
    rather than leaking it. The failure is no longer silent.

Only the switch to the requested device is checked. The switch back to the
previous device stays best effort, because there is nothing useful to do if
restoring fails.

Test plan

Three new tests in backends/cuda/runtime/test/test_cuda_allocator.cpp ask for
device index device_count, which is one past the last valid ordinal on any
machine, so the switch always fails:

  • AllocateOnMissingDeviceFails
  • CopyHostToDeviceOnMissingDeviceFails
  • CopyDeviceToHostOnMissingDeviceFails

DeviceIndex is int8_t, so each of the three first checks that one-past-the-last
ordinal still fits in it: at 128 or more visible GPUs the count wraps to a negative
index, or at 256 back onto real device 0, and neither is a valid absent ordinal. The
check is per-test rather than in SetUp so it does not also skip the six device-0
tests. 127 devices still runs; 128 skips.

Built and ran the suite on one NVIDIA H100.

With this change:

[  PASSED  ] 9 tests.

Confirmed the tests really catch the bug. With the allocator reverted to the
old code and the same three tests:

[  FAILED  ] CudaAllocatorTest.AllocateOnMissingDeviceFails
[  FAILED  ] CudaAllocatorTest.CopyHostToDeviceOnMissingDeviceFails
[  FAILED  ] CudaAllocatorTest.CopyDeviceToHostOnMissingDeviceFails

The allocate failure shows the old behavior directly: the call reported success
for a device that does not exist.

The message the fixed build logs:

CudaAllocator::allocate: cudaSetDevice(1) failed: invalid device ordinal

The six tests already in that file still pass, so the change does not disturb
the normal single device path.

clang-format reports no changes needed on either touched file.

Not covered

The cudaGetDevice failure branch is not covered by these tests. On a working CUDA
host that call does not fail, and there is no injection point in the suite, so
reverting just that branch leaves all three tests passing. Reaching it would need a
function-pointer seam or an injected fake CUDA runtime, both larger than the guard
they would protect.

deallocate only logs, so no test asserts on it. Everything here ran on a
single GPU machine, so the switch always failed with an invalid ordinal. A
device that exists but is unavailable, for example one in exclusive compute
mode, takes the same code path but was not exercised.

Copilot AI lite review requested due to automatic review settings August 24, 2026 17:39
@pytorch-bot

pytorch-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/22093

Note: Links to docs will display an error until the docs builds have been completed.

⏳ No Failures, 182 Pending

As of commit afa82ae with merge base 368a849 (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 24, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

Copilot AI review requested due to automatic review settings August 24, 2026 21:40

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@linux-foundation-easycla

linux-foundation-easycla Bot commented Aug 24, 2026

Copy link
Copy Markdown

CLA Missing ID

  • ✅ login: shoumikhin / name: Anthony Shoumikhin (bb5339d)
  • ❌ The email address for the commit (afa82ae) is not linked to the GitHub account, preventing the EasyCLA check. Consult this Help Article and GitHub Help to resolve. (To view the commit's email address, add .patch at the end of this PR page's URL.) For further assistance with EasyCLA, please visit our EasyCLA portal and chat with our support bot.

shoumikhin and others added 2 commits August 24, 2026 15:57
CudaAllocator switches the current CUDA device before it allocates,
frees or copies, and switches back afterwards. The result of that switch
was thrown away in all three places.

When the switch fails, for example because the requested device index
does not exist on this machine, the work still went ahead on whatever
device happened to be current. allocate() then returned success with a
pointer that lives on a different device than the caller asked for. The
caller stores the requested index next to that pointer, so the pointer
and its recorded device disagree from then on, and the mistake only
shows up much later as a wrong result or an unrelated CUDA error.

Now allocate() and the copy helpers log the CUDA error and return
Error::MemoryAllocationFailed and Error::Internal instead of going ahead.
deallocate() returns void and cudaFree works on a pointer from any
device under unified addressing, so it logs the error and still frees
rather than leaking.

Example, on a machine with one GPU:

```
auto r = CudaAllocator::instance().allocate(1024, /*index=*/1);
// before: r.ok() == true, pointer actually on device 0
// after:  r.ok() == false, r.error() == Error::MemoryAllocationFailed
```

Test plan:
Added three tests to test_cuda_allocator.cpp that ask for device index
device_count, which is one past the last valid ordinal on any machine,
so the switch always fails:
  AllocateOnMissingDeviceFails
  CopyHostToDeviceOnMissingDeviceFails
  CopyDeviceToHostOnMissingDeviceFails

Built and ran the suite on one NVIDIA H100.

With this change:
  [  PASSED  ] 9 tests.

With the allocator reverted to the old code and the same three tests:
  [  FAILED  ] CudaAllocatorTest.AllocateOnMissingDeviceFails
  [  FAILED  ] CudaAllocatorTest.CopyHostToDeviceOnMissingDeviceFails
  [  FAILED  ] CudaAllocatorTest.CopyDeviceToHostOnMissingDeviceFails
The allocate failure showed the old behavior directly: the call reported
success for a device that does not exist.

The logged message on the fixed build:
  CudaAllocator::allocate: cudaSetDevice(1) failed: invalid device ordinal

clang-format reports no changes needed on either touched file.

Not covered: the deallocate path only logs, so no test asserts on it.
…e tests

cudaSetDevice and cudaGetDevice failures were reported as MemoryAllocationFailed
and InvalidArgument. Neither fits: a device that cannot be selected is not out of
memory, and a well-formed ordinal that does not exist is an operational failure
rather than a bad argument. Use Internal, which is what Exception.h maps every
CUDA error to and what cuda_mutable_state.cpp already returns for this same call.

Also fail instead of continuing when cudaGetDevice fails, since without the
current device there is no way to switch or restore, and the allocation would
land on whatever device happened to be current.

The three tests that need a valid-but-absent ordinal now check that one exists in
DeviceIndex (int8_t) rather than gating in SetUp, which skipped the six device-0
tests too. The bound is <= max, so 127 runs and 128 skips.
Copilot AI review requested due to automatic review settings August 24, 2026 22:58
@shoumikhin
shoumikhin force-pushed the fix-cuda-setdevice-unchecked branch from b2fd544 to afa82ae Compare August 24, 2026 22:58

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin
shoumikhin merged commit 1e716a6 into main Aug 24, 2026
233 of 236 checks passed
@shoumikhin
shoumikhin deleted the fix-cuda-setdevice-unchecked branch August 24, 2026 23:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants