Skip to content

[WebGPU] Expose safe graph capture I/O in Python - #32074

Merged
Ananya Anand (4n4ny4) merged 21 commits into
microsoft:mainfrom
4n4ny4:webgpu-python-graph-capture-io
Sep 2, 2026
Merged

Ananya Anand (4n4ny4) merged 21 commits into
microsoft:mainfrom
4n4ny4:webgpu-python-graph-capture-io

Conversation

@4n4ny4

@4n4ny4 Ananya Anand (4n4ny4) commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Description

Adds a safe way to use WebGPU graph capture from Python. Replay reuses the exact buffers recorded at capture, and Python had no way to allocate those buffers, update them, or release a captured graph so anything you tried either failed or quietly returned stale results. This adds session-owned WebGPU OrtValues (plus support for ones from a shared allocator, which have no session), in-place updates, and release_captured_graph(id), and the first capturing run now pins its IOBinding so rebinding is rejected instead of silently writing to the original buffers. There's also one C++ change: WebGPU's CanCopy accepted any GPU device pair without checking vendor ID, so in a session with WebGPU and CUDA registered a CUDA copy would get routed to WebGPU and handed to Dawn as a WGPUBuffer. WebGPU now refuses GPU buffers from other vendors

Motivation and Context

Graph capture already exists in the WebGPU EP, but there was no supported way to reach it from Python. You could turn the session option on, but without fixed device buffers the results were wrong without raising an error — replay just kept using whatever was bound the first time.

On an NVIDIA TITAN V, capture takes YOLO26n from 5.885 ms to 5.475 ms p50, about 7%. Both numbers come from the script in this PR using its defaults, 5 fresh processes per arm with the arms alternating.

Tested locally against a WebGPU build on the TITAN V: 80 passed, 2 skipped (no onnx installed), 3 failures that are pre-existing and unrelated (missing LoRA test data, custom-ops library not built).

Ananya Anand and others added 3 commits August 13, 2026 15:23
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

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

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.

Pull request overview

This PR exposes a safe Python workflow for WebGPU graph capture/replay by introducing session-owned device OrtValue allocation, enforcing session provenance for session-scoped buffers, and restricting unsafe I/O patterns (raw pointers, DLPack export, cross-session use, and output rebinding) that can break captured replay semantics.

Changes:

  • Add session-owned OrtValue creation/update APIs and a Python-facing release_captured_graph() workflow to manage captured resources safely.
  • Enforce provenance/lifetime rules for session-scoped and WebGPU OrtValues across run, run_async, run_with_ort_values, IOBinding, and OrtValueVector usage patterns.
  • Add focused Python tests plus a reusable WebGPU graph-capture benchmark script demonstrating fixed device I/O and capture/replay.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
onnxruntime/test/python/webgpu_graph_capture_benchmark.py Adds a standalone benchmark demonstrating fixed WebGPU I/O binding, capture/replay, in-place updates, and explicit output readback.
onnxruntime/test/python/onnxruntime_test_python.py Adds unit tests covering session-scoped OrtValue semantics and WebGPU graph-capture safety restrictions.
onnxruntime/python/onnxruntime_pybind_state.cc Adds pybind methods for session-owned OrtValue allocation/update plus WebGPU graph-capture status/release plumbing.
onnxruntime/python/onnxruntime_pybind_ortvalue.cc Blocks unsafe WebGPU operations (data_ptr, numpy, DLPack) and prevents putting WebGPU OrtValues into raw OrtValueVector.
onnxruntime/python/onnxruntime_pybind_iobinding.cc Ensures WebGPU binding uses the correct allocator name and keeps the session alive for SessionIOBinding.
onnxruntime/python/onnxruntime_inference_collection.py Adds Python APIs for session-owned OrtValues, graph-capture release, and provenance validation/enforcement in Session, IOBinding, and OrtValue.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@qjia7 Jiajia Qin (qjia7) 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.

Review frame

  • Review contract: Reviewed PR 32074 at exact head a30855282ecb8da56d8d6cdd4cb3b74165f0575a against its recorded base 3d3cfa6c004551ee31d3acff6d01fb572712ca91, including all six changed files, issue discussion, reviews, inline discussion, and head CI. GitHub reports only the passing license/cla check.
  • Problem/feature validity: Validated. WebGPU replay retains the bind groups/buffer handles recorded during capture, while the pre-PR Python allocation path does not obtain the allocator from a particular session. Python therefore needs a session-scoped way to allocate and update fixed WebGPU inputs/outputs and to release captured resources.
  • Risk/scope: Deep. This adds public Python APIs and changes allocator, data-transfer, I/O-binding, graph-ID, and object-lifetime behavior across Python and C++.
  • Direction gate: Pass, with incomplete enforcement. Session allocator lookup plus Python-side session provenance is the right ownership direction and is consistent with InferenceSession::GetAllocator, SessionIOBinding, and the WebGPU EP's per-session context. The owner solution must additionally associate each captured graph ID with immutable I/O buffer identities until release; C1 is the missing part of that safety boundary. The implementation also has the compatibility and transfer-routing gaps below.

Confirmed findings

C1 [P2]: Reject a different or mutated I/O binding while a graph ID is captured

Suggested inline location: onnxruntime/python/onnxruntime_inference_collection.py:534

Suggested comment:

[P2] Please associate each captured graph ID with the IOBinding and fixed OrtValues used for capture, and reject a different/rebound/cleared binding until release_captured_graph(id) succeeds. This check currently establishes only that the binding belongs to the same session. Once graph 0 is captured, InferenceSession::RunImpl replays graph 0 by ID and WebGPU's ReplayGraph submits the previously captured bind groups; it never consults the buffers in the newly supplied binding. The new test demonstrates the resulting silent failure at lines 2069-2079: running alternate_io_binding updates the original output and leaves the alternate output zero. Clearing the original binding before release can additionally return those buffers to WebGPU's cache while the captured bind group still identifies them. This contradicts the PR's claim that unsafe buffer-rebinding operations are rejected. Please enforce the lifecycle in the API (or expose a capture object that owns the fixed binding), and change this test to expect rejection rather than stale output.

Evidence and attribution: The PR exposes the previously unavailable session-owned buffers and advertises a safe Python workflow. Session.run_with_iobinding adds only a session-identity check. The PR's own test proves that another same-session binding is accepted and silently ignored by replay.

S1 [P2]: Preserve the documented per-run gpu_graph_id=-1 escape hatch

Suggested inline location: onnxruntime/python/onnxruntime_inference_collection.py:323

Suggested comment:

