Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion cmake/onnxruntime_providers_cuda_plugin.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -412,8 +412,27 @@ if(NOT DEFINED onnxruntime_PLUGIN_EP_VERSION)
set(onnxruntime_PLUGIN_EP_VERSION "${ORT_VERSION}-dev")
endif()

# Bake the minimum compatible ORT version (the single source of truth lives in
# plugin-ep-cuda/MIN_ONNXRUNTIME_VERSION) into the EP DLL so it can be enforced at runtime by
# onnxruntime::ep::ApiInit(). Format is strict "MAJOR.MINOR.PATCH".
set(_ORT_PLUGIN_EP_CUDA_MIN_ORT_VERSION_FILE "${REPO_ROOT}/plugin-ep-cuda/MIN_ONNXRUNTIME_VERSION")
# Re-run CMake configure when the version file changes so the baked-in value stays in sync.
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${_ORT_PLUGIN_EP_CUDA_MIN_ORT_VERSION_FILE}")
file(STRINGS "${_ORT_PLUGIN_EP_CUDA_MIN_ORT_VERSION_FILE}" _ORT_PLUGIN_EP_CUDA_MIN_ORT_VERSION LIMIT_COUNT 1)
string(STRIP "${_ORT_PLUGIN_EP_CUDA_MIN_ORT_VERSION}" _ORT_PLUGIN_EP_CUDA_MIN_ORT_VERSION)
if(NOT _ORT_PLUGIN_EP_CUDA_MIN_ORT_VERSION)
message(FATAL_ERROR "CUDA plugin EP minimum ORT version file is missing or empty: "
"${_ORT_PLUGIN_EP_CUDA_MIN_ORT_VERSION_FILE}")
endif()
# ApiInit() strictly parses "MAJOR.MINOR.PATCH"; fail fast on any malformed value.
if(NOT _ORT_PLUGIN_EP_CUDA_MIN_ORT_VERSION MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+$")
message(FATAL_ERROR "CUDA plugin EP minimum ORT version must be \"MAJOR.MINOR.PATCH\", got "
"\"${_ORT_PLUGIN_EP_CUDA_MIN_ORT_VERSION}\" from "
"${_ORT_PLUGIN_EP_CUDA_MIN_ORT_VERSION_FILE}")
endif()

# Symbol visibility — only export CreateEpFactories and ReleaseEpFactory
target_compile_definitions(onnxruntime_providers_cuda_plugin PRIVATE ORT_API_MANUAL_INIT BUILD_CUDA_EP_AS_PLUGIN ORT_USE_EP_API_ADAPTERS=1 ONNX_ML=1 ONNX_NAMESPACE=onnx ONNX_USE_LITE_PROTO=1 ORT_PLUGIN_EP_VERSION="${onnxruntime_PLUGIN_EP_VERSION}")
target_compile_definitions(onnxruntime_providers_cuda_plugin PRIVATE ORT_API_MANUAL_INIT BUILD_CUDA_EP_AS_PLUGIN ORT_USE_EP_API_ADAPTERS=1 ONNX_ML=1 ONNX_NAMESPACE=onnx ONNX_USE_LITE_PROTO=1 ORT_PLUGIN_EP_VERSION="${onnxruntime_PLUGIN_EP_VERSION}" ORT_PLUGIN_EP_MIN_ORT_VERSION="${_ORT_PLUGIN_EP_CUDA_MIN_ORT_VERSION}")

if (onnxruntime_USE_CUDA_NHWC_OPS)
target_compile_definitions(onnxruntime_providers_cuda_plugin PRIVATE ENABLE_CUDA_NHWC_OPS)
Expand Down
28 changes: 28 additions & 0 deletions docs/cuda_plugin_ep/QUICK_START.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ build.bat --cmake_generator "Visual Studio 17 2022" --config Release --build_whe
--cmake_extra_defines "onnxruntime_BUILD_CUDA_EP_AS_PLUGIN=ON"
```

## Minimum ONNX Runtime Version

The plugin is compiled against the ONNX Runtime headers in this repository, but it is designed to load into an **older** ONNX Runtime runtime as well. The minimum compatible version is declared in [`plugin-ep-cuda/MIN_ONNXRUNTIME_VERSION`](../../plugin-ep-cuda/MIN_ONNXRUNTIME_VERSION) (currently **1.24.4**) and is the single source of truth:

- It is baked into the plugin library at build time (`ORT_PLUGIN_EP_MIN_ORT_VERSION`) and enforced at load time.
- It is documented as the minimum core ONNX Runtime version in the `onnxruntime-ep-cuda12`/`onnxruntime-ep-cuda13` Python wheel and NuGet package READMEs. The plugin packages do **not** declare a hard dependency on the core `onnxruntime` package, so you install a compatible `onnxruntime` (version `>=<min>`) separately.

At load time, `CreateEpFactories()` negotiates the API version with the runtime: it reads the runtime's reported version, enforces the minimum, and requests the `OrtApi` that matches the **runtime** (not the build). If the runtime is older than the minimum, registration fails with a descriptive error instead of crashing. As a result, the same plugin binary works with any ONNX Runtime from the minimum version onward, including runtimes newer than the version it was built against (API versions are backward compatible because they are only appended to).

## Running

When the plugin is built, it will produce `libonnxruntime_providers_cuda_plugin.so` (or `.dll` on Windows) in the build output directory alongside `libonnxruntime.so`.
Expand Down Expand Up @@ -150,6 +159,25 @@ python test_cuda_plugin_ep.py

The script validates plugin registration, device enumeration, provider options, operator coverage, and that key nodes are actually assigned to `CudaPluginExecutionProvider`.

### Test against the minimum supported ORT version

The plugin must keep working on the oldest supported ONNX Runtime (see [Minimum ONNX Runtime Version](#minimum-onnx-runtime-version)), not just the version it was built against. To validate this locally, install the minimum base runtime and run the same test against the freshly built plugin library:

```bash
# 1. Build the plugin (see "Build Instructions"). This produces the plugin .so and installs the
# matching, freshly built onnxruntime wheel.

# 2. Replace the base runtime with the minimum supported version.
pip install "onnxruntime==$(cat plugin-ep-cuda/MIN_ONNXRUNTIME_VERSION)" --force-reinstall

# 3. Point the test at the freshly built plugin library and run it.
export ORT_CUDA_PLUGIN_PATH=/path/to/build/Release/libonnxruntime_providers_cuda_plugin.so
cd onnxruntime/test/python/transformers
python test_cuda_plugin_ep.py
```

This loads the plugin (compiled against the latest headers) into the minimum supported ONNX Runtime, exercising the API-version negotiation in `CreateEpFactories()`. If the plugin accidentally depends on an API newer than the declared minimum, the test fails here. The same check runs automatically in the `Test Linux CUDA Plugin EP` CI stage.


## Verification
You can generate a parity report comparing the kernels available in the plugin EP versus the statically linked CUDA EP.
Expand Down
39 changes: 39 additions & 0 deletions docs/cuda_plugin_ep/cuda_plugin_ep_design.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,45 @@ The plugin exports exactly two C symbols:

All other symbols have hidden visibility.

### 2.5 ORT Version Compatibility and API Negotiation

The plugin is built against the ORT headers in this repository (`ORT_API_VERSION`) but is designed to load into an **older** ORT runtime as well, down to the floor declared in [`plugin-ep-cuda/MIN_ONNXRUNTIME_VERSION`](../../plugin-ep-cuda/MIN_ONNXRUNTIME_VERSION) (currently `1.24.4`). The floor is a single source of truth:

- **Build time:** `cmake/onnxruntime_providers_cuda_plugin.cmake` reads the file and bakes it into the DLL as the `ORT_PLUGIN_EP_MIN_ORT_VERSION` preprocessor definition.
- **Packaging:** the Python wheel (`onnxruntime>=<floor>`, via `plugin-ep-cuda/python/build_wheel.py`) and the NuGet package (`plugin-ep-cuda/csharp/pack_nuget.py`) read the same file.

`CreateEpFactories()` negotiates the API version with the runtime instead of hard-coding it:

1. It calls `onnxruntime::ep::ApiInit(ort_api_base, ORT_PLUGIN_EP_MIN_ORT_VERSION)` (from `include/onnxruntime/ep/api.h`). `ApiInit()` parses the runtime version string reported by `OrtApiBase::GetVersionString()`, enforces the minimum, requests the `OrtApi` matching the **runtime's** version, and initializes the C++ API (`Ort::InitApi`). If the runtime is older than the floor, or the requested API/EP API is unavailable, `ApiInit()` throws and the factory creation fails with a descriptive `OrtStatus` (constructed conservatively via the v1 `OrtApi`, since the C++ API is not yet initialized at that point).
2. Every EP-facing callback struct (`OrtEpFactory`, `OrtEp`, `OrtAllocator`, `OrtSyncStreamImpl`, `OrtSyncNotificationImpl`, `OrtDataTransferImpl`, `OrtEpProfilerImpl`, `OrtLoopKernelHelper`, `OrtScanKernelHelper`) initializes its `ort_version_supported`/`version` field to `ORT_API_VERSION` — the ORT API version the plugin was compiled with — consistent with every other EP API struct. ORT uses this field only to avoid reading struct fields that did not exist when the plugin was compiled; it is append-only, so reporting the compiled version is always safe. Whether the plugin may *call* a newer `OrtApi`/`OrtEpApi` function is a separate concern governed by the runtime API version (`onnxruntime::ep::CurrentOrtApiVersion()`), handled by the capability gating in §2.6 — not by lowering this field.

> **Lowering the floor.** The floor reflects the newest `OrtApi`/`OrtEpApi` function the plugin actually calls. The kernel-registry EP path (`CreateKernelRegistry`, `KernelRegistry_AddKernel`, `GetKernelRegistry`, `EpGraphSupportInfo_*`, `CreateIfKernel`/`CreateLoopKernel`/`CreateScanKernel`) is `\since 1.24`; the stream, memory-device, and data-transfer EP functions are `\since 1.23`. The `Test Linux CUDA Plugin EP` stage (`plugin-linux-cuda-test-stage.yml`) installs the floor version of the base `onnxruntime` package and runs the plugin test against it, so an accidental dependency on a newer API is caught in CI. The Python plugin-loading helpers (`register_execution_provider_library`, `get_ep_devices`, `add_provider_for_devices`) must also exist in the floor's base package; the same test validates this.

### 2.6 API Version Audit and Defensive Capability Gating

Because the plugin binary may load into an older runtime, every `OrtApi`/`OrtEpApi` function it calls must exist in that runtime. The following audit records the newest `\since` version of each API surface the plugin uses (verified against the `\since` annotations in `onnxruntime_c_api.h` and `onnxruntime_ep_c_api.h`). It is the justification for the `1.24.4` floor and identifies exactly which features depend on APIs newer than the floor.

| API surface | Newest `\since` used | Representative functions |
| --- | --- | --- |
| `OrtApi` — direct calls (`ort_api_.*`, `Ort::GetApi().*`) | **1.23** | `SyncStream_GetHandle`, `GetTensorSizeInBytes`, `GetRunConfigEntry`, `CreateMemoryInfo_V2`, `Graph_GetNumNodes`/`Graph_GetNodes` (older: `CreateStatus`, `Logger_LogMessage`, `*KeyValuePairs`, `HardwareDevice_*`, `MemoryInfoGet*`, `GetSessionConfigEntry`) |
| `OrtEpApi` — direct calls (`ep_api_.*`, `Ort::GetEpApi().*`) | **1.24** | `CreateKernelRegistry`, `KernelRegistry_AddKernel`, `ReleaseKernelRegistry`, `CreateIfKernel`/`CreateLoopKernel`/`CreateScanKernel`, `EpGraphSupportInfo_LookUpKernel` (older: `MemoryDevice_*`, `MemoryInfo_GetMemoryDevice`, `SyncStream_*`, `EpDevice_AddAllocatorInfo`, `EpGraphSupportInfo_AddSingleNode`, `CreateEpDevice`/`ReleaseEpDevice`) |
| EP profiler API (only when built with `ENABLE_CUDA_PROFILING`) | **1.25** | `CreateProfilingEvent`, `ProfilingEventsContainer_AddEvents`, `ReleaseProfilingEvent` (called from `cuda_profiler_plugin.cc` via the `Ort::ProfilingEvent` / `Ort::UnownedProfilingEventsContainer` wrappers) |

`provider_api_shims.cc` uses only internal helpers (`GetEnvironmentVar`, `MLFloat16` conversions), and the plugin uses no Model Editor, Model Package, or Compile API. **Apart from the optional EP profiler, every API the plugin calls is `\since 1.24` or older**, so the true compatibility floor is `1.24.4`.

**Defensive capability gating.** Reading a struct field is safe because the field is append-only and ORT only reads fields it knows about. The real hazard is *calling* an `OrtApi`/`OrtEpApi` function that the (possibly older) runtime does not provide. The correct guard for that is the runtime API version, `onnxruntime::ep::CurrentOrtApiVersion()`, not `ort_version_supported`. The `CudaEp` constructor (`cuda_ep.cc`) therefore reads `const uint32_t ort_version = onnxruntime::ep::CurrentOrtApiVersion();` and only installs an `OrtEp` callback when that runtime version is new enough to provide both the callback field and every API its implementation calls:

| `OrtEp` callback | `\since` | Installed only when |
| --- | --- | --- |
| `Sync` | 1.25 | `ort_version >= 25` |
| `CreateProfiler` | 1.25 | `ort_version >= 25` **and** `ENABLE_CUDA_PROFILING` |
| `IsGraphCaptureEnabled`, `IsGraphCaptured`, `ReplayGraph`, `GetGraphCaptureNodeAssignmentPolicy` | 1.26 | `ort_version >= 26` |
| `GetAvailableResource` | 1.26 | `ort_version >= 26` |

All other `OrtEp` and `OrtEpFactory` callbacks are `\since 1.24` or older and are installed unconditionally. Gating `CreateProfiler` is what makes the three `\since 1.25` profiler functions unreachable on an older runtime: when the profiler is never created, ORT never drives the `OrtEpProfilerImpl` callbacks that call them.

The gates use **graceful degradation rather than throwing**: the gated callbacks are all optional capabilities (per-run sync, EP-level GPU profiling, CUDA-graph capture/replay, device-memory budgeting), so disabling them on an older runtime still yields a fully functional EP — inference runs, just without that specific feature. This was validated by loading the plugin (built against the latest headers) into both the latest runtime (full test suite passes) and an `onnxruntime==1.24.4` runtime (the EP registers, enumerates devices, and runs inference correctly with the newer callbacks left null).

---

## 3. Type Resolution — How Kernel Code Compiles Unchanged
Expand Down
6 changes: 3 additions & 3 deletions include/onnxruntime/core/session/onnxruntime_ep_c_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -2116,10 +2116,10 @@ typedef enum OrtGraphCaptureNodeAssignmentPolicy {
* \since Version 1.22.
*/
struct OrtEp {
/** \brief The ONNX Runtime version the execution provider was compiled with.
/** \brief The ONNX Runtime API version the execution provider was compiled with.
*
* Implementation should set to ORT_API_VERSION.
* ORT will use this to ensure it does not call functions that were not available when the library was compiled.
* Implementation should set this to ORT_API_VERSION.
* ORT uses this to avoid calling functions that were not available when the EP was compiled.
*
* \since Version 1.22.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ void RestoreDeviceIfKnown(bool restore_prev_device, int prev_device) noexcept {
CudaDeviceAllocator::CudaDeviceAllocator(const OrtMemoryInfo* memory_info, int device_id)
: CudaAllocatorBase(CudaAllocatorKind::kDevice, memory_info),
device_id_(device_id) {
version = kCudaPluginEpMinOrtApiVersion;
version = ORT_API_VERSION;
Alloc = AllocImpl;
Free = FreeImpl;
Info = InfoImpl;
Expand Down Expand Up @@ -96,7 +96,7 @@ CudaExternalDeviceAllocator::CudaExternalDeviceAllocator(const OrtMemoryInfo* me
alloc_fn_(reinterpret_cast<ExternalAlloc>(alloc_fn)),
free_fn_(reinterpret_cast<ExternalFree>(free_fn)),
empty_cache_fn_(reinterpret_cast<ExternalEmptyCache>(empty_cache_fn)) {
version = kCudaPluginEpMinOrtApiVersion;
version = ORT_API_VERSION;
Alloc = AllocImpl;
Free = FreeImpl;
Info = InfoImpl;
Expand Down Expand Up @@ -171,7 +171,7 @@ CudaExternalDeviceAllocator::CudaExternalDeviceAllocator(const OrtMemoryInfo* me

CudaPinnedAllocator::CudaPinnedAllocator(const OrtMemoryInfo* memory_info)
: CudaAllocatorBase(CudaAllocatorKind::kPinned, memory_info) {
version = kCudaPluginEpMinOrtApiVersion;
version = ORT_API_VERSION;
Alloc = AllocImpl;
Free = FreeImpl;
Info = InfoImpl;
Expand Down
2 changes: 1 addition & 1 deletion onnxruntime/core/providers/cuda/plugin/cuda_arena.h
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ class CudaArenaAllocator final : public CudaAllocatorBase {
CudaArenaAllocator(CudaAllocatorKind kind, const OrtMemoryInfo* memory_info,
std::unique_ptr<ArenaImpl> impl)
: CudaAllocatorBase(kind, memory_info), impl_(std::move(impl)) {
version = kCudaPluginEpMinOrtApiVersion;
version = ORT_API_VERSION;
Alloc = AllocImpl;
Reserve = ReserveImpl;
Free = FreeImpl;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Status PluginIfKernel::CreateControlFlowKernelImpl(const OrtKernelInfo* info, Or
// ===================================================================

PluginLoopHelper::PluginLoopHelper() : OrtLoopKernelHelper{} {
ort_version_supported = kCudaPluginEpMinOrtApiVersion;
ort_version_supported = ORT_API_VERSION;
Release = ReleaseImpl;
ConcatOutput = ConcatOutputImpl;
}
Expand Down Expand Up @@ -163,7 +163,7 @@ Status PluginLoopKernel::CreateControlFlowKernelImpl(const OrtKernelInfo* info,
// ===================================================================

PluginScanHelper::PluginScanHelper() : OrtScanKernelHelper{} {
ort_version_supported = kCudaPluginEpMinOrtApiVersion;
ort_version_supported = ORT_API_VERSION;
Release = ReleaseImpl;
Transpose = TransposeImpl;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ CudaDataTransfer::CudaDataTransfer(const OrtApi& ort_api, const OrtEpApi& ep_api
: OrtDataTransferImpl{},
ort_api_(ort_api),
ep_api_(ep_api) {
ort_version_supported = kCudaPluginEpMinOrtApiVersion;
ort_version_supported = ORT_API_VERSION;
Release = ReleaseImpl;
CanCopy = CanCopyImpl;
CopyTensors = CopyTensorsImpl;
Expand Down
Loading
Loading