Skip to content

Commit 5f94d6c

Browse files
Improve error message when EPContext node is not assigned to an EP (microsoft#27474)
### Description Improves the error message returned by ORT when loading a compiled model in a session that does not have the required execution provider(s). For example, ORT now returns the following error when creating session with a model compiled explicitly for OpenVINO EP without adding OpenVINO EP to the session: > EPContext node generated by 'OpenVINOExecutionProvider' is not compatible with any execution provider added to the session. EPContext node name: 'EPContextNode0'. Available session execution providers: [CPUExecutionProvider]. Compare the above message with the more generic message that is currently returned: >Could not find an implementation for EPContext(1) node with name 'EPContextNode0' ### Motivation and Context Improves diagnosability when loading of pre-compiled models fails. Specifically, the ambiguity of the original message led to many hours spent debugging an error where a compiled model failed to run because the expected `OrtEpDevice` was inadvertently not added to a session.
1 parent a16cf05 commit 5f94d6c

2 files changed

Lines changed: 88 additions & 3 deletions

File tree

onnxruntime/core/framework/session_state.cc

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,10 +1257,41 @@ using NodePlacementSet = std::unordered_set<std::string>;
12571257

12581258
static Status VerifyEachNodeIsAssignedToAnEpImpl(const Graph& graph, bool is_verbose,
12591259
NodePlacementMap& node_placements,
1260-
NodePlacementSet& node_placement_provider_set) {
1260+
NodePlacementSet& node_placement_provider_set,
1261+
const ExecutionProviders& providers) {
12611262
for (const auto& node : graph.Nodes()) {
12621263
const auto& node_provider = node.GetExecutionProviderType();
12631264
if (node_provider.empty()) {
1265+
// Provide a more descriptive error for EPContext nodes that were not assigned to an EP.
1266+
if (node.OpType() == "EPContext") {
1267+
// Get information about who generated the EPContext node from the 'source' attribute.
1268+
// Commonly, 'source' will be the name of the EP that generated the node, but that is not required.
1269+
// An EP may choose to use a different source identifier.
1270+
std::string source = "(unknown)";
1271+
const auto& attrs = node.GetAttributes();
1272+
auto it = attrs.find("source");
1273+
1274+
if (it != attrs.end() && it->second.has_s()) {
1275+
source = it->second.s();
1276+
}
1277+
1278+
const auto& ep_ids = providers.GetIds();
1279+
std::ostringstream session_ep_names;
1280+
1281+
for (size_t i = 0; i < ep_ids.size(); ++i) {
1282+
if (i > 0) {
1283+
session_ep_names << ", ";
1284+
}
1285+
session_ep_names << ep_ids[i];
1286+
}
1287+
1288+
return ORT_MAKE_STATUS(ONNXRUNTIME, NOT_IMPLEMENTED,
1289+
"EPContext node generated by '", source, "' is not ",
1290+
"compatible with any execution provider added to the session. ",
1291+
"EPContext node name: '", node.Name(), "'. Available session execution providers: [",
1292+
session_ep_names.str(), "].");
1293+
}
1294+
12641295
return ORT_MAKE_STATUS(ONNXRUNTIME, NOT_IMPLEMENTED,
12651296
"Could not find an implementation for ",
12661297
node.OpType(), "(", node.SinceVersion(), ") node with name '", node.Name(), "'");
@@ -1280,7 +1311,7 @@ static Status VerifyEachNodeIsAssignedToAnEpImpl(const Graph& graph, bool is_ver
12801311
const auto subgraphs = node.GetSubgraphs();
12811312
for (const auto& subgraph : subgraphs) {
12821313
ORT_RETURN_IF_ERROR(VerifyEachNodeIsAssignedToAnEpImpl(*subgraph, is_verbose, node_placements,
1283-
node_placement_provider_set));
1314+
node_placement_provider_set, providers));
12841315
}
12851316
}
12861317
}
@@ -1299,7 +1330,8 @@ static Status VerifyEachNodeIsAssignedToAnEp(const Graph& graph, const logging::
12991330
const bool is_verbose_mode = false;
13001331
#endif // !defined(ORT_MINIMAL_BUILD)
13011332

1302-
ORT_RETURN_IF_ERROR(VerifyEachNodeIsAssignedToAnEpImpl(graph, is_verbose_mode, node_placements, node_placement_provider_set));
1333+
ORT_RETURN_IF_ERROR(VerifyEachNodeIsAssignedToAnEpImpl(graph, is_verbose_mode, node_placements,
1334+
node_placement_provider_set, providers));
13031335

13041336
#if !defined(ORT_MINIMAL_BUILD)
13051337
// print placement info

onnxruntime/test/autoep/test_execution.cc

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
#include <filesystem>
5+
#include <string_view>
56
#include <vector>
67
// #include <absl/base/config.h>
78
#include <gsl/gsl>
@@ -400,6 +401,58 @@ TEST(OrtEpLibrary, PluginEp_GenEpContextModel) {
400401
}
401402
}
402403

404+
// Test loading a compiled model without registering the required EP with the session.
405+
// We expect to get an explicit error that says that an EPContext node generated by "example_ep"
406+
// was not assigned to the appropriate EP.
407+
TEST(OrtEpLibrary, PluginEp_ErrorWhenLoadEPContextModel_WithoutRequiredEp) {
408+
RegisteredEpDeviceUniquePtr example_ep;
409+
ASSERT_NO_FATAL_FAILURE(Utils::RegisterAndGetExampleEp(*ort_env, Utils::example_ep_info, example_ep));
410+
Ort::ConstEpDevice plugin_ep_device(example_ep.get());
411+
412+
// Create a compiled model for the example EP.
413+
const ORTCHAR_T* compiled_model_file = ORT_TSTR("plugin_ep_compiled_test_errorwhenloadwithoutep.onnx");
414+
{
415+
const ORTCHAR_T* input_model_file = ORT_TSTR("testdata/mul_1.onnx");
416+
std::filesystem::remove(compiled_model_file);
417+
418+
Ort::SessionOptions session_options;
419+
std::unordered_map<std::string, std::string> ep_options;
420+
421+
session_options.AppendExecutionProvider_V2(*ort_env, {plugin_ep_device}, ep_options);
422+
423+
Ort::ModelCompilationOptions compile_options(*ort_env, session_options);
424+
compile_options.SetFlags(OrtCompileApiFlags_ERROR_IF_NO_NODES_COMPILED);
425+
compile_options.SetInputModelPath(input_model_file);
426+
compile_options.SetOutputModelPath(compiled_model_file);
427+
428+
ASSERT_CXX_ORTSTATUS_OK(Ort::CompileModel(*ort_env, compile_options));
429+
ASSERT_TRUE(std::filesystem::exists(compiled_model_file));
430+
}
431+
432+
// Create a session without the plugin EP and expect an error.
433+
{
434+
Ort::SessionOptions session_options;
435+
436+
try {
437+
Ort::Session session(*ort_env, compiled_model_file, session_options);
438+
FAIL() << "Expected error when loading compiled model without the necessary EP";
439+
} catch (const Ort::Exception& e) {
440+
std::string error_msg = e.what();
441+
std::string_view expected_msg_prefix =
442+
"EPContext node generated by 'example_ep' is not compatible with any execution provider "
443+
"added to the session.";
444+
std::string_view expected_session_eps = "[CPUExecutionProvider]";
445+
446+
EXPECT_TRUE(error_msg.find(expected_msg_prefix) != std::string::npos &&
447+
error_msg.find(expected_session_eps) != std::string::npos)
448+
<< "Error should mention EPContext node's required EP and the available EPs:\n"
449+
<< error_msg;
450+
}
451+
}
452+
453+
std::filesystem::remove(compiled_model_file);
454+
}
455+
403456
// Generate an EPContext model with a plugin EP that uses a virtual GPU.
404457
TEST(OrtEpLibrary, PluginEp_VirtGpu_GenEpContextModel) {
405458
RegisteredEpDeviceUniquePtr example_ep;

0 commit comments

Comments
 (0)