[P2] Please make this validation depend on the effective gpu_graph_id instead of rejecting every run whenever the session option enables capture. Core defines kGraphAnnotationSkip = -1, and CachedExecutionProviderForGraphReplay::AllowGraphCaptureOnRun() returns false for that ID; WebGPU's OnRunStart likewise does not create a per-graph manager or begin capture for -1. Such a run is therefore the supported non-capturing path and transient feeds are safe. Before this PR, Python callers could use it, but run, run_async, and run_with_ort_values now reject it before C++ sees the run options, and the bind-time IOBinding checks also make a skipped run with ordinary bindings impossible. Please allow gpu_graph_id=-1, defer fixed-buffer validation until run_with_iobinding where the run options are known, and add regression coverage for both convenience-run and IOBinding skip paths.

Evidence and attribution: InferenceSession::CachedExecutionProviderForGraphReplay explicitly reserves -1 as kGraphAnnotationSkip, and the recursive capture/replay path is guarded by AllowGraphCaptureOnRun. The unconditional Python guard is added by this PR and removes that existing behavior.

C2 [P2]: Do not let the session-wide transfer search route CUDA buffers through WebGPU

Suggested inline location: onnxruntime/python/onnxruntime_pybind_state.cc:144

Suggested comment:

[P2] Please ensure this session-scoped copy selects the transfer implementation for the tensors' actual provider. These new creation/update APIs are generic (the CPU path is tested and device_type="cuda" is accepted), but DataTransferManager::CopyTensor chooses the first registered transfer whose CanCopy returns true. The built-in WebGPU DataTransfer::CanCopy accepts every CPU/GPU or GPU/GPU pair without checking vendor ID. In a session registered as [WebGpuExecutionProvider, CUDAExecutionProvider], updating a session-created CUDA OrtValue therefore selects WebGPU first; DataTransferImpl::CopyTensor casts the CUDA allocation to WGPUBuffer, and BufferManager::Upload passes it to wgpuBufferGetMappedRange. The plugin WebGPU transfer already avoids this by requiring vendor ID 0. Please mirror that vendor/device check in the built-in transfer (or restrict this API to locations it can select unambiguously) and add a mixed-provider regression test.

Evidence and attribution: The PR's CopySessionOrtValue rejects only GPU-to-GPU pairs where exactly one memory-info name is WEBGPU_BUFFER; CPU-to-CUDA and CUDA-to-CUDA take the unrestricted session-wide search. The unsafe WebGPU cast is a direct consequence. The PR newly exposes this path for arbitrary session allocators and routes all session-scoped updates through it.

Clarifications

Q1: The reported 8.9% speedup is not reproducible from the added benchmark

webgpu_graph_capture_benchmark.py unconditionally enables graph capture at line 53 and has no baseline mode, so the same script cannot produce the stated 5.506 ms non-capture result. Please provide the exact baseline/capture commands and raw result JSON (including backend and input-copy mode), or add a capture toggle so the comparison uses identical fixed I/O, readback, warmup, and timing boundaries.

Test coverage

  • C1: The WebGPU test exercises the wrong-binding case, but asserts the silent stale-output behavior instead of enforcing the advertised safety invariant.
  • S1: No test covers gpu_graph_id=-1 on a graph-enabled WebGPU session.
  • C2: No test covers a session with WebGPU followed by another GPU provider.
  • The positive WebGPU test is skipped when the EP or adapter is unavailable. At review time GitHub reports only license/cla; there is no WebGPU-enabled build/test result attached to the PR.
  • Local static verification: both changed Python modules compile with Python's compile() and git diff --check is clean. I did not claim runtime or C++ build success because the available local build does not contain a PR-compatible Python WebGPU extension.

Verdict

The feature is valid and the session-provenance design direction is appropriate. C1 and C2 block the advertised safe workflow because accepted calls can silently target old buffers or pass a foreign GPU handle to Dawn. S1 is an existing graph-control compatibility regression and should also be fixed before merge. Q1 remains a performance-evidence clarification. I found no cleanup-only items worth posting.

Comment thread onnxruntime/python/onnxruntime_inference_collection.py Outdated
Ananya Anand and others added 4 commits August 18, 2026 14:47
A WebGPU OrtValue can be created from an environment-registered shared
allocator instead of a session allocator. `_is_webgpu_buffer` is derived purely
from the tensor's allocator name (`Location().name == WEBGPU_BUFFER`) and
carries no session information, and the shared-allocator factories
(`OrtValue.ortvalue_from_shape_and_type` -> `GetSharedAllocator`, and the
`memory_info` overload -> `GetRegisteredSharedAllocator`) never attach a session
to the Python wrapper. Such a value therefore arrives with
`_is_webgpu_buffer=True` and `_session=None`, so treating "is a WebGPU buffer"
as "belongs to this session" is unsound by construction rather than merely
strict.

That state is also the default rather than an exotic one: the environment
registers a shared allocator for every EP device publishing a
`device_memory_info`, with no user call required. This change is what makes it
reachable, because it adds the `"webgpu"` alias to
`get_vendor_id_for_device_type`, which is exactly what makes
`OrtDevice.make("webgpu", 0)` resolve to the shared allocator's lookup key --
and then rejected every OrtValue produced that way.

Key off a *different* owning session instead, matching what the non-WebGPU
branch directly below already did:

- `_validate_ortvalue_ownership` rejects a foreign session first, then steers
  WebGPU values to IOBinding regardless of ownership. Drops the `allow_webgpu`
  parameter: all five call sites (and every call site on this branch) invoked it
  without the argument, so it was always `False` and its `if not allow_webgpu:
  raise` branch was unconditional in practice. No caller relied on it and the
  observable semantics are unchanged -- WebGPU buffers are still always rejected
  on the plain `run()` paths and must go through IOBinding.
- `bind_ortvalue_input`/`bind_ortvalue_output` collapse three checks into two;
  the generic cross-session check subsumes the WebGPU-specific one.
- Graph capture still requires a WebGPU device buffer but no longer requires a
  session to own it. Capture replays against the original buffers, so a bound
  value must stay alive at a fixed address for the captured graph's lifetime --
  and that is a property of the OrtValue rather than of its allocator. `Tensor`
  owns an `AllocatorPtr` to the allocator that frees it and only frees in
  `~Tensor`, and for tensor-backed buffers `BufferManager::Release` is reachable
  only through `GpuBufferAllocator::Free` (its only other caller returns an
  internal uniform buffer, never tensor memory), so while the OrtValue is alive
  the handle cannot be released, recycled, or reassigned; `p_data_` is never
  rewritten, so it cannot relocate; and `WebGpuContext::Replay` re-issues the
  bind groups recorded at capture rather than re-resolving a buffer from an
  allocator. Each of those is provenance-independent, and for the default device
  both allocators resolve to the same context buffer manager anyway. What
  neither provenance protects is releasing a bound value while a captured graph
  still references it: `_session` pins the session, not the buffer, so the
  strict check never provided the protection an asymmetry would have implied.

