diff --git a/.github/workflows/windows_openvino.yml b/.github/workflows/windows_openvino.yml index d87a4919a86fd..36cc1aebad8af 100644 --- a/.github/workflows/windows_openvino.yml +++ b/.github/workflows/windows_openvino.yml @@ -51,12 +51,12 @@ jobs: with: architecture: x64 - - name: Download OpenVINO Toolkit v2026.1.0 + - name: Download OpenVINO Toolkit v2026.2.1 env: - OpenVINOVersion: 2026.1.0 + OpenVINOVersion: 2026.2.1 shell: pwsh run: | - $Url ="https://storage.openvinotoolkit.org/repositories/openvino/packages/2026.1/windows_vc_mt/openvino_toolkit_windows_vc_mt_2026.1.0.21367.63e31528c62_x86_64.zip" + $Url ="https://storage.openvinotoolkit.org/repositories/openvino/packages/2026.2.1/windows_vc_mt/openvino_toolkit_windows_vc_mt_2026.2.1.21919.ede283a88e3_x86_64.zip" $OutputPath = "$env:RUNNER_TEMP\openvino.zip" $ExtractPath = "$env:RUNNER_TEMP\openvino-v$env:OpenVINOVersion" $TempExtractPath = "$env:RUNNER_TEMP\openvino_temp" @@ -99,7 +99,7 @@ jobs: shell: pwsh # Use $GITHUB_ENV to set the variable for subsequent steps run: | - $openVinoRootDir = Join-Path $env:RUNNER_TEMP "openvino-v2026.1.0" + $openVinoRootDir = Join-Path $env:RUNNER_TEMP "openvino-v2026.2.1" echo "OpenVINORootDir=$openVinoRootDir" >> $env:GITHUB_ENV - name: Print OpenVINORootDir after downloading OpenVINO diff --git a/cmake/onnxruntime_providers_openvino.cmake b/cmake/onnxruntime_providers_openvino.cmake index 882fc56d9a40b..1584d9f703575 100644 --- a/cmake/onnxruntime_providers_openvino.cmake +++ b/cmake/onnxruntime_providers_openvino.cmake @@ -13,8 +13,8 @@ # Header paths find_package(OpenVINO REQUIRED COMPONENTS Runtime ONNX) - if(OpenVINO_VERSION VERSION_LESS 2025.0) - message(FATAL_ERROR "OpenVINO 2025.0 and newer are supported. Please, use latest OpenVINO release") + if(OpenVINO_VERSION VERSION_LESS 2026.0) + message(FATAL_ERROR "OpenVINO 2026.0 and newer are supported. Please, use latest OpenVINO release") endif() if(OpenVINO_VERSION VERSION_GREATER_EQUAL 2024.4) diff --git a/onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc b/onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc index a21916be4c59b..b2519692a17f8 100644 --- a/onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc +++ b/onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc @@ -1,6 +1,7 @@ // Copyright (C) Intel Corporation // Licensed under the MIT License +#include #include #include #include @@ -243,10 +244,18 @@ std::shared_ptr EPCtxHandler::Initialize(const std::vectorDeserialize(ss); } } else { - ORT_THROW_IF_ERROR(utils::ValidateExternalDataPath(session_context.GetOutputModelPath(), - std::filesystem::path(ep_cache_context))); - std::filesystem::path ep_context_path = session_context.GetOutputModelPath().parent_path() / ep_cache_context; - if (ep_context_path.extension() != ".xml") { + const std::filesystem::path cache_context_path{ep_cache_context}; + // Compare the extension case-insensitively so that ".XML", ".Xml", etc. are also treated as XML. + std::string extension = cache_context_path.extension().string(); + std::transform(extension.begin(), extension.end(), extension.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + const bool is_xml = (extension == ".xml"); + const std::filesystem::path& validation_base_path = is_xml + ? session_context.GetModelPath() + : session_context.GetOutputModelPath(); + ORT_THROW_IF_ERROR(utils::ValidateExternalDataPath(validation_base_path, cache_context_path)); + const std::filesystem::path ep_context_path = validation_base_path.parent_path() / cache_context_path; + if (!is_xml) { shared_context = shared_context_manager_->GetOrCreateSharedContext(ep_context_path); shared_context->Deserialize(); } diff --git a/onnxruntime/core/providers/openvino/ov_versions/capability.cc b/onnxruntime/core/providers/openvino/ov_versions/capability.cc index e932595f48565..d5bb021ccef44 100644 --- a/onnxruntime/core/providers/openvino/ov_versions/capability.cc +++ b/onnxruntime/core/providers/openvino/ov_versions/capability.cc @@ -41,16 +41,14 @@ GetCapability::GetCapability(const EPCtxHandler& ep_ctx_handler, npu_qdq_optimizer_enabled = true; // see data_ops.cc ~615 where we check for int16 types for gpu, this may change to a better approach later } -#if OPENVINO_VERSION_MAJOR == 2026 && OPENVINO_VERSION_MINOR == 1 +#if OPENVINO_VERSION_MAJOR == 2026 && OPENVINO_VERSION_MINOR == 2 + data_ops_ = std::make_unique(graph_viewer_, V_2026_2, device_type_, npu_qdq_optimizer_enabled); +#elif OPENVINO_VERSION_MAJOR == 2026 && OPENVINO_VERSION_MINOR == 1 data_ops_ = std::make_unique(graph_viewer_, V_2026_1, device_type_, npu_qdq_optimizer_enabled); #elif OPENVINO_VERSION_MAJOR == 2026 && OPENVINO_VERSION_MINOR == 0 data_ops_ = std::make_unique(graph_viewer_, V_2026_0, device_type_, npu_qdq_optimizer_enabled); -#elif OPENVINO_VERSION_MAJOR == 2025 && OPENVINO_VERSION_MINOR == 4 - data_ops_ = std::make_unique(graph_viewer_, V_2025_4, device_type_, npu_qdq_optimizer_enabled); -#elif OPENVINO_VERSION_MAJOR == 2025 && OPENVINO_VERSION_MINOR == 3 - data_ops_ = std::make_unique(graph_viewer_, V_2025_3, device_type_, npu_qdq_optimizer_enabled); #else - data_ops_ = std::make_unique(graph_viewer_, V_2026_1, device_type_, npu_qdq_optimizer_enabled); + data_ops_ = std::make_unique(graph_viewer_, V_2026_2, device_type_, npu_qdq_optimizer_enabled); #endif } diff --git a/onnxruntime/core/providers/openvino/ov_versions/data_ops.cc b/onnxruntime/core/providers/openvino/ov_versions/data_ops.cc index 37306d97b06ab..7aae81b987c0d 100644 --- a/onnxruntime/core/providers/openvino/ov_versions/data_ops.cc +++ b/onnxruntime/core/providers/openvino/ov_versions/data_ops.cc @@ -408,7 +408,7 @@ void DataOps::populate_op_mode_supported() { // populate unsupportedmode_t { - UnsupportedOpMode obj = {{V_2024_1, V_2024_2, V_2024_3, V_2024_4, V_2024_5, V_2024_6, V_2025_0, V_2025_1, V_2025_2, V_2025_3, V_2025_4, V_2026_0, V_2026_1}, + UnsupportedOpMode obj = {{V_2024_1, V_2024_2, V_2024_3, V_2024_4, V_2024_5, V_2024_6, V_2025_0, V_2025_1, V_2025_2, V_2025_3, V_2025_4, V_2026_0, V_2026_1, V_2026_2}, [this](const Node* node, const InitializedTensorSet&) { // If the Input of ReduceMax op is UINT8, it is rejected (Due to output mismatch) for (size_t i = 0; i < node->InputDefs().size(); i++) { @@ -425,7 +425,7 @@ void DataOps::populate_op_mode_supported() { { UnsupportedOpMode obj = {{V_2023_1, V_2023_2, V_2023_3, V_2024_0, V_2024_1, V_2024_2, V_2024_3, V_2024_4, V_2024_5, V_2024_6, V_2025_0, V_2025_1, - V_2025_2, V_2025_3, V_2025_4, V_2026_0, V_2026_1}, + V_2025_2, V_2025_3, V_2025_4, V_2026_0, V_2026_1, V_2026_2}, [this](const Node* node, const InitializedTensorSet&) { const auto& input_args = node->InputDefs(); const auto& input_arg = (input_args.size() > 1) ? input_args[1] : input_args[0]; @@ -445,7 +445,7 @@ void DataOps::populate_op_mode_supported() { { UnsupportedOpMode obj = {{V_2023_1, V_2023_2, V_2023_3, V_2024_0, V_2024_1, V_2024_2, V_2024_3, V_2024_4, V_2024_5, V_2024_6, V_2025_0, V_2025_1, - V_2025_2, V_2025_3, V_2025_4, V_2026_0, V_2026_1}, + V_2025_2, V_2025_3, V_2025_4, V_2026_0, V_2026_1, V_2026_2}, [this](const Node* node, const InitializedTensorSet&) { // If the operator is unsqueeze // If axes is an input, then we cannot produce a static graph. @@ -461,7 +461,7 @@ void DataOps::populate_op_mode_supported() { } { UnsupportedOpMode obj = {{V_2023_1, V_2023_2, V_2023_3, V_2024_0, V_2024_1, V_2024_2, V_2024_3, V_2024_4, V_2024_5, - V_2024_6, V_2025_0, V_2025_1, V_2025_2, V_2025_3, V_2025_4, V_2026_0, V_2026_1}, + V_2024_6, V_2025_0, V_2025_1, V_2025_2, V_2025_3, V_2025_4, V_2026_0, V_2026_1, V_2026_2}, [this](const Node* node, const InitializedTensorSet&) { // check for attributes auto& upsample_attr = node->GetAttributes(); @@ -492,7 +492,7 @@ void DataOps::populate_op_mode_supported() { { UnsupportedOpMode obj = {{V_2023_1, V_2023_2, V_2023_3, V_2024_0, V_2024_1, V_2024_2, V_2024_3, V_2024_4, V_2024_5, V_2024_6, V_2025_0, V_2025_1, - V_2025_2, V_2025_3, V_2025_4, V_2026_0, V_2026_1}, + V_2025_2, V_2025_3, V_2025_4, V_2026_0, V_2026_1, V_2026_2}, [this](const Node* node, const InitializedTensorSet&) { auto& attributes = node->GetAttributes(); if (attributes.count("coordinate_transformation_mode") > 0) { diff --git a/onnxruntime/core/providers/openvino/ov_versions/data_ops.h b/onnxruntime/core/providers/openvino/ov_versions/data_ops.h index af4bdc6efffff..1084e9204c8e5 100644 --- a/onnxruntime/core/providers/openvino/ov_versions/data_ops.h +++ b/onnxruntime/core/providers/openvino/ov_versions/data_ops.h @@ -40,7 +40,8 @@ enum versionNum { V_2025_3, V_2025_4, V_2026_0, - V_2026_1 + V_2026_1, + V_2026_2 }; using VersionNum = enum versionNum; diff --git a/onnxruntime/test/providers/openvino/openvino_ep_context_test.cc b/onnxruntime/test/providers/openvino/openvino_ep_context_test.cc index b5a8280a087a2..c085d17703496 100644 --- a/onnxruntime/test/providers/openvino/openvino_ep_context_test.cc +++ b/onnxruntime/test/providers/openvino/openvino_ep_context_test.cc @@ -1,12 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#include #include +#include #include +#include +#include #include "core/framework/provider_options.h" #include "core/framework/tensor_shape.h" #include "core/common/float16.h" +#include "core/common/cpuid_info.h" #include "test/util/include/test_utils.h" #include "test/util/include/test/test_environment.h" @@ -26,6 +31,45 @@ using namespace onnxruntime::logging; extern std::unique_ptr ort_env; +namespace { + +// Returns true only on Intel CPUs. +// +// The OVIR EP context tests are gated on this because that path is currently +// validated only on Intel silicon. In particular, embed_mode = 0 dumps the +// compiled model to a separate .bin file and memory-maps it back on reload; +// this round-trip is unsupported on non-Intel CPUs (e.g. AMD), where it can +// crash. On those CPUs the OVIR EP context tests are skipped. +bool IsIntelCPU() { + return onnxruntime::CPUIDInfo::GetCPUIDInfo().GetCPUVendor() == "Intel"; +} + +// Runs a mul_1-style model (X[3,2] -> Y[3,2], Y = X * {1,2,3,4,5,6}) with +// X = all 2.0f and validates that Y == {2,4,6,8,10,12}. +void RunAndValidate(Ort::Session& session) { + const std::array input_shape = {3, 2}; + std::vector input_data(6, 2.0f); + Ort::MemoryInfo mem_info = Ort::MemoryInfo::CreateCpu(OrtAllocatorType::OrtDeviceAllocator, OrtMemTypeDefault); + Ort::Value input_tensor = Ort::Value::CreateTensor( + mem_info, input_data.data(), input_data.size(), input_shape.data(), input_shape.size()); + + const std::array input_names = {"X"}; + const std::array output_names = {"Y"}; + std::vector output_tensors(1); + + session.Run(Ort::RunOptions{nullptr}, input_names.data(), &input_tensor, 1, + output_names.data(), output_tensors.data(), 1); + + ASSERT_TRUE(output_tensors[0].IsTensor()); + ASSERT_EQ(output_tensors[0].GetTensorTypeAndShapeInfo().GetElementCount(), 6u); + + const float* out_data = output_tensors[0].GetTensorData(); + EXPECT_THAT(std::vector(out_data, out_data + 6), + ::testing::ElementsAre(2.f, 4.f, 6.f, 8.f, 10.f, 12.f)); +} + +} // namespace + class OVEPEPContextTests : public ::testing::Test { }; @@ -72,5 +116,191 @@ TEST_F(OVEPEPContextTests, OVEPEPContextFolderPath) { } } +// Runs an existing OVIR-encapsulated EP context model: "mul_1_ep_ctx_ovir.onnx" +// wraps a single EPContext node whose "ep_cache_context" points to a sibling +// OpenVINO IR (".xml" + ".bin"), so OVEP imports it via read_model()/ +// compile_model() instead of a pre-compiled blob. +// +// OVIR detection is filename-based (".onnx" -> ".xml"), so the model must be +// loaded from a path with the ".xml"/".bin" siblings next to it. +// +// CPU only. +class OVEPEPContextOVIRTests : public ::testing::Test { + protected: + void SetUp() override { + if (!IsIntelCPU()) { + GTEST_SKIP() << "OVIR EP context is only validated on Intel CPUs; skipping on non-Intel silicon."; + } + } + + static constexpr const char* kDevice = "CPU"; + static constexpr const ORTCHAR_T* kOvirModelPath = ORT_TSTR("testdata/mul_1_ep_ctx_ovir.onnx"); +}; + +TEST_F(OVEPEPContextOVIRTests, RunEpCtxOvirModel) { + ASSERT_TRUE(std::filesystem::exists(kOvirModelPath)) + << "Missing OVIR EP context model. Expected testdata/mul_1_ep_ctx_ovir.onnx " + "(with sibling .xml and .bin files)."; + + // Set up session options targeting CPU. + Ort::SessionOptions session_options; + std::unordered_map ov_options = {{"device_type", kDevice}}; + session_options.AppendExecutionProvider_OpenVINO_V2(ov_options); + + // Load the OVIR EP context model from disk (path-based load is required for + // OVIR encapsulation detection). + Ort::Session session(*ort_env, kOvirModelPath, session_options); + + RunAndValidate(session); +} + +// Negative / security test: an OVIR-encapsulated EP context model whose +// "ep_cache_context" attribute points outside the model directory via "../" +// traversal (e.g. "../../../etc/evil.xml") must be rejected at session-creation +// time rather than silently reading an arbitrary file off disk. + +TEST_F(OVEPEPContextOVIRTests, RejectsEpCacheContextPathTraversal) { + ASSERT_TRUE(std::filesystem::exists(kOvirModelPath)) + << "Missing OVIR EP context model. Expected testdata/mul_1_ep_ctx_ovir.onnx " + "(with sibling .xml and .bin files)."; + + // Load the known-good OVIR EP context model and rewrite its EPContext node so + // that ep_cache_context escapes the model directory. + ONNX_NAMESPACE::ModelProto model_proto; + ASSERT_STATUS_OK(Model::Load(kOvirModelPath, model_proto)); + + // Malicious relative path that escapes the model directory. The ".xml" + // extension routes validation through the OVIR ".xml" branch in + // EPCtxHandler::Initialize() (validated against the input model's directory), + // and "evil.xml" matches the "evil.onnx" output stem below so the node is also + // recognized as OVIR-encapsulated. + const std::string malicious_xml_path = "../../../etc/evil.xml"; + + bool patched = false; + for (auto& node : *model_proto.mutable_graph()->mutable_node()) { + if (node.op_type() != "EPContext") { + continue; + } + for (auto& attr : *node.mutable_attribute()) { + if (attr.name() == "embed_mode") { + attr.set_i(0); // force non-embed so the path (not an inline blob) is validated + } else if (attr.name() == "ep_cache_context") { + attr.set_s(malicious_xml_path); + patched = true; + } + } + } + ASSERT_TRUE(patched) << "Test model did not contain an EPContext ep_cache_context attribute to patch."; + + // Write the tampered model to a dedicated subfolder. The malicious ".xml" is + // intentionally never created on disk: validation must reject the path before + // any attempt to read it. + const std::filesystem::path out_dir = std::filesystem::path("testdata") / "ovir_epctx_path_traversal"; + std::filesystem::remove_all(out_dir); + std::filesystem::create_directories(out_dir); + const std::filesystem::path malicious_model = out_dir / "evil.onnx"; + { + std::ofstream ofs(malicious_model, std::ios::binary); + ASSERT_TRUE(ofs.is_open()) << "Failed to open " << malicious_model; + ASSERT_TRUE(model_proto.SerializeToOstream(&ofs)) << "Failed to serialize tampered model."; + } + + Ort::SessionOptions session_options; + std::unordered_map ov_options = {{"device_type", kDevice}}; + session_options.AppendExecutionProvider_OpenVINO_V2(ov_options); + + bool threw = false; + std::string error_message; + try { + Ort::Session session(*ort_env, malicious_model.c_str(), session_options); + } catch (const Ort::Exception& ex) { + threw = true; + error_message = ex.what(); + } + + std::filesystem::remove_all(out_dir); + + ASSERT_TRUE(threw) + << "Session creation should have rejected the path-traversal ep_cache_context, but it succeeded."; + EXPECT_THAT(error_message, ::testing::HasSubstr("escapes model directory")) + << "Expected a path-escape rejection. Actual error: " << error_message; +} + +// Generates an EP context model from the OVIR-encapsulated source model and +// then loads + runs the generated model, covering both EP context embed modes: +// embed_mode = 1: the compiled context is serialized INLINE into the .onnx. +// embed_mode = 0: the compiled context is dumped to a separate file and only +// its filename is stored in the .onnx EPContext node. +// +// embed_mode is only honored during generation (it is written into the +// EPContext node), so it must be exercised via a generate-then-run flow rather +// than by setting the option on a run-only session. +// +// CPU only. Parameter: embed_mode_enabled. +class OVEPOVIRModelsExportEPContextTests : public ::testing::TestWithParam { + protected: + void SetUp() override { + if (!IsIntelCPU()) { + GTEST_SKIP() << "OVIR EP context export is only validated on Intel CPUs; skipping on non-Intel silicon."; + } + } + + static constexpr const char* kDevice = "CPU"; + static constexpr const ORTCHAR_T* kOvirModelPath = ORT_TSTR("testdata/mul_1_ep_ctx_ovir.onnx"); +}; + +TEST_P(OVEPOVIRModelsExportEPContextTests, ExportEpCtxFromOVIRModel) { + const bool embed_mode = GetParam(); + + ASSERT_TRUE(std::filesystem::exists(kOvirModelPath)) + << "Missing OVIR EP context model. Expected testdata/mul_1_ep_ctx_ovir.onnx " + "(with sibling .xml and .bin files)."; + + // Generate the EP context model into a dedicated subfolder so that the + // separately-dumped blob (embed_mode = 0) doesn't collide with testdata. + const std::filesystem::path out_dir = + std::filesystem::path("testdata") / (std::string("ovir_epctx_export_embed_") + (embed_mode ? "on" : "off")); + std::filesystem::remove_all(out_dir); + std::filesystem::create_directories(out_dir); + const std::filesystem::path epctx_model = out_dir / "mul_1_ovir_epctx_export.onnx"; + + // --- Generate EP context model --- + { + Ort::SessionOptions session_options; + session_options.AddConfigEntry(kOrtSessionOptionEpContextEnable, "1"); + session_options.AddConfigEntry(kOrtSessionOptionEpContextFilePath, epctx_model.string().c_str()); + session_options.AddConfigEntry(kOrtSessionOptionEpContextEmbedMode, embed_mode ? "1" : "0"); + std::unordered_map ov_options = {{"device_type", kDevice}}; + session_options.AppendExecutionProvider_OpenVINO_V2(ov_options); + + // Creating the session triggers EP context export to epctx_model. + Ort::Session session(*ort_env, kOvirModelPath, session_options); + } + + ASSERT_TRUE(std::filesystem::exists(epctx_model)) + << "EP context model was not generated at " << epctx_model; + + // --- Load + run the generated EP context model --- + { + Ort::SessionOptions session_options; + std::unordered_map ov_options = {{"device_type", kDevice}}; + session_options.AppendExecutionProvider_OpenVINO_V2(ov_options); + + Ort::Session session(*ort_env, epctx_model.c_str(), session_options); + + RunAndValidate(session); + } + + std::filesystem::remove_all(out_dir); +} + +INSTANTIATE_TEST_SUITE_P( + OVEP_Tests, + OVEPOVIRModelsExportEPContextTests, + ::testing::Bool(), + [](const ::testing::TestParamInfo& info) { + return std::string("embed_") + (info.param ? "on" : "off"); + }); + } // namespace test } // namespace onnxruntime diff --git a/onnxruntime/test/providers/openvino/openvino_ep_workload_type_test.cc b/onnxruntime/test/providers/openvino/openvino_ep_workload_type_test.cc new file mode 100644 index 0000000000000..7d042b2c2e1ee --- /dev/null +++ b/onnxruntime/test/providers/openvino/openvino_ep_workload_type_test.cc @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include +#include +#include +#include +#include +#include + +#include "core/session/onnxruntime_cxx_api.h" +#include "gtest/gtest.h" + +extern std::unique_ptr ort_env; + +constexpr const ORTCHAR_T* kSqueezeNetModelUri = + ORT_TSTR("testdata/squeezenet/model.onnx"); + +class OVEPWorkloadTypeTests : public ::testing::Test { + protected: + // Check whether the NPU device can be registered at all. + static bool IsNPUAvailable() { + try { + Ort::SessionOptions opts; + std::unordered_map ov; + ov["device_type"] = "NPU"; + opts.AppendExecutionProvider_OpenVINO_V2(ov); + return true; + } catch (...) { + return false; + } + } + + // Allow NPU resources to be fully released between tests. + // Without this delay the NPU driver may fail to re-initialise. + // Skip the delay when the test was skipped (e.g. no NPU available), since no + // NPU resources were acquired and the sleep would only add build latency. + void TearDown() override { + if (IsSkipped()) { + return; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + + static Ort::Session CreateSqueezeNetSession( + Ort::SessionOptions& session_options, + std::unordered_map& ov_options) { + session_options.SetIntraOpNumThreads(1); + session_options.SetGraphOptimizationLevel( + GraphOptimizationLevel::ORT_ENABLE_ALL); + session_options.AppendExecutionProvider_OpenVINO_V2(ov_options); + + return Ort::Session(*ort_env, kSqueezeNetModelUri, session_options); + } + + // Run a single inference on the SqueezeNet session and return output + static std::vector RunSqueezeNet(Ort::Session& session, + const std::string& phase_label) { + Ort::AllocatorWithDefaultOptions allocator; + std::string input_name = + session.GetInputNameAllocated(0, allocator).get(); + std::string output_name = + session.GetOutputNameAllocated(0, allocator).get(); + const char* input_names[] = {input_name.c_str()}; + const char* output_names[] = {output_name.c_str()}; + + // SqueezeNet input: 1 × 3 × 224 × 224 = 150 528 floats + std::vector input_shape = {1, 3, 224, 224}; + constexpr size_t kInputSize = 1 * 3 * 224 * 224; + std::vector input_values(kInputSize, 1.0f); + + Ort::MemoryInfo mem_info = + Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); + + Ort::Value input_tensor = Ort::Value::CreateTensor( + mem_info, input_values.data(), input_values.size(), + input_shape.data(), input_shape.size()); + + auto outputs = session.Run(Ort::RunOptions{nullptr}, input_names, + &input_tensor, 1, output_names, 1); + + EXPECT_EQ(outputs.size(), 1u) << phase_label; + if (outputs.empty()) return {}; + + auto type_shape = outputs[0].GetTensorTypeAndShapeInfo(); + std::vector out_shape = type_shape.GetShape(); + + // Expected: [1, 1000, 1, 1] + EXPECT_EQ(out_shape.size(), 4u) << phase_label; + if (out_shape.size() == 4u) { + EXPECT_EQ(out_shape[0], 1) << phase_label; + EXPECT_EQ(out_shape[1], 1000) << phase_label; + EXPECT_EQ(out_shape[2], 1) << phase_label; + EXPECT_EQ(out_shape[3], 1) << phase_label; + } + + size_t num_elements = type_shape.GetElementCount(); + EXPECT_EQ(num_elements, 1000u) << phase_label; + + const float* out_data = outputs[0].GetTensorData(); + std::vector result(out_data, out_data + num_elements); + + for (size_t i = 0; i < num_elements; ++i) { + EXPECT_TRUE(std::isfinite(result[i])) + << phase_label << " index " << i << " is not finite"; + } + + return result; + } + + // Compare two output vectors element-wise within a tolerance. + static void CompareOutputs(const std::vector& expected, + const std::vector& actual, + const std::string& label, + float tolerance = 1e-4f) { + ASSERT_EQ(expected.size(), actual.size()) << label << " size mismatch"; + for (size_t i = 0; i < expected.size(); ++i) { + EXPECT_NEAR(expected[i], actual[i], tolerance) + << label << " mismatch at index " << i; + } + } +}; + +namespace onnxruntime { +namespace test { + +// Test 1: Dynamic workload-type switching with consistency check +// Baseline (no workload type) → Efficient → Default +TEST_F(OVEPWorkloadTypeTests, OVEPWorkloadTypeDynamicSwitch) { + if (!IsNPUAvailable()) { + GTEST_SKIP() << "NPU device not available, skipping workload type test"; + } + + Ort::SessionOptions session_options; + std::unordered_map ov_options; + ov_options["device_type"] = "NPU"; + + Ort::Session session = CreateSqueezeNetSession(session_options, ov_options); + + const char* const keys[] = {"ep.dynamic.workload_type"}; + + // Phase 1: Baseline (no workload type set) + auto baseline_output = RunSqueezeNet(session, "Baseline"); + + // Phase 2: Switch to Efficient + const char* const eff_val[] = {"Efficient"}; + session.SetEpDynamicOptions(keys, eff_val, 1); + auto efficient_output = RunSqueezeNet(session, "Efficient"); + + // Phase 3: Switch to Default + const char* const def_val[] = {"Default"}; + session.SetEpDynamicOptions(keys, def_val, 1); + auto default_output = RunSqueezeNet(session, "Default"); + + // All modes should produce the same results + CompareOutputs(baseline_output, efficient_output, + "Baseline vs Efficient"); + CompareOutputs(baseline_output, default_output, + "Baseline vs Default"); +} + +// Test 2: Multiple inferences per workload mode +// Runs 10 inferences in each mode: +// Baseline × 10 → Efficient × 10 → Default × 10 +TEST_F(OVEPWorkloadTypeTests, OVEPWorkloadTypeMultipleInferencesPerMode) { + if (!IsNPUAvailable()) { + GTEST_SKIP() << "NPU device not available, skipping workload type test"; + } + + Ort::SessionOptions session_options; + std::unordered_map ov_options; + ov_options["device_type"] = "NPU"; + + Ort::Session session = CreateSqueezeNetSession(session_options, ov_options); + + const char* const keys[] = {"ep.dynamic.workload_type"}; + const char* const eff_val[] = {"Efficient"}; + const char* const def_val[] = {"Default"}; + + constexpr int kIterationsPerMode = 10; + + // Phase 1: Baseline – 10 runs without workload type + // Save the first run as the reference output. + auto reference_output = RunSqueezeNet(session, "Baseline iter 0"); + for (int i = 1; i < kIterationsPerMode; ++i) { + auto output = RunSqueezeNet(session, "Baseline iter " + std::to_string(i)); + CompareOutputs(reference_output, output, + "Baseline iter " + std::to_string(i) + " vs reference"); + } + + // Phase 2: Efficient – 10 runs + session.SetEpDynamicOptions(keys, eff_val, 1); + for (int i = 0; i < kIterationsPerMode; ++i) { + auto output = RunSqueezeNet(session, "Efficient iter " + std::to_string(i)); + CompareOutputs(reference_output, output, + "Efficient iter " + std::to_string(i) + " vs reference"); + } + + // Phase 3: Default – 10 runs + session.SetEpDynamicOptions(keys, def_val, 1); + for (int i = 0; i < kIterationsPerMode; ++i) { + auto output = RunSqueezeNet(session, "Default iter " + std::to_string(i)); + CompareOutputs(reference_output, output, + "Default iter " + std::to_string(i) + " vs reference"); + } +} + +} // namespace test +} // namespace onnxruntime diff --git a/onnxruntime/test/testdata/mul_1_ep_ctx_ovir.bin b/onnxruntime/test/testdata/mul_1_ep_ctx_ovir.bin new file mode 100644 index 0000000000000..cc239b1d36632 Binary files /dev/null and b/onnxruntime/test/testdata/mul_1_ep_ctx_ovir.bin differ diff --git a/onnxruntime/test/testdata/mul_1_ep_ctx_ovir.onnx b/onnxruntime/test/testdata/mul_1_ep_ctx_ovir.onnx new file mode 100644 index 0000000000000..2c616129077ef Binary files /dev/null and b/onnxruntime/test/testdata/mul_1_ep_ctx_ovir.onnx differ diff --git a/onnxruntime/test/testdata/mul_1_ep_ctx_ovir.xml b/onnxruntime/test/testdata/mul_1_ep_ctx_ovir.xml new file mode 100644 index 0000000000000..aab7c2607a5ec --- /dev/null +++ b/onnxruntime/test/testdata/mul_1_ep_ctx_ovir.xml @@ -0,0 +1,56 @@ + + + + + + + + 3 + 2 + + + + + + + + 3 + 2 + + + + + + + + 3 + 2 + + + 3 + 2 + + + + + 3 + 2 + + + + + + + 3 + 2 + + + + + + + + + + + diff --git a/tools/ci_build/github/linux/docker/inference/x86_64/python/openvino/Dockerfile b/tools/ci_build/github/linux/docker/inference/x86_64/python/openvino/Dockerfile index 03f351d942e70..17038fd4e080d 100644 --- a/tools/ci_build/github/linux/docker/inference/x86_64/python/openvino/Dockerfile +++ b/tools/ci_build/github/linux/docker/inference/x86_64/python/openvino/Dockerfile @@ -22,8 +22,8 @@ RUN dnf install -y --nodocs \ && dnf clean all \ && rm -rf /var/cache/dnf -ENV INTEL_OPENVINO_DIR=/opt/intel/openvino_2026.1.0 -ARG OPENVINO_PACKAGE_URL=https://storage.openvinotoolkit.org/repositories/openvino/packages/2026.1/linux/openvino_toolkit_rhel8_2026.1.0.21367.63e31528c62_x86_64.tgz +ENV INTEL_OPENVINO_DIR=/opt/intel/openvino_2026.2.1 +ARG OPENVINO_PACKAGE_URL=https://storage.openvinotoolkit.org/repositories/openvino/packages/2026.2.1/linux/openvino_toolkit_rhel8_2026.2.1.21919.ede283a88e3_x86_64.tgz ARG TEMP_DIR=/tmp/openvino_installer RUN mkdir -p ${TEMP_DIR} && \