Skip to content
Merged
37 changes: 37 additions & 0 deletions include/onnxruntime/core/session/onnxruntime_ep_c_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -2965,6 +2965,43 @@ struct OrtEpFactory {
*/
ORT_API2_STATUS(DeinitGraphicsInterop, _In_ OrtEpFactory* this_ptr,
_In_ const OrtEpDevice* ep_device);

/** \brief Select the best compiled model candidate from metadata lists.
*
* Evaluates each candidate metadata against the given hardware devices and returns the selected index.
*
* Each candidate's OrtKeyValuePairs can have multiple entries and the entry with "ep_compatibility_info" key is
* required; its associated value is the compatibility information stored in onnx model metadata.
Comment thread
skottmckay marked this conversation as resolved.
Outdated
*
* Context about having this function:
* The existing ValidateCompiledModelCompatibilityInfo() alone is not sufficient for some EPs to determine the best
* compatible model when there are multiple candidates. For example, an EP may support multiple compilation modes
Comment thread
skottmckay marked this conversation as resolved.
* (e.g., "speed optimized" vs "memory optimized") that produce different compatibility strings. The EP can implement
* this function to evaluate the candidate strings and select the best compatible one based on its own criteria and
* the target devices.
*
* If all candidates are unsupported, this function succeeds and sets `selected_index` to SIZE_MAX.
*
* \note The implementer should validate the compatibility of each candidate (e.g., by calling
* ValidateCompiledModelCompatibilityInfo for each one) before determining the best match.
*
* \param[in] this_ptr The OrtEpFactory instance.
* \param[in] devices Target hardware devices.
* \param[in] num_devices Number of devices.
* \param[in] candidates Array of OrtKeyValuePairs pointers (one per model variant).
* \param[in] num_candidates Number of candidates.
* \param[out] selected_index Selected candidate index, or SIZE_MAX if all unsupported.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.27.
*/
ORT_API2_STATUS(SelectBestModelCandidate, _In_ OrtEpFactory* this_ptr,
_In_reads_(num_devices) const OrtHardwareDevice* const* devices,
_In_ size_t num_devices,
_In_reads_(num_candidates) const OrtKeyValuePairs* const* candidates,
_In_ size_t num_candidates,
_Out_ size_t* selected_index);
};

#ifdef __cplusplus
Expand Down
1 change: 1 addition & 0 deletions onnxruntime/core/session/plugin_ep/ep_factory_internal.cc
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ EpFactoryInternal::EpFactoryInternal(std::unique_ptr<EpFactoryInternalImpl> impl
OrtEpFactory::GetHardwareDeviceIncompatibilityDetails = Forward::GetHardwareDeviceIncompatibilityDetails;
OrtEpFactory::InitGraphicsInterop = Forward::InitGraphicsInterop;
OrtEpFactory::DeinitGraphicsInterop = Forward::DeinitGraphicsInterop;
OrtEpFactory::SelectBestModelCandidate = Forward::SelectBestModelCandidate;
}