This deliberately extends past the reviewed guard into `update_inplace`, which
had the same defect and refused a session-less WebGPU destination outright.
Without that, a shared buffer could be bound but never filled and the
relaxation would be inert. It now routes through whichever session owns either
side and falls back to the shared-allocator data transfer path when neither
does.

Source contiguity normalization is also hoisted above the numpy dispatch. It
previously ran only in the session-owned branch, so the session-less path
handed a possibly non-contiguous array to a copy that reads it as contiguous --
silently wrong data, no error. That bug is pre-existing and already reachable
for CPU OrtValues; relaxing the WebGPU guards increases its reachability rather
than introducing it, so it is fixed here alongside.

Error messages drop the now-inaccurate "session-scoped" qualifier.

Tests: add `test_device_ortvalue_provenance_rules`, a hermetic CPU-only matrix
over (no owner / this session / another session) x (device / non-device) that
runs in CI without a GPU, since every WebGPU Python test currently skips there.
It asserts the distinct rejection messages in both directions, so a session-less
device buffer is provably rejected for needing IOBinding and never for
belonging to a session that did not create it. Flip the WebGPU test assertions
that required session ownership, and add `gpu_graph_id=-1` coverage showing that
an opted-out run tracks the current input rather than replaying.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
C1: pin the captured I/O binding per graph annotation ID.

Replay re-issues the bind groups recorded at capture and never consults a
newly supplied binding, so `run_with_iobinding` accepting a different or
rebound binding silently targeted the original buffers. The first
non-skipped run for a `gpu_graph_id` now pins the IOBinding and the exact
OrtValues bound to it; a different IOBinding, a changed buffer set, a
rebind, or a clear is rejected until `release_captured_graph(id)` succeeds,
which unpins it. The pin holds strong references to the bound OrtValues, so
a captured buffer also cannot be freed and recycled into the WebGPU buffer
cache while the captured graph still names it. WebGPU buffer handles are
opaque and deliberately do not expose `data_ptr`, so buffer identity is the
OrtValue object itself.

S1: honor the documented per-run `gpu_graph_id=-1` escape hatch.

Core reserves -1 as `InferenceSession::kGraphAnnotationSkip`,
`AllowGraphCaptureOnRun()` returns false for it, and the WebGPU EP neither
creates a per-graph buffer manager nor begins capture, so such a run is the
supported non-capturing path and transient feeds are safe. `run`,
`run_async` and `run_with_ort_values` rejected every run whenever the
session option enabled capture, removing graph control Python callers had
before. Validation now depends on the effective ID. The bind-time fixed
buffer checks are likewise deferred to `run_with_iobinding`, where the run
options are known, so a skipped run works with ordinary bindings; a
capturing run still requires fixed WebGPU device OrtValues.

C2: do not let the session-wide transfer search route foreign GPU buffers
through WebGPU.

`DataTransferManager::CopyTensor` picks the first registered transfer whose
`CanCopy` accepts the pair, and the built-in WebGPU transfer accepted every
CPU/GPU and GPU/GPU pair without checking the vendor. In a session
registered as [WebGpuExecutionProvider, CUDAExecutionProvider] a CUDA copy
therefore selected WebGPU, which reinterpreted the CUDA allocation as a
`WGPUBuffer` and handed that foreign handle to Dawn. WebGPU allocations
always carry `VendorIds::NONE`, so refuse GPU devices with any other vendor
ID, matching the guard the plugin data transfer already applies.

Q1: make the benchmark able to produce its own baseline.

`webgpu_graph_capture_benchmark.py` hard-enabled graph capture, so the
script could not produce the non-capture arm it was being compared against.
Add `--graph-capture` / `--no-graph-capture` (capture stays the default) so
both arms come from identical fixed I/O, readback, warmup and timing
boundaries, and record the mode in the result JSON.

Tests: `test_graph_annotation_id_run_option` covers the effective-ID parsing
hermetically on CPU, which matters because every WebGPU Python test skips on
current CI agents; it also caught that `RunOptions.get_run_config_entry`
raises rather than returning a default for an unset key, which would have
broken every run that passed a RunOptions without `gpu_graph_id`. The WebGPU
test now asserts that a different binding is rejected instead of asserting
the silent stale-output behavior, and covers the skip path through both
`run_with_iobinding` and the convenience `run` API.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
_GRAPH_ANNOTATION_SKIP mirrors InferenceSession::kGraphAnnotationSkip.
Nothing enforces that the two stay in sync, so record the linkage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

@qjia7 Jiajia Qin (qjia7) 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.

Review frame

  • Review contract: Re-audited PR 32074 at exact head 48a2b3c66ae1385424f2c08fd8ea2d8f13d407d2. There is no new code range beyond the prior re-review against a30855282ecb8da56d8d6cdd4cb3b74165f0575a. Current PR text, discussion, reviews, inline comments, and CI were fetched again. All visible required jobs have completed successfully, including Linux WebGPU, external-Dawn, static/plugin, and macOS WebGPU builds/tests.
  • Problem/feature validity: Validated. WebGPU replay retains the bind groups and buffer handles recorded during capture. Python needs fixed WebGPU buffers whose lifetime spans capture/replay, in-place updates, explicit readback, and captured-graph release.
  • Risk/scope: Deep. The PR adds public Python APIs and graph-ID state, allocator/data-transfer routing, IOBinding lifetime enforcement, provider-reset interactions, and support for WebGPU allocator-backed values.
  • Direction gate: Pass for graph-ID/IOBinding pinning, but fail for the additional session-owned OrtValue abstraction. The existing OrtValue factories and copy_tensors API already map to the environment-registered WebGPU shared allocator and transfer. Without a custom external device, those and the session allocator all resolve to WebGPU context 0, so graph capture does not require parallel session-specific allocation/copy APIs or Python ownership rules. Supporting shared allocators with a custom external device requires a separate C++ change and is deferred as an explicit TODO.

Prior issue resolution

  • Prior C1 (different/rebound IOBinding): Resolved for a stable session. The first successful capturing run records the IOBinding/signature, holds the bound OrtValues, rejects rebinding/clearing and different bindings, and unpins only after native release succeeds.
  • Prior S1 (gpu_graph_id=-1): Resolved. All convenience run APIs now inspect the effective graph ID, and IOBinding validation is deferred until run_with_iobinding, so skipped runs can use transient bindings.
  • Prior C2 (CUDA handle routed through WebGPU): The unsafe path is fixed: built-in WebGPU CanCopy now rejects non-NONE GPU vendors before DataTransferManager can select it. T1 records the remaining regression-test gap.
  • Prior Q1 (benchmark baseline): Resolved. The benchmark now supports matched --graph-capture and --no-graph-capture arms and records the selected mode.
  • Shared-allocator review comment: The existing shared allocator is the appropriate default-context path. Shared allocation for custom external devices remains a C++ follow-up because the shared allocator currently always resolves through DefaultContext().

