Repository navigation
[WebGPU] Support concurrent use with explicit Session streams - #32587
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Critical and moderate review findings remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds per-Session WebGPU command recording and stream-aware allocation/transfer handling while synchronizing shared context resources.
Changes:
- Isolates Session recording and graph-capture state.
- Adds explicit sync streams and stream-aware allocator behavior.
- Adds native and AutoEP concurrency coverage.
Unresolved findings affect allocator exception safety, dispatch ordering, synchronization, stream registration, error-scope ownership, and graph-copy routing.
File summaries
| File | Summary |
|---|---|
onnxruntime/test/providers/webgpu/webgpu_context_test.cc |
Updates context tests for explicit recordings. |
onnxruntime/test/providers/webgpu/concurrent_context_test.cc |
Adds shared-context concurrency coverage. |
onnxruntime/test/autoep/test_webgpu_concurrency.cc |
Adds AutoEP concurrency coverage. |
onnxruntime/core/providers/webgpu/webgpu_provider_factory.cc |
Adds plugin transfer and allocator integration. |
onnxruntime/core/providers/webgpu/webgpu_kernel.cc |
Integrates kernel recording and graph-copy routing. |
onnxruntime/core/providers/webgpu/webgpu_execution_provider.h |
Defines Session recording ownership. |
onnxruntime/core/providers/webgpu/webgpu_execution_provider.cc |
Handles EP lifecycle, recording, flushing, and Memcpy routing. |
onnxruntime/core/providers/webgpu/webgpu_context.h |
Defines recording-aware context state. |
onnxruntime/core/providers/webgpu/webgpu_context.cc |
Implements recording, flushing, and capture handling. |
onnxruntime/core/providers/webgpu/session_buffer_pool.cc |
Handles Session buffer-pool management. |
onnxruntime/core/providers/webgpu/program_manager.h |
Updates pipeline-cache interfaces. |
onnxruntime/core/providers/webgpu/program_manager.cc |
Synchronizes shared pipeline-cache access. |
onnxruntime/core/providers/webgpu/ep/README.md |
Documents Session streams and limitations. |
onnxruntime/core/providers/webgpu/ep/factory.cc |
Registers plugin stream integration. |
onnxruntime/core/providers/webgpu/ep/ep.h |
Declares EP sync-stream interfaces. |
onnxruntime/core/providers/webgpu/ep/ep.cc |
Implements sync-stream creation. |
onnxruntime/core/providers/webgpu/data_transfer.h |
Defines stream-bound transfer interfaces. |
onnxruntime/core/providers/webgpu/data_transfer.cc |
Implements stream and notification handling. |
onnxruntime/core/providers/webgpu/compute_context.h |
Updates recording-aware compute helpers. |
onnxruntime/core/providers/webgpu/compute_context.cc |
Supports per-recording compute encoders. |
onnxruntime/core/providers/webgpu/buffer_manager.h |
Defines recording-scoped buffer APIs and synchronization. |
onnxruntime/core/providers/webgpu/buffer_manager.cc |
Synchronizes shared buffer-cache operations. |
onnxruntime/core/providers/webgpu/allocator.h |
Declares stream-aware allocator interfaces. |
onnxruntime/core/providers/webgpu/allocator.cc |
Implements recording-aware and stream-aware allocation. |
Review details
Suppressed comments (4)
onnxruntime/core/providers/webgpu/buffer_manager.cc:545
- The GPU-to-GPU copy is recorded before
recording.deferred_dispatchesis encoded. For a graph path that copies a value after a deferred compute dispatch,Flushwill submit this copy before that compute, so the destination can contain stale data. Encode the deferred dispatches before recordingCopyBufferToBuffer, matchingDownloadand cached-buffer clearing.
auto& command_encoder = context_.GetCommandEncoder(recording);
context_.EndComputePass(recording);
command_encoder.CopyBufferToBuffer(src, 0, dst, 0, copy_size);
onnxruntime/core/providers/webgpu/ep/factory.cc:296
IsStreamAwareImpl()now advertises factory-level stream support, but the callback used by the publicCreateSyncStreamForEpDeviceAPI still unconditionally returnsORT_NOT_IMPLEMENTED. Internal session setup callsEp::CreateSyncStreamForDeviceImpland therefore hides this mismatch, while applications creating a stream from anOrtEpDevicewill fail despite the advertised capability. Please implement the factory callback (or update the awareness/registration contract so this public capability is not advertised while EP-level session streams remain registered).
return true;
}
OrtStatus* ORT_API_CALL Factory::CreateSyncStreamForDeviceImpl(
OrtEpFactory* /*this_ptr*/,
const OrtMemoryDevice* /*memory_device*/,
const OrtKeyValuePairs* /*stream_options*/,
OrtSyncStreamImpl** stream) noexcept {
EXCEPTION_TO_RETURNED_STATUS_BEGIN
*stream = nullptr;
return Api().ort.CreateStatus(ORT_NOT_IMPLEMENTED,
"CreateSyncStreamForDevice is not implemented for this EP factory.");
onnxruntime/core/providers/webgpu/webgpu_context.h:114
- This mutex only serializes one Session's recording, while
WebGpuContext::PushErrorScope/PopErrorScopeoperate on the sharedWGPUDeviceerror-scope stack.OnRunStartandOnRunEndcall them for every Session, so interleaved runs can pop another Session's scope, misattribute validation errors, or fail to pop. Protect the device-wide scope lifetime (or disable per-Session scopes when a device is shared); locking this per-recording mutex alone cannot establish the required LIFO pairing.
std::recursive_mutex mutex;
onnxruntime/core/providers/webgpu/webgpu_execution_provider.cc:880
OnRunStartpushes a validation error scope oncontext_.Device()and this path later pops it, but that scope stack is shared by every Session using context 0. Two runs can interleave A.Push, B.Push, A.Pop, B.Pop, causing each run to consume the other's validation errors and report a failure to the wrong caller; implicit device synchronization does not make the nesting per-session. The per-recording lock here cannot fix that, so serialize the complete error-scope lifetime or disable per-run scopes for shared concurrent contexts.
Status flush_status = context_.Flush(BufferManager(), *recording_);
- Files reviewed: 24/24 changed files
- Comments generated: 4
- Review effort level: Lite (auto)
Note
Copilot is running an experiment and ran this review at Lite.
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
/run bots |
22fefef to
c3eb238
Compare
Isolate command recording per Session while sharing synchronized WebGPU buffer management. Route copies and allocations through explicit Session streams and preserve deferred-dispatch ordering. Contain Session allocator Free exceptions with diagnostics, serialize FillZero recording, and explicitly reject unsupported factory-owned streams. Add focused regression coverage and keep the tests compatible with native/plugin and Dawn build configurations. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
c3eb238 to
be4e4e3
Compare
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Build allocator and concurrency integration tests in onnxruntime_provider_test so Windows plugin CI runs them without a shared ORT core. Reuse the test runner's plugin registration and isolate registration-order coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Move the stream and notification bridge into webgpu/ep and update its callers without changing behavior. Document built-in and plugin data-transfer creation paths, wrapper layers, and recording ownership. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Jiajia Qin (qjia7)
left a comment
There was a problem hiding this comment.
The per-Session recording and explicit-stream direction is appropriate, and the latest commits resolve the plugin build/test-coverage issue. Two correctness issues still block merge: native streamless Session allocations can submit their cached-buffer clear after caller data is uploaded, and profiling state remains shared across otherwise concurrent Sessions. There is also a conditional shader-dump race. Details are inline.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 24bea439-587e-4530-b46c-cc99658698d6
2f92707 to
be4e297
Compare
|
I will add a version check to support 1.28.3 in the following PR. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 24bea439-587e-4530-b46c-cc99658698d6
Reuse GpuBufferAllocator for Env allocations with independently owned recording and context. Move plugin allocator wrappers and stream allocation into ep, clarify ownership, and cover allocator lifetime and cached-clear behavior. Set the minimum supported ONNX Runtime version to 1.30.1. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Reopen this PR to re-trigger the pipepline. |
Remove redundant transfer locking and avoid waiting for streamless GPU copies while preserving submission ordering. Clarify scratch allocation performance and future batch-aware buffer retirement, and align documentation and regression naming. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep submitting the owning Session recording before consumers use a different recording, including streamless node-dump readbacks. Remove the extra queue-completion wait and preserve WebGPU ordering through its shared queue and blocking CPU readbacks. Document submission-only stream and notification semantics, including GPU-backed output completion. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Jiajia Qin (qjia7)
left a comment
There was a problem hiding this comment.
Let's merge this first and handle the follow-up work separately. Thanks.
### Description Remove the mutex and pending-buffer list from `CommandRecordingState`. Each Session owns its command recording, while plugin streamless allocations and transfers use independent recordings. `BufferManager` owns deferred buffer releases and protects them with its existing cache mutex. - Plain plugin allocations submit cached-buffer clears before returning. Streamless GPU copies also submit before returning, so later Session work on the shared queue observes them. Matching explicit-stream operations continue to use the Session recording. - Reusable buffers remain pending until their recording submits. Pending entries are grouped by recording; another recording's submission cannot return them to the pool. An atomic work flag lets concurrent frees distinguish pending from submitted work without inspecting encoders or capture state. Non-reusing caches bypass this tracking. - Flush retires entries in both the active manager and the context-shared manager. Graph cache policy applies only to the active manager, so frees during replay and uniforms used during prepacking return to the correct pools. Failure and Session destruction discard abandoned entries. - Keep concurrency support scoped to plugin WebGPU. Preserve the standalone shader-dump concurrency test and sequential buffer-pool memory diagnostic while removing tests that require built-in inference concurrency. Follow-up to #32587. Initial-capture coverage includes independent Sessions and serialized Runs for different graph IDs within one Session, using preallocated bindings. ### Dependency Builds on #32902, now merged into `main`, which serializes graph-state checks and replay with normal Session execution. This PR targets `main` and contains only the WebGPU changes. Standard PR CI runs automatically on updates. ### Validation Native Windows Release validation before rebasing onto the latest main: - Plugin tests: 43 passed across suite and focused runs; two existing tests disabled. - Built-in WebGPU suite: 37 tests passed; one configuration-specific adapter test skipped. - Core/session suite: 78 tests passed. - Plugin concurrency stress: 12 tests repeated 20 times with shuffled order, all 240 executions passed. - Initial capture and replay: three tests repeated 20 times with shuffled order, all 60 executions passed. - Restored shader-dump test and sequential memory diagnostic: both passed. The diagnostic was explicitly enabled with two Sessions, 1 MB tensors, two iterations and no hold time; it remains disabled by default. - Lint and whitespace checks passed. Rebase verification: all five WebGPU patches are unchanged according to `git range-diff`, and `git diff --check` passed.
Summary
Support concurrent use of independent WebGPU Sessions by separating Session command recording from context-shared device resources. The plugin uses explicit Session-owned streams rather than TLS or creation-thread-based transfer routing. This PR develops the explicit-stream alternative to #29851; it is no longer a frozen copy of the original historical implementation.
OrtEp::CreateSyncStreamForDevice. Each plugin stream references its owning EP's recording; it does not create a separate WebGPU queue. Stream-bearing transfers preserve that ordering, including BindInput copies, while graph Memcpy kernels access their owning EP directly. No new public EP API is introduced.webgpu/ep. Native/built-in WebGPU uses EP recording directly and excludes these plugin adapters.CreateGPUTensorthrough the kernel's explicit sync stream, preserving clear/copy/dispatch ordering without submitting each scratch allocation. Move plugin regression coverage intoonnxruntime_provider_testand serialize shared shader-dump writes.Allocator ownership and ordering
Env and Session device allocators reuse
GpuBufferAllocator, held byAllocatorPtr; the ABI wrappers differ because only the Session wrapper exposes stream allocation.OrtAllocator)onnxruntime::ep::adapter::AllocatorWebGpuSessionAllocatorAlloc/ null streamAllocOnStreamcallback exposedAllocOnStreamThe distinction is stream-ordered versus streamless allocation, not internal versus external API use. A shared buffer cache does not imply shared recording. Callers must operate on independent tensors or synchronize writes before another Session consumes shared data.
Related: #32561 #31627.
Validation
Local Windows validation of the allocator refactor:
onnxruntime_provider_testfor both plugin and native/built-in WebGPU configurations.AllocOnStream.Plugin coverage includes CPU I/O, graph-internal CPU/GPU copies, mixed feed copies, CPU outputs bound to GPU, concurrent Sessions, and same-Session/dedicated-Session allocator concurrency. This is targeted local validation, not a full-suite or current CI-success claim, and does not establish parallel Run execution on one Session.
These build/test runs preceded the final minimum-version change to 1.30.1. That change was checked at source/configuration level; the plugin was not rebuilt afterward or tested against an actual ORT 1.30.1 runtime.
Limitations and deferred work
plugin-ep-webgpu/MIN_ONNXRUNTIME_VERSION), with stream support required. This replaces the previous 1.31.0 minimum; actual 1.30.1 runtime compatibility remains unvalidated locally.ImplicitDeviceSynchronizationfeature must be enabled. ORT requests it for internally created devices; applications providing an external device must request it when creating that device. Initialization rejects native devices without it, even for serial use. This requirement does not apply to WASM.Known correctness issues not addressed in this PR
Built-in EP: external Session-allocator use overlapping a Run on the same Session (C3). Plain allocations through
OrtApi::CreateAllocatorfollowed by streamlessOrtEnv::CopyTensorsuploads are not supported while the allocator-owning Session has an active Run. A reused buffer's clear can remain deferred on the Session recording while the upload is submitted through the Env's separate recording; the later clear can overwrite the uploaded data. The ordering fix and a public-API regression remain deferred. This specific limitation is in the built-in EP path: plugin Session allocators submit plainAllocclears before returning and defer only stream-orderedAllocOnStreamcalls.Concurrent Session execution with profiling enabled (C4). Profiling query resources, pending events, timestamp offsets, and lifecycle state remain shared at the WebGpuContext level. Per-Session recording locks do not isolate them, so overlapping profiled Sessions can race and mix or lose GPU events. State/lifecycle isolation or an explicit serialized/rejected-overlap policy, with regression coverage, remains deferred.
These are known correctness limitations, not runtime-enforced exclusions. Applications must avoid these overlaps until follow-up fixes are in place.
Other out-of-scope or unvalidated scenarios