onnxruntime/onnxruntime/test/opaque_api/test_opaque_api.cc
Changming Sun 7ff5c0e5a3
CMake changes (#2961)
1. Add support for vstest. 
2. Add support for vcpkg. To use it:
  ```bat
   vcpkg install zlib:x64-windows benchmark:x64-windows gtest:x64-windows protobuf:x64-windows pybind11:x64-windows re2:x64-windows
   mkdir build
   cmake ..\cmake -DCMAKE_BUILD_TYPE=Debug -A x64 -T host=x64 -DCMAKE_TOOLCHAIN_FILE=C:\vcpkg\scripts\buildsystems\vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows -Donnxruntime_PREFER_SYSTEM_LIB=ON
  ```
3. New cmake option: onnxruntime_PREFER_SYSTEM_LIB, which allows user using the preinstall libs instead of the things in onnxruntime submodule.
4. New cmake option: onnxruntime_ENABLE_MEMLEAK_CHECKER, which allows user turn on/off the memory leak checker by @RyanUnderhill in Windows Debug Build. The checker doesn't work with vstest.
4. Fix the post merge pipeline(Mainly for test coverage report).
5. Ignore the compile warning from the Featurizer library code
6. Apply "/utf-8" VC compile flag to our code. Without this, you can't build onnxruntime on Chinese Windows.
7. Remove the SingleUnitTestProject cmake option because it's deprecated more than one year and nobody is using it.
8. Move opaque api tests to onnxruntime_test_all
9. Enable "/W4" on CUDA ep's C++ code(Not the *.cu files), and fix some warnings, add some extra checks.
10. Delete the onnxruntime::test::TestEnvironment class.
11. Add a DLLmain for onnxruntime.dll. 
12. Allow dynamic link to libprotobuf
2020-02-03 19:33:14 -08:00

266 lines
11 KiB
C++

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#include <algorithm>
#include <core/common/logging/logging.h>
#include "core/framework/data_types.h"
#include "core/framework/execution_providers.h"
#include "core/framework/kernel_registry.h"
#include "core/framework/op_kernel.h"
#include "core/framework/session_state.h"
#include "core/graph/model.h"
#include "core/graph/op.h"
#include "core/providers/cpu/cpu_execution_provider.h"
#include "core/session/onnxruntime_cxx_api.h"
#include "gtest/gtest.h"
#include "onnx/defs/schema.h"
#include "test/providers/provider_test_utils.h"
#include "test/framework/test_utils.h"
using namespace ONNX_NAMESPACE;
using namespace onnxruntime::common;
// Data container used to ferry data through C API
extern "C" struct ExperimentalDataContainer {
// This is a string Tensor
// OrtValue will need to be released when reading/writing is complete
// by the client code
OrtValue* str_;
};
extern std::unique_ptr<Ort::Env> ort_env;
namespace onnxruntime {
// A new Opaque type representation
extern const char kMsTestDomain[] = "com.microsoft.test";
extern const char kTestOpaqueType[] = "ComplexOpaqueType";
// This is the actual Opaque type CPP representation
class ExperimentalType {
public:
std::string str_; // Pass a string
};
// The example demonstrates an approach for more complex types and, therefore,
// data containers. It is all too easy to have a data container that has all
// the primitive things. To have more complex objects inside we'd have to employ
// more complex objects, such as Tensors, Maps and Sequences that themselves may
// potentially contain complex objects.
// This example demonstrates how we pass a single const char* string within a scalar
// Tensor that would store data within std::string object. Eventually, the data makes it
// to Opaque experimental type that simply contains std::string
template <>
struct NonTensorTypeConverter<ExperimentalType> {
// This will get OrtValue from the ExperimentalDataContainer
// that contains tensor(string), we then repackage string into ExperimentalType
// and put that ExperiementalType into the OrtValue that is being used as input to a graph
static void FromContainer(MLDataType dtype, const void* data, size_t data_size, OrtValue& output) {
ORT_ENFORCE(data_size == sizeof(ExperimentalDataContainer), "Expecting an instance of ExperimentalDataContainer");
const ExperimentalDataContainer* container = reinterpret_cast<const ExperimentalDataContainer*>(data);
ORT_ENFORCE(container->str_->IsTensor(), "Expecting a string Tensor");
const Tensor& str_tensor = container->str_->Get<Tensor>();
std::unique_ptr<ExperimentalType> p(new ExperimentalType);
p->str_ = *str_tensor.Data<std::string>();
output.Init(p.release(), dtype, dtype->GetDeleteFunc());
}
// Reading string from the experimental type
// On the way back we create an OrtValue within ExperimentalDataContainer and put
// Tensor(string) back into it from ExperiementalType.
static void ToContainer(const OrtValue& input, size_t data_size, void* data) {
ORT_ENFORCE(data_size == sizeof(ExperimentalDataContainer), "Expecting an instance of ExperimentalDataContainer");
ExperimentalDataContainer* container = reinterpret_cast<ExperimentalDataContainer*>(data);
// Create and populate Tensor
TensorShape shape({1});
std::shared_ptr<IAllocator> allocator = std::make_shared<CPUAllocator>();
std::unique_ptr<Tensor> tp(new Tensor(DataTypeImpl::GetType<std::string>(), shape, allocator));
*tp->MutableData<std::string>() = input.Get<ExperimentalType>().str_;
std::unique_ptr<OrtValue> ort_val(new OrtValue);
const auto* dtype = DataTypeImpl::GetType<Tensor>();
ort_val->Init(tp.release(), dtype, dtype->GetDeleteFunc());
container->str_ = ort_val.release();
}
};
// Register ExperimentalType as Opaque. There will be a call to place it into a map during the execution part
ORT_REGISTER_OPAQUE_TYPE(ExperimentalType, kMsTestDomain, kTestOpaqueType);
// Now write the actual kernel that will operate on the custom Opaque type
// This kernel will take the input as a custom type and will output
// custom type with a different string. This kernel will take the
// original string will replace any instances of 'h' with '_'
class OpaqueCApiTestKernel final : public OpKernel {
public:
OpaqueCApiTestKernel(const OpKernelInfo& info) : OpKernel{info} {}
Status Compute(OpKernelContext* ctx) const override {
const ExperimentalType* input = ctx->Input<ExperimentalType>(0);
std::string result = input->str_;
std::replace(result.begin(), result.end(), 'h', '_');
ExperimentalType* output = ctx->Output<ExperimentalType>(0);
output->str_ = std::move(result);
return Status::OK();
}
};
ONNX_OPERATOR_KERNEL_EX(
OpaqueCApiTestKernel,
kMSFeaturizersDomain,
1,
kCpuExecutionProvider,
KernelDefBuilder()
.TypeConstraint("T", DataTypeImpl::GetType<ExperimentalType>()),
OpaqueCApiTestKernel);
#define ONNX_TEST_OPERATOR_SCHEMA(name) \
ONNX_TEST_OPERATOR_SCHEMA_UNIQ_HELPER(__COUNTER__, name)
#define ONNX_TEST_OPERATOR_SCHEMA_UNIQ_HELPER(Counter, name) \
ONNX_TEST_OPERATOR_SCHEMA_UNIQ(Counter, name)
#define ONNX_TEST_OPERATOR_SCHEMA_UNIQ(Counter, name) \
static ONNX_NAMESPACE::OpSchemaRegistry::OpSchemaRegisterOnce( \
op_schema_register_once##name##Counter) ONNX_UNUSED = \
ONNX_NAMESPACE::OpSchema(#name, __FILE__, __LINE__)
static void RegisterCustomKernel() {
// Register our custom type
MLDataType dtype = DataTypeImpl::GetType<ExperimentalType>();
DataTypeImpl::RegisterDataType(dtype);
// Registry the schema
ONNX_TEST_OPERATOR_SCHEMA(OpaqueCApiTestKernel)
.SetDoc("Replace all of h chars to _ in the original string contained within experimental type")
.SetDomain(onnxruntime::kMSFeaturizersDomain)
.SinceVersion(1)
.Input(
0,
"custom_type_with_string",
"Our custom type that has a string with h characters",
"T",
OpSchema::Single)
.Output(
0,
"custom_type_with_string",
"Custom type that has the original string with h characters substituted for _",
"T",
OpSchema::Single)
.TypeConstraint(
"T",
{"opaque(com.microsoft.test,ComplexOpaqueType)"},
"Custom type");
// Register kernel directly to KernelRegistry
// because we can not create custom ops with Opaque types
// as input
BuildKernelCreateInfoFn fn = BuildKernelCreateInfo<ONNX_OPERATOR_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSFeaturizersDomain, 1, OpaqueCApiTestKernel)>;
auto kernel_registry = CPUExecutionProvider(CPUExecutionProviderInfo()).GetKernelRegistry();
kernel_registry->Register(fn());
}
namespace test {
std::string CreateModel() {
RegisterCustomKernel();
Model model("ModelWithOpaque", false, logging::LoggingManager::DefaultLogger());
auto& graph = model.MainGraph();
std::vector<onnxruntime::NodeArg*> inputs;
std::vector<onnxruntime::NodeArg*> outputs;
{
TypeProto exp_type_proto(*DataTypeImpl::GetType<ExperimentalType>()->GetTypeProto());
// Find out the shape
auto& input_arg = graph.GetOrCreateNodeArg("Input", &exp_type_proto);
inputs.push_back(&input_arg);
//Output is our custom data type. This will return an Opaque type proto
auto& output_arg = graph.GetOrCreateNodeArg("Output", &exp_type_proto);
outputs.push_back(&output_arg);
auto& node = graph.AddNode("OpaqueCApiTestKernel", "OpaqueCApiTestKernel", "Replace all h to underscore",
inputs, outputs, nullptr, onnxruntime::kMSFeaturizersDomain);
node.SetExecutionProviderType(onnxruntime::kCpuExecutionProvider);
}
EXPECT_TRUE(graph.Resolve().IsOK());
// Get a proto and load from it
std::string serialized_model;
auto model_proto = model.ToProto();
EXPECT_TRUE(model_proto.SerializeToString(&serialized_model));
return serialized_model;
}
TEST(OpaqueApiTest, RunModelWithOpaqueInputOutput) {
std::string model_str = CreateModel();
try {
// initialize session options if needed
Ort::SessionOptions session_options;
Ort::Session session(*ort_env.get(), model_str.data(), model_str.size(), session_options);
Ort::AllocatorWithDefaultOptions allocator;
// Expecting one input
size_t num_input_nodes = session.GetInputCount();
EXPECT_EQ(num_input_nodes, 1U);
const char* input_name = session.GetInputName(0, allocator);
size_t num_output_nodes = session.GetOutputCount();
EXPECT_EQ(num_output_nodes, 1U);
const char* output_name = session.GetOutputName(0, allocator);
const char* const input_names[] = {input_name};
const char* const output_names[] = {output_name};
// Input
const std::string input_string{"hi, hello, high, highest"};
// Expected output
const std::string expected_output{"_i, _ello, _ig_, _ig_est"};
// Place a string into Tensor OrtValue and assign to the container
std::vector<int64_t> input_dims{1};
Ort::Value container_str = Ort::Value::CreateTensor(allocator, input_dims.data(), input_dims.size(), ONNX_TENSOR_ELEMENT_DATA_TYPE_STRING);
// No C++ Api to either create a string Tensor or to fill one with string, so we use C
const char* const input_char_string[] = {input_string.c_str()};
Ort::ThrowOnError(Ort::GetApi().FillStringTensor(static_cast<OrtValue*>(container_str), input_char_string, 1U));
// We put this into our container now
// This container life-span is supposed to eclipse the model running time
ExperimentalDataContainer container{static_cast<OrtValue*>(container_str)};
// Now we put our container into OrtValue
Ort::Value container_val = Ort::Value::CreateOpaque(kMsTestDomain, kTestOpaqueType, container);
Ort::Value output_val(nullptr); // empty
Ort::RunOptions run_options;
session.Run(run_options, input_names, &container_val, num_input_nodes,
output_names, &output_val, num_output_nodes);
ExperimentalDataContainer result;
// Need to verify that the output match the expected one
output_val.GetOpaqueData(kMsTestDomain, kTestOpaqueType, result);
// Wrap the resulting OrtValue into Ort::Value for C++ access and automatic cleanup
Ort::Value str_tensor_value(result.str_);
// Run some checks here
ASSERT_TRUE(str_tensor_value.IsTensor());
Ort::TypeInfo result_type_info = str_tensor_value.GetTypeInfo();
auto tensor_info = result_type_info.GetTensorTypeAndShapeInfo();
ASSERT_EQ(tensor_info.GetElementType(), ONNX_TENSOR_ELEMENT_DATA_TYPE_STRING);
ASSERT_EQ(tensor_info.GetDimensionsCount(), 1U);
// Get the actual value and compare
auto str_len = str_tensor_value.GetStringTensorDataLength();
ASSERT_EQ(str_len, expected_output.length());
std::unique_ptr<char[]> actual_result_string(new char[str_len + 1]);
size_t offset = 0;
str_tensor_value.GetStringTensorContent(actual_result_string.get(), str_len, &offset, 1);
actual_result_string[str_len] = 0;
ASSERT_EQ(expected_output.compare(actual_result_string.get()), 0);
} catch (const std::exception& ex) {
std::cerr << "Exception: " << ex.what() << std::endl;
ASSERT_TRUE(false);
}
}
} // namespace test
} // namespace onnxruntime