Confirmed findings

C1 [P2]: Clear or reject captured state before replacing the session

Suggested inline location: onnxruntime/python/onnxruntime_inference_collection.py:307

Suggested comment:

[P2] Please integrate _captured_graph_bindings with set_providers() before replacing _sess. After graph 0 is captured, this method destroys/replaces the owning Python session without removing graph 0 from _captured_graph_bindings or from the old IOBinding's _pinned_graph_ids. A new IOBinding from the replacement session is then rejected as "captured with a different IOBinding", even though the new native session has no captured graph. Calling release_captured_graph(0) afterward releases the new session (where graph 0 does not exist) and unpins the old binding while its old SessionIOBinding can still keep the old native session alive. Please either reject set_providers() until every captured graph is explicitly released, or release each graph through the old session and clear both sides of the Python bookkeeping before _reset_session(). Add a capture -> set_providers() -> new capture regression test.

Evidence and attribution: set_providers() directly calls _reset_session(). The new _captured_graph_bindings map is initialized only in Session.__init__, is populated by run_with_iobinding(), and is otherwise removed only by release_captured_graph(). SessionIOBinding uses py::keep_alive<1, 2>, so an old IOBinding can retain the old native session after the Python session wrapper is reset. This stale cross-session state is introduced by the new pinning implementation.

D1 [P2]: Reuse the existing OrtValue allocation and copy APIs

Suggested inline location: onnxruntime/python/onnxruntime_inference_collection.py:489

Suggested comment:

[P2] Please use the existing OrtValue.ortvalue_from_shape_and_type / ortvalue_from_numpy factories and copy_tensors path instead of adding parallel session-specific allocation and copy APIs. CUDA graph capture already uses those APIs. For WebGPU without a custom external device, the environment-registered shared allocator, its data transfer, and session allocators all resolve through WebGpuContextFactory context 0, so their WGPUBuffers belong to the same Dawn device even when the allocator instances differ. The missing work is to recognize "webgpu" as GPU/vendor NONE with allocator name WEBGPU_BUFFER, not to add Session.create_ortvalue_*, InferenceSession.update_ortvalue_inplace, Python _session provenance, and blanket restrictions on copy_tensors and vector APIs. Please keep the existing allocation/update surface, add only the WebGPU mapping and capture-lifetime enforcement, and adapt onnxruntime/test/python/onnxruntime_test_python_cudagraph.py into a shared CUDA/WebGPU graph-capture test, renaming it to onnxruntime_test_python_graphcapture.py and updating tools/ci_build/build.py to run it for both CUDA and WebGPU builds. Custom-external-device shared allocation should remain an explicit C++ TODO: the shared allocator/data transfer must retain and use that external device instead of DefaultContext() before that configuration is supported.

Evidence and attribution: OrtApi::GetSharedAllocator, CreateTensorAsOrtValue, and CopyTensors are the existing public native contracts, and the Python OrtValue factories/update methods already expose that workflow. The WebGPU factory registers a shared allocator and matching transfer backed by DefaultContext(). The new session methods duplicate those operations and directly expose InferenceSession::GetDataTransferManager(), for which there is no corresponding public session-copy API. Their Python ownership model then requires new restrictions throughout run APIs, IOBinding, OrtValueVector, and copy_tensors.

C2 [P2]: Do not base safety checks on a mutable cached device marker

Suggested inline location: onnxruntime/python/onnxruntime_inference_collection.py:1259

Suggested comment:

[P2] Please do not cache _is_webgpu_buffer as a mutable Python attribute or add a separate _is_webgpu_buffer pybind API for it. Capture and copy validation treat this bit as authoritative, but callers can overwrite it, and the new CPU-only provenance test does exactly that; a CPU OrtValue can therefore be made to pass the fixed-WebGPU-buffer check while a real WebGPU value can be made to bypass WebGPU-specific guards. The C++ device_name() change already derives "webgpu" from the tensor's actual OrtMemoryInfo, so Python can query that read-only native result when needed. Keep the private C++ IsWebGpuBuffer helper for native data_ptr/NumPy/DLPack validation, but remove the redundant mutable Python marker/binding.

Evidence and attribution: OrtValue.__init__ copies _ortvalue._is_webgpu_buffer() into an ordinary assignable field, and _validate_ortvalue_ownership, _validate_capture_bindings, update_inplace, and copy_tensors trust that field. test_device_ortvalue_provenance_rules assigns it on CPU-backed values, demonstrating that it is not tied to the underlying tensor location.

S1 [P2]: Do not disable existing vector and copy APIs for WebGPU

Suggested inline location: onnxruntime/python/onnxruntime_inference_collection.py:618

Suggested comment:

[P2] Please remove the blanket WebGPU restrictions added to copy_tensors, run_with_ortvaluevector, and OrtValueVector.push_back. The default-context WebGPU values should come from the environment-registered shared allocator and are intentionally supported by the environment-level transfer; a non-capturing WebGPU session with CPU-only vectors is also unrelated to opaque WebGPU storage. These APIs worked before this PR and are being disabled only to support the new session-owned Python abstraction. Reusing the existing shared-allocation model removes that ownership problem and preserves compatibility. Add coverage for CPU<->WebGPU and WebGPU<->WebGPU copy_tensors, shared WebGPU vectors, and CPU-only vector execution with WebGPU registered.

Evidence and attribution: The Python guard checks only "WebGpuExecutionProvider" in self._providers; it does not inspect capture state, graph assignment, or the supplied vectors. The C++ OrtValueVector.push_back guard checks only WEBGPU_BUFFER and cannot distinguish shared from session-owned storage. Both therefore reject safe pre-existing uses in order to compensate for provenance tracked only in the Python wrapper.

T1 [P2]: Add a regression test for the foreign-GPU transfer guard

Suggested inline location: onnxruntime/core/providers/webgpu/data_transfer.cc:40

Suggested comment:

[P2] Please add focused regression coverage for the built-in DataTransfer::CanCopy predicate covering CPU<->WebGPU, WebGPU<->WebGPU, and rejection of NVIDIA/AMD GPU endpoints. This guard prevents a foreign GPU pointer from being reinterpreted and passed to Dawn as a WGPUBuffer, but the new tests do not exercise this predicate or a mixed WebGPU/CUDA session. If constructing DataTransfer without a live BufferManager makes a device-free unit test impractical, please extract the device-compatibility predicate into a testable helper or cover it in the WebGPU-enabled provider tests.

