Skip to content

cuda.core: accept stream-captured memcpy nodes in MemcpyNode.update - #3029

Merged
Andy-Jost merged 2 commits into
NVIDIA:mainfrom
Andy-Jost:ajost/fix-2649-unified-memcpy-update
Oct 6, 2026
Merged

Andy-Jost merged 2 commits into
NVIDIA:mainfrom
Andy-Jost:ajost/fix-2649-unified-memcpy-update

Conversation

@Andy-Jost

Copy link
Copy Markdown
Contributor

Summary

MemcpyNode.update() raised NotImplementedError for every memcpy node produced by stream capture. The driver records both operands of a captured cuMemcpyAsync (Buffer.copy_from, Buffer.copy_to) with the unified memory type, and the descriptor check admitted only host and device operands. The error message blamed multidimensional, pitched, or array-backed copies, which did not describe the rejected node.

Changes

  • _is_supported_memcpy_descriptor accepts CU_MEMORYTYPE_UNIFIED for one-dimensional, unpitched, unoffset descriptors. The pitch and offset checks are unchanged, so multidimensional and array-backed nodes are still rejected with the existing message.
  • MemcpyNode.update() reads a unified operand from the device-pointer field. A replaced operand keeps the unified type and its new address goes into that field. The driver rejects a change of an operand's memory type in an executable update (cuGraphExecMemcpyNodeSetParams and cuGraphExecUpdate both compare the type of each operand), so keeping the type lets Graph.update() apply the change to an instantiated graph. Operands recorded as host or device memory are classified as before.
  • ExecutableMemcpyNode.update() reads the recorded descriptor of the node and keeps a unified operand unified for the same reason. Without this, the executable view rejected every node from stream capture with CUDA_ERROR_INVALID_VALUE.
  • MemcpyNode.__repr__ shows U for unified operands and A for array operands instead of D for every non-host type.
  • Tests: a captured copy accepts a size update; a source or destination replacement keeps the recorded types, and both a fresh instantiation and Graph.update() on an earlier one copy what the updated node describes; the executable view of a captured node accepts new operands and a new size. The existing rejection test for pitched descriptors is unchanged.
  • Release note under "Fixes and enhancements".

Related Work

Closes #2649. Multidimensional, pitched, and array-backed descriptors remain out of scope here and are tracked in #2420.

🤖 Generated with Claude Code

The driver records both operands of a captured cuMemcpyAsync with the
unified memory type. The descriptor check in MemcpyNode.update admitted
only host and device operands, so every memcpy node produced by stream
capture was rejected with a message about multidimensional copies.

Accept the unified type for one-dimensional descriptors and read such
operands from the device-pointer field. A replaced operand keeps the
unified type, in the definition node and in the executable view, because
the driver rejects a change of an operand's memory type in an executable
update; Graph.update therefore applies the change as well. The node repr
shows U for unified operands.

Closes NVIDIA#2649

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Andy-Jost Andy-Jost added this to the cuda.core 1.3.0 milestone Oct 6, 2026
@Andy-Jost Andy-Jost added bug Something isn't working P1 Medium priority - Should do cuda.core Everything related to the cuda.core module labels Oct 6, 2026
@copy-pr-bot

copy-pr-bot Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@Andy-Jost Andy-Jost self-assigned this Oct 6, 2026
@Andy-Jost

Copy link
Copy Markdown
Contributor Author

/ok to test fb71159

@github-actions

github-actions Bot commented Oct 6, 2026 •

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

The update() docstrings on MemcpyNode and ExecutableMemcpyNode now say
only what a caller needs: nodes recorded by stream capture are supported.
The driver's memory-type bookkeeping stays in code comments.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Andy-Jost
Andy-Jost marked this pull request as ready for review October 6, 2026 14:39
@Andy-Jost
Andy-Jost requested a review from juenglin October 6, 2026 14:39
@juenglin

juenglin commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

/ok to test 46ca8ac

@juenglin

juenglin commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Reviewed at 46ca8ac; built it locally and ran tests/graph (531 passed, 23 skipped on an RTX 6000 Ada). The core logic looks right to me. Three small follow-ups, in order of importance:

1. The release note overstates what Graph.update() accepts.

The note says a replaced operand stays unified because the driver rejects a memory-type change, "so Graph.update() applies the change as well." That holds when the replacement is a device buffer. It does not hold when the replacement is a host buffer. I captured a device-to-device copy and replaced src with a pinned host buffer, then with a pageable host pointer (ctypes array):

