mirror of
https://github.com/saymrwulf/onnxruntime.git
synced 2026-07-12 17:57:38 +00:00
260 lines
11 KiB
C++
260 lines
11 KiB
C++
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// Licensed under the MIT License.
|
|
|
|
#include "core/common/logging/logging.h"
|
|
#include "core/framework/utils.h"
|
|
#include "core/session/inference_session.h"
|
|
|
|
#include "test/framework/test_utils.h"
|
|
#include "test/test_environment.h"
|
|
#include "test/providers/internal_testing/internal_testing_execution_provider.h"
|
|
#include "test/util/include/asserts.h"
|
|
#include "test/util/include/inference_session_wrapper.h"
|
|
#include "test/util/include/test_utils.h"
|
|
|
|
#include "gtest/gtest.h"
|
|
#include "gmock/gmock.h"
|
|
|
|
using namespace ONNX_NAMESPACE;
|
|
using namespace onnxruntime::logging;
|
|
|
|
namespace onnxruntime {
|
|
|
|
namespace test {
|
|
|
|
static void CreateSession(const SessionOptions& so, std::unique_ptr<InferenceSessionWrapper>& session,
|
|
const ORTCHAR_T* model_path = ORT_TSTR("testdata/mnist.onnx"), // arbitrary test model
|
|
bool enable_custom_ep = true,
|
|
const std::unordered_set<std::string>* override_supported_ops = nullptr) {
|
|
session = onnxruntime::make_unique<InferenceSessionWrapper>(so, GetEnvironment());
|
|
|
|
// set supported ops to ops that are ideally found consecutively in the model.
|
|
// we can say the EP potentially handles them all, but can also test removing handling of one or more ops
|
|
// at runtime to simulate a lower spec device where not all ops can be handled. this allows us to test
|
|
// that we can revert ops back to the CPU implementation successfully
|
|
const std::unordered_set<std::string> default_supported_ops{"Conv", "Add", "Relu", "MaxPool"};
|
|
const std::unordered_set<std::string>* supported_ops = override_supported_ops ? override_supported_ops
|
|
: &default_supported_ops;
|
|
|
|
if (enable_custom_ep) {
|
|
ASSERT_STATUS_OK(session->RegisterExecutionProvider(
|
|
onnxruntime::make_unique<InternalTestingExecutionProvider>(*supported_ops)));
|
|
}
|
|
|
|
ASSERT_STATUS_OK(session->Load(model_path));
|
|
ASSERT_STATUS_OK(session->Initialize());
|
|
}
|
|
|
|
static void ExecuteMnist(InferenceSessionWrapper& session, bool custom_ep_enabled) {
|
|
// validate that we can execute the model. the dummy internal testing EP just creates empty output so the
|
|
// values in the output aren't relevant. all we care about is that we can execute the model and produce output.
|
|
OrtValue ml_value_x;
|
|
TensorShape input_shape{1, 1, 28, 28};
|
|
std::vector<float> input(input_shape.Size(), 1.f);
|
|
|
|
CreateMLValue<float>(input_shape.GetDims(), input.data(), OrtMemoryInfo(), &ml_value_x);
|
|
|
|
NameMLValMap feeds;
|
|
feeds.insert(std::make_pair("Input3", ml_value_x));
|
|
|
|
// prepare outputs
|
|
std::vector<std::string> output_names;
|
|
output_names.push_back("Plus214_Output_0");
|
|
std::vector<OrtValue> fetches;
|
|
|
|
ASSERT_STATUS_OK(session.Run(feeds, output_names, &fetches));
|
|
|
|
if (custom_ep_enabled) {
|
|
// check that the output is all zeros. the dummy EP produces output of the correct shape with all zeros, so any
|
|
// downstream operations should still result in zeros for this model
|
|
// OR it should equal the bias in the final Add operation, which is in the Parameter194 initializer
|
|
const auto& t = fetches[0].Get<Tensor>();
|
|
const auto data = t.DataAsSpan<float>();
|
|
|
|
int idx = 0;
|
|
const auto& session_state = session.GetSessionState();
|
|
ASSERT_STATUS_OK(session_state.GetOrtValueNameIdxMap().GetIdx("Parameter194", idx));
|
|
const auto& initializer = session_state.GetConstantInitializedTensors().at(idx);
|
|
const auto expected = initializer.Get<Tensor>().DataAsSpan<float>();
|
|
|
|
ASSERT_THAT(data, ::testing::ContainerEq(expected));
|
|
}
|
|
}
|
|
|
|
#if !defined(ORT_MINIMAL_BUILD)
|
|
TEST(InternalTestingEP, TestSaveAndLoadOrtModel) {
|
|
const ORTCHAR_T* ort_model_path = ORT_TSTR("testdata/mnist.test_output.ort");
|
|
|
|
//
|
|
// First load the onnx format model and save as an ORT model.
|
|
// This should preserve the nodes the custom EP can handle.
|
|
//
|
|
std::unique_ptr<InferenceSessionWrapper> session;
|
|
SessionOptions so;
|
|
so.optimized_model_filepath = ort_model_path;
|
|
|
|
CreateSession(so, session);
|
|
// this graph should include the original nodes that the custom EP will take at runtime
|
|
auto num_nodes = session->GetGraph().NumberOfNodes();
|
|
|
|
//
|
|
// Second, load the ORT format model with just the CPU EP to make sure it can be executed. This tests that the
|
|
// fallback to the CPU EP kernel hashes works.
|
|
//
|
|
std::unique_ptr<InferenceSessionWrapper> session2;
|
|
|
|
so.optimized_model_filepath.clear();
|
|
bool enable_custom_ep = false;
|
|
|
|
CreateSession(so, session2, ort_model_path, enable_custom_ep);
|
|
const auto& graph1 = session2->GetGraph();
|
|
// model should have all the original nodes and we should be able to execute with the fallback to CPU EP
|
|
ASSERT_EQ(graph1.NumberOfNodes(), num_nodes);
|
|
ExecuteMnist(*session2, enable_custom_ep);
|
|
session2 = nullptr;
|
|
|
|
//
|
|
// Finally, load the ORT format model with the custom EP enabled. This tests that we support runtime compilation
|
|
// for the ORT format model.
|
|
//
|
|
enable_custom_ep = true;
|
|
CreateSession(so, session2, ort_model_path, enable_custom_ep);
|
|
const auto& graph2 = session2->GetGraph();
|
|
// model should be able to be loaded, and we should compile using custom ep. that will result in one node for the
|
|
// custom EP (with Conv/Add/Relu/MaxPool), one for a reshape, and one for the fused MatMul+Add.
|
|
ASSERT_EQ(graph2.NumberOfNodes(), 3);
|
|
ExecuteMnist(*session2, enable_custom_ep);
|
|
}
|
|
|
|
TEST(InternalTestingEP, PreventSaveOfModelWithCompiledOps) {
|
|
const ORTCHAR_T* ort_model_path = ORT_TSTR("testdata/mnist.ort");
|
|
|
|
// make sure we can't save a model with compiled ops. input/output model format doesn't matter
|
|
SessionOptions so;
|
|
so.optimized_model_filepath = ORT_TSTR("invalid_model.ort");
|
|
|
|
auto session = onnxruntime::make_unique<InferenceSessionWrapper>(so, GetEnvironment());
|
|
|
|
const std::unordered_set<std::string> supported_ops{"Conv", "Add", "Relu", "MaxPool"};
|
|
ASSERT_STATUS_OK(session->RegisterExecutionProvider(
|
|
onnxruntime::make_unique<InternalTestingExecutionProvider>(supported_ops)));
|
|
|
|
ASSERT_STATUS_OK(session->Load(ort_model_path));
|
|
auto status = session->Initialize();
|
|
ASSERT_FALSE(status.IsOK()) << "Initialize should have failed when trying to save model with compiled kernels";
|
|
ASSERT_THAT(status.ErrorMessage(), ::testing::HasSubstr("Unable to serialize model as it contains compiled nodes"));
|
|
}
|
|
#endif // !defined(ORT_MINIMAL_BUILD)
|
|
|
|
// test to validate a minimal build
|
|
TEST(InternalTestingEP, TestLoadOrtModel) {
|
|
const ORTCHAR_T* ort_model_path = ORT_TSTR("testdata/mnist.ort");
|
|
|
|
std::unique_ptr<InferenceSessionWrapper> session;
|
|
bool enable_custom_ep = true;
|
|
|
|
CreateSession(SessionOptions{}, session, ort_model_path, enable_custom_ep);
|
|
ExecuteMnist(*session, enable_custom_ep);
|
|
}
|
|
|
|
// test that is the custom EP cannot take all nodes due to device limitations
|
|
// that we fallback to the CPU implementations and can execute the model
|
|
TEST(InternalTestingEP, TestLoadOrtModelWithReducedOpCoverage) {
|
|
const ORTCHAR_T* ort_model_path = ORT_TSTR("testdata/mnist.ort");
|
|
const std::unordered_set<std::string> supported_ops{"Conv", "Add", "Relu" /*, "MaxPool"*/};
|
|
|
|
std::unique_ptr<InferenceSessionWrapper> session;
|
|
bool enable_custom_ep = true;
|
|
|
|
CreateSession(SessionOptions{}, session, ort_model_path, enable_custom_ep, &supported_ops);
|
|
|
|
const auto& graph = session->GetGraph();
|
|
// Conv+Add gets fused by level 1 optimizer into single node. The 'Conv'/'Add'/'Relu' nodes should be compiled and
|
|
// handled by the custom EP. fallback to CPU for MaxPool.
|
|
ASSERT_EQ(graph.NumberOfNodes(), 6);
|
|
const auto& func_mgr = session->GetSessionState().GetFuncMgr();
|
|
NodeComputeInfo* compute_func = nullptr;
|
|
|
|
for (const auto& node : graph.Nodes()) {
|
|
EXPECT_EQ(supported_ops.count(node.OpType()), size_t(0))
|
|
<< "Nodes with supported op types should have been replaced. Node with type " << node.OpType() << " was not.";
|
|
if (node.GetExecutionProviderType() == utils::kInternalTestingExecutionProvider) {
|
|
EXPECT_STATUS_OK(func_mgr.GetFuncs(node.Name(), compute_func));
|
|
EXPECT_NE(compute_func, nullptr);
|
|
}
|
|
}
|
|
|
|
ExecuteMnist(*session, enable_custom_ep);
|
|
}
|
|
|
|
TEST(InternalTestingEP, TestMinimalRegistrationOfEPwithGetCapability) {
|
|
// TODO: In a full build we want to be able to call GetCapability for the NNAPI EP and produce an ORT format model
|
|
// with nodes correctly preserved. That requires being able to do a minimal registration of that EP where
|
|
// GetCapability is fully implemented, but Compile is a stub that just throws NOT_IMPLEMENTED if someone attempts
|
|
// to execute a model in that InferenceSession.
|
|
}
|
|
|
|
// count nodes assigned to the test EP and make sure they all have valid compute funcs
|
|
static int CountAndValidateAssignedNodes(const Graph& current_graph,
|
|
const std::unordered_set<std::string>& supported_ops,
|
|
const FuncManager& func_mgr) {
|
|
int count = 0;
|
|
|
|
for (const auto& node : current_graph.Nodes()) {
|
|
EXPECT_EQ(supported_ops.count(node.OpType()), size_t(0))
|
|
<< "Nodes with supported op types should have been replaced. Node with type " << node.OpType() << " was not.";
|
|
if (node.GetExecutionProviderType() == utils::kInternalTestingExecutionProvider) {
|
|
NodeComputeInfo* compute_func = nullptr;
|
|
EXPECT_STATUS_OK(func_mgr.GetFuncs(node.Name(), compute_func));
|
|
EXPECT_NE(compute_func, nullptr);
|
|
++count;
|
|
}
|
|
|
|
if (node.ContainsSubgraph()) {
|
|
for (const auto& entry : node.GetSubgraphs()) {
|
|
count += CountAndValidateAssignedNodes(*entry, supported_ops, func_mgr);
|
|
}
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
// Test model that contains a subgraph. This model has a Loop and an If so multiple layers of nested subgraphs.
|
|
// There are Add nodes in the Loop and If subgraphs so we should see the custom EP taking nodes at both these levels.
|
|
TEST(InternalTestingEP, TestModelWithSubgraph) {
|
|
const ORTCHAR_T* ort_model_path = ORT_TSTR("testdata/ort_github_issue_4031.onnx.ort");
|
|
const std::unordered_set<std::string> supported_ops{"Add"};
|
|
|
|
std::unique_ptr<InferenceSessionWrapper> session;
|
|
bool enable_custom_ep = true;
|
|
|
|
CreateSession(SessionOptions{}, session, ort_model_path, enable_custom_ep, &supported_ops);
|
|
|
|
const auto& graph = session->GetGraph();
|
|
const auto& func_mgr = session->GetSessionState().GetFuncMgr();
|
|
|
|
int num_replaced_nodes = CountAndValidateAssignedNodes(graph, supported_ops, func_mgr);
|
|
|
|
// One Add node in the Loop. One Add node in each branch of the If inside the Loop body
|
|
ASSERT_EQ(num_replaced_nodes, 3);
|
|
|
|
OrtValue ml_value;
|
|
|
|
// this is a bit of a hack. the correct output is the input value + 2, so if we start with -2 the result is 0.
|
|
// the output from fused nodes using the testing EP is always 0, so we should match the expected output this way
|
|
// as we replace all the Add nodes with something that returns 0.
|
|
// RunAndVerifyOutputsWithEP checks that nodes are assigned to the EP so we know it's being used to execute the model
|
|
CreateMLValue<float>(TestCPUExecutionProvider()->GetAllocator(0, OrtMemTypeDefault), {1}, {-2.f},
|
|
&ml_value);
|
|
NameMLValMap feeds;
|
|
feeds.insert(std::make_pair("state_var_in", ml_value));
|
|
// compare outputs from CPU EP vs custom EP
|
|
RunAndVerifyOutputsWithEP(ort_model_path,
|
|
"InternalTestingEP.TestModelWithSubgraph",
|
|
onnxruntime::make_unique<InternalTestingExecutionProvider>(supported_ops),
|
|
feeds);
|
|
}
|
|
|
|
} // namespace test
|
|
} // namespace onnxruntime
|