Evidence and attribution: The eight-line vendor guard is the complete fix for the prior unsafe transfer selection. No current-head C++ or Python test references the built-in WebGPU DataTransfer::CanCopy behavior or registers WebGPU with another GPU provider.

Clarifications

None.

Test coverage

  • C1: Current tests cover rebinding and release within one session, but not provider/session replacement while a graph is pinned.
  • D1: Current tests exercise the newly added session allocation/copy surface instead of proving that the existing shared allocator, OrtValue factories, and copy_tensors path support default-context WebGPU graph capture. The existing CUDA graph test is not extended to cover WebGPU.
  • C2: The CPU-only provenance test mutates _is_webgpu_buffer rather than exercising native memory metadata, which demonstrates the marker is mutable but does not validate real WebGPU provenance.
  • S1: Current tests assert rejection from a capture-enabled WebGPU session, but do not cover the previously supported non-capturing CPU-vector case or shared default-context WebGPU vectors.
  • T1: No test exercises the new built-in WebGPU CanCopy vendor guard.
  • The graph-capture behavior test still skips when WebGPU or a Dawn adapter is unavailable. The PR body reports a local WebGPU run, and all visible CI jobs now pass, including Linux WebGPU, external-Dawn, static/plugin, and macOS WebGPU builds/tests. Those results do not establish coverage for C1's provider-reset sequence, D1's existing copy path, S1's CPU-vector compatibility case, or T1's built-in transfer predicate.
  • Static evidence: the three changed Python files compile, and the complete current-head diff passes git diff --check. Runtime tests were unavailable because this managed worktree has no built Python extension or test binary.

Verdict

The feature is valid and the graph-ID/IOBinding pinning direction is appropriate, but the session-owned OrtValue direction should be removed. C1 blocks provider-reset correctness. D1 reuses the existing shared allocator, OrtValue factories, and native CopyTensors contract instead of adding Python-only session allocation/copy semantics. C2 removes a mutable duplicate of native memory metadata from safety decisions. S1 restores existing vector and copy APIs that the session-provenance design unnecessarily disables. Custom-external-device shared allocation is explicitly deferred to a C++ follow-up that makes the shared allocator retain and use the external device. T1 requests durable coverage for the safety-critical transfer fix. There are no remaining clarification requests or cleanup-only items.

Ananya Anand and others added 8 commits August 21, 2026 11:51
CanCopy() gained a vendor guard so a foreign GPU handle (e.g. a CUDA
pointer in a multi-EP build) is never reinterpreted as a WGPUBuffer, but
nothing tested it.

DataTransfer holds a BufferManager reference, so constructing one needs a
live device. Extract the device-compatibility decision into a static
IsSupportedDevicePair() helper that touches no state, and test it directly.
WebGPU EP tests build into onnxruntime_provider_test.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…session

Session._reset_session() cleared eleven fields and recreated _sess but left
_captured_graph_bindings pointing at the replaced C.InferenceSession. A
capture followed by set_providers() therefore left cross-session state: the
stale entry rejected a fresh IOBinding from the replacement session as
"captured with a different IOBinding", the old IOBinding stayed pinned and
could no longer be rebound or cleared, and a later release_captured_graph(0)
released the graph on the replacement session while unpinning a binding that
belonged to the old one.

Release each captured graph through the session that captured it, before any
teardown, so a failure leaves the session intact rather than half torn down.

The CPU regression test reproduces the bookkeeping run_with_iobinding()
records on capture; capture itself needs a device, but the state it leaves
behind does not. The WebGPU test covers the real
capture -> set_providers() -> capture again sequence end to end.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
OrtValue.__init__ cached ortvalue._is_webgpu_buffer() into an ordinary
assignable attribute, and four sites trusted it: the run-path device check in
Session._validate_ortvalue_ownership, IOBinding._validate_capture_bindings,
the update_inplace mismatch check, and copy_tensors. Two other sites already
called the native method instead, so the same question had two answers
depending on where it was asked. Assigning the attribute was enough to make a
CPU tensor pass graph-capture validation, which requires device-resident
values; test_device_ortvalue_provenance_rules did exactly that.

Delegate to the native method through a read-only property so every site
resolves it the same way and provenance cannot be spoofed. The provenance
test loses the half it was only simulating and keeps the session-ownership
rules it genuinely exercises.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Both were hard-rejected with an ORT_ENFORCE, but both already worked and
were useful before this feature branch. numpy() falls through to
CreateDataTransferMemCpy(), which resolves the environment-registered WebGPU
data transfer and returns correct host data, and data_ptr() returns the
WGPUBuffer handle, which is exactly what a WebGPU interop caller needs.

Rejecting them removed working behaviour without replacing it, so drop both
guards. The DLPack guards stay: those really are broken for an opaque buffer
handle, because DLPack promises a dereferenceable address.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…identity

run_with_ortvaluevector was the only run API that decided by provider name:

    if "WebGpuExecutionProvider" in self._providers:
        raise RuntimeError(...)

run(), run_with_ort_values() and run_async() all gate on
_validate_graph_capture_run_api() instead. So this refused a CPU-only
OrtValue vector on a WebGPU session that was not capturing at all, and it
ignored capture state, graph assignment, and whether the supplied vectors
were even device-backed. That use worked before this feature branch.

Call the same validation the other three run APIs already call. This is a
consistency fix reusing already-tested logic, not a new policy: a raw vector
is unsafe only while capture is armed, because replay re-issues the buffers
recorded at capture and would silently ignore the values passed here. The
gpu_graph_id=-1 opt-out now works here exactly as it does everywhere else.

The stated reason for the old guard -- that raw vectors "cannot retain
session ownership" -- was also untrue; see the following commit.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The guard refused every WebGPU output with "it cannot retain session
provenance". That reason is not true of this code: the vector returned by
SessionIOBinding.get_outputs is a pybind reference_internal, which is defined
as a reference plus keep_alive<0,1>, so it already keeps the IOBinding alive,
and the IOBinding already keeps the session alive via the keep_alive<1,2>
added earlier on this branch. Together with keep_alive<0,1> on
OrtValueVector.__getitem__ the chain is complete:

    OrtValue -> OrtValueVector -> SessionIOBinding -> InferenceSession

So the lifetime the guard was compensating for is structurally guaranteed,
and refusing the call only removed access.

The new test pins that chain rather than the guard: it reaches a WebGPU
output through the raw vector, drops the IOBinding and the session, and then
releases the buffer. Without the full chain that release would call
GpuBufferAllocator::Free() through a buffer-manager getter capturing a dead
EP, so the test fails loudly if a future change breaks a link.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Neither message described its real hazard, and push_back's cited a property
of the code that does not hold.

