| ## @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 |