// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. #include "core/graph/onnx_protobuf.h" #include "core/session/inference_session.h" #include #include #include #include #include "core/common/logging/logging.h" #include "core/framework/execution_provider.h" #include "core/framework/op_kernel.h" #include "core/framework/session_state.h" #include "core/graph/graph_viewer.h" #include "core/graph/model.h" #include "core/graph/op.h" #include "core/providers/cpu/cpu_execution_provider.h" #include "core/providers/cpu/math/element_wise_ops.h" #include "core/framework/tensorprotoutils.h" #include "test/capturing_sink.h" #include "test/test_environment.h" #include "test_utils.h" #include "gtest/gtest.h" #include "core/graph/schema_registry.h" #include "core/framework/customregistry.h" using namespace ONNX_NAMESPACE; using namespace onnxruntime::common; namespace onnxruntime { namespace test { // Foo kernel which is doing Add template class FooKernel : public OpKernel { public: FooKernel(const OpKernelInfo& info) : OpKernel(info) {} Status Compute(OpKernelContext* context) const { const auto* X = context->Input(0); const auto* W = context->Input(1); auto X_Data = X->Data(); auto W_Data = W->Data(); auto shape = X->Shape().GetDims(); auto* Y = context->Output(0, shape); auto* Y_Data = Y->MutableData(); size_t size = 1; for (size_t i = 0; i < shape.size(); i++) { size *= shape[i]; } for (size_t i = 0; i < size; i++) { Y_Data[i] = X_Data[i] + W_Data[i]; } return Status::OK(); } }; ONNX_NAMESPACE::OpSchema GetFooSchema() { ONNX_NAMESPACE::OpSchema schema("Foo", "unknown", 0); schema.Input(0, "A", "First operand, should share the type with the second operand.", "T"); schema.Input( 1, "B", "Second operand. With broadcasting can be of smaller size than A. " "If broadcasting is disabled it should be of the same size.", "T"); schema.Output(0, "C", "Result, has same dimensions and type as A", "T"); schema.TypeConstraint( "T", OpSchema::numeric_types_for_math_reduction(), "Constrain input and output types to high-precision numeric tensors."); schema.SinceVersion(7); return schema; } //For test purpose, we register this Foo kernel to Mul op. //Once the custom schema is ready, should update this. KernelDefBuilder FooKernelDef(const char* schema_name) { KernelDefBuilder def; def.SetName(schema_name) .SetDomain(onnxruntime::kOnnxDomain) .SinceVersion(7) .Provider(onnxruntime::kCpuExecutionProvider) .TypeConstraint("T", DataTypeImpl::GetTensorType()); return def; } OpKernel* CreateFooKernel(const OpKernelInfo& kernel_info) { return new FooKernel(kernel_info); } // kernel with optional outputs KernelDefBuilder OptionalKernelDef() { KernelDefBuilder def; def.SetName("OptionalOp") .SetDomain(onnxruntime::kOnnxDomain) .SinceVersion(6) .Provider(onnxruntime::kCpuExecutionProvider) .TypeConstraint("T", DataTypeImpl::GetTensorType()); return def; } ONNX_NAMESPACE::OpSchema GetOptionalOpSchema() { ONNX_NAMESPACE::OpSchema schema("OptionalOp", "unknown", 0); schema.Input(0, "X", "First operand, should share the type with the second operand.", "T"); schema.Input( 1, "W", "Second operand. If provided, add it to the output", "T", OpSchema::Optional); schema.Output(0, "Y", "Result, has same dimensions and type as A", "T"); schema.Output(1, "Y2", "Result, has same dimensions and type as A", "T", OpSchema::Optional); schema.TypeConstraint( "T", OpSchema::numeric_types_for_math_reduction(), "Constrain input and output types to high-precision numeric tensors."); schema.SinceVersion(6); return schema; } template class OptionalOpKernel : public OpKernel { public: OptionalOpKernel(const OpKernelInfo& info) : OpKernel(info) {} Status Compute(OpKernelContext* context) const { const auto* X = context->Input(0); const auto* W = context->Input(1); auto* X_Data = X->Data(); auto& shape = X->Shape().GetDims(); auto* Y = context->Output(0, shape); auto* Y_Data = Y->MutableData(); size_t size = 1; for (size_t i = 0; i < shape.size(); i++) { size *= shape[i]; } for (size_t i = 0; i < size; i++) { Y_Data[i] = X_Data[i]; } auto* Y2 = context->Output(1, shape); // Y2 is used or not if (Y2) { auto Y2_Data = Y2->MutableData(); for (size_t i = 0; i < size; i++) { Y2_Data[i] = X_Data[i]; } } //W is used or not if (W) { auto* W_Data = W->Data(); for (size_t i = 0; i < size; i++) { Y_Data[i] += W_Data[i]; } if (Y2) { auto* Y2_Data = Y2->MutableData(); for (size_t i = 0; i < size; i++) { Y2_Data[i] += W_Data[i]; } } } return Status::OK(); } }; OpKernel* CreateOptionalOpKernel(const OpKernelInfo& kernel_info) { return new OptionalOpKernel(kernel_info); } static const std::string MUL_MODEL_URI = "testdata/mul_1.onnx"; static const std::string FOO_MODEL_URI = "testdata/foo_1.onnx"; static const std::string FOO_TRUNCATE_MODEL_URI = "testdata/foo_2.onnx"; static const std::string OPTIONAL_MODEL1_URI = "testdata/optional_1.onnx"; void RunSession(InferenceSession& session_object, RunOptions& run_options, std::vector& dims_x, std::vector& values_x, std::vector& dims_y, std::vector& values_y) { // prepare inputs OrtValue ml_value; CreateMLValue(TestCPUExecutionProvider()->GetAllocator(0, OrtMemTypeDefault), dims_x, values_x, &ml_value); NameMLValMap feeds; feeds.insert(std::make_pair("X", ml_value)); // prepare outputs std::vector output_names; output_names.push_back("Y"); std::vector fetches; // Now run common::Status st = session_object.Run(run_options, feeds, output_names, &fetches); std::cout << "Run returned status: " << st.ErrorMessage() << std::endl; EXPECT_TRUE(st.IsOK()); ASSERT_EQ(1u, fetches.size()); auto& rtensor = fetches.front().Get(); TensorShape expected_shape(dims_y); //Use reinterpret_cast to bypass a gcc bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=51213 EXPECT_EQ(*reinterpret_cast*>(&expected_shape), *reinterpret_cast*>(&rtensor.Shape())); const std::vector found(rtensor.template Data(), rtensor.template Data() + expected_shape.Size()); ASSERT_EQ(values_y, found); } TEST(CustomKernelTests, CustomKernelWithBuildInSchema) { SessionOptions so; so.session_logid = "InferenceSessionTests.NoTimeout"; // Register a foo kernel which is doing Add, but bind to Mul. std::shared_ptr registry = std::make_shared(); InferenceSession session_object{so, GetEnvironment()}; EXPECT_TRUE(session_object.RegisterCustomRegistry(registry).IsOK()); auto def = FooKernelDef("Mul"); EXPECT_TRUE(registry->RegisterCustomKernel(def, CreateFooKernel).IsOK()); EXPECT_TRUE(session_object.Load(MUL_MODEL_URI).IsOK()); EXPECT_TRUE(session_object.Initialize().IsOK()); RunOptions run_options; run_options.run_tag = "one session/one tag"; // prepare inputs std::vector dims_x = {3, 2}; std::vector values_x = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}; // prepare expected inputs and outputs std::vector expected_dims_y = {3, 2}; // now the expected value should be Add's result. std::vector expected_values_y = {2.0f, 4.0f, 6.0f, 8.0f, 10.0f, 12.0f}; // Now run RunSession(session_object, run_options, dims_x, values_x, expected_dims_y, expected_values_y); } TEST(CustomKernelTests, CustomKernelWithCustomSchema) { SessionOptions so; so.session_logid = "InferenceSessionTests.NoTimeout"; std::shared_ptr registry = std::make_shared(); InferenceSession session_object{so, GetEnvironment()}; EXPECT_TRUE(session_object.RegisterCustomRegistry(registry).IsOK()); //register foo schema auto foo_schema = GetFooSchema(); std::vector schemas = {foo_schema}; EXPECT_TRUE(registry->RegisterOpSet(schemas, onnxruntime::kOnnxDomain, 5, 7).IsOK()); auto def = FooKernelDef("Foo"); //Register a foo kernel which is doing Add, but bind to Mul. EXPECT_TRUE(registry->RegisterCustomKernel(def, CreateFooKernel).IsOK()); EXPECT_TRUE(session_object.Load(FOO_MODEL_URI).IsOK()); EXPECT_TRUE(session_object.Initialize().IsOK()); RunOptions run_options; run_options.run_tag = "one session/one tag"; // prepare inputs std::vector dims_x = {3, 2}; std::vector values_x = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}; // prepare expected inputs and outputs std::vector expected_dims_y = {3, 2}; // now the expected value should be Add's result. std::vector expected_values_y = {2.0f, 4.0f, 6.0f, 8.0f, 10.0f, 12.0f}; // Now run RunSession(session_object, run_options, dims_x, values_x, expected_dims_y, expected_values_y); } TEST(CustomKernelTests, CustomKernelWithOptionalOutput) { SessionOptions so; so.session_logid = "InferenceSessionTests.NoTimeout"; //reigster optional schema auto optional_schema = GetOptionalOpSchema(); std::vector schemas = {optional_schema}; std::shared_ptr registry = std::make_shared(); EXPECT_TRUE(registry->RegisterOpSet(schemas, onnxruntime::kOnnxDomain, 5, 7).IsOK()); auto def = OptionalKernelDef(); //Register a foo kernel which is doing Add, but bind to Mul. EXPECT_TRUE(registry->RegisterCustomKernel(def, CreateOptionalOpKernel).IsOK()); InferenceSession session_object{so, GetEnvironment()}; EXPECT_TRUE(session_object.RegisterCustomRegistry(registry).IsOK()); EXPECT_TRUE(session_object.Load(OPTIONAL_MODEL1_URI).IsOK()); EXPECT_TRUE(session_object.Initialize().IsOK()); RunOptions run_options; run_options.run_tag = "one session/one tag"; // prepare inputs std::vector dims_x = {3, 2}; std::vector values_x = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}; // prepare expected inputs and outputs std::vector expected_dims_y = {3, 2}; // now the expected value should be equal result. std::vector expected_values_y = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}; // Now run RunSession(session_object, run_options, dims_x, values_x, expected_dims_y, expected_values_y); } } // namespace test } // namespace onnxruntime