push_back: the hazard is lifetime, and it is specific to a standalone vector.
The vector returned by SessionIOBinding::get_outputs is reference_internal
and so keeps the session alive, but a user-built OrtValueVector has no parent
and cannot. A WebGPU buffer allocated by a session's EP is released through
GpuBufferAllocator, whose buffer-manager getter captures that EP, so freeing
it after the session is gone is a use-after-free. C++ cannot currently tell a
session-allocated WebGPU value from one backed by a shared allocator, since
their OrtMemoryInfo is identical, so the guard has to stay; the TODO ties its
removal to giving GpuBufferAllocator a reference to its BufferManager's owner.

push_back_batch: nothing to do with lifetime. It wraps foreign storage by raw
address, and a WebGPU allocation is an opaque WGPUBuffer handle rather than an
addressable pointer, so no torch tensor can back one.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The guard rejected a value if it was session-scoped OR a WebGPU buffer, and
both halves were wrong.

Being session-scoped is not a hazard here. copy_tensors is synchronous and
the caller holds the Python wrappers, and therefore the session, for its whole
duration, so there is no window in which the allocator can outlive its EP.

Being a WebGPU buffer is not a hazard either. CPU-to-WebGPU, WebGPU-to-WebGPU
and WebGPU-to-CPU copies all work through the environment-registered WebGPU
data transfer, and worked before this feature branch.

The real hazard is narrower: that data transfer resolves the *default* WebGPU
context, so a buffer belonging to a caller-supplied context would be copied on
the wrong wgpu::Device -- silently, because Dawn reports it on the async device
error callback rather than at the call site, if validation is enabled at all.
Reject exactly that, and say so.

This is a partial mitigation, and the TODO in the code says so. A value can
only be attributed to a context when it carries session provenance, because
every WebGPU allocation reports OrtDevice(GPU, VendorIds::NONE, 0) whatever
its context. Closing the hole properly needs WebGPU allocations to carry their
real context id, which would also let the data transfer refuse the copy itself
instead of relying on a Python-side guard. numpy() has the same exposure and
no session to consult, so it is unaffected either way.

WebGpuExecutionProvider::GetDeviceId() already returns the context id, so the
new binding is a plain getter returning -1 when the session has no WebGPU EP.

A real second context needs caller-supplied Dawn instance and device handles,
which Python cannot produce, so the rejection branch is covered by unit-testing
the predicate with an injected context id. That test runs on CPU-only machines,
which matters because no CI leg runs this file on a WebGPU host.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

@qjia7 Jiajia Qin (qjia7) 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.

Review frame

  • Review contract: Re-reviewed PR 32074 at exact head
    364fad2d9d7516371704099af29ba256b58a6047, with base
    3d3cfa6c004551ee31d3acff6d01fb572712ca91, and specifically audited the four-commit range
    48a2b3c66ae1385424f2c08fd8ea2d8f13d407d2..364fad2d9d7516371704099af29ba256b58a6047.
    Current metadata, discussion, reviews, inline comments, CI, the incremental diff, and the complete PR diff were fetched
    read-only.
  • Problem/feature validity: Validated. WebGPU graph replay retains recorded bind groups and buffer handles, so Python
    needs fixed device buffers whose lifetime spans capture/replay, in-place input updates, explicit readback, and graph
    release.
  • Risk/scope: Deep. The PR adds public Python allocation/copy APIs, graph-ID and IOBinding lifetime state,
    allocator/data-transfer routing, provider-reset behavior, and restrictions on existing OrtValue APIs.
  • Direction gate: Pass for graph-ID/IOBinding pinning and lifecycle enforcement, but fail for the parallel
    session-owned OrtValue abstraction. For WebGPU without a custom external device, the environment shared allocator,
    its transfer, and session allocators all use WebGpuContextFactory context 0 and therefore the same Dawn device.
    Existing OrtValue factories and copy_tensors already expose the required native shared-allocation/copy contracts.
    Custom-external-device shared allocation remains a native C++ TODO: shared allocators/transfers must retain and use
    the external device instead of DefaultContext() before that configuration is supported.

Prior issue resolution

  • Captured graph/IOBinding identity and rebinding: Resolved. A successful capture pins its IOBinding and buffer
    signature until native graph release succeeds.
  • gpu_graph_id=-1 capture opt-out: Resolved. Convenience run APIs preserve the native per-run skip behavior.
  • Foreign GPU handles selected by WebGPU DataTransfer: Resolved. WebGPU rejects vendor-tagged GPU endpoints.
  • Reproducible benchmark baseline: Resolved. Capture and no-capture modes are both available and reported.
  • Captured state surviving set_providers(): Resolved in 0f42878a4b. _reset_session() releases every captured
    graph through the old native session before replacing it, and new synthetic and real WebGPU tests cover reset and
    recapture.
  • Mutable _is_webgpu_buffer safety marker: Resolved in 9588dadf6b. Python now exposes a read-only property that
    queries native tensor memory metadata on every access; the spoofing test was removed.
  • Missing CanCopy regression test: Resolved in 53d701e238. The device-pair predicate is directly tested for
    CPU/WebGPU endpoints and NVIDIA, AMD, Intel, and Microsoft GPU rejection; the new source is included by the WebGPU
    provider-test source glob.
  • WebGPU numpy() and data_ptr(): No new finding. 364fad2d9d restores behavior that predated this branch.
    numpy() uses the environment-registered transfer for normal context-0 buffers, while data_ptr() preserves the
    established opaque WGPUBuffer-handle interop behavior. DLPack remains correctly rejected because it requires a
    dereferenceable address.

Confirmed findings

D1 [P2]: Reuse the existing OrtValue allocation and copy APIs

Suggested inline location: onnxruntime/python/onnxruntime_inference_collection.py:489

Suggested comment:

[P2] This direction remains unresolved at the new head. Please use the existing
OrtValue.ortvalue_from_shape_and_type / ortvalue_from_numpy factories and copy_tensors path instead of adding
parallel session-specific allocation and copy APIs. CUDA graph capture already uses those APIs. For WebGPU without
a custom external device, the environment-registered shared allocator, its data transfer, and session allocators all
resolve through WebGpuContextFactory context 0, so their WGPUBuffers belong to the same Dawn device even when
allocator instances differ. The missing work is to recognize "webgpu" as GPU/vendor NONE with allocator name
WEBGPU_BUFFER, not to add Session.create_ortvalue_*, InferenceSession.update_ortvalue_inplace, Python
_session provenance, and the restrictions that follow from that ownership model. Please keep the existing
allocation/update surface, add only the WebGPU mapping and capture-lifetime enforcement, and adapt
onnxruntime/test/python/onnxruntime_test_python_cudagraph.py into a shared CUDA/WebGPU graph-capture test, renaming
it to onnxruntime_test_python_graphcapture.py and updating tools/ci_build/build.py to run it for both CUDA and
WebGPU builds. Custom-external-device shared allocation should remain an explicit C++ TODO: the shared
allocator/data transfer must retain and use that external device instead of DefaultContext() before that
configuration is supported.

