| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967 |
- ## @package onnx
- # Module caffe2.python.onnx.backend
- """Backend for running ONNX on Caffe2
- To run this, you will need to have Caffe2 installed as well.
- """
- import collections
- import sys
- import zipfile
- import itertools
- # When onnx is built against a version of protobuf that is older than
- # that which is vendored with caffe2, onnx will crash if caffe2's
- # vendored protobuf is loaded first. We can work around this by
- # importing onnx first, which will cause it to go out and pick up the
- # system protobuf.
- import onnx.backend
- from caffe2.python import core, workspace, rnn_cell, gru_cell
- from caffe2.python.model_helper import ModelHelper
- from caffe2.proto import caffe2_pb2
- import caffe2.python.utils
- import numpy as np
- import onnx
- from onnx import TensorProto
- import onnx.numpy_helper
- import onnx.defs
- import onnx.shape_inference
- import onnx.utils
- from onnx.backend.base import Backend, Device, DeviceType, namedtupledict
- from caffe2.python.onnx.workspace import Workspace
- from caffe2.python.onnx.backend_rep import Caffe2Rep
- import caffe2.python._import_c_extension as C
- import warnings
- def force_unicode(s):
- try:
- return s.decode('utf-8')
- except AttributeError:
- return s
- def get_device_option(device):
- m = {DeviceType.CPU: caffe2_pb2.CPU,
- DeviceType.CUDA: workspace.GpuDeviceType}
- return core.DeviceOption(m[device.type], device.device_id)
- class OnnxAttributes(dict):
- """
- This is a more convenient way to work with ONNX/Caffe2 attributes
- that is not the protobuf representation.
- """
- @staticmethod
- def from_onnx(args):
- d = OnnxAttributes()
- for arg in args:
- d[arg.name] = convertAttributeProto(arg)
- return d
- def caffe2(self, kmap=lambda k: k):
- for k, v in self.items():
- if kmap(k) != '':
- yield caffe2.python.utils.MakeArgument(kmap(k), v)
- # TODO: Move this into ONNX main library
- def convertAttributeProto(onnx_arg):
- """
- Convert an ONNX AttributeProto into an appropriate Python object
- for the type.
- NB: Tensor attribute gets returned as the straight proto.
- """
- if onnx_arg.HasField('f'):
- return onnx_arg.f
- elif onnx_arg.HasField('i'):
- return onnx_arg.i
- elif onnx_arg.HasField('s'):
- return onnx_arg.s
- elif onnx_arg.HasField('t'):
- return onnx_arg.t # this is a proto!
- elif onnx_arg.HasField('g'):
- return Caffe2Backend._graph_to_net(onnx_arg.g, Caffe2Backend._known_opset_version)
- elif len(onnx_arg.floats):
- return list(onnx_arg.floats)
- elif len(onnx_arg.ints):
- return list(onnx_arg.ints)
- elif len(onnx_arg.strings):
- return list(onnx_arg.strings)
- elif len(onnx_arg.graphs):
- retval = []
- # TODO: this doesn't work with RNN ops
- for g in onnx_arg.graphs:
- retval.append(Caffe2Backend._graph_to_net(g, Caffe2Backend._known_opset_version))
- return retval
- else:
- raise ValueError("Unsupported ONNX attribute: {}".format(onnx_arg))
- # TODO: Move this into ONNX main library
- class OnnxNode(object):
- """
- Reimplementation of NodeProto from ONNX, but in a form
- more convenient to work with from Python.
- We may temporarily edit these nodes to get them into Caffe2 form,
- before actually translating into the Caffe2 protobuf, since this
- is easier than decomposing everything, and putting it back together
- when we're ready.
- """
- def __init__(self, node):
- self.name = str(node.name)
- self.op_type = str(node.op_type)
- self.attrs = OnnxAttributes.from_onnx(node.attribute)
- self.inputs = list(node.input)
- self.outputs = list(node.output)
- Caffe2Ops = collections.namedtuple('Caffe2Ops', ['ops', 'init_ops', 'interface_blobs'])
- class Caffe2Backend(Backend):
- # The greatest version of the ONNX operator set which we are aware of.
- # Models whose version is larger than this will cause us to emit a warning
- # that we are attempting to translate on a "best effort" basis.
- #
- # If you increase this, make SURE you cross-reference all BC-breaking
- # changes from one version to the next, and any that you did not
- # implement, mark as broken in _broken_operators
- _known_opset_version = 9
- # This dictionary will record operators which are KNOWN to be
- # broken, so we give a good error message rather than do something
- # bogus and then fail.
- _broken_operators = {
- # 'BrokenOp': version_it_was_broken_in
- }
- # 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.
- _renamed_operators = {
- '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',
- 'Loop': 'ONNXWhile',
- 'Tile': 'NumpyTile',
- 'RandomNormal': 'GaussianFill',
- 'RandomUniform': 'UniformFill',
- }
- _global_renamed_attrs = {'kernel_shape': 'kernels'}
- _per_op_renamed_attrs = {
- 'Squeeze': {'axes': 'dims'},
- 'Unsqueeze': {'axes': 'dims'},
- 'Transpose': {'perm': 'axes'},
- 'Upsample': {'mode': '',
- 'scales': ''},
- 'ConvTranspose': {'output_padding': 'adjs'},
- 'Selu': {'gamma': 'scale'},
- 'If': {'then_branch': 'then_net',
- 'else_branch': 'else_net'},
- 'RandomUniform': {'low': 'min',
- 'high': 'max'}
- }
- # 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
- _special_operators = {
- 'LSTM': '_create_rnn_variant',
- 'GRU': '_create_rnn_variant',
- 'RNN': '_create_rnn_variant',
- 'Loop': '_create_loop',
- 'If': '_create_if',
- 'Upsample': '_create_upsample',
- 'RandomNormal': '_create_gaussian_fill'
- }
- # Dummy name generator
- _dummy_name = C.DummyName()
- @classmethod
- def dummy_name(cls):
- return cls._dummy_name.new_dummy_name()
- # NB: By default, you will use the LATEST definition of the operator,
- # so this interface MAY make BC-breaking changes. Specify an
- # opset_version if you don't want this to version.
- @classmethod
- def run_node(cls, node, inputs, device='CPU', opset_version=_known_opset_version, outputs_info=None):
- super(Caffe2Backend, cls).run_node(node, inputs, device=device,
- outputs_info=outputs_info, opset_version=opset_version)
- value_infos = []
- device_option = get_device_option(Device(device))
- ws = Workspace()
- with core.DeviceScope(device_option): # temporary!
- if isinstance(inputs, dict):
- for key, value in inputs.items():
- ws.FeedBlob(key, value)
- value_infos.append(onnx.helper.make_tensor_value_info(
- name=key,
- elem_type=onnx.mapping.NP_TYPE_TO_TENSOR_TYPE[value.dtype],
- shape=value.shape).SerializeToString())
- else:
- assert len(node.input) == len(inputs), "{}: expected {} but got {}".format(
- node.op_type, len(node.input), len(inputs))
- for key, value in zip(node.input, inputs):
- ws.FeedBlob(key, value)
- value_infos.append(onnx.helper.make_tensor_value_info(
- name=key,
- elem_type=onnx.mapping.NP_TYPE_TO_TENSOR_TYPE[value.dtype],
- shape=value.shape).SerializeToString())
- ops = []
- cbackend = C.Caffe2Backend(cls._dummy_name)
- ops_str = cbackend.convert_node(node.SerializeToString(), value_infos, opset_version)
- for s in ops_str[0] + ops_str[1]:
- op = caffe2_pb2.OperatorDef()
- op.ParseFromString(s)
- op.device_option.CopyFrom(device_option)
- ops.append(op)
- ws.RunOperatorsOnce(ops)
- output_values = [ws.FetchBlob(name) for name in node.output]
- return namedtupledict('Outputs', node.output)(*output_values)
- @classmethod
- def _create_tensor_filling_op(cls, onnx_tensor, name=None):
- """
- Given an Onnx TensorProto, translate it into a Caffe2 operator
- which produces the given tensor filling op.
- """
- assert name or onnx_tensor.name
- name = name or onnx_tensor.name
- c2_op = caffe2_pb2.OperatorDef()
- c2_values = c2_op.arg.add()
- c2_values.name = "values"
- def tensor2list(onnx_tensor):
- # Use the onnx.numpy_helper because the data may be raw
- return onnx.numpy_helper.to_array(onnx_tensor).flatten().tolist()
- if onnx_tensor.data_type in [TensorProto.FLOAT]:
- c2_op.type = 'GivenTensorFill'
- c2_values.floats.extend(tensor2list(onnx_tensor))
- elif onnx_tensor.data_type in [TensorProto.DOUBLE]:
- c2_op.type = 'GivenTensorDoubleFill'
- c2_values.floats.extend(tensor2list(onnx_tensor))
- elif onnx_tensor.data_type in [TensorProto.INT64,
- TensorProto.UINT32]:
- c2_op.type = 'GivenTensorInt64Fill'
- c2_values.ints.extend(tensor2list(onnx_tensor))
- elif onnx_tensor.data_type in [TensorProto.UINT8,
- TensorProto.INT8,
- TensorProto.UINT16,
- TensorProto.INT16,
- TensorProto.INT32]:
- c2_op.type = 'GivenTensorIntFill'
- c2_values.ints.extend(tensor2list(onnx_tensor))
- elif onnx_tensor.data_type == TensorProto.BOOL:
- c2_op.type = 'GivenTensorBoolFill'
- c2_values.ints.extend(tensor2list(onnx_tensor))
- elif onnx_tensor.data_type == TensorProto.STRING:
- c2_op.type = 'GivenTensorStringFill'
- c2_values.strings.extend(onnx_tensor.string_data)
- else:
- raise RuntimeError(
- "unrecognized tensor type {}".format(onnx_tensor.data_type))
- c2_shape = c2_op.arg.add()
- c2_shape.name = "shape"
- c2_shape.ints.extend(onnx_tensor.dims)
- c2_op.output.append(name)
- return c2_op
- @classmethod
- def _rnn_reform_weights(cls, reforms, name, hidden_size, init_net, gates, reorder_indices):
- for name_from, name_to, do_concat, extra_dims in reforms:
- gate_blobs = ['%s/%s_%s' % (name, prefix, name_to) for prefix in gates]
- for i, x in enumerate(gate_blobs):
- dim0 = i * hidden_size, (i+1) * hidden_size
- starts, ends = zip(dim0, *extra_dims)
- init_net.Slice(name_from, x, starts=starts, ends=ends)
- if do_concat:
- reordered_gate_blobs = [gate_blobs[i] for i in reorder_indices]
- init_net.Concat(reordered_gate_blobs, ['%s/%s' % (name, name_to), cls.dummy_name()], axis=0)
- @classmethod
- def _make_rnn_direction(cls, input_blob, B, W, R, initial_states_and_names, sequence_lens,
- pred_mh, init_net,
- input_size, hidden_size, num_gates, direction_offset,
- Bi, Br, W_, R_,
- reform, make_cell, keep_outputs):
- name = cls.dummy_name()
- # input and recurrence biases are squashed together in onnx
- # but not in caffe2
- gates_hidden_size = num_gates * hidden_size
- bias_offset = 2 * direction_offset * gates_hidden_size
- weight_offset = direction_offset * gates_hidden_size
- Bi = init_net.Slice(B, name + Bi,
- starts=[bias_offset + 0 * gates_hidden_size],
- ends =[bias_offset + 1 * gates_hidden_size])
- Br = init_net.Slice(B, name + Br,
- starts=[bias_offset + 1 * gates_hidden_size],
- ends =[bias_offset + 2 * gates_hidden_size])
- W_ = init_net.Slice(W, name + W_,
- starts=[weight_offset + 0 * gates_hidden_size, 0],
- ends =[weight_offset + 1 * gates_hidden_size,-1])
- R_ = init_net.Slice(R, name + R_,
- starts=[weight_offset + 0 * gates_hidden_size, 0],
- ends =[weight_offset + 1 * gates_hidden_size,-1])
- initial_states_sliced = []
- for initial_state, name_suffix in initial_states_and_names:
- initial_states_sliced.append(
- pred_mh.net.Slice(initial_state, name + name_suffix,
- starts=[direction_offset + 0, 0, 0],
- ends =[direction_offset + 1,-1,-1]))
- if direction_offset == 1:
- if sequence_lens is not None:
- seq_lens_for_reverse = sequence_lens
- else:
- input_shape = pred_mh.net.Shape(input_blob, name + '/input_shape')
- batch_size = pred_mh.net.Slice(input_shape, name + '/batch_size_slice', starts=[1], ends=[2])
- seq_len = pred_mh.net.Slice(input_shape, name + '/seq_len_slice', starts=[0], ends=[1])
- dummy_sequence_lens = pred_mh.net.Tile([seq_len, batch_size], name + '/dummy_sequence_lens', axis=0)
- pred_mh.net.Reshape(dummy_sequence_lens, [dummy_sequence_lens, cls.dummy_name()], shape=[-1])
- seq_lens_for_reverse = pred_mh.net.Cast(dummy_sequence_lens, name + '/seq_lens_for_reverse', to=core.DataType.INT32)
- reform(Bi, Br, W_, R_, name, hidden_size, init_net)
- if direction_offset == 1:
- input = pred_mh.net.ReversePackedSegs(
- [input_blob, seq_lens_for_reverse], name + "/input-reversed")
- else:
- input = input_blob
- outputs = keep_outputs(list(make_cell(
- pred_mh,
- input,
- sequence_lens,
- initial_states_sliced,
- input_size,
- hidden_size,
- name,
- drop_states=False,
- forward_only=True,
- )))
- if direction_offset == 1:
- outputs[0] = pred_mh.net.ReversePackedSegs(
- [outputs[0], seq_lens_for_reverse], name + "/output-reversed")
- return outputs
- @classmethod
- def _create_rnn_variant(cls, init_model, pred_model, n, opset_version):
- assert init_model is not None, "cannot convert RNNs without access to the full model"
- assert pred_model is not None, "cannot convert RNNs without access to the full model"
- attrs = dict(n.attrs) # make a copy, which is safe to mutate
- hidden_size = attrs.pop('hidden_size')
- direction = force_unicode(attrs.pop('direction', 'forward'))
- if n.op_type == 'RNN':
- activation = force_unicode(attrs.pop('activations', ('tanh',))[0].lower())
- elif n.op_type == 'GRU':
- linear_before_reset = attrs.pop('linear_before_reset', 0)
- assert not attrs, "unsupported RNN attributes: " + str(attrs.keys())
- assert direction in ['forward', 'bidirectional'], "unsupported backwards RNN/GRU/LSTM"
- if n.op_type in ['RNN', 'GRU']:
- input_blob, W, R, B, sequence_lens, initial_h = n.inputs
- elif n.op_type == 'LSTM':
- input_blob, W, R, B, sequence_lens, initial_h, initial_c = n.inputs
- if sequence_lens == "":
- sequence_lens = None
- for x in itertools.chain(init_model.graph.input,
- init_model.graph.value_info,
- pred_model.graph.input,
- pred_model.graph.value_info):
- if x.name == W:
- input_size = x.type.tensor_type.shape.dim[2].dim_value
- break
- else:
- raise RuntimeError("best-effort shape inference for RNN/GRU/LSTM failed")
- pred_mh = ModelHelper()
- init_net = core.Net("init-net")
- init_net.Reshape(W, [W, cls.dummy_name()], shape=[1,-1,0])
- init_net.Squeeze(W, W, dims=[0])
- init_net.Reshape(R, [R, cls.dummy_name()], shape=[1,-1,0])
- init_net.Squeeze(R, R, dims=[0])
- init_net.Reshape(B, [B, cls.dummy_name()], shape=[1,-1])
- init_net.Squeeze(B, B, dims=[0])
- if n.op_type == 'RNN':
- def reform(*args):
- pass
- def make_cell(*args, **kwargs):
- return rnn_cell.BasicRNN(*args, activation=activation, **kwargs)
- def make_rnn(direction_offset):
- return cls._make_rnn_direction(
- input_blob, B, W, R, [(initial_h, '/initial_h')], sequence_lens,
- pred_mh, init_net, input_size, hidden_size, 1, direction_offset,
- "/i2h_b", "/gates_t_b", "/i2h_w", "/gates_t_w",
- reform, make_cell, lambda x: x)
- elif n.op_type == 'GRU':
- def reform(Bi, Br, W_, R_, name, hidden_size, init_net):
- # caffe2 has a different order from onnx. We need to rearrange
- # z r h -> r z h
- reforms = ((W_, 'i2h_w', True, [(0,-1)]),
- (R_, 'gate_t_w', False, [(0,-1)]),
- (Bi, 'i2h_b', True, []),
- (Br, 'gate_t_b', False, []))
- cls._rnn_reform_weights(reforms, name, hidden_size, init_net,
- ['update', 'reset', 'output'], [1, 0, 2])
- def make_cell(*args, **kwargs):
- return gru_cell.GRU(*args, linear_before_reset=linear_before_reset, **kwargs)
- def make_rnn(direction_offset):
- return cls._make_rnn_direction(
- input_blob, B, W, R, [(initial_h, '/initial_h')], sequence_lens,
- pred_mh, init_net, input_size, hidden_size, 3, direction_offset,
- "_bias_i2h", "_bias_gates", "/i2h_w_pre", "/gates_t_w_pre",
- reform, make_cell, lambda x: x)
- elif n.op_type == 'LSTM':
- def reform(Bi, Br, W_, R_, name, hidden_size, init_net):
- # caffe2 has a different order from onnx. We need to rearrange
- # i o f c -> i f o c
- reforms = ((W_, 'i2h_w', True, [(0, -1)]),
- (R_, 'gates_t_w', True, [(0, -1)]),
- (Bi, 'i2h_b' , True, []),
- (Br, 'gates_t_b', True, []))
- cls._rnn_reform_weights(reforms, name, hidden_size, init_net,
- ['input', 'output', 'forget', 'cell'], [0, 2, 1, 3])
- def make_cell(*args, **kwargs):
- return rnn_cell.LSTM(*args, **kwargs)
- def make_rnn(direction_offset):
- return cls._make_rnn_direction(
- input_blob, B, W, R, [(initial_h, '/initial_h'), (initial_c, '/initial_c')], sequence_lens,
- pred_mh, init_net, input_size, hidden_size, 4, direction_offset,
- "/i2h_b", "/gates_t_b", "/i2h_w", "/gates_t_w",
- reform, make_cell, lambda x: [x[0], x[1], x[3]])
- if direction == 'forward':
- outputs = make_rnn(0)
- # in the forward case, storage is shared between the
- # last outputs. We need to decouple them so that the
- # VariableLengthSequencePadding only mutates
- # n.outputs[0]
- for i in range(1, len(outputs)):
- pred_mh.net.Copy(outputs[i], n.outputs[i])
- if sequence_lens is not None:
- pred_mh.net.VariableLengthSequencePadding(
- [outputs[0], sequence_lens], [outputs[0]])
- pred_mh.net.ExpandDims([outputs[0]], [n.outputs[0]], dims=[1])
- elif direction == 'bidirectional':
- outputs_f = make_rnn(0)
- outputs_b = make_rnn(1)
- concatted_output, _ = pred_mh.net.Concat(
- [outputs_f[0], outputs_b[0]], [cls.dummy_name(), cls.dummy_name()], axis=2)
- if sequence_lens is not None:
- pred_mh.net.VariableLengthSequencePadding(
- [concatted_output, sequence_lens], [concatted_output])
- reshaped_output, _ = pred_mh.net.Reshape(concatted_output, [cls.dummy_name(), cls.dummy_name()], shape=[0,0,-1,2])
- pred_mh.net.Transpose(reshaped_output, n.outputs[0], axes=[0,2,1,3])
- for i in range(1, len(n.outputs)):
- pred_mh.net.Concat([outputs_f[i], outputs_b[i]],
- [n.outputs[i], cls.dummy_name()], axis=0)
- # We want to decide whether to put all of our weight-reshaping
- # operators in the init net or the predict net. We can put
- # them in the init net iff the inputs to those operators are
- # already available, either as graph initializers, or as the
- # output of other operators in the init net. The latter case
- # occurs, for example, when exporting from pytorch to onnx.
- # In most production use, we expect has_initializers to be
- # true.
- initializers = {i.name for i in init_model.graph.initializer}
- outputs = {output for node in init_model.graph.node for output in node.output}
- has_initializers = all(x in initializers or x in outputs for x in (W, R, B))
- pred_ops = []
- init_ops = []
- (init_ops if has_initializers else pred_ops).extend(init_net.Proto().op)
- pred_ops.extend(pred_mh.Proto().op)
- return Caffe2Ops(pred_ops, init_ops, list(pred_mh.Proto().external_input))
- @classmethod
- def _create_control_op(cls, init_model, pred_model, n, opset_version):
- control_inputs = []
- if '__control_inputs' in n.attrs:
- control_inputs.extend(n.attrs['__control_inputs'])
- node = cls._common_onnx_node_to_caffe2_op(init_model, pred_model, n, opset_version)
- node.control_input.extend(control_inputs)
- return Caffe2Ops([node], [], [])
- @classmethod
- def _remove_ssa(cls, net, remap_dict):
- for op in net.op:
- for i, name in enumerate(op.output):
- if name in remap_dict:
- op.output[i] = remap_dict[name]
- for i, out in enumerate(net.external_output):
- if out in remap_dict:
- net.external_output[i] = remap_dict[out]
- @classmethod
- def _create_if(cls, init_model, pred_model, n, opset_version):
- ops = cls._create_control_op(init_model, pred_model, n, opset_version)
- assert ops[0][0].type == 'If'
- if_op = ops[0][0]
- then_net = else_net = None
- control_inputs = []
- for arg in if_op.arg:
- if arg.name == 'then_net':
- then_net = arg.n
- if arg.name == 'else_net':
- else_net = arg.n
- if arg.name == '__control_inputs':
- control_inputs = arg.strings
- assert then_net and else_net
- then_net_outs = then_net.external_output
- else_net_outs = else_net.external_output
- op_outputs = if_op.output
- assert len(then_net_outs) == len(else_net_outs)
- assert len(else_net_outs) == len(op_outputs)
- for arg in if_op.arg:
- if arg.name == 'then_net':
- arg.n.external_input.extend(control_inputs)
- if arg.name == 'else_net':
- arg.n.external_input.extend(control_inputs)
- return ops
- @classmethod
- def _create_loop(cls, init_model, pred_model, n, opset_version):
- ops = cls._create_control_op(init_model, pred_model, n, opset_version)
- assert ops[0][0].type == 'ONNXWhile'
- while_op = ops[0][0]
- while_op.arg.extend([caffe2.python.utils.MakeArgument('has_trip_count', True)])
- while_op.arg.extend([caffe2.python.utils.MakeArgument('has_cond', True)])
- while_op.arg.extend([caffe2.python.utils.MakeArgument('disable_scopes', True)])
- control_inputs = []
- for arg in while_op.arg:
- if arg.name == '__control_inputs':
- control_inputs = arg.strings
- num_loop_carried_deps = 0
- for arg in while_op.arg:
- if arg.name == 'body':
- num_loop_carried_deps = len(arg.n.external_input) - 2
- arg.n.external_input.extend(control_inputs)
- while_op.arg.extend([
- caffe2.python.utils.MakeArgument('num_loop_carried_deps',
- num_loop_carried_deps)
- ])
- return ops
- @classmethod
- def _substitute_raw_value(cls, tp, raw_values_dict):
- if tp.HasField('raw_data') and tp.raw_data == bytes(b'__EXTERNAL'):
- if tp.name not in raw_values_dict:
- raise RuntimeError('TensorProto for value {} referenced raw data but it was not found!'.format(tp.name))
- else:
- tp.raw_data = raw_values_dict[tp.name]
- @classmethod
- def _visit_and_substitute_raw_values(cls, nodes, raw_values_dict):
- for node in nodes:
- for attr in node.attribute:
- if attr.HasField('t'):
- cls._substitute_raw_value(attr.t, raw_values_dict)
- for t in attr.tensors:
- cls._substitute_raw_value(t, raw_values_dict)
- if attr.HasField('g'):
- cls._visit_and_substitute_raw_values(attr.g.node, raw_values_dict)
- for g in attr.graphs:
- cls._visit_and_substitute_raw_values(g.node, raw_values_dict)
- @classmethod
- def _external_value_resolution_pass(cls, model, raw_values_dict):
- for init in model.graph.initializer:
- cls._substitute_raw_value(init, raw_values_dict)
- cls._visit_and_substitute_raw_values(model.graph.node, raw_values_dict)
- @classmethod
- def _direct_initialize_parameters(cls, initializer, ws, device_option):
- for tp in initializer:
- ws.FeedBlob(tp.name, onnx.numpy_helper.to_array(tp), device_option)
- @classmethod
- def _direct_initialize_inputs(cls, inputs, initialized, ws, device_option):
- for value_info in inputs:
- if value_info.name in initialized:
- continue
- shape = list(d.dim_value for d in value_info.type.tensor_type.shape.dim)
- ws.FeedBlob(
- value_info.name,
- np.ones(shape, dtype=onnx.mapping.TENSOR_TYPE_TO_NP_TYPE[value_info.type.tensor_type.elem_type]),
- device_option)
- @staticmethod
- def optimize_onnx(input, init=False, predict=False):
- passes = ['fuse_consecutive_transposes',
- 'eliminate_nop_transpose',
- 'fuse_transpose_into_gemm',
- 'lift_lexical_references']
- if init:
- passes.append('split_init')
- if predict:
- passes.append('split_predict')
- try:
- out = onnx.optimizer.optimize(input, passes)
- except AttributeError:
- warnings.warn("OptimizerWarning: optimizer module not found in ONNX version {}".format(onnx.__version__))
- # ONNX does no ship onnx.optimizer since version 1.9+
- import onnxoptimizer
- out = onnxoptimizer.optimize(input, passes)
- return out
- @classmethod
- def prepare_zip_archive(cls, file, device='CPU', **kwargs):
- with zipfile.ZipFile(file, mode='r') as z:
- with z.open('__MODEL_PROTO', 'r') as f:
- model = onnx.load(f);
- blob_names = set(z.namelist()) - set('__MODEL_PROTO')
- # TODO: make this more efficient
- raw_values_dict = {}
- for name in blob_names:
- with z.open(name, 'r') as blob_file:
- raw_values_dict[name] = blob_file.read()
- return cls.prepare(model, device, raw_values_dict=raw_values_dict, **kwargs)
- @classmethod
- def prepare(cls, model, device='CPU', raw_values_dict=None, **kwargs):
- '''
- For Onnx Caffe2Backend, we require that init_graph don't initialize the actual input of the predict_graph,
- for example, if "img" is the input blob for the predict_net, we require that in init_graph and in
- initializer of the predict_graph, "img" is not initalized. We don't have a check for this, since
- there is no way we can know which blob is the input of the predict_graph.
- '''
- if not kwargs.pop('no_check_UNSAFE', False):
- super(Caffe2Backend, cls).prepare(model, device, **kwargs)
- opset_version = None
- for imp in model.opset_import:
- if not imp.HasField("domain") or imp.domain == "":
- opset_version = imp.version
- if imp.version > cls._known_opset_version:
- warnings.warn("This version of onnx-caffe2 targets ONNX operator set version {}, but the model we are trying to import uses 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.".format(cls._known_opset_version, imp.version))
- else:
- warnings.warn("Unrecognized operator set {}".format(imp.domain))
- if opset_version is None:
- if model.ir_version >= 0x00000003:
- raise RuntimeError("Model with IR version >= 3 did not specify ONNX operator set version (onnx-caffe2 requires it)")
- else:
- opset_version = 1
- # Prior to onnx version update to onnx-1.8.0, errors caused by failures in
- # in the onnx shape inference call were being supressed. Hence a try-catch block
- # is added around the infer_shapes call to avoid these failures and preserve status
- try:
- model = onnx.shape_inference.infer_shapes(model)
- except RuntimeError:
- warnings.warn("ShapeInferenceWarning: Inferred shape and existing shape differ in rank")
- ws = Workspace()
- device_option = get_device_option(Device(device))
- init_net, predict_net = cls._onnx_model_to_caffe2_net(model, device, opset_version, False)
- if raw_values_dict:
- cls._external_value_resolution_pass(model, raw_values_dict)
- # Directly load initializer data into blobs in workspace
- cls._direct_initialize_parameters(
- model.graph.initializer,
- ws,
- device_option,
- )
- initialized = {init.name for init in model.graph.initializer}
- cls._direct_initialize_inputs(
- model.graph.input,
- initialized,
- ws,
- device_option,
- )
- uninitialized = [value_info.name for value_info in model.graph.input if value_info.name not in initialized]
- retval = Caffe2Rep(init_net, predict_net, ws, uninitialized)
- return retval
- @classmethod
- # TODO: This method needs a refactor for clarity
- def _onnx_node_to_caffe2_op(cls, init_model, pred_model, node_def, opset_version):
- cbackend = C.Caffe2Backend(cls._dummy_name)
- if cbackend.support_onnx_import(node_def.op_type):
- # extract value infos from pred model (value infos of
- # node's inputs that are in init model should be all
- # available in pred model)
- value_infos = []
- for name in node_def.input:
- if pred_model is not None:
- for vi in itertools.chain(pred_model.graph.input,
- pred_model.graph.output,
- pred_model.graph.value_info):
- if vi.name == name:
- value_infos.append(vi.SerializeToString())
- op_strs = cbackend.convert_node(node_def.SerializeToString(), value_infos, opset_version)
- init_ops = []
- for s in op_strs[0]:
- op = caffe2_pb2.OperatorDef()
- op.ParseFromString(s)
- init_ops.append(op)
- ops = []
- for s in op_strs[1]:
- op = caffe2_pb2.OperatorDef()
- op.ParseFromString(s)
- ops.append(op)
- return Caffe2Ops(ops, init_ops, [])
- if node_def.op_type in cls._special_operators:
- translator = getattr(cls, cls._special_operators[node_def.op_type])
- else:
- translator = cls._common_onnx_node_to_caffe2_op
- ops = translator(init_model, pred_model, OnnxNode(node_def), opset_version)
- if isinstance(ops, Caffe2Ops):
- return ops
- if not isinstance(ops, collections.abc.Iterable):
- ops = [ops]
- return Caffe2Ops(ops, [], [])
- _broadcast_operators = {
- 'Add',
- 'Sub',
- }
- @classmethod
- def _common_onnx_node_to_caffe2_op(cls, init_model, pred_model, onnx_node, opset_version):
- """
- 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 '_global_renamed_attrs' and
- '_per_op_renamed_attrs'
- If you're writing a custom translator, consider calling this first,
- and then fixing things up further.
- """
- c2_op = caffe2_pb2.OperatorDef()
- c2_op.input.extend(onnx_node.inputs)
- c2_op.output.extend(onnx_node.outputs)
- c2_op.name = onnx_node.name
- onnx_op_type = onnx_node.op_type
- broken_version = cls._broken_operators.get(onnx_op_type, float('Inf'))
- if broken_version <= opset_version:
- raise ValueError(
- "Don't know how to translate op {} in ONNX operator set v{} (I only support prior to v{})".format(onnx_op_type, opset_version, broken_version))
- c2_op.type = cls._renamed_operators.get(onnx_op_type, onnx_op_type)
- if not core.IsOperator(c2_op.type):
- raise ValueError(
- "Don't know how to translate op {}".format(onnx_op_type))
- def kmap(k):
- if (onnx_op_type in cls._per_op_renamed_attrs and
- k in cls._per_op_renamed_attrs[onnx_op_type]):
- return cls._per_op_renamed_attrs[onnx_op_type][k]
- if k in cls._global_renamed_attrs:
- return cls._global_renamed_attrs[k]
- return k
- c2_op.arg.extend(onnx_node.attrs.caffe2(kmap=kmap))
- if opset_version < 7:
- # onnx opset 7 and newest caffe2 have adopted full onnx broadcast semantics
- # so we don't need this hack anymore
- if c2_op.type in cls._broadcast_operators:
- already_broadcast = False
- for arg in c2_op.arg:
- if arg.name == 'broadcast':
- already_broadcast = True
- if not already_broadcast:
- c2_op.arg.extend([caffe2.python.utils.MakeArgument('broadcast', 1)])
- return c2_op
- @staticmethod
- def _all_names_in_graph(graph):
- if graph is None:
- return set()
- names = set()
- names.update(value_info.name for value_info in graph.input)
- names.update(value_info.name for value_info in graph.output)
- for node in graph.node:
- names.update(node.input)
- names.update(node.output)
- return names
- @classmethod
- def _graph_to_net(cls, onnx_graph, opset_version):
- net = caffe2_pb2.NetDef()
- for node in onnx_graph.node:
- try:
- c2ops = cls._onnx_node_to_caffe2_op(
- None, None, node, opset_version)
- except Exception as e:
- print('ONNX FATAL:', e)
- continue
- net.op.extend(c2ops.init_ops)
- net.op.extend(c2ops.ops)
- net.external_input.extend(c2ops.interface_blobs)
- net.external_output.extend(
- value_info.name for value_info in onnx_graph.output)
- net.external_input.extend(
- value_info.name for value_info in onnx_graph.input)
- return net
- @classmethod
- def _onnx_model_to_caffe2_net(cls, onnx_model, device, opset_version, include_initializers):
- device_option = get_device_option(Device(device))
- # Prior to onnx version update to onnx-1.8.0, errors caused by failures in
- # in the onnx shape inference call were being supressed. Hence a try-catch block
- # is added around the infer_shapes call to avoid these failures and preserve status
- try:
- onnx_model = onnx.utils.polish_model(onnx_model)
- except RuntimeError:
- warnings.warn("ShapeInferenceWarning: Inferred shape and existing shape differ in rank")
- except AttributeError:
- warnings.warn("ShapeInferenceWarning: utils module not found in ONNX version {}".format(onnx.__version__))
- # Optimizer module has been removed in ONNX-1.9 or later, warn caller if that is the case
- try:
- init_model = cls.optimize_onnx(onnx_model, init=True)
- pred_model = cls.optimize_onnx(onnx_model, predict=True)
- except ModuleNotFoundError:
- warnings.warn("OptimizerWarning: onnxoptimizer module not installed. "
- "init_model and pred_model models will not be splitted, which can cause a runtime error")
- init_model = onnx_model
- pred_model = onnx_model
- init_net = caffe2_pb2.NetDef()
- pred_net = caffe2_pb2.NetDef()
- init_net.name = onnx_model.graph.name + '_init'
- pred_net.name = onnx_model.graph.name + '_predict'
- if include_initializers:
- init_net.op.extend(cls._create_tensor_filling_op(tp) for tp in onnx_model.graph.initializer)
- cls._dummy_name.reset(cls._all_names_in_graph(init_model.graph) | cls._all_names_in_graph(pred_model.graph))
- errors = []
- for net, model in ( (init_net, init_model), (pred_net, pred_model) ):
- net.device_option.CopyFrom(device_option)
- for node in model.graph.node:
- try:
- c2ops = cls._onnx_node_to_caffe2_op(
- init_model, pred_model, node, opset_version)
- except Exception as e:
- msg = 'Error while processing node: {}. Exception: {}'.format(node, e)
- errors.append(msg)
- print('ONNX FATAL:', msg, file=sys.stderr)
- continue
- init_net.op.extend(c2ops.init_ops)
- net.op.extend(c2ops.ops)
- net.external_input.extend(c2ops.interface_blobs)
- net.external_output.extend(
- value_info.name for value_info in model.graph.output)
- net.external_input.extend(
- value_info.name for value_info in model.graph.input)
- if len(errors) > 0:
- raise RuntimeError(
- "ONNX conversion failed, encountered {} errors:\n\n{}".format(
- len(errors), "\n\n".join(errors)))
- return init_net, pred_net
- # wrapper for backwards compatibility
- @classmethod
- def onnx_graph_to_caffe2_net(cls, model, device="CPU", opset_version=_known_opset_version):
- return cls._onnx_model_to_caffe2_net(model, device=device, opset_version=opset_version, include_initializers=True)
- @classmethod
- def supports_device(cls, device_str):
- device = Device(device_str)
- if device.type == DeviceType.CPU:
- return True
- elif core.IsGPUDeviceType(device.type):
- return workspace.has_gpu_support
- return False
- @classmethod
- def is_compatible(cls, model, device='CPU', **kwargs):
- if hasattr(super(Caffe2Backend, cls), 'is_compatible') \
- and callable(super(Caffe2Backend, cls).is_compatible):
- if not super(Caffe2Backend, cls).is_compatible(model, device, **kwargs):
- return False
- # TODO: should have an unspported list of operators, be optimistic for now
- return True
- prepare = Caffe2Backend.prepare
- prepare_zip_archive = Caffe2Backend.prepare_zip_archive
- run_node = Caffe2Backend.run_node
- run_model = Caffe2Backend.run_model
- supports_device = Caffe2Backend.supports_device # noqa
- is_compatible = Caffe2Backend.is_compatible
|