#include "caffe2/onnx/backend.h" #include "caffe2/core/logging.h" #include "caffe2/core/operator.h" #include "caffe2/onnx/device.h" #include "caffe2/onnx/helper.h" #include "caffe2/utils/map_utils.h" #include "caffe2/utils/proto_utils.h" #ifndef C10_MOBILE #include "onnx/checker.h" #endif #include "google/protobuf/io/coded_stream.h" #include "google/protobuf/io/zero_copy_stream_impl_lite.h" #include #include #include #include #include #include namespace caffe2 { namespace onnx { namespace { bool AlmostEqual(double a, double b) { constexpr static double kEps = 1e-15; return (fabs(a - b) < kEps); } template bool TryConvertingTensorRawValues( const TensorProto& onnx_tensor, ::google::protobuf::RepeatedField* field) { if (!onnx_tensor.has_raw_data()) { return false; } size_t raw_size = onnx_tensor.raw_data().size(); CAFFE_ENFORCE_EQ(raw_size % sizeof(T), 0); size_t num_elements = raw_size / sizeof(T); const void* src_ptr = static_cast(onnx_tensor.raw_data().data()); field->Resize(num_elements, 0); void* target_ptr = static_cast(field->mutable_data()); memcpy(target_ptr, src_ptr, raw_size); return true; } bool IsOperator(const std::string& op_type) { // pull in all the operators upon first invocation // Intentional leaky static std::set* ops_ = new std::set(caffe2::GetRegisteredOperators()); return ops_->count(caffe2::OpRegistryKey(op_type, "DEFAULT")); } caffe2::DeviceOption GetDeviceOption(const Device& onnx_device) { static const std::unordered_map m = { {DeviceType::CPU, caffe2::DeviceType::CPU}, {DeviceType::CUDA, caffe2::DeviceType::CUDA}}; caffe2::DeviceOption d; d.set_device_type(static_cast(m.at(onnx_device.type))); d.set_device_id(onnx_device.device_id); return d; } template U LookUpWithDefault( const std::unordered_map& map, const T& key, const U& default_value) { const auto it = map.find(key); if (it == map.end()) { return default_value; } else { return it->second; } } void UpdateNames( std::shared_ptr dummy, const caffe2::OperatorDef& op) { for (const auto& n : op.input()) { dummy->AddName(n); } for (const auto& n : op.output()) { dummy->AddName(n); } } void BuildOperator( caffe2::OperatorDef* c2_op, const std::string& op_type, const std::vector& inputs, const std::vector& outputs, const std::vector& args) { c2_op->set_name(""); c2_op->set_type(op_type); for (const auto& input : inputs) { c2_op->add_input(input); } for (const auto& output : outputs) { c2_op->add_output(output); } for (const auto& arg : args) { auto* tmp = c2_op->add_arg(); tmp->CopyFrom(arg); } } void BuildOperator( caffe2::OperatorDef* c2_op, const std::string& op_type, const std::vector& inputs, const std::vector& outputs) { std::vector empty; BuildOperator(c2_op, op_type, inputs, outputs, empty); } void CopyOnnxAttrValueToCaffe2Arg( caffe2::Argument* arg, const AttributeProto& attr) { if (attr.has_f()) { arg->set_f(attr.f()); } else if (attr.has_i()) { arg->set_i(attr.i()); } else if (attr.has_s()) { arg->set_s(attr.s()); } else if (attr.has_t()) { // For proto, we convert it to serialized string std::string buffer; attr.t().SerializeToString(&buffer); arg->set_s(buffer); } else if (attr.floats_size()) { arg->mutable_floats()->CopyFrom(attr.floats()); } else if (attr.ints_size()) { arg->mutable_ints()->CopyFrom(attr.ints()); } else if (attr.strings_size()) { arg->mutable_strings()->CopyFrom(attr.strings()); } else { CAFFE_THROW("Unsupported ONNX attribute: ", attr.name()); } } } // namespace OnnxAttributes::OnnxAttributes(const NodeProto& node) { for (const auto& attr : node.attribute()) { onnx_attrs_.emplace(attr.name(), &attr); } } template <> int64_t OnnxAttributes::get(const std::string& key) const { int64_t value = 0; const auto it = onnx_attrs_.find(key); if (it != onnx_attrs_.end()) { const AttributeProto& attr = *it->second; value = attr.i(); } return value; } template <> float OnnxAttributes::get(const std::string& key) const { float value = 0.0; const auto it = onnx_attrs_.find(key); if (it != onnx_attrs_.end()) { const AttributeProto& attr = *it->second; value = attr.f(); } return value; } template <> ::google::protobuf::RepeatedPtrField OnnxAttributes::get( const std::string& key) const { ::google::protobuf::RepeatedPtrField value; const auto it = onnx_attrs_.find(key); if (it != onnx_attrs_.end()) { const AttributeProto& attr = *it->second; value.CopyFrom(attr.strings()); } return value; } template <> ::google::protobuf::RepeatedField<::google::protobuf::int64> OnnxAttributes::get(const std::string& key) const { ::google::protobuf::RepeatedField<::google::protobuf::int64> value; const auto it = onnx_attrs_.find(key); if (it != onnx_attrs_.end()) { const AttributeProto& attr = *it->second; value.CopyFrom(attr.ints()); } return value; } template <> ::google::protobuf::RepeatedField OnnxAttributes::get( const std::string& key) const { ::google::protobuf::RepeatedField value; const auto it = onnx_attrs_.find(key); if (it != onnx_attrs_.end()) { const AttributeProto& attr = *it->second; value.CopyFrom(attr.floats()); } return value; } template <> const TensorProto* OnnxAttributes::get(const std::string& key) const { const TensorProto* value = nullptr; const auto it = onnx_attrs_.find(key); if (it != onnx_attrs_.end()) { const AttributeProto& attr = *it->second; value = &attr.t(); } return value; } ::google::protobuf::RepeatedPtrField OnnxAttributes::OnnxAttrToCaffe2Arg( std::function mapper) const { ::google::protobuf::RepeatedPtrField args; for (const auto& kv : onnx_attrs_) { // If the attribute was rewritten, we use it instead. Note that the // rewritten attribute still has the unmapped name const auto& attr = rewritten_onnx_attrs_.count(kv.first) ? rewritten_onnx_attrs_.at(kv.first) : (*kv.second); auto* arg = args.Add(); arg->set_name(mapper(attr.name())); CopyOnnxAttrValueToCaffe2Arg(arg, attr); } for (const auto& kv : rewritten_onnx_attrs_) { // If rewritten attribute doesn't appear in the original attributes, this is // a newlly added one and we need to add this to argument too if (!onnx_attrs_.count(kv.first)) { const auto& attr = kv.second; auto* arg = args.Add(); arg->set_name(mapper(attr.name())); CopyOnnxAttrValueToCaffe2Arg(arg, attr); } } return args; } const std::unordered_map& Caffe2Backend::get_broken_operators() const { const static std::unordered_map kBrokenOperators{}; return kBrokenOperators; } // Temporary hack for RNN related operators, as we don't have C++ interface in // C2 to build those operators yet const std::unordered_set& Caffe2Backend::get_rnn_operators() const { const static std::unordered_set kRNNOperators{ "LSTM", "GRU", "RNN"}; return kRNNOperators; } // Operators that are different between Caffe2 and // ONNX but only in their name. // In most cases, this should be empty - as the effort of ONNX is // to unify the operator definitions. const std::unordered_map& Caffe2Backend::get_renamed_operators() const { const static std::unordered_map kRenamedOperators{ {"Caffe2ConvTranspose", "ConvTranspose"}, {"GlobalMaxPool", "MaxPool"}, {"GlobalAveragePool", "AveragePool"}, {"Pad", "PadImage"}, {"Neg", "Negative"}, {"BatchNormalization", "SpatialBN"}, {"InstanceNormalization", "InstanceNorm"}, {"MatMul", "BatchMatMul"}, {"Upsample", "ResizeNearest"}, {"Identity", "Copy"}, {"InstanceNormalization", "InstanceNorm"}, {"Equal", "EQ"}, {"Less", "LT"}, {"Greater", "GT"}, {"Unsqueeze", "ExpandDims"}, {"Tile", "NumpyTile"}, {"DynamicSlice", "Slice"}, {"ConstantOfShape", "ConstantFill"}, {"RandomNormal", "GaussianFill"}, {"RandomNormalLike", "GaussianFill"}}; return kRenamedOperators; } const std::unordered_map& Caffe2Backend::get_renamed_attrs() const { const static std::unordered_map kRenamedAttrs{ {"kernel_shape", "kernels"}}; return kRenamedAttrs; } const std:: unordered_map>& Caffe2Backend::get_per_op_renamed_attrs() const { const static std:: unordered_map> kPerOpRenamedAttrs = { {"Squeeze", {{"axes", "dims"}}}, {"Unsqueeze", {{"axes", "dims"}}}, {"Transpose", {{"perm", "axes"}}}, {"ConvTranspose", {{"output_padding", "adjs"}}}, {"Selu", {{"gamma", "scale"}}}}; return kPerOpRenamedAttrs; } // operators whose behavior is different beyond renaming // the value is an attribute of this class that is a // function from ToffeIR node_def to caffe2 op_def const std::unordered_map& Caffe2Backend::get_special_operators() const { const static std:: unordered_map kSpecialOperators = { {"ArgMax", &Caffe2Backend::CreateArgMaxMin}, {"ArgMin", &Caffe2Backend::CreateArgMaxMin}, {"Cast", &Caffe2Backend::CreateCast}, {"Constant", &Caffe2Backend::CreateConstant}, {"ConstantOfShape", &Caffe2Backend::CreateConstantOfShape}, {"Conv", &Caffe2Backend::CreateConvPoolOpBase}, {"AveragePool", &Caffe2Backend::CreateConvPoolOpBase}, {"GlobalAveragePool", &Caffe2Backend::CreateConvPoolOpBase}, {"GlobalMaxPool", &Caffe2Backend::CreateConvPoolOpBase}, {"MaxPool", &Caffe2Backend::CreateConvPoolOpBase}, {"Reshape", &Caffe2Backend::CreateReshape}, {"Int8Reshape", &Caffe2Backend::CreateReshape}, {"Gather", &Caffe2Backend::CreateGather}, {"Gemm", &Caffe2Backend::CreateGemm}, {"Pad", &Caffe2Backend::CreatePad}, {"Concat", &Caffe2Backend::CreateConcat}, {"Int8Concat", &Caffe2Backend::CreateConcat}, {"LogSoftmax", &Caffe2Backend::CreateLogSoftmax}, {"Slice", &Caffe2Backend::CreateSlice}, {"Split", &Caffe2Backend::CreateSplit}, {"Reciprocal", &Caffe2Backend::CreateReciprocal}, {"BatchNormalization", &Caffe2Backend::CreateBatchNormalization}, {"MatMul", &Caffe2Backend::CreateMatMul}, {"Upsample", &Caffe2Backend::CreateUpsample}, {"Dropout", &Caffe2Backend::CreateDropout}, {"LRN", &Caffe2Backend::CreateLRN}, {"DynamicSlice", &Caffe2Backend::CreateDynamicSlice}, {"RandomNormal", &Caffe2Backend::CreateRandomNormal}, {"RandomNormalLike", &Caffe2Backend::CreateRandomNormal}, {"Where", &Caffe2Backend::CreateWhereOp}, {"NonZero", &Caffe2Backend::CreateNonZeroOp}, {"Multinomial", &Caffe2Backend::CreateMultinomialOp}}; return kSpecialOperators; } //============================ // Special Operator Converters //============================ Caffe2Ops Caffe2Backend::CreateArgMaxMin( OnnxNode* onnx_node, const ConversionContext& ctx) { auto& attributes = onnx_node->attributes; if (!attributes.HasAttribute("axis")) { auto* attr = attributes.AddRewrittenAttribute("axis"); attr->set_i(0); } return CommonOnnxNodeToCaffe2Ops(onnx_node, ctx); } Caffe2Ops Caffe2Backend::CreateCast( OnnxNode* onnx_node, const ConversionContext& ctx) { auto c2_op = CommonOnnxNodeToCaffe2Ops(onnx_node, ctx); auto onnx_dtype = onnx_node->attributes.get("to", TensorProto::UNDEFINED); auto c2_dtype = caffe2::TensorProto::UNDEFINED; switch (onnx_dtype) { case ::ONNX_NAMESPACE::TensorProto::FLOAT: c2_dtype = caffe2::TensorProto::FLOAT; break; case ::ONNX_NAMESPACE::TensorProto::UINT8: c2_dtype = caffe2::TensorProto::UINT8; break; case ::ONNX_NAMESPACE::TensorProto::INT8: c2_dtype = caffe2::TensorProto::INT8; break; case ::ONNX_NAMESPACE::TensorProto::UINT16: c2_dtype = caffe2::TensorProto::UINT16; break; case ::ONNX_NAMESPACE::TensorProto::INT16: c2_dtype = caffe2::TensorProto::INT16; break; case ::ONNX_NAMESPACE::TensorProto::INT32: c2_dtype = caffe2::TensorProto::INT32; break; case ::ONNX_NAMESPACE::TensorProto::INT64: c2_dtype = caffe2::TensorProto::INT64; break; case ::ONNX_NAMESPACE::TensorProto::STRING: c2_dtype = caffe2::TensorProto::STRING; break; case ::ONNX_NAMESPACE::TensorProto::BOOL: c2_dtype = caffe2::TensorProto::BOOL; break; case ::ONNX_NAMESPACE::TensorProto::FLOAT16: c2_dtype = caffe2::TensorProto::FLOAT16; break; case ::ONNX_NAMESPACE::TensorProto::DOUBLE: c2_dtype = caffe2::TensorProto::DOUBLE; break; case ::ONNX_NAMESPACE::TensorProto::UINT32: case ::ONNX_NAMESPACE::TensorProto::UINT64: case ::ONNX_NAMESPACE::TensorProto::COMPLEX64: case ::ONNX_NAMESPACE::TensorProto::COMPLEX128: case ::ONNX_NAMESPACE::TensorProto::UNDEFINED: c2_dtype = caffe2::TensorProto::UNDEFINED; break; }; CAFFE_ENFORCE_NE( c2_dtype, caffe2::TensorProto::UNDEFINED, "Casting to '", onnx_dtype, "' dtype is not supported"); CAFFE_ENFORCE_EQ( c2_op.ops.Get(0).arg().size(), 1, "Unexpected number of attributes in 'Cast'"); c2_op.ops.Mutable(0)->mutable_arg(0)->set_i(c2_dtype); return c2_op; } Caffe2Ops Caffe2Backend::CreateConstant( OnnxNode* onnx_node, const ConversionContext& ctx) { CAFFE_ENFORCE_EQ(onnx_node->node.output_size(), 1); Caffe2Ops ret; auto* c2_op = ret.ops.Add(); const auto* value = onnx_node->attributes.get("value"); BuildTensorFillingOp(c2_op, *value, onnx_node->node.output(0)); return ret; } Caffe2Ops Caffe2Backend::CreateConstantOfShape( OnnxNode* onnx_node, const ConversionContext& ctx) { CAFFE_ENFORCE_EQ(onnx_node->node.input_size(), 1); CAFFE_ENFORCE_EQ(onnx_node->node.output_size(), 1); Caffe2Ops ret; auto* c2_op = ret.ops.Add(); const auto* value = onnx_node->attributes.get("value"); if (value) { BuildTensorFillingOp( c2_op, *value, onnx_node->node.output(0), onnx_node->node.input(0)); } else { c2_op->set_type("ConstantFill"); c2_op->add_input(onnx_node->node.input(0)); c2_op->add_output(onnx_node->node.output(0)); auto c2_input_as_shape = c2_op->add_arg(); c2_input_as_shape->set_name("input_as_shape"); c2_input_as_shape->set_i(1); } return ret; } // Note [Caffe2 ConvPoolOpBase] // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // To understand what is going on here, we have to talk a little bit about // Caffe2's internals. // // First, it's important to know that all of Caffe2's pooling and convolution // operators inherit from "ConvPoolOpBase", which is an abstract class that // defines all of the attributes (kernels, dilations, strides, etc) which one // sees on these operators. Unfortunately, Caffe2's documentation generator // doesn't know how to handle cases like this, so for example, if you look at // the docs for MaxPool at // you won't see any // of the attributes. You have to go source diving to find the information; in // particular, you want to look at: // https://github.com/caffe2/caffe2/blob/master/caffe2/operators/conv_pool_op_base.h // This class handles *global* pooling as well. // // Second, it's important to know what Caffe2 expects for padding, which can // be somewhat difficult to understand from the code because Caffe2 handles // both singular/pluralized spellings of padding, and there is also legacy // padding business. The short version of the story is that, for NON-legacy // padding (which is what we want to output), padding is expected to be // *twice* the size of kernels. So if you have a 2D convolution, Caffe2 // will accept two values in 'kernels', but FOUR values in 'pads'; // furthermore, this is *mandatory.* // // Finally, ConvPoolOpBase is not the only class of it's kind; there is // be tricked by the fact that Conv and ConvTranspose have similar // parameters; they exercise different codepaths and need to be handled // differently. Caffe2Ops Caffe2Backend::CreateConvPoolOpBase( OnnxNode* onnx_node, const ConversionContext& ctx) { const auto& node = onnx_node->node; auto& attributes = onnx_node->attributes; if (node.op_type().find("Global") == 0) { auto* attr = attributes.AddRewrittenAttribute("global_pooling"); attr->set_i(1); } if (attributes.HasAttribute("kernel_shape") && attributes.HasAttribute("pads")) { auto kernel_shape = attributes .get<::google::protobuf::RepeatedField<::google::protobuf::int64>>( "kernel_shape"); auto pads = attributes .get<::google::protobuf::RepeatedField<::google::protobuf::int64>>( "pads"); if (kernel_shape.size() == pads.size()) { // Caffe2 requires pads to be twice the size of kernels. auto* attr = attributes.AddRewrittenAttribute("pads"); attr->mutable_ints()->CopyFrom(pads); attr->mutable_ints()->MergeFrom(pads); } } return CommonOnnxNodeToCaffe2Ops(onnx_node, ctx); } Caffe2Ops Caffe2Backend::CreateReshape( OnnxNode* onnx_node, const ConversionContext& ctx) { auto c2_op = CommonOnnxNodeToCaffe2Ops(onnx_node, ctx); CAFFE_ENFORCE_EQ(c2_op.ops.size(), 1); auto* op = c2_op.ops.Mutable(0); op->add_output(dummy_->NewDummyName()); return c2_op; } Caffe2Ops Caffe2Backend::CreateRandomNormal( OnnxNode* onnx_node, const ConversionContext& ctx) { auto& attributes = onnx_node->attributes; if (attributes.HasAttribute("seed")) { CAFFE_THROW("Caffe2 GaussianFill does not support random seed"); } if (attributes.HasAttribute("dtype")) { if (attributes.get("dtype") != TensorProto::FLOAT) { CAFFE_THROW("Caffe2 GaussianFill only support FLOAT dtype"); } attributes.remove("dtype"); } if (attributes.HasAttribute("scale")) { auto scale = attributes.get("scale"); auto* attr = attributes.AddRewrittenAttribute("std"); attr->set_f(scale); attributes.remove("scale"); } return CommonOnnxNodeToCaffe2Ops(onnx_node, ctx); } Caffe2Ops Caffe2Backend::CreateWhereOp( OnnxNode* onnx_node, const ConversionContext& ctx) { // The native Caffe2 op doesn't support broadcasting, so we defer the handling // of this op to the ATen library that does. onnx::NodeProto converted; converted.CopyFrom(onnx_node->node); converted.set_op_type("ATen"); onnx::AttributeProto* attr = converted.add_attribute(); attr->set_name("operator"); attr->set_s("where"); OnnxNode new_node(converted); return CommonOnnxNodeToCaffe2Ops(&new_node, ctx); } Caffe2Ops Caffe2Backend::CreateNonZeroOp( OnnxNode* onnx_node, const ConversionContext& ctx) { // Native Caffe2 doesn't support NonZero, fallback to ATen. // ATen nonzero is equivalent to Transpose(ONNX::NonZero). onnx::NodeProto converted; converted.CopyFrom(onnx_node->node); auto nonzero_output = dummy_->NewDummyName(); converted.set_output(0, nonzero_output); converted.set_op_type("ATen"); onnx::AttributeProto* attr = converted.add_attribute(); attr->set_name("operator"); attr->set_s("nonzero"); OnnxNode new_node(converted); auto ret = CommonOnnxNodeToCaffe2Ops(&new_node, ctx); auto* c2_transpose = ret.ops.Add(); BuildOperator( c2_transpose, "Transpose", {nonzero_output}, {onnx_node->node.output(0)}); return ret; } Caffe2Ops Caffe2Backend::CreateMultinomialOp( OnnxNode* onnx_node, const ConversionContext& ctx) { // Fallback to ATen. // ATen::Multinomial takes probabilities as input, ONNX Multinomial expects // input to be log probabilities. Caffe2Ops ret; auto c2_exp_output = dummy_->NewDummyName(); auto* c2_exp = ret.ops.Add(); BuildOperator(c2_exp, "Exp", {onnx_node->node.input(0)}, {c2_exp_output}); auto* c2_multinomial = ret.ops.Add(); caffe2::Argument c2_arg_op; c2_arg_op.set_name("operator"); c2_arg_op.set_s("multinomial"); // ONNX Multinomial only supports replacement=True. caffe2::Argument c2_arg_rep; c2_arg_rep.set_name("replacement"); c2_arg_rep.set_i(1); auto& onnx_attributes = onnx_node->attributes; caffe2::Argument c2_arg_num; c2_arg_num.set_name("num_samples"); c2_arg_num.set_i(onnx_attributes.get("sample_size")); // ONNX Multinomial has attribute dtype in {int64, int32}, which specifies // output datatype. ATen::Multinomial output dtype is always int64. auto onnx_dtype = onnx_attributes.get("dtype", TensorProto::UNDEFINED); if (onnx_dtype == ::ONNX_NAMESPACE::TensorProto::INT64) { BuildOperator( c2_multinomial, "ATen", {c2_exp_output}, {onnx_node->node.output(0)}, {c2_arg_op, c2_arg_rep, c2_arg_num}); } else if (onnx_dtype == ::ONNX_NAMESPACE::TensorProto::INT32) { auto c2_multinomial_output = dummy_->NewDummyName(); BuildOperator( c2_multinomial, "ATen", {c2_exp_output}, {c2_multinomial_output}, {c2_arg_op, c2_arg_rep, c2_arg_num}); auto* c2_cast = ret.ops.Add(); caffe2::Argument to; to.set_name("to"); to.set_i(caffe2::TensorProto::INT32); BuildOperator( c2_cast, "Cast", {c2_multinomial_output}, {onnx_node->node.output(0)}, {to}); } else { CAFFE_THROW( "ONNX does not support dtype other than int32/int64 in Multinomial, but get ", onnx_dtype); } return ret; } Caffe2Ops Caffe2Backend::CreateReciprocal( OnnxNode* onnx_node, const ConversionContext& ctx) { const auto& node = onnx_node->node; if (node.input_size() != 1 || node.output_size() != 1) { CAFFE_THROW("Caffe2 Reciprocal should have 1 input and 1 output"); } Caffe2Ops ret; auto* c2_op = ret.ops.Add(); BuildOperator(c2_op, "Reciprocal", {node.input(0)}, {node.output(0)}, {}); return ret; } Caffe2Ops Caffe2Backend::CreateGather( OnnxNode* onnx_node, const ConversionContext& ctx) { const auto& node = onnx_node->node; if (node.input_size() < 2 || node.output_size() < 1) { CAFFE_THROW("Caffe2 Gather should have 2 inputs and 1 output"); } Caffe2Ops ret; auto* c2_op = ret.ops.Add(); std::vector inputs; inputs.emplace_back(node.input(0)); inputs.emplace_back(node.input(1)); std::vector outputs; outputs.emplace_back(node.output(0)); auto axis = onnx_node->attributes.get("axis", 0L); if (axis == 0) { BuildOperator(c2_op, "Gather", inputs, outputs); } else if (axis == 1) { BuildOperator(c2_op, "BatchGather", inputs, outputs); } else { CAFFE_THROW( "Caffe2 only supports Gather with axis being 0 or 1, ", "whereas axis is ", axis); } return ret; } Caffe2Ops Caffe2Backend::CreateGemm( OnnxNode* onnx_node, const ConversionContext& ctx) { const auto& node = onnx_node->node; if (node.input_size() < 3 || node.output_size() < 1) { CAFFE_THROW("Caffe2 Gemm should have 3 inputs and 1 output"); } Caffe2Ops ret; auto input_a = node.input(0); auto input_b = node.input(1); auto input_c = node.input(2); auto output = node.output(0); auto alpha = onnx_node->attributes.get("alpha", 1.0); auto beta = onnx_node->attributes.get("beta", 1.0); if (!AlmostEqual(alpha, 1)) { auto scaled_a = dummy_->NewDummyName(); caffe2::Argument scale; scale.set_name("scale"); scale.set_f(alpha); auto* c2_op = ret.ops.Add(); BuildOperator(c2_op, "Scale", {input_a}, {scaled_a}, {scale}); input_a = scaled_a; } if (!AlmostEqual(beta, 1)) { auto scaled_c = dummy_->NewDummyName(); caffe2::Argument scale; scale.set_name("scale"); scale.set_f(beta); auto* c2_op = ret.ops.Add(); BuildOperator(c2_op, "Scale", {input_c}, {scaled_c}, {scale}); input_c = scaled_c; } auto trans_a = onnx_node->attributes.get("transA", 0L); auto trans_b = onnx_node->attributes.get("transB", 0L); // Support broadcast by default when opset_version > 6. auto broadcast = onnx_node->attributes.get( "broadcast", (ctx.opset_version() > 6) ? 1L : 0L); // If the c's shape information is available and c is a 1d tensor(except // c is a scalar), use FC aggressively. auto check_fc = [&]() -> bool { const auto input_c_vi_iter = ctx.value_infos().find(node.input(2)); if (input_c_vi_iter == ctx.value_infos().end()) { return false; } const auto input_c_shape = input_c_vi_iter->second.type().tensor_type().shape(); if (input_c_shape.dim_size() != 1) { return false; } // c is a scalar. if (input_c_shape.dim(0).dim_value() == 1) { const auto input_b_vi_iter = ctx.value_infos().find(node.input(1)); // If the b's shape is not available, skip FC. if (input_b_vi_iter == ctx.value_infos().end()) { return false; } const auto input_b_shape = input_b_vi_iter->second.type().tensor_type().shape(); int input_b_last_dim_index = (trans_b) ? 0 : 1; // If b's last dim is not 1, skip FC. if (input_b_shape.dim_size() <= input_b_last_dim_index || input_b_shape.dim(input_b_last_dim_index).dim_value() != 1) { return false; } } return true; }; if (!trans_a && broadcast && check_fc()) { auto* c2_op = ret.ops.Add(); if (trans_b) { BuildOperator(c2_op, "FC", {input_a, input_b, input_c}, {output}); } else { BuildOperator( c2_op, "FCTransposed", {input_a, input_b, input_c}, {output}); } } else { auto ab = dummy_->NewDummyName(); caffe2::Argument arg_trans_a; arg_trans_a.set_name("trans_a"); arg_trans_a.set_i(trans_a); caffe2::Argument arg_trans_b; arg_trans_b.set_name("trans_b"); arg_trans_b.set_i(trans_b); auto* c2_op = ret.ops.Add(); BuildOperator( c2_op, "MatMul", {input_a, input_b}, {ab}, {arg_trans_a, arg_trans_b}); c2_op = ret.ops.Add(); if (ctx.opset_version() >= 7) { BuildOperator(c2_op, "Add", {ab, input_c}, {output}); } else { caffe2::Argument arg_broadcast; arg_broadcast.set_name("broadcast"); arg_broadcast.set_i(broadcast); BuildOperator(c2_op, "Add", {ab, input_c}, {output}, {arg_broadcast}); } } return ret; } Caffe2Ops Caffe2Backend::CreatePad( OnnxNode* onnx_node, const ConversionContext& ctx) { auto& attributes = onnx_node->attributes; ::google::protobuf::RepeatedField<::google::protobuf::int64> pads; std::string pad_name = ctx.opset_version() < 2 ? "paddings" : "pads"; pads = attributes .get<::google::protobuf::RepeatedField<::google::protobuf::int64>>( pad_name); std::string str; std::stringstream ss; ss << "["; for (const auto& i : pads) { ss << i << ", "; } ss << "]"; str = ss.str(); // Guard the invalid (negative) pads attribute. for (const auto i : pads) { if (i < 0) { CAFFE_THROW("ONNX does not support negative pads in Pad, but get ", str); } } // first two dim is for batch and channel. Note that now all the values are // non-negative if (!(pads.size() == 8 && (pads.Get(0) + pads.Get(1) + pads.Get(4) + pads.Get(5) == 0))) { CAFFE_THROW( "Caffe2 only supports padding 2D Tensor, whereas padding is ", str); } // rewrite the padding info auto* attr = attributes.AddRewrittenAttribute(pad_name); attr->add_ints(pads.Get(2)); attr->add_ints(pads.Get(3)); attr->add_ints(pads.Get(6)); attr->add_ints(pads.Get(7)); return CommonOnnxNodeToCaffe2Ops(onnx_node, ctx); } // TODO: Caffe2 Concat has an extra output. It should be only // used when doing training, so we should change Caffe2 to allow // 1 output. Caffe2Ops Caffe2Backend::CreateConcat( OnnxNode* onnx_node, const ConversionContext& ctx) { auto c2_op = CommonOnnxNodeToCaffe2Ops(onnx_node, ctx); CAFFE_ENFORCE_EQ(c2_op.ops.size(), 1); auto* op = c2_op.ops.Mutable(0); op->add_output(dummy_->NewDummyName()); return c2_op; } Caffe2Ops Caffe2Backend::CreateLogSoftmax( OnnxNode* onnx_node, const ConversionContext& ctx) { const auto& node = onnx_node->node; if (node.input_size() < 1 || node.output_size() < 1) { CAFFE_THROW("LogSoftmax should have 1 input and 1 output"); } auto axis = onnx_node->attributes.get("axis", 1L); caffe2::Argument arg_axis; arg_axis.set_name("axis"); arg_axis.set_i(axis); auto softmax_a = dummy_->NewDummyName(); Caffe2Ops ret; auto* c2_op = ret.ops.Add(); BuildOperator(c2_op, "Softmax", {node.input(0)}, {softmax_a}, {arg_axis}); c2_op = ret.ops.Add(); BuildOperator(c2_op, "Log", {softmax_a}, {node.output(0)}); return ret; } Caffe2Ops Caffe2Backend::CreateSlice( OnnxNode* onnx_node, const ConversionContext& ctx) { auto op_tmp = CommonOnnxNodeToCaffe2Ops(onnx_node, ctx); CAFFE_ENFORCE_EQ(op_tmp.ops.size(), 1); auto* op = op_tmp.ops.Mutable(0); std::unordered_map args; for (auto& arg : *op->mutable_arg()) { args.emplace(arg.name(), &arg); } caffe2::Argument starts_vals; starts_vals.set_name("values"); auto pos = args.find("starts"); if (pos != args.end()) { for (auto i : pos->second->ints()) { starts_vals.add_ints(i < 0 ? i - 1 : i); } args.erase(pos); } caffe2::Argument ends_vals; ends_vals.set_name("values"); pos = args.find("ends"); if (pos != args.end()) { for (auto i : pos->second->ints()) { if (i == std::numeric_limits::max()) { ends_vals.add_ints(-1); } else { ends_vals.add_ints(i < 0 ? i - 1 : i); } } args.erase(pos); } caffe2::Argument axes_vals; axes_vals.set_name("values"); pos = args.find("axes"); if (pos != args.end()) { for (auto i : pos->second->ints()) { axes_vals.add_ints(i); } args.erase(pos); } else { auto ndim = starts_vals.ints_size(); for (int64_t i = 0; i < ndim; ++i) { axes_vals.add_ints(i); } } CAFFE_ENFORCE_GE(op->input_size(), 1); auto data = op->input(0); auto shape_tensor = dummy_->NewDummyName(); Caffe2Ops ret; auto* c2_op = ret.ops.Add(); BuildOperator(c2_op, "Shape", {data}, {shape_tensor}); auto axes_tensor = dummy_->NewDummyName(); c2_op = ret.ops.Add(); { caffe2::Argument shape; shape.set_name("shape"); shape.add_ints(axes_vals.ints_size()); BuildOperator( c2_op, "GivenTensorIntFill", {}, {axes_tensor}, {shape, axes_vals}); } auto starts_vals_tensor = dummy_->NewDummyName(); auto starts_tensor = dummy_->NewDummyName(); c2_op = ret.ops.Add(); { caffe2::Argument shape_starts; shape_starts.set_name("shape"); shape_starts.add_ints(starts_vals.ints_size()); BuildOperator( c2_op, "GivenTensorInt64Fill", {}, {starts_vals_tensor}, {shape_starts, starts_vals}); } caffe2::Argument dtype; dtype.set_name("dtype"); dtype.set_i(static_cast(caffe2::TensorProto::INT64)); caffe2::Argument constant; constant.set_name("value"); constant.set_i(0); c2_op = ret.ops.Add(); BuildOperator( c2_op, "ConstantFill", {shape_tensor}, {starts_tensor}, {dtype, constant}); c2_op = ret.ops.Add(); BuildOperator( c2_op, "ScatterAssign", {starts_tensor, axes_tensor, starts_vals_tensor}, {starts_tensor}); // Slice only accepts starts as int caffe2::Argument to; to.set_name("to"); to.set_i(static_cast(caffe2::TensorProto::INT32)); auto ends_vals_tensor = dummy_->NewDummyName(); auto ends_tensor = dummy_->NewDummyName(); c2_op = ret.ops.Add(); { caffe2::Argument shape_ends; shape_ends.set_name("shape"); shape_ends.add_ints(ends_vals.ints_size()); BuildOperator( c2_op, "GivenTensorInt64Fill", {}, {ends_vals_tensor}, {shape_ends, ends_vals}); } constant.set_i(-1); c2_op = ret.ops.Add(); BuildOperator( c2_op, "ConstantFill", {shape_tensor}, {ends_tensor}, {dtype, constant}); c2_op = ret.ops.Add(); BuildOperator( c2_op, "ScatterAssign", {ends_tensor, axes_tensor, ends_vals_tensor}, {ends_tensor}); // attach the original op at the end c2_op = ret.ops.Add(); c2_op->CopyFrom(*op); c2_op->mutable_input()->Clear(); c2_op->add_input(data); c2_op->add_input(starts_tensor); c2_op->add_input(ends_tensor); c2_op->mutable_arg()->Clear(); for (const auto& kv : args) { c2_op->add_arg()->CopyFrom(*kv.second); } return ret; } // Do the following: // for a given index tensor (i.e. `starts` or `ends`): // 1) Hilariously subtract 1 from the value if it is negative. This due to // the behavior of Caffe2's slice operator not matching that of ONNX's slice // 2) Fully expand the index tensor out to the rank of the data tensor. // pseudocode: indices_full = zeros(rank); indices_full[axes] = indices.int() std::string Caffe2Backend::PreprocessSliceIndexTensor( OnnxNode* onnx_node, Caffe2Ops& ret, std::string indices_tensor, std::string axes_tensor, std::string rank_tensor, std::string zero_tensor, std::string one_tensor, int default_value) { auto indices_tensor_full = dummy_->NewDummyName(); { caffe2::Argument value; value.set_name("value"); value.set_i(default_value); caffe2::Argument dtype; dtype.set_name("dtype"); dtype.set_i(static_cast(caffe2::TensorProto::INT64)); caffe2::Argument input_as_shape; input_as_shape.set_name("input_as_shape"); input_as_shape.set_i(1); auto c2_op = ret.ops.Add(); BuildOperator( c2_op, "ConstantFill", {rank_tensor}, {indices_tensor_full}, {value, dtype, input_as_shape}); } // Subtract 1 from each element of the indices tensor that is negative auto lt_tensor = dummy_->NewDummyName(); { caffe2::Argument broadcast; broadcast.set_name("broadcast"); broadcast.set_i(1); auto c2_op = ret.ops.Add(); BuildOperator( c2_op, "LT", {indices_tensor, zero_tensor}, {lt_tensor}, {broadcast}); } auto sub_one_tensor = dummy_->NewDummyName(); { caffe2::Argument broadcast; broadcast.set_name("broadcast"); broadcast.set_i(1); auto c2_op = ret.ops.Add(); BuildOperator( c2_op, "Sub", {indices_tensor, one_tensor}, {sub_one_tensor}, {broadcast}); } auto indices_tensor_adjusted = dummy_->NewDummyName(); auto c2_op = ret.ops.Add(); BuildOperator( c2_op, "Conditional", {lt_tensor, sub_one_tensor, indices_tensor}, {indices_tensor_adjusted}, {}); // Fill in values specified from the partially-specified ONNX indices tensor c2_op = ret.ops.Add(); BuildOperator( c2_op, "ScatterAssign", {indices_tensor_full, axes_tensor, indices_tensor_adjusted}, {indices_tensor_full}); return indices_tensor_full; } Caffe2Ops Caffe2Backend::CreateDynamicSlice( OnnxNode* onnx_node, const ConversionContext& ctx) { auto op_tmp = CommonOnnxNodeToCaffe2Ops(onnx_node, ctx); CAFFE_ENFORCE_EQ(op_tmp.ops.size(), 1); auto* op = op_tmp.ops.Mutable(0); std::unordered_map args; for (auto& arg : *op->mutable_arg()) { args.emplace(arg.name(), &arg); } CAFFE_ENFORCE_GE(op->input_size(), 1); auto data = op->input(0); Caffe2Ops ret; // First get the shape of the input tensor auto* c2_op = ret.ops.Add(); auto size_tensor = dummy_->NewDummyName(); BuildOperator(c2_op, "Shape", {data}, {size_tensor}); // Now get the rank of the tensor by getting the shape of the shape of // the input tensor c2_op = ret.ops.Add(); auto rank_tensor = dummy_->NewDummyName(); BuildOperator(c2_op, "Shape", {size_tensor}, {rank_tensor}); // Axes tensor will be used to populate the fully-specified starts and ends // arguments to the caffe2 Slice operator. std::string axes_tensor; if (onnx_node->node.input_size() > 3) { axes_tensor = onnx_node->node.input(3); } else { axes_tensor = dummy_->NewDummyName(); auto* c2_op = ret.ops.Add(); BuildOperator(c2_op, "Range", {rank_tensor}, {axes_tensor}, {}); } // Useful int tensors auto define_integer_constant = [this, &ret](int val) { caffe2::Argument value; value.set_name("value"); value.set_i(val); caffe2::Argument dtype; dtype.set_name("dtype"); dtype.set_i(static_cast(caffe2::TensorProto::INT64)); caffe2::Argument shape; shape.set_name("shape"); shape.add_ints(1); auto c2_op = ret.ops.Add(); auto name = dummy_->NewDummyName(); BuildOperator(c2_op, "ConstantFill", {}, {name}, {value, dtype, shape}); return name; }; auto zero_tensor = define_integer_constant(0); auto one_tensor = define_integer_constant(1); auto starts_tensor_full = PreprocessSliceIndexTensor( onnx_node, ret, onnx_node->node.input(1), // starts axes_tensor, rank_tensor, zero_tensor, one_tensor, 0); auto ends_tensor_full = PreprocessSliceIndexTensor( onnx_node, ret, onnx_node->node.input(2), // ends axes_tensor, rank_tensor, zero_tensor, one_tensor, -1); // attach the original op at the end c2_op = ret.ops.Add(); c2_op->CopyFrom(*op); c2_op->mutable_input()->Clear(); c2_op->add_input(data); c2_op->add_input(starts_tensor_full); c2_op->add_input(ends_tensor_full); c2_op->mutable_arg()->Clear(); for (const auto& kv : args) { c2_op->add_arg()->CopyFrom(*kv.second); } return ret; } Caffe2Ops Caffe2Backend::CreateBatchNormalization( OnnxNode* onnx_node, const ConversionContext& ctx) { auto& attributes = onnx_node->attributes; if (ctx.opset_version() < 6) { attributes.remove("consumed_inputs"); } if (ctx.opset_version() >= 7) { auto* attr = attributes.AddRewrittenAttribute("is_test"); attr->set_i(1); } if (attributes.HasAttribute("spatial") && attributes.get("spatial") == 1) { attributes.remove("spatial"); } return CommonOnnxNodeToCaffe2Ops(onnx_node, ctx); } Caffe2Ops Caffe2Backend::CreateSplit( OnnxNode* onnx_node, const ConversionContext& ctx) { auto& attributes = onnx_node->attributes; if (!attributes.HasAttribute("axis")) { auto* attr = attributes.AddRewrittenAttribute("axis"); attr->set_i(0); } return CommonOnnxNodeToCaffe2Ops(onnx_node, ctx); } Caffe2Ops Caffe2Backend::CreateMatMul( OnnxNode* onnx_node, const ConversionContext& ctx) { const auto& node = onnx_node->node; if (node.input_size() != 2) { CAFFE_THROW("MatMul should have 2 inputs"); } auto c2_op = CommonOnnxNodeToCaffe2Ops(onnx_node, ctx); CAFFE_ENFORCE_EQ(c2_op.ops.size(), 1); auto* op = c2_op.ops.Mutable(0); auto* broadcast_arg = op->add_arg(); broadcast_arg->set_name("broadcast"); broadcast_arg->set_i(1); return c2_op; } Caffe2Ops Caffe2Backend::CreateUpsample( OnnxNode* onnx_node, const ConversionContext& ctx) { auto& attributes = onnx_node->attributes; attributes.remove("mode"); if (ctx.opset_version() >= 7 && ctx.opset_version() < 9) { const auto& scales = attributes.get<::google::protobuf::RepeatedField>("scales"); if (scales.size() != 4) { CAFFE_THROW("The scales argument should have size 4"); } else if ( !AlmostEqual(scales.Get(0), 1) || !AlmostEqual(scales.Get(1), 1)) { CAFFE_THROW("The first two elements in the scales argument must be 1"); } attributes.remove("scales"); auto c2_op = CommonOnnxNodeToCaffe2Ops(onnx_node, ctx); auto* op = c2_op.ops.Mutable(0); auto* c2_height = op->add_arg(); c2_height->set_name("height_scale"); c2_height->set_f(scales.Get(2)); auto* c2_width = op->add_arg(); c2_width->set_name("width_scale"); c2_width->set_f(scales.Get(3)); return c2_op; } else if (ctx.opset_version() >= 9) { const auto& node = onnx_node->node; if (node.input_size() != 2) { CAFFE_THROW("Expects 2 input in upsample after onnx version 9"); } Caffe2Ops ret; // Slice the input {1, 1, height, width} -> {height, width} auto* c2_op = ret.ops.Add(); auto sliced_input = dummy_->NewDummyName(); caffe2::Argument arg_starts, arg_ends; arg_starts.set_name("starts"); arg_starts.add_ints(2); arg_ends.set_name("ends"); arg_ends.add_ints(-1); BuildOperator( c2_op, "Slice", {node.input(1)}, {sliced_input}, {arg_starts, arg_ends}); // Upsample c2_op = ret.ops.Add(); BuildOperator( c2_op, "ResizeNearest", {node.input(0), sliced_input}, {node.output(0)}, {}); return ret; } return CommonOnnxNodeToCaffe2Ops(onnx_node, ctx); } Caffe2Ops Caffe2Backend::CreateDropout( OnnxNode* onnx_node, const ConversionContext& ctx) { if (ctx.opset_version() >= 7) { auto& attributes = onnx_node->attributes; auto* attr = attributes.AddRewrittenAttribute("is_test"); attr->set_i(1); } return CommonOnnxNodeToCaffe2Ops(onnx_node, ctx); } Caffe2Ops Caffe2Backend::CreateLRN( OnnxNode* onnx_node, const ConversionContext& ctx) { auto c2_op = CommonOnnxNodeToCaffe2Ops(onnx_node, ctx); const auto& attributes = onnx_node->attributes; if (!attributes.HasAttribute("alpha")) { auto* arg = c2_op.ops.Mutable(0)->add_arg(); arg->set_name("alpha"); arg->set_f(1e-4); } if (!attributes.HasAttribute("beta")) { auto* arg = c2_op.ops.Mutable(0)->add_arg(); arg->set_name("beta"); arg->set_f(0.75); } return c2_op; } //============================================== // Rest of the member functions for Caffe2Backend //============================================== std::unordered_set Caffe2Backend::AllNamesInGraph( const GraphProto& graph) { std::unordered_set names; for (const auto& input : graph.input()) { names.emplace(input.name()); } for (const auto& output : graph.output()) { names.emplace(output.name()); } for (const auto& node : graph.node()) { for (const auto& n : node.input()) { names.emplace(n); } for (const auto& n : node.output()) { names.emplace(n); } } return names; } // This translator performs the basic translation of ONNX nodes into // Caffe2 operators. Besides doing a straightforward marshalling from // one format to another, it also does these extra things: // // - Renames operators based on 'renamed_operators' // - Renames attributes based on 'renamed_attrs' and // 'get_per_op_renamed_attrs' // // If you're writing a custom translator, consider calling this first, // and then fixing things up further. Caffe2Ops Caffe2Backend::CommonOnnxNodeToCaffe2Ops( OnnxNode* onnx_node, const ConversionContext& ctx) { Caffe2Ops ret; auto* c2_op = ret.ops.Add(); const auto& node = onnx_node->node; c2_op->mutable_input()->MergeFrom(node.input()); c2_op->mutable_output()->MergeFrom(node.output()); c2_op->set_name(node.name()); // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) const auto onnx_op_type = node.op_type(); auto broken_version = caffe2::get_default( get_broken_operators(), onnx_op_type, std::numeric_limits::max()); if (broken_version <= ctx.opset_version()) { CAFFE_THROW( "Don't know how to translate op ", onnx_op_type, " in ONNX operator set v", ctx.opset_version(), " (I only support prior to v", broken_version); } c2_op->set_type( caffe2::get_default(get_renamed_operators(), onnx_op_type, onnx_op_type)); if (!IsOperator(c2_op->type())) { CAFFE_THROW("Don't know how to translate op ", onnx_op_type); } auto mapper = [&, this](const std::string& k) { const auto it = get_per_op_renamed_attrs().find(onnx_op_type); if (it != get_per_op_renamed_attrs().end()) { const auto it_op = it->second.find(k); if (it_op != it->second.end()) { return it_op->second; } } const auto it_global = get_renamed_attrs().find(k); if (it_global != get_renamed_attrs().end()) { return it_global->second; } return k; }; c2_op->mutable_arg()->MergeFrom( onnx_node->attributes.OnnxAttrToCaffe2Arg(mapper)); return ret; } Caffe2Ops Caffe2Backend::ConvertNode( const std::string& node_str, const ConversionContext& ctx) { ::google::protobuf::RepeatedPtrField nodes; auto* n = nodes.Add(); ParseProtoFromLargeString(node_str, n); ModelProto init_model; ModelProto pred_model; OnnxNode onnx_node = OnnxNode(nodes.Get(0)); return OnnxNodeToCaffe2Ops(init_model, pred_model, ctx, &onnx_node); } void Caffe2Backend::CheckOpSchemaArguments( const caffe2::OpSchema& schema, const caffe2::OperatorDef& op) { const auto& schema_args = schema.args(); if (schema_args.size() > 0) { std::vector argnames; std::transform( schema_args.begin(), schema_args.end(), std::back_inserter(argnames), [](caffe2::OpSchema::Argument elem) { return elem.name(); }); for (const auto& arg : op.arg()) { if (std::count(argnames.begin(), argnames.end(), arg.name()) == 0) { CAFFE_THROW( "Don't know how to map unexpected argument ", arg.name(), " (from operator ", op.type(), ")"); } } } else { // A number of C2 operators do not declare proper arguments. Let's log the // error VLOG(2) << "Operator " << op.type() << " does not declare arguments in its schema. Please file a Caffe2 issue."; } } Caffe2Ops Caffe2Backend::OnnxNodeToCaffe2Ops( const ModelProto& init_model, const ModelProto& pred_model, const ConversionContext& ctx, OnnxNode* onnx_node) { Caffe2Ops res; if (get_special_operators().count(onnx_node->node.op_type())) { res = (this->*get_special_operators().at(onnx_node->node.op_type()))( onnx_node, ctx); } else { res = CommonOnnxNodeToCaffe2Ops(onnx_node, ctx); } for (const auto& result_op : res.ops) { const auto* schema = OpSchemaRegistry::Schema(result_op.type()); if (schema) { CheckOpSchemaArguments(*schema, result_op); } else { CAFFE_THROW( "Caffe2 has no such operator, could not find schema for ", result_op.type()); } } return res; } void Caffe2Backend::OnnxToCaffe2( caffe2::NetDef* init_net, caffe2::NetDef* pred_net, const ModelProto& onnx_model, const std::string& device, int opset_version, bool include_initializers, const std::vector& extras) { auto device_option = GetDeviceOption(Device(device)); ModelProto init_model = ModelProto(); ModelProto pred_model = onnx_model; pred_model.mutable_graph()->mutable_initializer()->Clear(); init_net->set_name(onnx_model.graph().name() + "_init"); pred_net->set_name(onnx_model.graph().name() + "_predict"); // Convert initializer if necessary if (include_initializers) { for (const auto& tp : onnx_model.graph().initializer()) { auto* c2_op = init_net->add_op(); BuildTensorFillingOp(c2_op, tp); } } auto name_set = AllNamesInGraph(init_model.graph()); auto name_set_pred = AllNamesInGraph(pred_model.graph()); name_set.insert(name_set_pred.begin(), name_set_pred.end()); dummy_->Reset(name_set); ValueInfoMap graph_value_infos{}; for (const auto& vi : pred_model.graph().input()) { graph_value_infos[vi.name()].CopyFrom(vi); } for (const auto& vi : pred_model.graph().output()) { graph_value_infos[vi.name()].CopyFrom(vi); } for (const auto& vi : pred_model.graph().value_info()) { graph_value_infos[vi.name()].CopyFrom(vi); } size_t idx_extra = 0; auto converter = [&](const ModelProto& model, caffe2::NetDef* net) mutable { net->mutable_device_option()->CopyFrom(device_option); for (const auto& node : model.graph().node()) { auto* init_net_tmp = include_initializers ? init_net : net; // For RNN operators, we rely on Python code to convert them for us, and // we simply deserilize the string. This is hack and eventually we want to // get rid of this to have one flow. Note that we need to update the dummy // name generator to avoid having duplicated names between Python and C++ // generated dummies if (get_rnn_operators().count(node.op_type())) { if (idx_extra < extras.size()) { const auto& c2ops = extras[idx_extra++]; for (const auto& op : c2ops.init_ops) { UpdateNames(dummy_, op); } init_net_tmp->mutable_op()->MergeFrom(c2ops.init_ops); for (const auto& op : c2ops.ops) { UpdateNames(dummy_, op); } net->mutable_op()->MergeFrom(c2ops.ops); for (const auto& input : c2ops.interface_blobs) { dummy_->AddName(input); } net->mutable_external_input()->MergeFrom(c2ops.interface_blobs); } else { CAFFE_THROW( "Don't know how to convert ", node.op_type(), " without enough extra preconverted string"); } } else { ValueInfoMap value_infos{}; for (const auto& name : node.input()) { auto iter = graph_value_infos.find(name); if (iter != graph_value_infos.end()) { value_infos[name].CopyFrom(iter->second); } } auto onnx_node = OnnxNode(node); auto c2ops = OnnxNodeToCaffe2Ops( init_model, pred_model, {value_infos, opset_version}, &onnx_node); init_net_tmp->mutable_op()->MergeFrom(c2ops.init_ops); net->mutable_op()->MergeFrom(c2ops.ops); net->mutable_external_input()->MergeFrom(c2ops.interface_blobs); } } for (const auto& value : model.graph().output()) { net->add_external_output(value.name()); } for (const auto& value : model.graph().input()) { net->add_external_input(value.name()); } }; converter(init_model, init_net); converter(pred_model, pred_net); } Caffe2BackendRep* Caffe2Backend::Prepare( const std::string& onnx_model_str, const std::string& device, const std::vector& extras) { Caffe2BackendRep* rep = new Caffe2BackendRep(); ModelProto onnx_model; ParseProtoFromLargeString(onnx_model_str, &onnx_model); #ifndef C10_MOBILE ::ONNX_NAMESPACE::checker::check_model(onnx_model); #endif int opset_version = -1; for (const auto& imp : onnx_model.opset_import()) { if ((!imp.has_domain()) || imp.domain().empty()) { opset_version = imp.version(); if (opset_version > kKnownOpsetVersion) { std::cout << "This version of onnx-caffe2 targets ONNX operator set version " << kKnownOpsetVersion << ", but the model we are trying to import uses version " << opset_version << ". We will try to import it anyway, " << "but if the model uses operators which had BC-breaking changes " "in the intervening versions, import will fail." << std::endl; } } else { std::cout << "Unrecognized operator set " << opset_version << std::endl; } } if (opset_version < 0) { if (onnx_model.ir_version() >= 0x00000003) { CAFFE_THROW( "Model with IR version >= 3 did not specify ONNX operator set " "version (onnx-caffe2 requires it)"); } else { opset_version = 1; } } // TODO: avoid extra copy by directly feed initializers to backend blobs OnnxToCaffe2( &rep->init_net(), &rep->pred_net(), onnx_model, device, opset_version, true, extras); // Get a list of uninitialized inputs to help with the inference setup auto& uninitialized_inputs = rep->uninitialized_inputs(); std::unordered_set initialized_inputs; for (const auto& tp : onnx_model.graph().initializer()) { initialized_inputs.emplace(tp.name()); } for (const auto& input : onnx_model.graph().input()) { if (!initialized_inputs.count(input.name())) { uninitialized_inputs.emplace_back(input.name()); } } return rep; } template void ConvertIntegralValueToCaffe2( caffe2::OperatorDef* c2_op, caffe2::Argument* c2_values, const TensorProto& onnx_tensor) { c2_op->set_type( onnx_tensor.data_type() == TensorProto::BOOL ? "GivenTensorBoolFill" : "GivenTensorIntFill"); ::google::protobuf::RepeatedField tmp; const ::google::protobuf::RepeatedField* src = &tmp; bool converted = TryConvertingTensorRawValues(onnx_tensor, &tmp); if (converted) { for (const auto i : *src) { c2_values->add_ints(i); } } else { const ::google::protobuf::RepeatedField<::google::protobuf::int32>* int32_src = &onnx_tensor.int32_data(); for (const auto i : *int32_src) { c2_values->add_ints(i); } } } template <> void ConvertIntegralValueToCaffe2<::google::protobuf::int64>( caffe2::OperatorDef* c2_op, caffe2::Argument* c2_values, const TensorProto& onnx_tensor) { c2_op->set_type("GivenTensorInt64Fill"); auto* ints = c2_values->mutable_ints(); if (!TryConvertingTensorRawValues<::google::protobuf::int64>( onnx_tensor, ints)) { ints->CopyFrom(onnx_tensor.int64_data()); } } template <> void ConvertIntegralValueToCaffe2<::google::protobuf::uint64>( caffe2::OperatorDef* c2_op, caffe2::Argument* c2_values, const TensorProto& onnx_tensor) { c2_op->set_type("GivenTensorInt64Fill"); ::google::protobuf::RepeatedField<::google::protobuf::uint64> tmp; const ::google::protobuf::RepeatedField<::google::protobuf::uint64>* src = &tmp; if (!TryConvertingTensorRawValues<::google::protobuf::uint64>( onnx_tensor, &tmp)) { src = &onnx_tensor.uint64_data(); } for (const auto i : *src) { c2_values->add_ints(i); } } void Caffe2Backend::BuildTensorFillingOp( caffe2::OperatorDef* c2_op, const TensorProto& onnx_tensor, const std::string& output_name, const std::string& shape_name) { auto fill_name = output_name.empty() ? onnx_tensor.name() : output_name; CAFFE_ENFORCE(!fill_name.empty()); if (onnx_tensor.has_segment()) { CAFFE_THROW("Currently not supporting loading segments."); } auto* c2_values = c2_op->add_arg(); // if shape_name is empty, we generate GivenTensorFill // otherwise, we generate ConstantFill, which accept shape as input if (shape_name.empty()) { // GivenTensor*Fill uses values c2_values->set_name("values"); if (onnx_tensor.data_type() == TensorProto::FLOAT) { c2_op->set_type("GivenTensorFill"); auto* floats = c2_values->mutable_floats(); if (!TryConvertingTensorRawValues(onnx_tensor, floats)) { floats->CopyFrom(onnx_tensor.float_data()); } } else if (onnx_tensor.data_type() == TensorProto::DOUBLE) { c2_op->set_type("GivenTensorDoubleFill"); ::google::protobuf::RepeatedField tmp; const ::google::protobuf::RepeatedField* src = &tmp; if (!TryConvertingTensorRawValues(onnx_tensor, &tmp)) { src = &onnx_tensor.double_data(); } for (const auto i : *src) { c2_values->add_floats(i); } } else if (onnx_tensor.data_type() == TensorProto::INT64) { ConvertIntegralValueToCaffe2<::google::protobuf::int64>( c2_op, c2_values, onnx_tensor); } else if (onnx_tensor.data_type() == TensorProto::UINT32) { ConvertIntegralValueToCaffe2<::google::protobuf::uint64>( c2_op, c2_values, onnx_tensor); // NOLINTNEXTLINE(bugprone-branch-clone) } else if (onnx_tensor.data_type() == TensorProto::BOOL) { ConvertIntegralValueToCaffe2<::google::protobuf::int8>( c2_op, c2_values, onnx_tensor); } else if (onnx_tensor.data_type() == TensorProto::UINT8) { ConvertIntegralValueToCaffe2<::google::protobuf::uint8>( c2_op, c2_values, onnx_tensor); } else if (onnx_tensor.data_type() == TensorProto::INT8) { ConvertIntegralValueToCaffe2<::google::protobuf::int8>( c2_op, c2_values, onnx_tensor); } else if (onnx_tensor.data_type() == TensorProto::UINT16) { ConvertIntegralValueToCaffe2<::google::protobuf::uint16>( c2_op, c2_values, onnx_tensor); } else if (onnx_tensor.data_type() == TensorProto::INT16) { ConvertIntegralValueToCaffe2<::google::protobuf::int16>( c2_op, c2_values, onnx_tensor); } else if (onnx_tensor.data_type() == TensorProto::INT32) { ConvertIntegralValueToCaffe2<::google::protobuf::int32>( c2_op, c2_values, onnx_tensor); } else if (onnx_tensor.data_type() == TensorProto::STRING) { c2_op->set_type("GivenTensorStringFill"); auto* strings = c2_values->mutable_strings(); strings->CopyFrom(onnx_tensor.string_data()); } else { CAFFE_THROW("unrecognized tensor type: ", onnx_tensor.data_type()); } auto* c2_shape = c2_op->add_arg(); c2_shape->set_name("shape"); for (const auto d : onnx_tensor.dims()) { c2_shape->add_ints(d); } } else { int value_size = 1; for (const auto d : onnx_tensor.dims()) { value_size *= d; } CAFFE_ENFORCE(value_size == 1); auto c2_input_as_shape = c2_op->add_arg(); c2_input_as_shape->set_name("input_as_shape"); c2_input_as_shape->set_i(1); c2_values->set_name("value"); auto* c2_dtype = c2_op->add_arg(); c2_dtype->set_name("dtype"); if (onnx_tensor.data_type() == TensorProto::FLOAT) { c2_dtype->set_i(caffe2::TensorProto::FLOAT); if (onnx_tensor.float_data_size() > 0) { c2_values->set_f(onnx_tensor.float_data(0)); } else { CAFFE_ENFORCE(onnx_tensor.raw_data().size() == sizeof(float)); // NOLINTNEXTLINE(cppcoreguidelines-init-variables) float f; memcpy(&f, onnx_tensor.raw_data().c_str(), sizeof(float)); c2_values->set_f(f); } } else if (onnx_tensor.data_type() == TensorProto::DOUBLE) { c2_dtype->set_i(caffe2::TensorProto::DOUBLE); if (onnx_tensor.double_data_size() > 0) { c2_values->set_f(static_cast(onnx_tensor.double_data(0))); } else { CAFFE_ENFORCE(onnx_tensor.raw_data().size() == sizeof(double)); // NOLINTNEXTLINE(cppcoreguidelines-init-variables) double d; memcpy(&d, onnx_tensor.raw_data().c_str(), sizeof(double)); c2_values->set_f(static_cast(d)); } } else if (onnx_tensor.data_type() == TensorProto::INT64) { c2_dtype->set_i(caffe2::TensorProto::INT64); if (onnx_tensor.int64_data_size() > 0) { c2_values->set_i(onnx_tensor.int64_data(0)); } else { CAFFE_ENFORCE(onnx_tensor.raw_data().size() == sizeof(int64_t)); // NOLINTNEXTLINE(cppcoreguidelines-init-variables) int64_t i; memcpy(&i, onnx_tensor.raw_data().c_str(), sizeof(int64_t)); c2_values->set_i(i); } } else if (onnx_tensor.data_type() == TensorProto::INT32) { c2_dtype->set_i(caffe2::TensorProto::INT32); if (onnx_tensor.int32_data_size() > 0) { c2_values->set_i(onnx_tensor.int32_data(0)); } else { CAFFE_ENFORCE(onnx_tensor.raw_data().size() == sizeof(int32_t)); // NOLINTNEXTLINE(cppcoreguidelines-init-variables) int32_t i; memcpy(&i, onnx_tensor.raw_data().c_str(), sizeof(int32_t)); c2_values->set_i(i); } } else { // TODO: to support more data type std::stringstream oss; oss << "Unsupported dtype: " << onnx_tensor.data_type(); CAFFE_THROW(oss.str()); } // ConstantFill uses value c2_op->set_type("ConstantFill"); c2_op->add_input(shape_name); } c2_op->add_output(fill_name); } bool Caffe2Backend::SupportOp(const std::string type) const { return get_special_operators().count(type); } } // namespace onnx } // namespace caffe2