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
17 changes: 13 additions & 4 deletions include/onnxruntime/core/session/onnxruntime_c_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -6895,11 +6895,20 @@ struct OrtApi {

/** \brief Validate a compiled model's compatibility information for one or more EP devices.
*
* \param[in] ep_devices The EP devices to validate against (e.g., from GetEpDevices).
* All devices must belong to the same execution provider.
* \param[in] num_ep_devices The number of EP devices provided.
* Validates an opaque compatibility string against the ordered EP device configuration that the caller intends to
* use. The caller is not expected to know which devices were used to compile the model. Device order may be
* significant, and the EP factory interprets the configuration using the same selection, fallback, and participation
* rules as OrtEpFactory::CreateEp.
*
* If the model is subsequently loaded, the caller should pass the same OrtEpDevice values in the same order to
* SessionOptionsAppendExecutionProvider_V2. This function validates a caller-selected configuration; it does not
* discover or return the device configuration for which an opaque compiled model was produced.
*
* \param[in] ep_devices The ordered EP devices to validate against (e.g., from GetEpDevices).
* All devices must belong to the same execution provider factory.
* \param[in] num_ep_devices The number of EP devices provided. Must be greater than zero.
* \param[in] compatibility_info The compatibility info string produced when the model was compiled.
* \param[out] out_status The resulting compatibility status for the EP devices.
* \param[out] out_status The compatibility status for the intended EP device configuration.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
Expand Down
12 changes: 9 additions & 3 deletions include/onnxruntime/core/session/onnxruntime_cxx_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -1187,9 +1187,15 @@ struct EpDevice : detail::EpDeviceImpl<OrtEpDevice> {

/** \brief Validate a compiled model's compatibility for one or more EP devices.
*
* Throws on error. Returns the resulting compatibility status.
* /// \param ep_devices The EP devices to check compatibility against.
* /// \param compatibility_info The compatibility string from the precompiled model to validate.
* The EP interprets the ordered device configuration using the same selection, fallback, and participation rules as
* OrtEpFactory::CreateEp. If the model is subsequently loaded, pass the same devices in the same order to
* Ort::SessionOptions::AppendExecutionProvider_V2.
*
* Throws on error.
*
* \param ep_devices The non-empty, ordered EP device configuration to check.
* \param compatibility_info The opaque compatibility string from the precompiled model.
* \return The compatibility status for the intended EP device configuration.
*/
OrtCompiledModelCompatibility GetModelCompatibilityForEpDevices(
const std::vector<ConstEpDevice>& ep_devices,
Expand Down
51 changes: 42 additions & 9 deletions include/onnxruntime/core/session/onnxruntime_ep_c_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -2400,6 +2400,11 @@ struct OrtEp {
* used to compile a given model. This string can later be used with ValidateCompiledModelCompatibilityInfo
* to determine if a compiled model is compatible with the EP.
*
* The format and contents of the string are defined by the EP and are opaque to ORT and applications. The EP should
* include enough information to determine whether the compiled model can run with a future EP configuration. This may
* include the devices used to compile the model, but applications are not expected to parse or otherwise recover that
* information from the string.
*
* The returned string should be a null-terminated, UTF-8 encoded string. ORT will copy it.
*
* \param[in] this_ptr The OrtEp instance.
Expand Down Expand Up @@ -2871,20 +2876,48 @@ struct OrtEpFactory {
*/
ORT_API_T(const char*, GetVersion, _In_ const OrtEpFactory* this_ptr);

/** \brief Validate the compatibility of a compiled model with the execution provider factory for one or more devices.
/** \brief Validate a compiled model against an ordered execution provider device configuration.
*
* Given a compatibility info string produced during model compilation, the EP factory determines whether the compiled
* model can run when the EP is created with the provided hardware devices. The string is opaque to ORT and the caller;
* the caller is not expected to know which devices were used to compile the model.
*
* `devices` is the non-empty, ordered device configuration that the caller intends to use. The caller should pass the
* corresponding OrtEpDevice values, in the same order, to OrtApi::SessionOptionsAppendExecutionProvider_V2 if it
* subsequently creates a session for the compiled model. Device order may be significant.
*
* The implementation must interpret the configuration using the same device selection, fallback, and participation
* rules as OrtEpFactory::CreateEp:
*
* - If CreateEp would select one effective device and ignore the remaining entries, validate that effective device.
* - If CreateEp would treat the entries as alternative or fallback devices, determine whether it can select a
* compatible execution plan. An unused device must not make the result less compatible.
* - If CreateEp would use multiple devices together, every device required by the effective execution plan must be
* compatible with its assigned part of the compiled model.
*
* Do not unconditionally validate each device independently and return the worst result. The meaning of a multi-device
* configuration is EP-defined. For example, an artifact compiled for one device can be optimal for an ordered
* configuration in which that device is selected and later entries are ignored. Conversely, an artifact partitioned
* across two devices is unsupported if either required device is absent.
*
* Given a compatibility info string produced during model compilation, the EP factory should determine whether the
* compiled model is compatible with the EP factory when targeting the provided hardware devices. All devices provided
* must belong to the same execution provider instance that this factory creates.
* A supported result must mean that creating the EP and loading the compiled model with the equivalent device
* configuration will not fail due to compiled-model incompatibility. OrtCompiledModelCompatibility_EP_NOT_APPLICABLE
* is an overall "no compatibility determination" result; it is not a required identity value for combining
* per-device results.
*
* The EP factory implementation should consider the set of devices (e.g., multi-adapter or multi-GPU scenarios) when
* evaluating compatibility and set `model_compatibility` accordingly.
* \note Validation can be performed outside of a session. For example, OrtApi::GetModelCompatibilityForEpDevices
* does not require a session, and model package variant selection calls this function to exclude incompatible
* variants before a session is created. The session options and per-device EP metadata that may later be passed to
* OrtEpFactory::CreateEp may therefore be unknown when this is called, and are not provided to this callback. An EP
* whose compatibility decision depends on configuration not represented by the hardware devices or compatibility
* string should avoid reporting a supported result unless it can make a reliable determination from the available
* information.
*
* \param[in] this_ptr The OrtEpFactory instance.
* \param[in] devices Array of OrtHardwareDevice pointers that the EP would run on. All must map to this EP.
* \param[in] num_devices Number of entries in `devices`.
* \param[in] devices Ordered array of OrtHardwareDevice pointers for the intended EP configuration.
* \param[in] num_devices Number of entries in `devices`. Must be greater than zero.
* \param[in] compatibility_info The compatibility information string produced when the model was compiled.
* \param[out] model_compatibility OrtCompiledModelCompatibility value describing the compatibility of the model with the EP.
* \param[out] model_compatibility Compatibility of the model with the intended EP device configuration.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
Expand Down
105 changes: 50 additions & 55 deletions onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

#include <cassert>
#include <limits>
#include <optional>
#include <string_view>

#include "ep.h"
#include "ep_allocator.h"
Expand All @@ -29,6 +31,29 @@ int CompatibilityRank(OrtCompiledModelCompatibility c) {
return 0;
}
}

constexpr std::string_view kVersionKey = "version=";
constexpr std::string_view kOrtApiVersionKey = "ort_api_version=";
constexpr std::string_view kHardwareArchitectureKey = "hardware_architecture=";

std::optional<std::string> GetCompatibilityField(const std::string& info, std::string_view key) {
for (size_t search_start = 0;;) {
const size_t key_position = info.find(key.data(), search_start, key.size());
if (key_position == std::string::npos) {
return std::nullopt;
}

if (key_position == 0 || info[key_position - 1] == ';') {
const size_t value_start = key_position + key.size();
const size_t value_end = info.find(';', value_start);
return value_end == std::string::npos
? info.substr(value_start)
: info.substr(value_start, value_end - value_start);
}

search_start = key_position + 1;
}
}
} // namespace

ExampleEpFactory::ExampleEpFactory(const char* ep_name, ApiPtrs apis, const OrtLogger& default_logger,
Expand Down Expand Up @@ -555,8 +580,8 @@ OrtStatus* ORT_API_CALL ExampleEpFactory::CreateExternalResourceImporterForDevic

OrtStatus* ORT_API_CALL ExampleEpFactory::ValidateCompiledModelCompatibilityInfoImpl(
OrtEpFactory* this_ptr,
const OrtHardwareDevice* const* /*devices*/,
size_t /*num_devices*/,
const OrtHardwareDevice* const* devices,
size_t num_devices,
const char* compatibility_info,
OrtCompiledModelCompatibility* model_compatibility) noexcept {
auto& factory = *static_cast<ExampleEpFactory*>(this_ptr);
Expand All @@ -565,79 +590,49 @@ OrtStatus* ORT_API_CALL ExampleEpFactory::ValidateCompiledModelCompatibilityInfo
return factory.ort_api.CreateStatus(ORT_INVALID_ARGUMENT, "model_compatibility cannot be nullptr");
}

// Parse the compatibility info to check if it matches our current configuration.
// The expected format is "ExampleEP;version=0.1.0;ort_api_version=24".
// For this example implementation, we simply check if the string starts with our EP name.
// Match CreateEpImpl's device-configuration requirements. A multi-device EP would instead apply the same
// selection, fallback, or joint-participation rules that its CreateEp implementation uses.
if (devices == nullptr || num_devices != 1 || devices[0] == nullptr) {
return factory.ort_api.CreateStatus(ORT_INVALID_ARGUMENT,
"Example EP only supports selection for one device.");
}

if (compatibility_info == nullptr || compatibility_info[0] == '\0') {
*model_compatibility = OrtCompiledModelCompatibility_EP_UNSUPPORTED;
*model_compatibility = OrtCompiledModelCompatibility_EP_NOT_APPLICABLE;
return nullptr;
}

std::string info(compatibility_info);
std::string expected_prefix = factory.ep_name_ + ";";

if (info.find(expected_prefix) != 0) {
// The compatibility info doesn't match our EP
*model_compatibility = OrtCompiledModelCompatibility_EP_UNSUPPORTED;
const std::string info(compatibility_info);
if (info.find(factory.ep_name_ + ";") != 0) {
*model_compatibility = OrtCompiledModelCompatibility_EP_NOT_APPLICABLE;
return nullptr;
}

// Parse version parts: "ExampleEP;version=X;ort_api_version=Y"
// Look for "version=" and extract the value
size_t version_pos = info.find("version=");
size_t ort_version_pos = info.find("ort_api_version=");

if (version_pos == std::string::npos) {
// Invalid format
// Expected format:
// "<ep_name>;version=<semver>;ort_api_version=<ORT_API_VERSION>;hardware_architecture=<architecture>".
const auto ep_version = GetCompatibilityField(info, kVersionKey);
if (!ep_version.has_value()) {
*model_compatibility = OrtCompiledModelCompatibility_EP_UNSUPPORTED;
return nullptr;
}

// Extract EP version (between "version=" and the next ";")
size_t version_start = version_pos + 8; // length of "version="
size_t version_end = info.find(';', version_start);
std::string ep_version = (version_end != std::string::npos)
? info.substr(version_start, version_end - version_start)
: info.substr(version_start);

// Check if the EP version matches our version
if (ep_version != factory.ep_version_) {
// Different EP version - might work but prefer recompilation
if (*ep_version != factory.ep_version_) {
*model_compatibility = OrtCompiledModelCompatibility_EP_SUPPORTED_PREFER_RECOMPILATION;
return nullptr;
}

// Check ORT API version if present
if (ort_version_pos != std::string::npos) {
size_t ort_version_start = ort_version_pos + 16; // length of "ort_api_version="
size_t ort_version_end = info.find(';', ort_version_start);
std::string ort_version = (ort_version_end != std::string::npos)
? info.substr(ort_version_start, ort_version_end - ort_version_start)
: info.substr(ort_version_start);
std::string current_ort_version = std::to_string(ORT_API_VERSION);
if (ort_version != current_ort_version) {
// Different ORT version - might still work but prefer recompilation
*model_compatibility = OrtCompiledModelCompatibility_EP_SUPPORTED_PREFER_RECOMPILATION;
return nullptr;
}
const auto ort_api_version = GetCompatibilityField(info, kOrtApiVersionKey);
if (ort_api_version.has_value() && *ort_api_version != std::to_string(ORT_API_VERSION)) {
*model_compatibility = OrtCompiledModelCompatibility_EP_SUPPORTED_PREFER_RECOMPILATION;
return nullptr;
}

// Check hardware architecture compatibility if that information is included in the compatibility_info string.
size_t hardware_arch_pos = info.find("hardware_architecture=");
if (hardware_arch_pos != std::string::npos) {
size_t hardware_arch_start = hardware_arch_pos + 22; // length of "hardware_architecture="
std::string hardware_arch = info.substr(hardware_arch_start);
std::string current_hardware_arch = "arch1"; // "arch1" is for test purpose.
// Replace with actual hardware architecture detection if needed
if (hardware_arch != current_hardware_arch) {
// Different hardware architecture - might still work but prefer recompilation
*model_compatibility = OrtCompiledModelCompatibility_EP_SUPPORTED_PREFER_RECOMPILATION;
return nullptr;
}
const auto hardware_architecture = GetCompatibilityField(info, kHardwareArchitectureKey);
if (hardware_architecture.has_value() && *hardware_architecture != "arch1") {
*model_compatibility = OrtCompiledModelCompatibility_EP_SUPPORTED_PREFER_RECOMPILATION;
return nullptr;
}

// Everything matches - the compiled model is fully compatible
*model_compatibility = OrtCompiledModelCompatibility_EP_SUPPORTED_OPTIMAL;
return nullptr;
}
Expand Down
36 changes: 36 additions & 0 deletions onnxruntime/test/autoep/test_model_package.cc
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,42 @@ TEST(ModelPackageTest, CheckCompiledModelCompatibilityInfo) {
std::filesystem::remove_all(package_root, ec);
}

TEST(ModelPackageTest, ValidateCompiledModelCompatibilityInfo_UsesIntendedDeviceConfiguration) {
RegisteredEpDeviceUniquePtr example_ep;
ASSERT_NO_FATAL_FAILURE(Utils::RegisterAndGetExampleEp(*ort_env, Utils::example_ep_info, example_ep));
Ort::ConstEpDevice plugin_ep_device(example_ep.get());

const std::string ort_api_version = std::to_string(ORT_API_VERSION);
const std::string compatible_info =
"example_ep;version=0.1.0;ort_api_version=" + ort_api_version + ";hardware_architecture=arch1";
const std::string suboptimal_info =
"example_ep;version=0.1.0;ort_api_version=" + ort_api_version + ";hardware_architecture=arch2";
const std::string malformed_info = "example_ep;ort_api_version=" + ort_api_version;
const std::string foreign_info = "other_ep;version=0.1.0;ort_api_version=" + ort_api_version;

const std::vector<Ort::ConstEpDevice> intended_configuration{plugin_ep_device};
EXPECT_EQ(Ort::GetModelCompatibilityForEpDevices(intended_configuration, compatible_info.c_str()),
OrtCompiledModelCompatibility_EP_SUPPORTED_OPTIMAL);
EXPECT_EQ(Ort::GetModelCompatibilityForEpDevices(intended_configuration, suboptimal_info.c_str()),
OrtCompiledModelCompatibility_EP_SUPPORTED_PREFER_RECOMPILATION);
EXPECT_EQ(Ort::GetModelCompatibilityForEpDevices(intended_configuration, malformed_info.c_str()),
OrtCompiledModelCompatibility_EP_UNSUPPORTED);
EXPECT_EQ(Ort::GetModelCompatibilityForEpDevices(intended_configuration, ""),
OrtCompiledModelCompatibility_EP_NOT_APPLICABLE);
EXPECT_EQ(Ort::GetModelCompatibilityForEpDevices(intended_configuration, foreign_info.c_str()),
OrtCompiledModelCompatibility_EP_NOT_APPLICABLE);

// The example EP's CreateEp implementation accepts exactly one device. Validation rejects the same invalid
// configuration instead of independently validating both entries and folding their compatibility results.
const OrtEpDevice* repeated_devices[] = {example_ep.get(), example_ep.get()};
OrtCompiledModelCompatibility compatibility = OrtCompiledModelCompatibility_EP_NOT_APPLICABLE;
Ort::Status status{Ort::GetApi().GetModelCompatibilityForEpDevices(
repeated_devices, 2, compatible_info.c_str(), &compatibility)};
ASSERT_FALSE(status.IsOK());
EXPECT_EQ(status.GetErrorCode(), ORT_INVALID_ARGUMENT);
EXPECT_THAT(status.GetErrorMessage(), ::testing::HasSubstr("only supports selection for one device"));
}

TEST(ModelPackageTest, ParseVariantsFromPackageRoot) {
const auto package_root = std::filesystem::temp_directory_path() / "ort_model_package_parse_from_package_root";
BuildTwoVariantPackage(package_root,
Expand Down
Loading