Step Pinned host Pageable host
MemcpyNode.update(src=...), then a fresh instantiate() works, both types stay U, copy is correct works, both types stay U, copy is correct
Graph.update() on an earlier instantiation fails, CU_GRAPH_EXEC_UPDATE_ERROR_PARAMETERS_CHANGED same
ExecutableMemcpyNode.update(src=...) fails, CUDA_ERROR_INVALID_VALUE same

This is not caused by this PR. A control with an ordinary GraphDefinition().memcpy() node (recorded as DEVICE, no capture) fails the same way when src is swapped from a device buffer to a pinned host buffer. That matches the driver docs for executable updates: the operand's device allocation or mapping cannot change, and changing the memory type is unsupported. Suggest narrowing the sentence so it doesn't imply every replacement can be applied to an instantiated graph, for example "replacing a device operand with another device buffer".

2. (Optional) Add a test that documents that boundary.

No test replaces a captured node's operand with a host buffer. A short one would capture a device-to-device copy, call update(src=<pinned host buffer>), check the copy after a fresh instantiate(), and check that Graph.update() and the executable view raise. It would pin down the behaviour in item 1 and show it is a driver restriction. This is new coverage for the feature, not a regression guard, because captured nodes raised NotImplementedError before this PR.

3. Add a comment in ExecutableMemcpyNode.update().

It reads the recorded memory types from the definition node (cuGraphMemcpyNodeGetParams on self._h_node), not from the instantiated graph. That is correct only while MemcpyNode.update() preserves unified types, which this PR guarantees. A one-line comment would record the dependency, so a later change to the definition-side update doesn't silently break the executable view.

Minor nits, no action needed: the three new tests unpack builder but never use it (it only keeps the captured graph alive, so a comment or del would show that), and the driver docs disallow CU_MEMORYTYPE_UNIFIED for managed-memory operands when any device reports CONCURRENT_MANAGED_ACCESS == 0. I did not test that case.

@Andy-Jost
Andy-Jost enabled auto-merge (squash) October 6, 2026 17:31
@Andy-Jost
Andy-Jost merged commit 4c893de into NVIDIA:main Oct 6, 2026
224 of 226 checks passed
@Andy-Jost
Andy-Jost deleted the ajost/fix-2649-unified-memcpy-update branch October 6, 2026 18:02
github-actions Bot pushed a commit that referenced this pull request Oct 7, 2026
Removed preview folders for the following PRs:
- PR #3022
- PR #3023
- PR #3027
- PR #3029
- PR #3036
- PR #3039
Andy-Jost added a commit that referenced this pull request Oct 8, 2026
…erand boundary (#3035)

* cuda.core: narrow the memcpy update release note and test the host-operand boundary

Follow-up to #3029. The release note said Graph.update() applies any
replaced operand of a captured memcpy node; the driver accepts only a
device-to-device replacement in an executable update, and a replacement
with host memory takes effect in a new instantiation. A test pins that
boundary, and ExecutableMemcpyNode.update() records why it may read the
memory types from the definition node.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* cuda.core: allow either executable-update outcome for a host operand

CI showed that Windows TCC drivers accept a host buffer as the replacement
operand of a captured memcpy node in an executable update, while Linux
drivers reject it. The test now accepts either outcome and checks the copy
when the update is accepted, and the release note says the driver decides.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* cuda.core: say what decides whether a host operand update is accepted

A diagnostic run across Linux, Windows MCDM, and Windows TCC rows showed
that the executable-update outcome follows the allocation kind, not the
platform: the driver compares the memory class of pool-backed and
virtual-memory operands and rejects a replacement by host memory or by a
cuMemAlloc buffer, while plain cuMemAlloc operands accept it. The TCC
runners report no memory-pool support, so the default memory resource is
not pool-backed there, which is why the update was accepted on them. The
release note and the test docstring now say so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(cuda.core): split the host-operand update test by allocation kind

Pool-backed operands go through the driver's memory-class comparison, so
that case now requires the PARAMETERS_CHANGED rejection. cuMemAlloc
operands skip the comparison and the driver may accept the change, so
that case keeps the lenient check and verifies the copy when accepted.
A device without memory-pool support runs only the cuMemAlloc case.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(cuda.core): instantiate before changing the captured node

The executable node update ran against a graph instantiated after the
definition node already pointed at host memory, so it restated the
operand and tested nothing. Both instantiations now precede the change.
Pool-backed operands require CUDA_ERROR_INVALID_VALUE from the node
update, the code cuGraphExecMemcpyNodeSetParams reports for the same
memory-class rejection. cuMemAlloc operands check the code when the
driver rejects.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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.

[BUG]: MemcpyNode.update() rejects stream-captured memcpy nodes

2 participants