Caffe2 - Python API
A deep learning, cross platform ML framework
backend.py
1 ## @package onnx
2 # Module caffe2.python.onnx.backend
3 
4 """Backend for running ONNX on Caffe2
5 
6 To run this, you will need to have Caffe2 installed as well.
7 """
8 from __future__ import absolute_import
9 from __future__ import division
10 from __future__ import print_function
11 from __future__ import unicode_literals
12 
13 import os
14 import collections
15 from subprocess import Popen, PIPE
16 import zipfile
17 
18 import caffe2
19 from caffe2.python import core, workspace, rnn_cell, gru_cell
20 from caffe2.python.model_helper import ModelHelper
21 from caffe2.proto import caffe2_pb2
23 import numpy as np
24 import onnx
25 from onnx import checker, GraphProto, TensorProto, AttributeProto, ModelProto
26 import onnx.numpy_helper
27 import onnx.defs
28 import onnx.optimizer
29 from onnx.backend.base import Backend, Device, DeviceType, namedtupledict
30 
31 from caffe2.python.onnx.workspace import Workspace
32 from caffe2.python.onnx.backend_rep import Caffe2Rep
33 from caffe2.python.onnx.backend_cpp_rep import Caffe2CppRep
34 from caffe2.python.onnx.helper import dummy_name
35 
37 
38 import warnings
39 
40 def force_unicode(s):
41  try:
42  return s.decode('utf-8')
43  except AttributeError:
44  return s
45 
46 def get_device_option(device):
47  m = {DeviceType.CPU: caffe2_pb2.CPU,
48  DeviceType.CUDA: caffe2_pb2.CUDA}
49  return core.DeviceOption(m[device.type], device.device_id)
50 
51 
52 class OnnxAttributes(dict):
53  """
54  This is a more convenient way to work with ONNX/Caffe2 attributes
55  that is not the protobuf representation.
56  """
57  @staticmethod
58  def from_onnx(args):
59  d = OnnxAttributes()
60  for arg in args:
61  d[arg.name] = convertAttributeProto(arg)
62  return d
63 
64  def caffe2(self, kmap=lambda k: k):
65  for k, v in self.items():
66  if kmap(k) != '':
67  yield caffe2.python.utils.MakeArgument(kmap(k), v)
68 
69 # TODO: Move this into ONNX main library
70 def convertAttributeProto(onnx_arg):
71  """
72  Convert an ONNX AttributeProto into an appropriate Python object
73  for the type.
74 
75  NB: Tensor attribute gets returned as the straight proto.
76  """
77  if onnx_arg.HasField('f'):
78  return onnx_arg.f
79  elif onnx_arg.HasField('i'):
80  return onnx_arg.i
81  elif onnx_arg.HasField('s'):
82  return onnx_arg.s
83  elif onnx_arg.HasField('t'):
84  return onnx_arg.t # this is a proto!
85  elif len(onnx_arg.floats):
86  return list(onnx_arg.floats)
87  elif len(onnx_arg.ints):
88  return list(onnx_arg.ints)
89  elif len(onnx_arg.strings):
90  return list(onnx_arg.strings)
91  else:
92  raise ValueError("Unsupported ONNX attribute: {}".format(onnx_arg))
93 
94 
95 # TODO: Move this into ONNX main library
96 class OnnxNode(object):
97  """
98  Reimplementation of NodeProto from ONNX, but in a form
99  more convenient to work with from Python.
100 
101  We may temporarily edit these nodes to get them into Caffe2 form,
102  before actually translating into the Caffe2 protobuf, since this
103  is easier than decomposing everything, and putting it back together
104  when we're ready.
105  """
106  def __init__(self, node):
107  self.name = str(node.name)
108  self.op_type = str(node.op_type)
109  self.attrs = OnnxAttributes.from_onnx(node.attribute)
110  self.inputs = list(node.input)
111  self.outputs = list(node.output)
112 
113 
114 Caffe2Ops = collections.namedtuple('Caffe2Ops', ['ops', 'init_ops', 'interface_blobs'])
115 
116 
117 class Caffe2Backend(Backend):
118 
119  # The greatest version of the ONNX operator set which we are aware of.
120  # Models whose version is larger than this will cause us to emit a warning
121  # that we are attempting to translate on a "best effort" basis.
122  #
123  # If you increase this, make SURE you cross-reference all BC-breaking
124  # changes from one version to the next, and any that you did not
125  # implement, mark as broken in _broken_operators
126  _known_opset_version = 6
127 
128  # This dictionary will record operators which are KNOWN to be
129  # broken, so we give a good error message rather than do something
130  # bogus and then fail.
131  _broken_operators = {
132  # 'BrokenOp': version_it_was_broken_in
133  }
134 
135  # Operators that are different between Caffe2 and
136  # ONNX but only in their name.
137  # In most cases, this should be empty - as the effort of ONNX is
138  # to unify the operator definitions.
139  _renamed_operators = {
140  'Caffe2ConvTranspose': 'ConvTranspose',
141  'GlobalMaxPool': 'MaxPool',
142  'GlobalAveragePool': 'AveragePool',
143  'Pad': 'PadImage',
144  'Neg': 'Negative',
145  'BatchNormalization': 'SpatialBN',
146  'InstanceNormalization': 'InstanceNorm',
147  'MatMul': 'BatchMatMul',
148  'Upsample': 'ResizeNearest',
149  'Identity': 'Copy',
150  'InstanceNormalization': 'InstanceNorm',
151  'Equal': 'EQ',
152  'Less': 'LT',
153  'Greater': 'GT',
154  'Unsqueeze': 'ExpandDims',
155  }
156 
157  _global_renamed_attrs = {'kernel_shape': 'kernels'}
158  _per_op_renamed_attrs = {
159  'Squeeze': {'axes': 'dims'},
160  'Unsqueeze': {'axes': 'dims'},
161  'Transpose': {'perm': 'axes'},
162  'Upsample': {'mode': ''},
163  'ConvTranspose': {'output_padding': 'adjs'},
164  'Selu': {'gamma': 'scale'},
165  }
166 
167  # operators whose behavior is different beyond renaming
168  # the value is an attribute of this class that is a
169  # function from ToffeIR node_def to caffe2 op_def
170  _special_operators = {
171  'LSTM': '_create_lstm',
172  'GRU': '_create_gru',
173  'RNN': '_create_rnn',
174  }
175 
176  # NB: By default, you will use the LATEST definition of the operator,
177  # so this interface MAY make BC-breaking changes. Specify an
178  # opset_version if you don't want this to version.
179  @classmethod
180  def run_node(cls, node, inputs, device='CPU', opset_version=_known_opset_version, outputs_info=None):
181  super(Caffe2Backend, cls).run_node(node, inputs, device=device, outputs_info=outputs_info)
182 
183  device_option = get_device_option(Device(device))
184  with Workspace(), core.DeviceScope(device_option): # temporary!
185  if isinstance(inputs, dict):
186  for key, value in inputs.items():
187  workspace.FeedBlob(key, value)
188  else:
189  assert len(node.input) == len(inputs), "{}: expected {} but got {}".format(
190  node.op_type, len(node.input), len(inputs))
191  for key, value in zip(node.input, inputs):
192  workspace.FeedBlob(key, value)
193 
194  ops = []
195  cbackend = C.Caffe2Backend()
196  ops_str = cbackend.convert_node(node.SerializeToString(), opset_version)
197  for s in ops_str[0] + ops_str[1]:
198  op = caffe2_pb2.OperatorDef()
199  op.ParseFromString(s)
200  op.device_option.CopyFrom(device_option)
201  ops.append(op)
202  # For testing
203  if "ONNX_CAFFE2_DEBUG" in os.environ:
204  init_ops, ops2, _ = cls._onnx_node_to_caffe2_op(
205  None, None, node, opset_version or cls._known_opset_version)
206  ops2 = init_ops + ops2
207  for op in ops2:
208  op.device_option.CopyFrom(device_option)
209  print("\nC++:\n{}\nPython:\n{}".format(ops, ops2))
210  workspace.RunOperatorsOnce(ops)
211  output_values = [workspace.FetchBlob(name) for name in node.output]
212  return namedtupledict('Outputs', node.output)(*output_values)
213 
214  @classmethod
215  def _create_tensor_filling_op(cls, onnx_tensor, name=None):
216  """
217  Given an Onnx TensorProto, translate it into a Caffe2 operator
218  which produces the given tensor filling op.
219  """
220  assert name or onnx_tensor.name
221  name = name or onnx_tensor.name
222 
223  c2_op = caffe2_pb2.OperatorDef()
224 
225  c2_values = c2_op.arg.add()
226  c2_values.name = "values"
227 
228  def tensor2list(onnx_tensor):
229  # Use the onnx.numpy_helper because the data may be raw
230  return onnx.numpy_helper.to_array(onnx_tensor).flatten().tolist()
231 
232  if onnx_tensor.data_type in [TensorProto.FLOAT]:
233  c2_op.type = 'GivenTensorFill'
234  c2_values.floats.extend(tensor2list(onnx_tensor))
235  elif onnx_tensor.data_type in [TensorProto.DOUBLE]:
236  c2_op.type = 'GivenTensorDoubleFill'
237  c2_values.floats.extend(tensor2list(onnx_tensor))
238  elif onnx_tensor.data_type in [TensorProto.INT64,
239  TensorProto.UINT32]:
240  c2_op.type = 'GivenTensorInt64Fill'
241  c2_values.ints.extend(tensor2list(onnx_tensor))
242  elif onnx_tensor.data_type in [TensorProto.UINT8,
243  TensorProto.INT8,
244  TensorProto.UINT16,
245  TensorProto.INT16,
246  TensorProto.INT32]:
247  c2_op.type = 'GivenTensorIntFill'
248  c2_values.ints.extend(tensor2list(onnx_tensor))
249  elif onnx_tensor.data_type == TensorProto.BOOL:
250  c2_op.type = 'GivenTensorBoolFill'
251  c2_values.ints.extend(tensor2list(onnx_tensor))
252  elif onnx_tensor.data_type == TensorProto.STRING:
253  c2_op.type = 'GivenTensorStringFill'
254  c2_values.strings.extend(onnx_tensor.string_data)
255  else:
256  raise RuntimeError(
257  "unrecognized tensor type {}".format(onnx_tensor.data_type))
258 
259  c2_shape = c2_op.arg.add()
260  c2_shape.name = "shape"
261  c2_shape.ints.extend(onnx_tensor.dims)
262 
263  c2_op.output.append(name)
264 
265  return c2_op
266 
267  @classmethod
268  def _rnn_shape_inference(cls, init_model, pred_model, n, input_blob, W):
269  # ad-hoc, informally-specified, bug-ridden, slow
270  # implementation of shape inference
271 
272  # if the weight matrices are directly provided as
273  # initializers, their dimensions should be available in the
274  # init net model.
275  for x in init_model.graph.input:
276  if x.name == W:
277  return x.type.tensor_type.shape.dim[1].dim_value
278 
279  # otherwise, assume that the input_blob is either a direct
280  # graph input, or another rnn op of the same type. This
281  # matches the pattern produced by exporting from pytorch
282  # (where the weight matrices are unusable for this purpose due
283  # to reshaping operations that lose shape information).
284  for x in pred_model.graph.input:
285  if x.name == input_blob:
286  return x.type.tensor_type.shape.dim[2].dim_value
287 
288  curr = n
289  while True:
290  for x in pred_model.graph.input:
291  if x.name == curr.inputs[0] and curr.op_type == 'Gather':
292  return x.type.tensor_type.shape.dim[1].dim_value
293  prev = [x for x in map(OnnxNode, pred_model.graph.node) if x.outputs[0] == curr.inputs[0]]
294  if len(prev) != 1:
295  return
296  prev = prev[0]
297  if prev.op_type == n.op_type:
298  return prev.attrs['hidden_size']
299  if prev.op_type == 'Transpose':
300  for x in pred_model.graph.input:
301  if x.name == prev.inputs[0]:
302  return x.type.tensor_type.shape.dim[2].dim_value
303  curr = prev
304 
305  @classmethod
306  def _create_rnn(cls, init_model, pred_model, n, opset_version):
307  assert init_model is not None, "cannot convert RNNs without access to the full model"
308  assert pred_model is not None, "cannot convert RNNs without access to the full model"
309 
310  attrs = dict(n.attrs) # make a copy, which is safe to mutate
311  hidden_size = attrs.pop('hidden_size')
312  activation = force_unicode(attrs.pop('activations', ('tanh',))[0])
313  direction = force_unicode(attrs.pop('direction', 'forward'))
314  assert not attrs, "unsupported RNN attributes: " + str(attrs.keys())
315  assert direction in ['forward', 'bidirectional'], "unsupported backwards RNN"
316 
317  input_blob, W, R, B, sequence_lens, initial_h = n.inputs
318 
319  if sequence_lens == "":
320  sequence_lens = None
321 
322  input_size = cls._rnn_shape_inference(init_model, pred_model, n, input_blob, W)
323  if input_size is None:
324  raise RuntimeError("best-effort shape inference for RNN input failed")
325 
326  init_net = core.Net("init-net")
327  pred_mh = ModelHelper()
328 
329  def make_rnn(direction_offset):
330  name = dummy_name()
331 
332  # input and recurrence biases are squashed together in
333  # onnx but not in caffe2
334 
335  bias_offset = 2 * direction_offset * hidden_size
336  init_net.Slice(B, name + "/i2h_b",
337  starts=[bias_offset + 0 * hidden_size],
338  ends =[bias_offset + 1 * hidden_size])
339  init_net.Slice(B, name + "/gates_t_b",
340  starts=[bias_offset + 1 * hidden_size],
341  ends =[bias_offset + 2 * hidden_size])
342 
343  weight_offset = direction_offset * hidden_size
344  init_net.Slice(W, name + '/i2h_w',
345  starts=[weight_offset + 0 * hidden_size, 0],
346  ends =[weight_offset + 1 * hidden_size,-1])
347  init_net.Slice(R, name + '/gates_t_w',
348  starts=[weight_offset + 0 * hidden_size, 0],
349  ends =[weight_offset + 1 * hidden_size,-1])
350 
351  initial_h_sliced = name + '/initial_h'
352  init_net.Slice(initial_h, initial_h_sliced,
353  starts=[direction_offset + 0, 0, 0],
354  ends =[direction_offset + 1,-1,-1])
355 
356  if direction_offset == 1:
357  input = pred_mh.net.ReversePackedSegs(
358  [input_blob, sequence_lens], name + "/input-reversed")
359  else:
360  input = input_blob
361 
362  hidden_t_all, hidden_t_last = rnn_cell.BasicRNN(
363  pred_mh,
364  input,
365  sequence_lens,
366  [initial_h_sliced],
367  input_size,
368  hidden_size,
369  name,
370  drop_states=False,
371  forward_only=True,
372  activation=activation
373  )
374 
375  if direction_offset == 1:
376  hidden_t_all = pred_mh.net.ReversePackedSegs(
377  [hidden_t_all, sequence_lens], name + "/output-reversed")
378 
379  return hidden_t_all, hidden_t_last
380 
381  if direction == 'forward':
382  hidden_t_all, hidden_t_last = make_rnn(0)
383 
384  # in the forward case, storage is shared between the two
385  # outputs. We need to decouple them so that the
386  # VariableLengthSequencePadding only mutates n.outputs[0]
387  pred_mh.net.Copy(hidden_t_last, n.outputs[1])
388 
389  pred_mh.net = pred_mh.net.Clone(
390  "dummy-clone-net",
391  blob_remap={ hidden_t_all: n.outputs[0] }
392  )
393  elif direction == 'bidirectional':
394  hidden_t_all_f, hidden_t_last_f = make_rnn(0)
395  hidden_t_all_b, hidden_t_last_b = make_rnn(1)
396  pred_mh.net.Concat([hidden_t_all_f, hidden_t_all_b],
397  [n.outputs[0], dummy_name()], axis=2)
398  pred_mh.net.Concat([hidden_t_last_f, hidden_t_last_b],
399  [n.outputs[1], dummy_name()], axis=0)
400 
401  if sequence_lens is not None:
402  pred_mh.net.VariableLengthSequencePadding(
403  [n.outputs[0], sequence_lens], [n.outputs[0]])
404 
405  return Caffe2Ops(list(pred_mh.Proto().op),
406  list(init_net.Proto().op),
407  list(pred_mh.Proto().external_input))
408 
409  @classmethod
410  def _create_lstm(cls, init_model, pred_model, n, opset_version):
411  assert init_model is not None, "cannot convert LSTMs without access to the full model"
412  assert pred_model is not None, "cannot convert LSTMs without access to the full model"
413 
414  attrs = dict(n.attrs) # make a copy, which is safe to mutate
415  hidden_size = attrs.pop('hidden_size')
416  direction = force_unicode(attrs.pop('direction', 'forward'))
417  assert not attrs, "unsupported LSTM attributes: " + str(attrs.keys())
418  assert direction in ['forward', 'bidirectional'], "unsupported backwards LSTM"
419 
420  input_blob, W, R, B, sequence_lens, initial_h, initial_c = n.inputs
421 
422  if sequence_lens == "":
423  sequence_lens = None
424 
425  input_size = cls._rnn_shape_inference(init_model, pred_model, n, input_blob, W)
426  if input_size is None:
427  raise RuntimeError("best-effort shape inference for LSTM input failed")
428 
429  init_net = core.Net("init-net")
430  pred_mh = ModelHelper()
431 
432  def make_lstm(direction_offset):
433  name = dummy_name()
434 
435  # input and recurrence biases are squashed together in
436  # onnx but not in caffe2
437 
438  bias_offset = 8 * direction_offset * hidden_size
439  Bi = init_net.Slice(B, name + "_bias_i2h",
440  starts=[bias_offset + 0 * hidden_size],
441  ends =[bias_offset + 4 * hidden_size])
442  Br = init_net.Slice(B, name + "_bias_gates",
443  starts=[bias_offset + 4 * hidden_size],
444  ends =[bias_offset + 8 * hidden_size])
445 
446  weight_offset = 4 * direction_offset * hidden_size
447  W_ = init_net.Slice(W, name + '/i2h_w_pre',
448  starts=[weight_offset + 0 * hidden_size, 0],
449  ends =[weight_offset + 4 * hidden_size,-1])
450  R_ = init_net.Slice(R, name + '/gates_t_w_pre',
451  starts=[weight_offset + 0 * hidden_size, 0],
452  ends =[weight_offset + 4 * hidden_size,-1])
453 
454  # caffe2 has a different order from onnx. We need to rearrange
455  # i o f c -> i f o c
456  reforms = ((W_, 'i2h_w', [(0, -1)]),
457  (R_, 'gates_t_w', [(0, -1)]),
458  (Bi, 'i2h_b' , []),
459  (Br, 'gates_t_b', []))
460  for name_from, name_to, extra_dims in reforms:
461  xi, xo, xf, xc = [name_from + suffix for suffix in ("_i", "_o", "_f", "_c")]
462  for i, x in enumerate([xi, xo, xf, xc]):
463  dim0 = i * hidden_size, (i+1) * hidden_size
464  starts, ends = zip(dim0, *extra_dims)
465  init_net.Slice(name_from, x, starts=starts, ends=ends)
466  init_net.Concat([xi, xf, xo, xc], ['%s/%s' % (name, name_to), dummy_name()], axis=0)
467 
468  initial_h_sliced = name + '/initial_h'
469  init_net.Slice(initial_h, initial_h_sliced,
470  starts=[direction_offset + 0, 0, 0],
471  ends =[direction_offset + 1,-1,-1])
472  initial_c_sliced = name + '/initial_c'
473  init_net.Slice(initial_c, initial_c_sliced,
474  starts=[direction_offset + 0, 0, 0],
475  ends =[direction_offset + 1,-1,-1])
476 
477  if direction_offset == 1:
478  input = pred_mh.net.ReversePackedSegs(
479  [input_blob, sequence_lens], name + "/input-reversed")
480  else:
481  input = input_blob
482 
483  hidden_t_all, hidden_t_last, _, cell_last, params = rnn_cell.LSTM(
484  pred_mh,
485  input,
486  sequence_lens,
487  [initial_h_sliced, initial_c_sliced],
488  input_size,
489  hidden_size,
490  name,
491  drop_states=False,
492  forward_only=True,
493  return_params=True
494  )
495 
496  if direction_offset == 1:
497  hidden_t_all = pred_mh.net.ReversePackedSegs(
498  [hidden_t_all, sequence_lens], name + "/output-reversed")
499 
500  return hidden_t_all, hidden_t_last, cell_last
501 
502  if direction == 'forward':
503  hidden_t_all, hidden_t_last, cell_last = make_lstm(0)
504 
505  # in the forward case, storage is shared between the three
506  # outputs. We need to decouple them so that the
507  # VariableLengthSequencePadding only mutates n.outputs[0]
508  pred_mh.net.Copy(hidden_t_last, n.outputs[1])
509  pred_mh.net.Copy(cell_last, n.outputs[2])
510 
511  pred_mh.net = pred_mh.net.Clone(
512  "dummy-clone-net",
513  blob_remap={ hidden_t_all: n.outputs[0] }
514  )
515  elif direction == 'bidirectional':
516  hidden_t_all_f, hidden_t_last_f, cell_last_f = make_lstm(0)
517  hidden_t_all_b, hidden_t_last_b, cell_last_b = make_lstm(1)
518  pred_mh.net.Concat([hidden_t_all_f, hidden_t_all_b],
519  [n.outputs[0], dummy_name()], axis=2)
520  pred_mh.net.Concat([hidden_t_last_f, hidden_t_last_b],
521  [n.outputs[1], dummy_name()], axis=0)
522  pred_mh.net.Concat([cell_last_f, cell_last_b],
523  [n.outputs[2], dummy_name()], axis=0)
524 
525  if sequence_lens is not None:
526  pred_mh.net.VariableLengthSequencePadding(
527  [n.outputs[0], sequence_lens], [n.outputs[0]])
528 
529  return Caffe2Ops(list(pred_mh.Proto().op),
530  list(init_net.Proto().op),
531  list(pred_mh.Proto().external_input))
532 
533  @classmethod
534  def _create_gru(cls, init_model, pred_model, n, opset_version):
535  assert init_model is not None, "cannot convert GRUs without access to the full model"
536  assert pred_model is not None, "cannot convert GRUs without access to the full model"
537 
538  attrs = dict(n.attrs) # make a copy, which is safe to mutate
539  hidden_size = attrs.pop('hidden_size')
540  linear_before_reset = attrs.pop('linear_before_reset', 0)
541  direction = force_unicode(attrs.pop('direction', 'forward'))
542  assert not attrs, "unsupported GRU attributes: " + str(attrs.keys())
543  assert direction in ['forward', 'bidirectional'], "unsupported backwards GRU"
544 
545  input_blob, W, R, B, sequence_lens, initial_h = n.inputs
546 
547  if sequence_lens == "":
548  sequence_lens = None
549 
550  input_size = cls._rnn_shape_inference(init_model, pred_model, n, input_blob, W)
551  if input_size is None:
552  raise RuntimeError("best-effort shape inference for GRU input failed")
553 
554  init_net = core.Net("init-net")
555  pred_mh = ModelHelper()
556 
557  def make_gru(direction_offset):
558  name = dummy_name()
559 
560  # input and recurrence biases are squashed together in
561  # onnx but not in caffe2
562 
563  bias_offset = 6 * direction_offset * hidden_size
564  Bi = init_net.Slice(B, name + "_bias_i2h",
565  starts=[bias_offset + 0 * hidden_size],
566  ends =[bias_offset + 3 * hidden_size])
567  Br = init_net.Slice(B, name + "_bias_gates",
568  starts=[bias_offset + 3 * hidden_size],
569  ends =[bias_offset + 6 * hidden_size])
570 
571  weight_offset = 3 * direction_offset * hidden_size
572  W_ = init_net.Slice(W, name + '/i2h_w_pre',
573  starts=[weight_offset + 0 * hidden_size, 0],
574  ends =[weight_offset + 3 * hidden_size,-1])
575  R_ = init_net.Slice(R, name + '/gates_t_w_pre',
576  starts=[weight_offset + 0 * hidden_size, 0],
577  ends =[weight_offset + 3 * hidden_size,-1])
578 
579  # caffe2 has a different order from onnx. We need to rearrange
580  # z r h -> r z h
581  reforms = ((W_, 'i2h_w', True, [(0,-1)]),
582  (R_, 'gate_t_w', False, [(0,-1)]),
583  (Bi, 'i2h_b', True, []),
584  (Br, 'gate_t_b', False, []))
585  for name_from, name_to, do_concat, extra_dims in reforms:
586  xz, xr, xh = ['%s/%s_%s' % (name, prefix, name_to) for prefix in ('update', 'reset', 'output')]
587  for i, x in enumerate([xz, xr, xh]):
588  dim0 = i * hidden_size, (i+1) * hidden_size
589  starts, ends = zip(dim0, *extra_dims)
590  init_net.Slice(name_from, x, starts=starts, ends=ends)
591  if do_concat:
592  init_net.Concat([xr, xz, xh], ['%s/%s' % (name, name_to), dummy_name()], axis=0)
593 
594  initial_h_sliced = name + '/initial_h'
595  init_net.Slice(initial_h, initial_h_sliced,
596  starts=[direction_offset + 0, 0, 0],
597  ends =[direction_offset + 1,-1,-1])
598 
599  if direction_offset == 1:
600  input = pred_mh.net.ReversePackedSegs(
601  [input_blob, sequence_lens], name + "/input-reversed")
602  else:
603  input = input_blob
604 
605  hidden_t_all, hidden_t_last = gru_cell.GRU(
606  pred_mh,
607  input,
608  sequence_lens,
609  [initial_h_sliced],
610  input_size,
611  hidden_size,
612  name,
613  drop_states=False,
614  forward_only=True,
615  linear_before_reset=linear_before_reset
616  )
617 
618  if direction_offset == 1:
619  hidden_t_all = pred_mh.net.ReversePackedSegs(
620  [hidden_t_all, sequence_lens], name + "/output-reversed")
621 
622  return hidden_t_all, hidden_t_last
623 
624  if direction == 'forward':
625  hidden_t_all, hidden_t_last = make_gru(0)
626 
627  # in the forward case, storage is shared between the two
628  # outputs. We need to decouple them so that the
629  # VariableLengthSequencePadding only mutates n.outputs[0]
630  pred_mh.net.Copy(hidden_t_last, n.outputs[1])
631 
632  pred_mh.net = pred_mh.net.Clone(
633  "dummy-clone-net",
634  blob_remap={ hidden_t_all: n.outputs[0] }
635  )
636  elif direction == 'bidirectional':
637  hidden_t_all_f, hidden_t_last_f = make_gru(0)
638  hidden_t_all_b, hidden_t_last_b = make_gru(1)
639  pred_mh.net.Concat([hidden_t_all_f, hidden_t_all_b],
640  [n.outputs[0], dummy_name()], axis=2)
641  pred_mh.net.Concat([hidden_t_last_f, hidden_t_last_b],
642  [n.outputs[1], dummy_name()], axis=0)
643 
644  if sequence_lens is not None:
645  pred_mh.net.VariableLengthSequencePadding(
646  [n.outputs[0], sequence_lens], [n.outputs[0]])
647 
648  return Caffe2Ops(list(pred_mh.Proto().op),
649  list(init_net.Proto().op),
650  list(pred_mh.Proto().external_input))
651 
652  @classmethod
653  def _substitute_raw_value(cls, tp, raw_values_dict):
654  if tp.HasField('raw_data') and tp.raw_data == bytes(b'__EXTERNAL'):
655  if tp.name not in raw_values_dict:
656  raise RuntimeError('TensorProto for value {} referenced raw data but it was not found!'.format(tp.name))
657  else:
658  tp.raw_data = raw_values_dict[tp.name]
659 
660  @classmethod
661  def _visit_and_substitute_raw_values(cls, nodes, raw_values_dict):
662  for node in nodes:
663  for attr in node.attribute:
664  if attr.HasField('t'):
665  cls._substitute_raw_value(attr.t, raw_values_dict)
666  for t in attr.tensors:
667  cls._substitute_raw_value(t, raw_values_dict)
668  if attr.HasField('g'):
669  cls._visit_and_substitute_raw_values(attr.g.node, raw_values_dict)
670  for g in attr.graphs:
671  cls._visit_and_substitute_raw_values(g.node, raw_values_dict)
672 
673  @classmethod
674  def _external_value_resolution_pass(cls, model, raw_values_dict):
675  for init in model.graph.initializer:
676  cls._substitute_raw_value(init, raw_values_dict)
677 
678  cls._visit_and_substitute_raw_values(model.graph.node, raw_values_dict)
679 
680 
681  @classmethod
682  def _direct_initialize_parameters(cls, initializer, ws, device_option):
683  for tp in initializer:
684  ws.FeedBlob(tp.name, onnx.numpy_helper.to_array(tp), device_option)
685 
686  @classmethod
687  def _direct_initialize_inputs(cls, inputs, initialized, ws, device_option):
688  for value_info in inputs:
689  if value_info.name in initialized:
690  continue
691  shape = list(d.dim_value for d in value_info.type.tensor_type.shape.dim)
692  ws.FeedBlob(value_info.name, np.ones(shape), device_option)
693 
694  @staticmethod
695  def optimize_onnx(input, init=False, predict=False):
696  passes = ['fuse_consecutive_transposes',
697  'eliminate_nop_transpose',
698  'fuse_transpose_into_gemm']
699  if init:
700  passes.append('split_init')
701  if predict:
702  passes.append('split_predict')
703  out = onnx.optimizer.optimize(input, passes)
704  return out
705 
706  @classmethod
707  def prepare_zip_archive(cls, file, device='CPU', **kwargs):
708  with zipfile.ZipFile(file, mode='r') as z:
709  with z.open('__MODEL_PROTO', 'r') as f:
710  model = onnx.load(f);
711  blob_names = set(z.namelist()) - set('__MODEL_PROTO')
712  # TODO: make this more efficient
713  raw_values_dict = {}
714  for name in blob_names:
715  with z.open(name, 'r') as blob_file:
716  raw_values_dict[name] = blob_file.read()
717 
718  cls._external_value_resolution_pass(model, raw_values_dict)
719  return cls.prepare(model, device, **kwargs)
720 
721  @classmethod
722  def prepare(cls, model, device='CPU', **kwargs):
723  '''
724  For Onnx Caffe2Backend, we require that init_graph don't initialize the actual input of the predict_graph,
725 
726  for example, if "img" is the input blob for the predict_net, we require that in init_graph and in
727  initializer of the predict_graph, "img" is not initalized. We don't have a check for this, since
728  there is no way we can know which blob is the input of the predict_graph.
729  '''
730  super(Caffe2Backend, cls).prepare(model, device, **kwargs)
731  opset_version = None
732  for imp in model.opset_import:
733  if not imp.HasField("domain") or imp.domain == "":
734  opset_version = imp.version
735  if imp.version > cls._known_opset_version:
736  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))
737  else:
738  warnings.warn("Unrecognized operator set {}".format(imp.domain))
739  if opset_version is None:
740  if model.ir_version >= 0x00000003:
741  raise RuntimeError("Model with IR version >= 3 did not specify ONNX operator set version (onnx-caffe2 requires it)")
742  else:
743  opset_version = 1
744 
745  # Check whether we have RNN related ops
746  pred_model = ModelProto()
747  pred_model.ParseFromString(cls.optimize_onnx(model.SerializeToString(), predict=True))
748  rnn_nodes = []
749  for node in pred_model.graph.node:
750  if node.op_type in {'LSTM', 'GRU', 'RNN'}:
751  rnn_nodes.append(node)
752 
753  # Build the C++ backend
754  # TODO: build a predictor that supports GPU
755  # And for RNN nets, we need to avoid adding init_net
756  use_cpp_backend = device == 'CPU' and not rnn_nodes
757  # use python backend for now
758  use_cpp_backend = False
759  if use_cpp_backend:
760  c2_rnn_ops = []
761  if rnn_nodes:
762  init_model = ModelProto()
763  init_model.ParseFromString(cls.optimize_onnx(model.SerializeToString(), init=True))
764  for node in rnn_nodes:
765  c2ops = cls._onnx_node_to_caffe2_op(
766  init_model, pred_model, node, opset_version)
767  init_ops = [x.SerializeToString() for x in c2ops.init_ops]
768  ops = [x.SerializeToString() for x in c2ops.ops]
769  external_inputs = c2ops.interface_blobs
770  c2_rnn_ops.append(C.Caffe2Ops(init_ops, ops, external_inputs))
771  del init_model
772 
773  cbackend = C.Caffe2Backend()
774  rep = cbackend.prepare(model.SerializeToString(), device, c2_rnn_ops)
775  # For testing
776  # Dump the net descriptions to file for comparison with the Python ones
777  if "ONNX_CAFFE2_DEBUG" in os.environ:
778  pred_net_str = rep.pred_net()
779  pn = caffe2_pb2.NetDef()
780  pn.ParseFromString(pred_net_str)
781  init_net_str = rep.init_net()
782  inn = caffe2_pb2.NetDef()
783  inn.ParseFromString(init_net_str)
784  with open("cpp.txt", "w") as f:
785  f.write("pred_net: \n{}".format(pn))
786 
787  rep_wrapper = Caffe2CppRep(rep)
788  return rep_wrapper
789  else:
790  ws = Workspace()
791  device_option = get_device_option(Device(device))
792 
793  # Directly load initializer data into blobs in workspace
795  model.graph.initializer,
796  ws,
797  device_option,
798  )
799 
800  initialized = {init.name for init in model.graph.initializer}
801 
803  model.graph.input,
804  initialized,
805  ws,
806  device_option,
807  )
808 
809  uninitialized = [value_info.name for value_info in model.graph.input if value_info.name not in initialized]
810 
811  init_net, predict_net = cls._onnx_model_to_caffe2_net(model, device, opset_version, False)
812  if "ONNX_CAFFE2_DEBUG" in os.environ:
813  with open("python.txt", "w") as f:
814  f.write("pred_net: \n{}".format(predict_net))
815  retval = Caffe2Rep(init_net, predict_net, ws, uninitialized)
816  return retval
817 
818 
819  @classmethod
820  # TODO: This method needs a refactor for clarity
821  def _onnx_node_to_caffe2_op(cls, init_model, pred_model, node_def, opset_version):
822  cbackend = C.Caffe2Backend()
823  if cbackend.support_onnx_import(node_def.op_type):
824  op_strs = cbackend.convert_node(node_def.SerializeToString(), opset_version)
825  init_ops = []
826  for s in op_strs[0]:
827  op = caffe2_pb2.OperatorDef()
828  op.ParseFromString(s)
829  init_ops.append(op)
830  ops = []
831  for s in op_strs[1]:
832  op = caffe2_pb2.OperatorDef()
833  op.ParseFromString(s)
834  ops.append(op)
835  return Caffe2Ops(ops, init_ops, [])
836 
837  if node_def.op_type in cls._special_operators:
838  translator = getattr(cls, cls._special_operators[node_def.op_type])
839  else:
840  translator = cls._common_onnx_node_to_caffe2_op
841  ops = translator(init_model, pred_model, OnnxNode(node_def), opset_version)
842  if isinstance(ops, Caffe2Ops):
843  return ops
844  if not isinstance(ops, collections.Iterable):
845  ops = [ops]
846  return Caffe2Ops(ops, [], [])
847 
848  @classmethod
849  def _common_onnx_node_to_caffe2_op(cls, init_model, pred_model, onnx_node, opset_version):
850  """
851  This translator performs the basic translation of ONNX nodes into
852  Caffe2 operators. Besides doing a straightforward marshalling from
853  one format to another, it also does these extra things:
854 
855  - Renames operators based on '_renamed_operators'
856  - Renames attributes based on '_global_renamed_attrs' and
857  '_per_op_renamed_attrs'
858 
859  If you're writing a custom translator, consider calling this first,
860  and then fixing things up further.
861  """
862  c2_op = caffe2_pb2.OperatorDef()
863 
864  c2_op.input.extend(onnx_node.inputs)
865  c2_op.output.extend(onnx_node.outputs)
866  c2_op.name = onnx_node.name
867 
868  onnx_op_type = onnx_node.op_type
869  broken_version = cls._broken_operators.get(onnx_op_type, float('Inf'))
870  if broken_version <= opset_version:
871  raise ValueError(
872  "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))
873  c2_op.type = cls._renamed_operators.get(onnx_op_type, onnx_op_type)
874  if not core.IsOperator(c2_op.type):
875  raise ValueError(
876  "Don't know how to translate op {}".format(onnx_op_type))
877 
878  def kmap(k):
879  if (onnx_op_type in cls._per_op_renamed_attrs and
880  k in cls._per_op_renamed_attrs[onnx_op_type]):
881  return cls._per_op_renamed_attrs[onnx_op_type][k]
882  if k in cls._global_renamed_attrs:
883  return cls._global_renamed_attrs[k]
884  return k
885  c2_op.arg.extend(onnx_node.attrs.caffe2(kmap=kmap))
886 
887  return c2_op
888 
889  @staticmethod
890  def _all_names_in_graph(graph):
891  if graph is None:
892  return set()
893 
894  names = set()
895  names.update(value_info.name for value_info in graph.input)
896  names.update(value_info.name for value_info in graph.output)
897  for node in graph.node:
898  names.update(node.input)
899  names.update(node.output)
900  return names
901 
902  @classmethod
903  def _onnx_model_to_caffe2_net(cls, onnx_model, device, opset_version, include_initializers):
904  device_option = get_device_option(Device(device))
905 
906  init_model = ModelProto()
907  init_model.ParseFromString(cls.optimize_onnx(onnx_model.SerializeToString(), init=True))
908 
909  pred_model = ModelProto()
910  pred_model.ParseFromString(cls.optimize_onnx(onnx_model.SerializeToString(), predict=True))
911 
912  init_net = caffe2_pb2.NetDef()
913  pred_net = caffe2_pb2.NetDef()
914 
915  init_net.name = onnx_model.graph.name + '_init'
916  pred_net.name = onnx_model.graph.name + '_predict'
917 
918  if include_initializers:
919  init_net.op.extend(cls._create_tensor_filling_op(tp) for tp in onnx_model.graph.initializer)
920 
921  dummy_name(cls._all_names_in_graph(init_model.graph) | cls._all_names_in_graph(pred_model.graph))
922 
923  success = True
924  for net, model in ( (init_net, init_model), (pred_net, pred_model) ):
925  net.device_option.CopyFrom(device_option)
926  for node in model.graph.node:
927  try:
928  c2ops = cls._onnx_node_to_caffe2_op(
929  init_model, pred_model, node, opset_version)
930  except Exception as e:
931  success = False
932  print('ONNX FATAL:', e)
933  continue
934  (init_net if include_initializers else net).op.extend(c2ops.init_ops)
935  net.op.extend(c2ops.ops)
936  net.external_input.extend(c2ops.interface_blobs)
937  net.external_output.extend(
938  value_info.name for value_info in model.graph.output)
939  net.external_input.extend(
940  value_info.name for value_info in model.graph.input)
941 
942  if not success:
943  raise RuntimeError('ONNX conversion failed')
944 
945  return init_net, pred_net
946 
947  # wrapper for backwards compatability
948  @classmethod
949  def onnx_graph_to_caffe2_net(cls, model, device="CPU", opset_version=_known_opset_version):
950  return cls._onnx_model_to_caffe2_net(model, device=device, opset_version=opset_version, include_initializers=True)
951 
952  @classmethod
953  def supports_device(cls, device_str):
954  device = Device(device_str)
955  if device.type == DeviceType.CPU:
956  return True
957  elif device.type == DeviceType.CUDA:
958  return workspace.has_gpu_support
959  return False
960 
961 
962 prepare = Caffe2Backend.prepare
963 
964 prepare_zip_archive = Caffe2Backend.prepare_zip_archive
965 
966 run_node = Caffe2Backend.run_node
967 
968 run_model = Caffe2Backend.run_model
969 
970 supports_device = Caffe2Backend.supports_device # noqa
def _substitute_raw_value(cls, tp, raw_values_dict)
Definition: backend.py:653
def _external_value_resolution_pass(cls, model, raw_values_dict)
Definition: backend.py:674
def _visit_and_substitute_raw_values(cls, nodes, raw_values_dict)
Definition: backend.py:661
def _direct_initialize_inputs(cls, inputs, initialized, ws, device_option)
Definition: backend.py:687
def optimize_onnx(input, init=False, predict=False)
Definition: backend.py:695
def _create_tensor_filling_op(cls, onnx_tensor, name=None)
Definition: backend.py:215
def _common_onnx_node_to_caffe2_op(cls, init_model, pred_model, onnx_node, opset_version)
Definition: backend.py:849
def _direct_initialize_parameters(cls, initializer, ws, device_option)
Definition: backend.py:682
def _onnx_model_to_caffe2_net(cls, onnx_model, device, opset_version, include_initializers)
Definition: backend.py:903
def _rnn_shape_inference(cls, init_model, pred_model, n, input_blob, W)
Definition: backend.py:268
def MakeArgument(key, value)
Definition: utils.py:116
def prepare(cls, model, device='CPU', kwargs)
Definition: backend.py:722
def _onnx_node_to_caffe2_op(cls, init_model, pred_model, node_def, opset_version)
Definition: backend.py:821