Evidence and attribution: OrtApi::GetSharedAllocator, CreateTensorAsOrtValue, and CopyTensors are the existing
public native contracts, and the Python OrtValue factories/update methods already expose that workflow. The WebGPU
factory registers a shared allocator and matching transfer backed by DefaultContext(). The new session methods
duplicate those operations and directly expose InferenceSession::GetDataTransferManager(), for which there is no
corresponding public session-copy API. The four new commits do not change these APIs or generalize the existing CUDA
graph test; tools/ci_build/build.py:1839-1841 still invokes
onnxruntime_test_python_cudagraph.py only when args.use_cuda.

S1 [P2]: Do not disable existing vector and copy APIs for WebGPU

Suggested inline location: onnxruntime/python/onnxruntime_inference_collection.py:618

Suggested comment:

[P2] Please remove the blanket WebGPU restrictions added to copy_tensors, run_with_ortvaluevector, and
OrtValueVector.push_back. A non-capturing WebGPU session with CPU-only vectors is unrelated to opaque WebGPU
storage, and default-context WebGPU values should come from the environment shared allocator and are intentionally
supported by the environment-level transfer. These APIs worked before this PR and are disabled only to compensate
for the new session-owned Python abstraction. Reusing the existing shared-allocation model removes that ownership
problem and preserves compatibility. Add coverage for CPU<->WebGPU and WebGPU<->WebGPU copy_tensors, shared
WebGPU vectors, and CPU-only vector execution with WebGPU registered.

Evidence and attribution: Session.run_with_ortvaluevector() rejects solely because WebGPU is registered, without
inspecting capture state or any supplied vector. OrtValueVector.push_back() rejects every native
WEBGPU_BUFFER, including shared context-0 values. Python copy_tensors() likewise rejects every WebGPU value before
calling the existing native C.copy_tensors. The new commits leave all three restrictions unchanged.

Clarifications

None.

Test and CI evidence

  • The new provider-reset tests directly cover stale capture bookkeeping and real WebGPU capture -> set_providers()
    -> recapture.
  • The read-only marker test confirms assignment is rejected and native memory metadata remains authoritative.
  • WebGpuDataTransferTest directly covers the safety-critical device predicate and builds through
    onnxruntime_provider_test.
  • Coverage still exercises the new session allocation/copy surface rather than proving graph capture with the existing
    shared OrtValue factories and copy_tensors. The existing CUDA graph test has not been generalized or added to the
    WebGPU runner.
  • All visible WebGPU jobs passed, including Linux x64, macOS arm64/x64, static/plugin, and external-Dawn builds/tests.
    The only failed check is the unrelated Linux TensorRT test job:
    onnxruntime_global_thread_pools_test hit cudaErrorMemoryAllocation: out of memory.
  • The complete current-head diff passes git diff --check, and the changed Python files compile. Runtime tests were
    unavailable locally because this managed worktree has no built Python extension or test binary.

Verdict

The four new commits correctly resolve all three outstanding correctness/test gaps they target, and restoring
numpy()/data_ptr() preserves prior WebGPU behavior without weakening the DLPack guard. No new regression was found.
The review remains blocked on one architectural root cause and its compatibility fallout: D1 should remove the
Python-only session allocation/copy layer in favor of the existing native shared allocator and CopyTensors
contracts, and S1 should restore the existing copy/vector APIs. Doing so would make the implementation substantially
clearer by removing _session provenance, duplicate creation/update bindings, and the blanket restrictions and tests
needed to maintain that parallel model.

BufferManager::MemCpy reports misuse with ORT_ENFORCE, which throws: an aliased
src/dst pair, an undersized destination and a still-mapped buffer all throw
rather than returning a Status. The shared data transfer reaches MemCpy through
WebGpuDataTransferImpl::CopyTensorsImpl, which is a noexcept C ABI callback and
only handled a returned non-OK Status. The exception therefore escaped a noexcept
frame and std::terminate killed the process with no message and no stack.

Wrap the callback body so any throw becomes an OrtStatus, which is what the
surrounding C API already expects.

This was unreachable from Python while copy_tensors rejected every WebGPU
OrtValue outright. Allowing the supported copies exposes it, so it is fixed and
pinned here rather than left for the first caller to hit:

  copy_tensors([a], [a])       aliased      -> was a silent 0xC0000409, now RuntimeError
  copy_tensors([64x64], [2x2]) undersized   -> was a silent 0xC0000409, now RuntimeError

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@4n4ny4

Copy link
Copy Markdown
Contributor Author

Thanks for the review. The three S1 restrictions are gone as of 91541a5: run_with_ortvaluevector now gates on capture state rather than provider name, the get_outputs_as_ortvaluevector guard is dropped, and copy_tensors only rejects values attributable to a non-default WebGPU context, so CPU to WebGPU and WebGPU to WebGPU copies all work now, with tests. Your review was right that the earlier commits left these unchanged. They were written but unpushed at that head. Removing the copy_tensors restriction also surfaced a crash worth flagging: copy_tensors with aliased src/dst, or an undersized destination, silently killed the process. BufferManager::MemCpy reports misuse via ORT_ENFORCE, which throws, but it is reached through CopyTensorsImpl, a noexcept C-ABI callback, so the throw hit std::terminate. Pre-existing, but only reachable once WebGPU values are allowed through, so I fixed it at the boundary in the same push. Two guards stayed: push_back, because a standalone vector cannot keep the session alive the way the IOBinding one does (which is why that could be unblocked and this could not), and push_back_batch, because it wraps raw pointers and a WebGPU allocation is an opaque handle.

On D1 you are right about the mechanism, and I checked rather than assumed: without a custom external device everything does resolve through context 0. The problem is that path is not reachable from the Python wheel build. cmake/onnxruntime_providers_webgpu.cmake:22-32 excludes ep/ when onnxruntime_USE_EP_API_ADAPTERS is off, and ep/factory.cc:107-108 holds the only EpDevice_AddAllocatorInfo calls, so the OrtEpDevice registers with no allocator and ortvalue_from_shape_and_type(..., "webgpu") throws while the session-owned one works. That is also why the shared graph-capture test is circular today: both the cudagraph and dmlgraph tests are built on those same global factories, so a WebGPU arm can only be written right now using the API you are asking me to remove. I would rather land the cmake fix first and move this layer onto it, but happy to file that follow-up, or do it in the other order if you prefer.

