Add Execution Provider conformance test suite - #28968
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a parameterized GoogleTest suite (EpContract/EpConformanceTest) under onnxruntime/test/framework/ to codify and enforce baseline invariants expected of all IExecutionProvider implementations (e.g., stable Type(), valid preferred layout, CPU memtype mapping, allocator behavior, optional data transfer correctness, and basic metadata query consistency).
Changes:
- Introduces a new parameterized conformance test fixture for
IExecutionProvidercontract invariants. - Registers CPU EP instances (arena + non-arena) and conditionally registers additional EPs behind
USE_*guards. - Validates allocator usability and (when applicable) CPU↔CPU
IDataTransfer::CopyTensorround-trip correctness.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
PreferredAllocatorsAllocateUsableMemory previously called Alloc()/Free() on every preferred allocator, only gating the host memory read/write behind UsesCpuMemory(). On WebGPU (arm64 Debug CI) the GpuBufferAllocator creates buffers mapped at creation; Free() routes through the buffer manager which throws EnforceBufferUnmapped ('Buffer is still mapped'), failing the test.
Restrict the standalone Alloc/Free exercise to CPU-accessible allocators, whose lifecycle is backend-agnostic, and GTEST_SKIP when an EP exposes no such allocator. Device allocators remain covered by PreferredAllocatorsAreNonNullAndRepeatable. Fixes the webgpu build-and-test (arm64, Debug) leg on PR #28968.
|
it's a good idea to have more tests that EP authors can use for verification.
|
|
I like the idea of defining invariants clearly |
|
Thanks @edgchen1 — agreed, the plugin A few thoughts on getting there:
So my proposal: land this PR as the |
I suppose it wouldn't hurt to test both the internal interface and the plugin EP interface, but I think the latter would be more useful for EP development going forward. Some of the ORT-owned EPs (like CUDA and WebGPU) have already been converted to plugin EPs. |
Parameterized GoogleTest suite (EpContract/EpConformanceTest) encoding universal IExecutionProvider invariants every EP must satisfy: stable Type(), valid preferred layout, CPU mem types map to CPU-accessible devices, non-null/repeatable preferred allocators, usable allocations, CPU data-transfer round-trip integrity, and consistent metadata queries. EPs are registered via factories (no static-init construction); unavailable EPs skip rather than fail. Adding a new EP is a single guarded line in GetEpConformanceParams().
PreferredAllocatorsAllocateUsableMemory previously called Alloc()/Free() on every preferred allocator, only gating the host memory read/write behind UsesCpuMemory(). On WebGPU (arm64 Debug CI) the GpuBufferAllocator creates buffers mapped at creation; Free() routes through the buffer manager which throws EnforceBufferUnmapped ('Buffer is still mapped'), failing the test.
Restrict the standalone Alloc/Free exercise to CPU-accessible allocators, whose lifecycle is backend-agnostic, and GTEST_SKIP when an EP exposes no such allocator. Device allocators remain covered by PreferredAllocatorsAreNonNullAndRepeatable. Fixes the webgpu build-and-test (arm64, Debug) leg on PR #28968.
Address PR review: in ORT_USE_EP_API_ADAPTERS builds, DefaultWebGpuExecutionProvider() ORT_ENFORCEs (aborting the entire unit-test run) when the dynamic plugin EP is initialized to a different EP name, instead of cleanly returning nullptr. Mirror the guard used by base_tester.cc and default_providers.cc by listing the built-in WebGPU EP only under 'defined(USE_WEBGPU) && !defined(ORT_USE_EP_API_ADAPTERS)'.
9a99a99 to
84c3fd4
Compare
…ext nodes, allocator info Extends the conformance suite with backend-agnostic checks that use only the base IExecutionProvider API (no dependency on the capability-segregation work): - GraphCaptureNodeAssignmentPolicyIsValid: policy is a defined enum value. - GetOrtEpIsNullForBuiltInEp: built-in EPs are not plugin-wrapped. - EpContextNodesEmptyOnFreshEp: no EPContext nodes before any compilation. - PreferredAllocatorInfoIsConsistent: allocator OrtMemoryInfo has a non-empty name and a valid allocator type. Also extends MetadataQueriesAreCallable with IsGraphCaptureEnabled() and ShouldConvertDataLayoutForOp() smoke calls. All 22 instances (11 invariants x 2 CPU configs) pass.
Extract the backend-agnostic EP conformance invariants into a shared header (test/util/include/ep_conformance_invariants.h) so the built-in and plugin suites share one source of truth. Add a plugin EP conformance suite in onnxruntime_provider_test (test/providers/ep_conformance_plugin_test.cc) that runs the same invariants against a dynamically-loaded plugin EP -- an OrtEp reached through the PluginExecutionProvider wrapper -- obtained from dynamic_plugin_ep_infra::MakeEp(). It covers arbitrary plugin EPs (example plugin EP, CUDA/WebGPU-as-plugin, or a runtime-specified one) and skips cleanly when no plugin EP is configured. Asserts the plugin-specific stable non-null GetOrtEp(). Refactor the built-in suite (test/framework/execution_provider_conformance_test.cc) to delegate to the shared invariants (behavior-preserving). Addresses review feedback to also test the plugin OrtEp interface for arbitrary plugin EPs in onnxruntime_provider_test, reusing the invariant definitions.
|
@edgchen1 done — the plugin-EP conformance suite is in, so we now cover both interfaces, with the plugin
The suite drives the plugin through the existing Verified locally (CPU RelWithDebInfo): built-in suite 22/22; plugin suite vs the example plugin EP 10 pass + 1 skip (the example EP advertises no CPU<->CPU data transfer, so that round-trip check skips). The CUDA-plugin and WebGPU-plugin CI legs exercise it against those plugin EPs. Kept in this PR per your "test both" note — happy to split the plugin suite into its own PR if you would prefer a smaller review. |
|
I recently created https://pypi.org/project/onnxruntime-ep-mlx. Would be a good candidate for a conformance test. |
|
Validated the new plugin conformance suite against
All applicable shared invariants passed, including plugin identity/lifetime, layout/device metadata, graph-capture policy, and fresh-EP state. I also added this result and the external-plugin invocation instructions to the PR description. |
|
Would implementing IDataTransfer be useful? |
|
@justinchuby I checked the MLX v0.3.0 implementation more closely. For its current memory model, implementing MLX intentionally exposes no ORT-visible device memory: It would become useful if MLX exposes an ORT-visible MLX memory domain/allocator (even host-accessible unified memory), needs explicit synchronization/copies between device identities, or wants values to remain in MLX memory across EP boundaries. Then it should implement One correction to my earlier wording: the current |
|
@justinchuby, @edgchen1, is there anything more that needs to be addressed or is it good to get your approval now? |
justinchuby
left a comment
There was a problem hiding this comment.
LGTM but I would leave it to members more familiar with this component.
Review summaryOverall this is well-built and well-documented, and it's correctly wired into the build (the Three issues are worth addressing before merge. They're all the same shape — spurious failures / false coverage confidence, not crashes — so none is structurally blocking. Major1. 2. The "factory returns 3. "Every EP" is enforced for only five EPs — Minor
Nits
What's solidThe Note: this is a static, source-grounded review against |
- Skip (not fail) when an EP factory throws during construction, e.g. CUDA's cudaSetDevice on a GPU-less machine, by wrapping MakeEp() in try/catch and logging the exception text before returning nullptr. - Stop asserting GetDeviceId() == GetDevice().Id(): GetDeviceId() is provider-defined (WebGPU returns a configurable context id while its OrtDevice id is fixed at 0) and is not required to match; it is now only smoke-called. - Drop the non-empty allocator-name assertion: an empty OrtMemoryInfo.name is permitted by the contract, so requiring one over-states it. - Strengthen CreatePreferredAllocators() repeatability: null-check the second call and compare per-position OrtMemoryInfo, not just the element count. - Rename DataTransferCpuRoundTrip* to DataTransferCpuCopy* across both suites and the shared helper (it is a single CPU-to-CPU copy, not a round-trip). - Self-document the plugin-EP factory flag (/*expects_plugin_ep=*/true), clarify in the header that coverage is opt-in per registered EP, and note that skip-capable helpers must be invoked as the last statement of a test body. - Plugin suite: move test-only includes inside the dynamic-plugin guard, drop a redundant self-namespace alias, and simplify a factory lambda. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Following up on the coverage point from the review above (the suite only covers 5 EPs, and a newly added EP is silently not conformance-tested). I looked into wiring this to the provider registry rather than the hand-maintained Keeping it out of this PR deliberately: widening the list would start running the invariants against EPs that have never been checked (QNN, OpenVINO, TensorRT, MIGraphX, CANN, CoreML...), which is the point of the change but is better landed as its own ratchet than bundled here. Tracked in #29914, with the proposed design, the four EPs that would need documented exemptions, and a staging plan. |
The allocator-name assertion was removed from the invariant helper, but the test-side comment still claimed a non-empty name is required. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Description
Adds shared, executable conformance checks for both ONNX Runtime execution-provider integration paths:
IExecutionProviderinterface.OrtEp/OrtEpFactoryABI, reached end-to-end viaPluginExecutionProviderafter dynamically loading the plugin library.The backend-agnostic invariants live in one header and are reused by both suites so their expectations cannot drift apart.
Files
onnxruntime/test/util/include/ep_conformance_invariants.honnxruntime/test/framework/execution_provider_conformance_test.ccEpContract/EpConformanceTest).onnxruntime/test/providers/ep_conformance_plugin_test.ccEpPluginConformanceTest) compiled intoonnxruntime_provider_test.Coverage
The built-in suite always covers the CPU EP in arena and non-arena configurations. CUDA, DML, WebGPU, and XNNPACK are registered behind their build guards. A factory that returns
nullptr(for example, an EP compiled but unavailable on the current machine) skips the affected test instead of failing.The plugin suite runs against whichever plugin initializes the existing dynamic-plugin test infrastructure. This includes:
ORT_UNIT_TEST_MAIN_DYNAMIC_PLUGIN_EP_CONFIG_JSONorORT_UNIT_TEST_MAIN_DYNAMIC_PLUGIN_EP_CONFIG_JSON_FILE.If no plugin is configured, the plugin tests skip cleanly.
Invariants enforced
Type()is non-empty and stable across calls and independent instances.GetPreferredLayout()returns a defined layout (NCHWorNHWC).CreatePreferredAllocators()returns no null entries and is repeatable.IDataTransferround trip preserves bytes exactly.GetDeviceId()agrees withGetDevice().Id().GetGraphCaptureNodeAssignmentPolicy()returns a defined policy value.GetOrtEp()matches provider kind: built-in EPs returnnullptr; plugin EPs return a stable, non-null backingOrtEp.GetEpContextNodes()is empty before any compilation has occurred.OrtMemoryInfo(non-empty name and valid allocator type).Only documented, backend-agnostic behavior is asserted. Non-CPU-accessible memory is never dereferenced by the test thread; backend-specific operations are skipped when their portable preconditions are unavailable.
Why this change matters
ONNX Runtime relies on more than 20 execution providers being substitutable behind a common framework contract, while external EP authors are expected to implement the public plugin ABI. Previously, the assumptions shared by those paths were implicit: distributed across interface comments, adapter logic, and downstream framework code.
That creates three recurring costs:
These suites turn the common baseline into an executable checklist and exercise the plugin ABI through the same adapter path used in production.
Running against an external plugin EP
Build
onnxruntime_provider_test, point the dynamic-plugin test infrastructure at the plugin library, and run only the plugin conformance suite. For example:The registration and selected EP names must match the names exposed by the plugin factory.
Testing
Local Windows RelWithDebInfo validation:
This is 11 shared invariants across the CPU arena and non-arena configurations. Plugin-enabled CI jobs run the same invariant set through the dynamically loaded plugin path; unsupported backend-specific portions skip rather than fail.
External plugin validation on macOS 14 / Apple Silicon:
onnxruntime-ep-mlx==0.3.0(MLXExecutionProvider, public plugin ABI version 27).onnxruntime_provider_testbuilt from this PR (ORT API version 28).The two skips are expected and contract-valid: MLX exposes no preferred allocator to exercise directly and no
IDataTransfer. All applicable shared invariants passed, including plugin identity/lifetime (GetOrtEp()), layout/device metadata, graph-capture policy, and fresh-EP state.Relationship to EP capability segregation (#29087)
This PR validates the baseline behavior common to every EP. It intentionally does not infer support for optional capabilities from legacy default virtual methods.
#29087 adds explicit capability-query hooks. If that work lands, capability-specific conformance checks (graph capture, tuning, compilation, and data-layout decisions) can be added only for EPs that advertise the corresponding capability. The PRs remain independent: this suite does not require #29087, and #29087 does not require this suite.