[Core] Modernize execution synchronization and cancellation - #29700
[Core] Modernize execution synchronization and cancellation#29700GopalakrishnanN wants to merge 9 commits into
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR modernizes ONNX Runtime’s graph execution synchronization and cancellation path by replacing busy-waiting with C++20 blocking primitives, making cancellation propagation and failure publication thread-safe, and updating call sites (execution steps, control-flow subgraphs, training partial execution, and Python bindings) to pass a std::stop_token snapshot by value.
Changes:
- Replace
StreamExecutionContext::WaitAll()spinning withstd::atomic::wait/notify_alland introduce a thread-safe “first failure wins” status publication plus context-local stop fan-out. - Snapshot and propagate termination via
std::stop_tokenthroughInferenceSession::Run/PartialRun, executors, kernel contexts, and CPU control-flow / contrib subgraphs. - Update tests and microbenchmarks to validate completion wake behavior, competing failures, cancellation fan-out/reset, and termination behavior.
Reviewed changes
Copilot reviewed 29 out of 29 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| onnxruntime/test/providers/cpu/controlflow/loop_test.cc | Updates loop termination test to use the new RunOptions termination API. |
| onnxruntime/test/onnx/microbenchmark/tptest.cc | Adds a microbenchmark for StreamExecutionContext completion wake behavior. |
| onnxruntime/test/framework/stream_execution_context_test.cc | Adds unit tests for CountDownBarrier, FirstFailureStatus, and RunOptions termination snapshot/reset. |
| onnxruntime/test/framework/parallel_executor_test.cc | Adds concurrent Run cancellation fan-out/reset coverage using a custom op that waits on cancellation. |
| onnxruntime/python/onnxruntime_pybind_state.cc | Rebinds RunOptions.terminate as a property backed by stop-token state, preserving the Python surface API. |
| onnxruntime/core/session/inference_session.h | Extends internal RunImpl to accept a std::stop_token snapshot. |
| onnxruntime/core/session/inference_session.cc | Snapshots termination token at Run/PartialRun entry and propagates it down to graph execution utilities. |
| onnxruntime/core/providers/cpu/controlflow/scan_utils.cc | Propagates stop-token cancellation into Scan subgraph execution. |
| onnxruntime/core/providers/cpu/controlflow/loop.cc | Propagates stop-token cancellation into Loop subgraph execution. |
| onnxruntime/core/providers/cpu/controlflow/if.cc | Propagates stop-token cancellation into If subgraph execution. |
| onnxruntime/core/framework/utils.h | Updates execution utility signatures to accept std::stop_token and removes the RunOptions-based ExecuteGraph overload. |
| onnxruntime/core/framework/utils.cc | Implements the updated stop-token-based ExecuteGraph/ExecuteSubgraph/ExecutePartialGraph plumbing. |
| onnxruntime/core/framework/stream_execution_context.h | Introduces atomic wait/notify completion barrier, first-failure status wrapper, and stop-token integration into the stream execution context. |
| onnxruntime/core/framework/stream_execution_context.cc | Implements first-failure publication, stop fan-out, scope-guarded task completion, and the new blocking WaitAll. |
| onnxruntime/core/framework/sequential_executor.h | Updates executor APIs to take std::stop_token rather than a bool& terminate flag. |
| onnxruntime/core/framework/sequential_executor.cc | Threads cancellation token through scheduling, kernel execution contexts, and partial execution. |
| onnxruntime/core/framework/sequential_execution_plan.h | Updates execution-step interface to take a stop-token. |
| onnxruntime/core/framework/run_options.cc | Implements mutex-protected stop-source state for RunOptions terminate/set/unset with snapshot semantics. |
| onnxruntime/core/framework/partial_graph_execution_state.h | Adds stop-token propagation for training partial execution contexts. |
| onnxruntime/core/framework/partial_graph_execution_state.cc | Resets/reuses StreamExecutionContext with per-run stop-token and re-computes valid streams consistently. |
| onnxruntime/core/framework/op_kernel_context_internal.h | Stores and exposes a per-run cancellation stop-token to kernels via OpKernelContextInternal. |
| onnxruntime/core/framework/execution_steps.h | Updates step Execute signatures to use stop-token. |
| onnxruntime/core/framework/execution_steps.cc | Threads stop-token through kernel launch and downstream scheduling steps. |
| onnxruntime/contrib_ops/cpu/transformers/greedy_search_impl_gpt.h | Propagates stop-token cancellation into contrib subgraph execution. |
| onnxruntime/contrib_ops/cpu/transformers/beam_search_impl_whisper.h | Propagates stop-token cancellation into contrib subgraph execution. |
| onnxruntime/contrib_ops/cpu/transformers/beam_search_impl_t5.h | Propagates stop-token cancellation into contrib subgraph execution. |
| onnxruntime/contrib_ops/cpu/transformers/beam_search_impl_gpt.h | Propagates stop-token cancellation into contrib subgraph execution. |
| include/onnxruntime/core/framework/run_options.h | Updates public RunOptions to provide stop-token based termination APIs and internal termination state storage. |
| cmake/CMakeLists.txt | Adds an AppleClang(<17) compile/link flag for libc++ stop-token support on legacy Xcode. |
Fix cross-platform CI failures introduced by the std::stop_token-based cancellation: - std::stop_token/stop_source/stop_callback are unavailable on the Android NDK libc++ and require -fexperimental-library on Apple libc++ before Xcode 26 (which then fails to link libc++experimental on iOS). Replace them with an in-house CancellationToken/CancellationSource/ CancellationCallback in core/framework/cancellation.h that mirrors the needed API using only <atomic>/<mutex>/<condition_variable>, available on every supported toolchain. std::atomic wait/notify (used by CountDownBarrier) is portable and kept as-is. - Make OrtRunOptions special members inline again so dynamically loaded provider unit-test libraries can resolve the constructor; ONNX Runtime only exports the C API, so an out-of-line OrtRunOptions() was an undefined symbol for libonnxruntime_providers_*_ut.so (CUDA/TensorRT). - Remove the -fexperimental-library CMake block that broke the iOS link. Public C ABI signatures and the cancellation error text are unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The modernized non-spinning executor synchronization and stop-token-style cancellation pull in the C++20 atomic wait/notify runtime, std::mutex/ condition_variable, and shared cancellation state that replace the former WaitAll() busy-spin and the plain OrtRunOptions::terminate bool. This grows the MinSizeRel arm64 libonnxruntime.so of the minimal-baseline Android build from ~1.41 MB to ~1.46 MB (measured sections total 1463980 bytes), which exceeds the previous 1440768-byte budget. Raise the threshold to 1474560 bytes (1440 KB) to cover the measured size with modest headroom. Increase is inherent to the feature. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…modernize-executor-waits
… notify - CountDownBarrier: store the counter as std::atomic<int32_t> instead of std::atomic_int_fast32_t so Set()/Get()/Inc() and the Inc() overflow guard all use the same 32-bit range (int_fast32_t is 64-bit on x86-64 Linux); Get() no longer needs a narrowing cast. - RunSince: mark the gsl::finally CompleteTask guard [[maybe_unused]] to avoid an unused-variable warning under MSVC /W4 /WX. - parallel_executor_test: remove the dead started_count_.notify_all(); the WaitForStartedCount helper polls and never waits on the atomic. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Build one CPU-only InferenceSession directly and run it concurrently instead of relying on OpTester provider enumeration. TensorRT builds enumerate only TensorRT in OpTester, so excluding TensorRT previously skipped both runs and made the fanout/reset coverage fail without exercising cancellation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Includes the upstream Windows QNN Java toolchain fix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Review synthesis — synchronization & cancellation modernizationI ran a multi-angle review over the whole diff (readability, correctness, adversarial/race, memory-model/spec, cross-module integration). Overall this is a solid, well-tested refactor and the hard core holds up: the Two issues in the new Major1.
(The path where 2. The 2-party stream barriers are Reachability depends on whether reused partial-graph contexts run a plan with 3. No dedicated conformance test for the new primitive — the 4. The parameter named Minor
Note on the
|
Keep completion state alive through the final atomic notification, rearm cross-stream barriers when partial execution contexts are reused, and avoid throwing task construction after reserving completion work. Harden portable cancellation and RunOptions move semantics, and add focused conformance tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Resolve the PR branch against the latest upstream main. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> # Conflicts: # .github/workflows/android.yml
Description
Modernize graph execution synchronization and cancellation using C++20 primitives:
StreamExecutionContext::WaitAll()busy-spinning withstd::atomic::wait/notify_alland acquire-release task-count publication.Session::Run()entry and propagate it by value through execution steps, kernel contexts, partial execution, CPU control-flow subgraphs, and contrib generation/search subgraphs.RunOptionsSetTerminatesafe during active runs.UnsetTerminateinstalls a fresh stop state for future runs while already-snapshotted tokens remain stopped.RunSince()exit and exception path complete its task exactly once with a scope guard.notify_all(), and re-arm cross-stream barriers when a partial-execution context is reused.onnxruntime::CancellationToken/CancellationSource/CancellationCallbackincore/framework/cancellation.h) rather thanstd::stop_token, because the<stop_token>family is not uniformly available across ORT's configured toolchains (see Portability). It providesstd::stop_callback-equivalent registration/deregistration semantics.OrtRunOptionsgains user-defined copy/move members so that a copied or moved-from instance keeps an independent, usable termination state. These allocate a small shared state and are therefore notnoexcept.Exiting due to terminate flag being set to true.). NoORT_API_VERSIONchange is required; the only public C header change is a documentation clarification onUnsetTerminate.Motivation and Context
The parallel executor previously kept the caller runnable until all worker tasks completed:
Under concurrent
ORT_PARALLELruns, these waiting callers compete with executor workers and can consume most available CPU capacity. The previous implementation also read and wrote a non-thread-safeStatusfrom multiple workers and used a plainbooltermination flag even though the API permits termination from another thread.The task counter now forms an acquire-release chain: every worker publishes its writes through an acq_rel decrement, the final transition to zero notifies waiters, and the caller observes completion with an acquire load. Downstream work increments the count before scheduling and before the parent can decrement, and incrementing a zero count is rejected.
RunOptionsnow uses a mutex-protected stop source. Active runs hold token snapshots, so one terminate request cancels all currently active runs using the options object, while reset affects only future runs. Internal worker failures use a local linked cancellation source so they stop sibling workers without mutating user-owned options.Performance
Completion primitive
On a 24-logical-CPU Xeon w5-2455X with 32 waiters, using an alternating preserved-binary A/B benchmark:
The sustained 32-waiter completion benchmark had a wall-time cost (795.8 us -> 1038.8 us, +30.5%) because parked threads must be rescheduled. The intended benefit is CPU efficiency and reduced worker starvation, not universally lower primitive wake latency.
End-to-end
Session::RunClean parent/modified Release binaries were compared with real CPU inference, one shared session,
ORT_PARALLEL, intra-op=1, inter-op=12, deterministic inputs, 10 alternating paired rounds, and paired-bootstrap 95% confidence intervals.SqueezeNet:
[+36.1%, +60.8%][+42.8%, +78.2%][+83.3%, +116.4%]For concurrency 24, 32, and 48, throughput and CPU/request improved in all 10 paired runs (two-sided exact sign test
p=0.002). At concurrency 48, effective process occupancy fell from 18.87 to 9.31 cores.For latency-sensitive MNIST at concurrency 48:
A 32-way
ORT_SEQUENTIALnegative control, which does not wait in the parallel executor, was neutral for average latency, throughput, P95/P99, and effective CPU occupancy. Low-concurrency parallel latency and throughput were also statistically inconclusive. This is a workload- and concurrency-specific improvement, not a universal latency claim.Measurement provenance: the end-to-end A/B compared
361184e619(base) against92e204b154(this change's initial implementation), on a single Windows/x86-64 host. Subsequent commits on this branch were the portability swap to the in-house cancellation primitive, lifetime/reuse hardening, and added tests; none of them alter the park-instead-of-spin completion mechanism that the measurement isolates.Validation
Validated on Windows/MSVC Release at the current head, which includes a merge of
main:onnxruntime_test_all: 1,899 tests from 147 suites — 1,872 passed, 27 skipped, 0 failures.CancellationTest.*,StreamExecutionContextTest.*,RunOptionsTerminationTest.*,ParallelExecutor.*): 15 tests from 4 suites, all passed.Loop.*:Scan*.*:If.*:ControlFlow*.*): 64 tests from 4 suites, all passed.Loop.InfiniteLoopTermination: passed using the thread-safeRunOptionshelper.onnxruntime_pybind11_statecompiled.onnxruntime_benchmarkbuilt and executed.git diff --check: clean.TSAN was not run because this Windows/MSVC environment does not have a readily configured TSAN target. This is the main acknowledged gap: the change is fundamentally about memory ordering, and repeated runs exercise timing rather than the happens-before graph. Reviewers who can run the focused suites under TSAN on Linux are very welcome to.
Portability
ORT already requires C++20, and
std::atomic::wait/notify_allis available across ORT's configured toolchains (MSVC, Android NDK 28 libc++, Emscripten 4.0.23), so the completion barrier uses it directly.The
<stop_token>family (std::stop_token/std::stop_source/std::stop_callback) is not uniformly available: Android NDK 28 libc++ does not provide it, and older Apple libc++ only exposes it behind-fexperimental-library, which in turn fails to linklibc++experimentalon iOS. Cancellation therefore uses a small portable in-house primitive built only on<atomic>,<mutex>,<condition_variable>, and<memory>, withstd::stop_callback-equivalent semantics (the callback destructor blocks until a concurrent invocation completes, with a same-thread guard to avoid self-deadlock). No experimental-library flag is required, and the previously proposed AppleClang compile/link flag has been removed. The primitive has dedicated conformance tests inonnxruntime/test/framework/cancellation_test.cccovering post-cancellation registration, exactly-once invocation under competing stop requests, destructor/invocation races, and callback-driven release of the owning state.The added synchronization/cancellation runtime slightly increases the minimal MinSizeRel arm64
libonnxruntime.so, so the AndroidAndroidBinarySizeCheckJob_MinimalBaselinesize budget was raised. The current value (1,484,800 bytes) also absorbs an independent increase made onmain.On threaded Wasm workers, atomic waiting can park. Browser main threads cannot block in
Atomics.wait, so Emscripten may retain spinning behavior there while preserving correctness.