@qjia7 Jiajia Qin (qjia7) 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.

I think we can limit this PR to basic WebGPU graph-capture support for the default context:

  1. Allocate fixed GPU input/output tensors through the session allocator using Session.create_ortvalue_from_shape_and_type.
  2. Upload CPU inputs with the environment-level onnxruntime.copy_tensors.
  3. Bind those GPU tensors with IOBinding and capture/replay the graph.
  4. Copy the GPU output into a CPU OrtValue with onnxruntime.copy_tensors and verify the result.

The native session allocator is already a public C/C++ API (OrtApi::CreateAllocator), so exposing the minimal Python allocation method is reasonable. For the default WebGPU context, the session allocator and environment data transfer both use context 0, so this workflow does not require an environment shared allocator.

Please keep the Python surface minimal: retain Session.create_ortvalue_from_shape_and_type and copy_tensors, but use allocation-plus-copy instead of adding Session.create_ortvalue_from_numpy and the session-specific update_ortvalue_inplace path. Shared-allocator support and adapting onnxruntime/test/python/onnxruntime_test_python_cudagraph.py into a common CUDA/WebGPU graph-capture test can be handled in a follow-up after static WebGPU shared-allocator registration is available.

Custom external WebGPU contexts should remain unsupported here with a native TODO.

Ananya Anand and others added 2 commits August 24, 2026 22:01
…pdate path

A device tensor is allocated from the session allocator and populated with the
environment-level copy_tensors, rather than through a session-specific upload
API. For the default WebGPU context the session allocator and the environment
data transfer both resolve context 0, so the two compose without an environment
shared allocator.

Removes Session.create_ortvalue_from_numpy and the session-routed
update_ortvalue_inplace path, which duplicated allocate-plus-copy behind a
second surface. OrtValue.update_inplace keeps its pre-existing non-session
behaviour; only the session-routed branch is gone, along with the pybind
overloads and the CopySessionOrtValue/UpdateSessionOrtValue helpers.

Session.create_ortvalue_from_shape_and_type and copy_tensors are retained: the
native session allocator is already public as OrtApi::CreateAllocator.

Tests follow the supported flow end to end: allocate fixed device tensors,
upload with copy_tensors, bind with IOBinding and capture/replay, then copy the
device output back to a CPU OrtValue and verify. The CPU-ownership test now
covers allocation and provenance only, since a CPU-to-CPU copy depends on a data
transfer that is not registered in every build.

Custom external WebGPU contexts remain unsupported, with the native TODO stating
that shared allocators and transfers must retain and use the external device
instead of resolving DefaultContext().

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ability

test_webgpu_graph_capture_session_ortvalues builds a session with
disable_cpu_ep_fallback and requests the WebGPU EP, so on a build without that EP
the nodes have nowhere to go and session creation fails with "graph nodes that
are assigned to the default CPU EP, but fallback to CPU EP has been explicitly
disabled".

The test lost its @unittest.skipIf when a later test was inserted directly
beneath the decorator, which rebound the decorator to the new function. Restore
it so the test skips on CPU-only builds, as the other WebGPU tests do.

The failure is invisible to a runtime check on a machine where the WebGPU EP is
compiled in, because the session simply succeeds there.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread onnxruntime/python/onnxruntime_pybind_iobinding.cc Outdated
Comment thread onnxruntime/python/onnxruntime_inference_collection.py Outdated
…wnership

Extract GetDeviceAllocatorName next to GetDeviceName and use it from both
CreateSessionOrtValue and the IOBinding paths, replacing the duplicated
GPU + VendorIds::NONE -> WEBGPU_BUFFER selection.

Drop the unconditional 'WebGPU OrtValues must be used with IOBinding'
rejection. It also fired on a WebGPU session that never enabled graph
capture, where a device OrtValue feed is an ordinary run. Accept a WebGPU
buffer owned by another session on the same WebGPU context.

Move the data-transfer predicate and the copy_tensors misuse fix out of
this PR; they are covered by microsoft#32315 and a separate branch. Drop the
graph-capture benchmark script.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Ananya Anand and others added 2 commits August 31, 2026 10:28
bind_ortvalue_input and bind_ortvalue_output still used the strict
same-session test, so a WebGPU buffer accepted by run() was rejected by
the binding path, which is the path device values are most likely to
take. Move the rule into _validate_ortvalue_session_compatibility and
call it from all three sites. The verb is a parameter so the run and
bind messages stay distinct.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
update_inplace still required the exact same session, so a WebGPU buffer
that bind_ortvalue_input accepted could not be copied into a value owned
by the target session. Both are device-to-device operations on one
context, so split the decision into _is_ortvalue_session_compatible and
have the raising helper and update_inplace share it.

The predicate treats a null target session as compatible, which keeps a
sessionless destination working and preserves the previous behaviour.
update_inplace keeps its own message because it compares two values
rather than a value against a session.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread onnxruntime/python/onnxruntime_inference_collection.py
@4n4ny4
Ananya Anand (4n4ny4) merged commit 1f71c9c into microsoft:main Sep 2, 2026
91 checks passed
Ananya Anand (4n4ny4) added a commit that referenced this pull request Sep 2, 2026
#32315)

### Description

`BufferManager::MemCpy` reports misuse with `ORT_ENFORCE`, which throws:
an aliased src/dst
pair, an undersized destination and a still-mapped buffer all throw
rather than returning a
Status. The shared data transfer reaches it through
`WebGpuDataTransferImpl::CopyTensorsImpl`,
which is a `noexcept` C ABI callback and only handled a returned non-OK
Status. The exception
escaped a `noexcept` frame, so `std::terminate` took the process down
with no message and no
stack.

This wraps the callback body so any throw becomes an `OrtStatus`, which
is what the
surrounding C API already expects.

The two sibling callbacks are deliberately left alone: `CanCopyImpl`
only calls C ABI function
pointers, and `ReleaseImpl` returns void, so converting a throw there
would mean swallowing it
rather than reporting it, which is a separate decision from this one.

### Motivation and Context

Not currently reachable from Python, because `copy_tensors` rejects
every WebGPU OrtValue
outright. It becomes reachable as soon as the supported copies are
allowed, which is what
#32074 does, so it is worth fixing at the boundary on its own rather
than landing inside a
larger change.

The regression test for it uses the session-scoped allocation APIs under
discussion in #32074
and is held there rather than duplicated here.

Co-authored-by: Ananya Anand <4n4ny4@users.noreply.github.com>
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.

3 participants