Skip to content

[CUDA] Fix same-buffer copy abort on retained in-memory initializers (#29713) - #29792

Merged
Tianlei Wu (tianleiwu) merged 2 commits into
microsoft:mainfrom
TrishaWadhwani:gh29713-cuda-same-buffer-copy
Nov 5, 2026
Merged

Tianlei Wu (tianleiwu) merged 2 commits into
microsoft:mainfrom
TrishaWadhwani:gh29713-cuda-same-buffer-copy

Conversation

@TrishaWadhwani

Copy link
Copy Markdown

Description

Fixes #29713.

On the CUDA EP, a device copy scheduled for a weight retained as an in-memory-external OrtValue initializer can have a source and destination that are the same physical host buffer. The host->host branch of GPUDataTransfer::CopyTensor/CopyTensorAsync did not handle this:

  • Non-cuda-graph: ORT_ENFORCE(dst_data != src_data) fired -> hard failure.
  • CUDA-graph capture: the HOST_ACCESSIBLE cudaStreamSynchronize is illegal during capture -> CUDA 900 -> cudaStreamEndCapture CUDA 901, invalidating the capture.

This became reachable once #29158 (initializer retention) landed.

Fix

Early-return the no-op self-copy at the top of the host->host branch in both methods, before the HOST_ACCESSIBLE stream sync (the key to keeping capture valid). This matches CPUDataTransfer::CopyTensor and the existing GPU->GPU guard.

Testing

Adds a CUDA EP internal regression test (gpu_data_transfer_test.cc) covering sync, async, and cuda-graph-capture self-copies. Verified failing before the fix (ORT_ENFORCE / CUDA 900->901) and passing after, on an RTX 3050 with a CUDA 12.8 sm_86 build.

…icrosoft#29713)

On the CUDA EP, a copy scheduled for a retained in-memory-external OrtValue
initializer can have a source and destination that are the same physical host
buffer. The host->host branch of GPUDataTransfer::CopyTensor/CopyTensorAsync
failed ORT_ENFORCE(dst_data != src_data) on such a self-copy (hard abort), and
inside a CUDA-graph capture region its cudaStreamSynchronize was illegal
(CUDA 900 -> 901, invalidating capture).

Early-return the no-op self-copy at the top of the host->host branch in both
methods, before the HOST_ACCESSIBLE stream sync (the key to keeping capture
valid). This matches CPUDataTransfer::CopyTensor and the existing device->device
guard.

Adds a CUDA EP internal regression test (gpu_data_transfer_test.cc) covering the
sync, async, and cuda-graph capture cases.

Co-authored-by: Cursor <cursoragent@cursor.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@tianleiwu Tianlei Wu (tianleiwu) 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.

Nice, focused fix. The key correctness point is right: in CopyTensorAsync the self-copy early-returns before the HOST_ACCESSIBLE cudaStreamSynchronize, which is exactly what keeps a CUDA-graph capture valid (avoids 900 → 901), and it matches CPUDataTransfer::CopyTensor and the existing device→device dst_data != src_data guards. Since the pre-fix path ORT_ENFORCE-crashed on dst == src, there was never any working behavior to preserve, so this strictly removes a failure path with no regression to legitimate copies.

The regression test is thorough — it covers sync, async, and (most importantly) the async self-copy inside a capture region, asserting cudaStreamEndCapture == cudaSuccess.

Only two optional test-hardening nitpicks below; neither blocks. LGTM.

@TrishaWadhwani

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

… tests

Make the CreatePreferredAllocators() ordering assumption explicit by asserting the
pinned allocator is HOST_ACCESSIBLE (and the stream's GPU allocator is GPU/DEFAULT),
so the tests fail loudly instead of silently exercising the wrong branch if the
ordering ever changes. Also reword the tensor-size comment: these tests call
CopyTensor{,Async} directly and never go through the retention/threshold path, so the
size does not affect which branch is exercised.

Addresses review nitpicks from @tianleiwu on microsoft#29792.

Co-authored-by: Cursor <cursoragent@cursor.com>
@TrishaWadhwani

Copy link
Copy Markdown
Author

Thanks for the review, Tianlei Wu (@tianleiwu)! Addressed both nitpicks in 5919f6b:

Added explicit assertions that the pinned allocator is HOST_ACCESSIBLE (and the stream's GPU allocator is GPU/DEFAULT) in all three tests, so they fail loudly rather than silently exercising the wrong branch if CreatePreferredAllocators() ordering ever changes.
Reworded the size comment since these tests call CopyTensor{,Async} directly (not through the retention/threshold path), the tensor size doesn't affect which branch runs.
Verified locally: all three tests pass with the fix and fail without it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CUDA EP aborts on a same-buffer copy of a retained in-memory initializer (ORT_ENFORCE(dst_data != src_data); CUDA-graph variant 900/901)

4 participants