InternalExecutionProviderFactory::InternalExecutionProviderFactory(EpFactoryInternal& ep_factory,
Expand Down
10 changes: 10 additions & 0 deletions onnxruntime/core/session/plugin_ep/ep_factory_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,16 @@ class EpFactoryInternal : public OrtEpFactory {
return impl_->DeinitGraphicsInterop(ep_device);
}

OrtStatus* SelectBestModelCandidate(_In_reads_(num_devices) const OrtHardwareDevice* const* devices,
_In_ size_t num_devices,
_In_reads_(num_candidates) const OrtKeyValuePairs* const* candidates,
_In_ size_t num_candidates,
_Out_ size_t* selected_index) noexcept {
return impl_->SelectBestModelCandidate(devices, num_devices,
candidates, num_candidates,
selected_index);
}

// Function ORT calls to release an EP instance.
void ReleaseEp(OrtEp* /*ep*/) noexcept {
// we never create an OrtEp so we should never be trying to release one
Expand Down
18 changes: 18 additions & 0 deletions onnxruntime/core/session/plugin_ep/ep_factory_internal_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,24 @@ class EpFactoryInternalImpl {
return nullptr;
}

virtual OrtStatus* SelectBestModelCandidate(
_In_reads_(num_devices) const OrtHardwareDevice* const* devices,
_In_ size_t num_devices,
_In_reads_(num_candidates) const OrtKeyValuePairs* const* candidates,
_In_ size_t num_candidates,
_Out_ size_t* selected_index) noexcept {
ORT_UNUSED_PARAMETER(devices);
ORT_UNUSED_PARAMETER(num_devices);
if (candidates == nullptr || num_candidates == 0 || selected_index == nullptr) {
return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT,
"Invalid arguments to SelectBestModelCandidate.");
}

// Default implementation: all candidates are unsupported, sets `selected_index` to SIZE_MAX.
*selected_index = SIZE_MAX;
return nullptr;
}

// Function ORT calls to release an EP instance.
void ReleaseEp(OrtEp* ep);

Expand Down
11 changes: 11 additions & 0 deletions onnxruntime/core/session/plugin_ep/forward_to_factory_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,17 @@ struct ForwardToFactoryImpl {
return static_cast<TFactory*>(this_ptr)->DeinitGraphicsInterop(ep_device);
}

static OrtStatus* ORT_API_CALL SelectBestModelCandidate(
OrtEpFactory* this_ptr,
_In_reads_(num_devices) const OrtHardwareDevice* const* devices,
size_t num_devices,
_In_reads_(num_candidates) const OrtKeyValuePairs* const* candidates,
size_t num_candidates,
size_t* selected_index) noexcept {
return static_cast<TFactory*>(this_ptr)->SelectBestModelCandidate(
devices, num_devices, candidates, num_candidates, selected_index);
}

static void ORT_API_CALL ReleaseEp(OrtEpFactory* this_ptr, OrtEp* ep) noexcept {
static_cast<TFactory*>(this_ptr)->ReleaseEp(ep);
}
Expand Down
72 changes: 72 additions & 0 deletions onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "ep_factory.h"

#include <cassert>
#include <limits>

#include "ep.h"
#include "ep_allocator.h"
Expand All @@ -13,6 +14,22 @@

#include "core/session/onnxruntime_session_options_config_keys.h"

namespace {
int CompatibilityRank(OrtCompiledModelCompatibility c) {
switch (c) {
case OrtCompiledModelCompatibility_EP_SUPPORTED_OPTIMAL:
return 3;
case OrtCompiledModelCompatibility_EP_SUPPORTED_PREFER_RECOMPILATION:
return 2;
case OrtCompiledModelCompatibility_EP_NOT_APPLICABLE:
return 1;
case OrtCompiledModelCompatibility_EP_UNSUPPORTED:
default:
return 0;
}
}
} // namespace

ExampleEpFactory::ExampleEpFactory(const char* ep_name, ApiPtrs apis, const OrtLogger& default_logger)
: OrtEpFactory{},
ApiPtrs(apis),
Expand Down Expand Up @@ -45,6 +62,7 @@ ExampleEpFactory::ExampleEpFactory(const char* ep_name, ApiPtrs apis, const OrtL
GetNumCustomOpDomains = GetNumCustomOpDomainsImpl;
GetCustomOpDomains = GetCustomOpDomainsImpl;
ValidateCompiledModelCompatibilityInfo = ValidateCompiledModelCompatibilityInfoImpl;
SelectBestModelCandidate = SelectBestModelCandidateImpl;

// setup the OrtMemoryInfo instances required by the EP.
// We pretend the device the EP is running on is GPU.
Expand Down Expand Up @@ -514,3 +532,57 @@ OrtStatus* ORT_API_CALL ExampleEpFactory::ValidateCompiledModelCompatibilityInfo
*model_compatibility = OrtCompiledModelCompatibility_EP_SUPPORTED_OPTIMAL;
return nullptr;
}

OrtStatus* ORT_API_CALL ExampleEpFactory::SelectBestModelCandidateImpl(
OrtEpFactory* this_ptr,
const OrtHardwareDevice* const* devices,
size_t num_devices,
const OrtKeyValuePairs* const* candidates,
size_t num_candidates,
size_t* selected_index) noexcept {
auto& factory = *static_cast<ExampleEpFactory*>(this_ptr);

if (selected_index == nullptr) {
return factory.ort_api.CreateStatus(ORT_INVALID_ARGUMENT, "selected_index cannot be nullptr");
}

*selected_index = std::numeric_limits<size_t>::max();

if (candidates == nullptr || num_candidates == 0) {
return factory.ort_api.CreateStatus(ORT_INVALID_ARGUMENT, "candidates cannot be nullptr or empty");
}

int best_rank = -1;
size_t best_idx = std::numeric_limits<size_t>::max();

for (size_t i = 0; i < num_candidates; ++i) {
const char* compatibility_info =
factory.ort_api.GetKeyValue(candidates[i], "ep_compatibility_info");

if (compatibility_info == nullptr) {
return factory.ort_api.CreateStatus(ORT_INVALID_ARGUMENT,
"candidate metadata is missing required key: ep_compatibility_info");
}

OrtCompiledModelCompatibility compatibility = OrtCompiledModelCompatibility_EP_UNSUPPORTED;
OrtStatus* status = ValidateCompiledModelCompatibilityInfoImpl(
this_ptr, devices, num_devices, compatibility_info, &compatibility);
if (status != nullptr) {
return status;
}

const int rank = CompatibilityRank(compatibility);
if (rank > best_rank) {
best_rank = rank;
best_idx = i;
}
}

if (best_rank <= CompatibilityRank(OrtCompiledModelCompatibility_EP_UNSUPPORTED)) {
*selected_index = std::numeric_limits<size_t>::max();
} else {
*selected_index = best_idx;
}

return nullptr;
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ class ExampleEpFactory : public OrtEpFactory, public ApiPtrs {
const char* compatibility_info,
OrtCompiledModelCompatibility* model_compatibility) noexcept;

static OrtStatus* ORT_API_CALL SelectBestModelCandidateImpl(
OrtEpFactory* this_ptr,
const OrtHardwareDevice* const* devices,
size_t num_devices,
const OrtKeyValuePairs* const* candidates,
size_t num_candidates,
size_t* selected_index) noexcept;

const std::string ep_name_; // EP name
const std::string vendor_{"Contoso"}; // EP vendor name
const uint32_t vendor_id_{0xB357}; // EP vendor ID
Expand Down
69 changes: 69 additions & 0 deletions onnxruntime/test/autoep/test_ep_compatibility.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#include <limits>
#include <string>

#include "gtest/gtest.h"

#include "core/session/onnxruntime_c_api.h"
#include "core/session/onnxruntime_cxx_api.h"
#include "core/session/abi_devices.h"
#include "test/autoep/test_autoep_utils.h"

using namespace onnxruntime;

TEST(EpCompatibilitySelectBestTest, SelectBestModelCandidate_UsesHardwareDevices) {
Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpCompatSelectBestExampleEp"};

onnxruntime::test::RegisteredEpDeviceUniquePtr example_ep;
onnxruntime::test::Utils::RegisterAndGetExampleEp(
env, onnxruntime::test::Utils::example_ep_info, example_ep);

ASSERT_NE(example_ep.get(), nullptr);

const OrtEpDevice* ep_device = example_ep.get();
OrtEpFactory* factory = ep_device->GetMutableFactory();
ASSERT_NE(factory, nullptr);
ASSERT_NE(factory->SelectBestModelCandidate, nullptr);

const OrtHardwareDevice* devices[] = {ep_device->device};

const std::string ep_name = factory->GetName(factory);

const std::string optimal =
ep_name + ";version=0.1.0;ort_api_version=" + std::to_string(ORT_API_VERSION) + ";hardware_architecture=arch1";

const std::string prefer_recompile =
ep_name + ";version=9.9.9;ort_api_version=" + std::to_string(ORT_API_VERSION) + ";hardware_architecture=arch1";

const std::string unsupported =
"SomeOtherEp;version=0.1.0;ort_api_version=" + std::to_string(ORT_API_VERSION) + ";hardware_architecture=arch1";

const OrtApi* api = OrtGetApiBase()->GetApi(ORT_API_VERSION);
ASSERT_NE(api, nullptr);

OrtKeyValuePairs* kvp0 = nullptr;
OrtKeyValuePairs* kvp1 = nullptr;
OrtKeyValuePairs* kvp2 = nullptr;
api->CreateKeyValuePairs(&kvp0);
api->CreateKeyValuePairs(&kvp1);
api->CreateKeyValuePairs(&kvp2);
api->AddKeyValuePair(kvp0, "ep_compatibility_info", unsupported.c_str());
api->AddKeyValuePair(kvp1, "ep_compatibility_info", prefer_recompile.c_str());
api->AddKeyValuePair(kvp2, "ep_compatibility_info", optimal.c_str());

const OrtKeyValuePairs* candidates[] = {kvp0, kvp1, kvp2};

size_t selected_index = std::numeric_limits<size_t>::max();
OrtStatus* st = factory->SelectBestModelCandidate(
factory, devices, 1, candidates, 3, &selected_index);

ASSERT_EQ(st, nullptr) << (st ? api->GetErrorMessage(st) : "");

EXPECT_EQ(selected_index, 2u);

api->ReleaseKeyValuePairs(kvp0);
api->ReleaseKeyValuePairs(kvp1);
api->ReleaseKeyValuePairs(kvp2);
}
Loading