| # |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| # |
| |
| from __future__ import division |
| |
| import numpy as np |
| import onnx.utils |
| import onnx |
| from onnx.backend.base import Backend, BackendRep |
| from onnx import (checker, helper, numpy_helper, GraphProto, NodeProto, |
| TensorProto, OperatorSetIdProto, optimizer) |
| import warnings |
| |
| from . import singa_wrap as singa |
| from . import autograd |
| from . import tensor |
| from singa import utils |
| |
| import collections |
| OrderedDict = collections.OrderedDict |
| namedtuple = collections.namedtuple |
| |
| |
| class SingaFrontend(object): |
| """ |
| This class provides mthods to convert model from singa to onnx. |
| """ |
| |
| # This number indicates the target onnx operator set version |
| _target_opset_version = 11 |
| |
| # beceuase singa's operators are different from onnx. |
| # we define a dict for the name projection |
| # "singa op name": "onnx op name" |
| _rename_operators = { |
| '_Conv2d': 'Conv', |
| 'ReLU': 'Relu', |
| 'MaxPool2d': 'MaxPool', |
| 'AvgPool2d': 'AveragePool', |
| 'SoftMax': 'Softmax', |
| 'Sigmoid': 'Sigmoid', |
| 'Add': 'Add', |
| 'Matmul': 'MatMul', |
| '_BatchNorm2d': 'BatchNormalization', |
| 'Concat': 'Concat', |
| 'Flatten': 'Flatten', |
| 'AddBias': 'Add', |
| 'Gemm': 'Gemm', |
| 'Reshape': 'Reshape', |
| 'Sum': 'Sum', |
| 'cos': 'Cos', |
| 'cosh': 'Cosh', |
| 'sin': 'Sin', |
| 'sinh': 'Sinh', |
| 'tan': 'Tan', |
| 'tanh': 'Tanh', |
| 'acos': 'Acos', |
| 'acosh': 'Acosh', |
| 'asin': 'Asin', |
| 'asinh': 'Asinh', |
| 'atan': 'Atan', |
| 'atanh': 'Atanh', |
| 'SeLU': 'Selu', |
| 'Elu': 'Elu', |
| 'Equal': 'equal', |
| 'Less': 'Less', |
| 'Sign': 'Sign', |
| 'Div': 'Div', |
| 'Sub': 'Sub', |
| 'Sqrt': 'Sqrt', |
| 'Log': 'Log', |
| 'Greater': 'Greater', |
| 'HardSigmoid': 'HardSigmoid', |
| 'Identity': 'Identity', |
| 'SoftPlus': 'Softplus', |
| 'SoftSign': 'Softsign', |
| 'Mean': 'Mean', |
| 'Pow': 'Pow', |
| 'Clip': 'Clip', |
| 'PRelu': 'PRelu', |
| 'Mul': 'Mul', |
| 'Transpose': 'Transpose', |
| 'Max': 'Max', |
| 'Min': 'Min', |
| 'Shape': 'Shape', |
| 'And': 'And', |
| 'Or': 'Or', |
| 'Xor': 'Xor', |
| 'Not': 'Not', |
| 'Negative': 'Neg', |
| 'Reciprocal': 'Reciprocal', |
| 'ConstantOfShape': 'ConstantOfShape', |
| 'Dropout': 'Dropout', |
| 'ReduceSum': 'ReduceSum', |
| 'ReduceMean': 'ReduceMean', |
| 'LeakyRelu': 'LeakyRelu', |
| 'GlobalAveragePool': 'GlobalAveragePool', |
| 'Squeeze': 'Squeeze', |
| 'Unsqueeze': 'Unsqueeze', |
| 'Slice': 'Slice', |
| 'Ceil': 'Ceil', |
| 'Split': 'Split', |
| 'Gather': 'Gather', |
| 'Tile': 'Tile', |
| 'NonZero': 'NonZero', |
| 'Cast': 'Cast', |
| 'OneHot': 'OneHot', |
| } |
| |
| # this dict indicates the operators that need extra handle |
| # each indicates a function name |
| _special_operators = { |
| '_Conv2d': '_create_conv_pool', |
| '_Pooling2d': '_create_conv_pool', |
| '_BatchNorm2d': '_create_batchnorm', |
| 'Concat': '_create_concat', |
| 'Flatten': '_create_flatten', |
| 'Gemm': '_create_gemm', |
| 'Reshape': '_create_reshape', |
| 'SoftMax': '_create_softmax', |
| 'SeLU': '_create_selu', |
| 'Elu': '_create_elu', |
| 'HardSigmoid': '_create_hardsigmoid', |
| 'Clip': '_create_clip', |
| 'Transpose': '_create_transpose', |
| 'ConstantOfShape': '_create_constantOfShape', |
| 'Dropout': '_create_dropout', |
| 'ReduceSum': '_create_reduceOp', |
| 'ReduceMean': '_create_reduceOp', |
| 'Squeeze': '_create_squeeze', |
| 'Unsqueeze': '_create_squeeze', |
| 'Slice': '_create_slice', |
| 'Split': '_create_split', |
| 'Gather': '_create_gather', |
| 'Tile': '_create_tile', |
| 'Cast': '_create_cast', |
| 'OneHot': '_create_onehot', |
| } |
| |
| # operators with bool output |
| _bool_operators = { |
| 'Equal': TensorProto.BOOL, |
| 'Greater': TensorProto.BOOL, |
| 'Less': TensorProto.BOOL, |
| 'And': TensorProto.BOOL, |
| 'Not': TensorProto.BOOL, |
| 'Or': TensorProto.BOOL, |
| 'Xor': TensorProto.BOOL, |
| 'Shape': TensorProto.INT64, |
| 'NonZero': TensorProto.INT64, |
| } |
| |
| # some ops(such as batchnorm) has inputs we cannot handle directly, |
| # so we record these items firstly so that we can handle then |
| # at other place. |
| _unhandled_operators = { |
| "_BatchNorm2d": "_special_handle_batchnorm", |
| "Reshape": "_special_handle_reshape", |
| "Clip": "_special_handle_clip", |
| "Slice": "_special_handle_slice", |
| "Gather": "_special_handle_gather", |
| "Tile": "_special_handle_tile", |
| "OneHot": "_special_handle_onehot", |
| } |
| |
| @classmethod |
| def _create_onehot(cls, op, op_t): |
| """ |
| get a onnx node from singa onthot |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| # axis, indices, depth, values |
| node.attribute.extend([ |
| helper.make_attribute('axis', op.axis), |
| ]) |
| for attr in ['depth', 'values']: |
| node.input.append(op.name + ":" + attr) |
| return node |
| |
| @classmethod |
| def _create_cast(cls, op, op_t): |
| """ |
| get a onnx node from singa cast |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| |
| map_dict = { |
| tensor.float32: TensorProto.FLOAT, # FLOAT to float32 |
| tensor.int32: TensorProto.INT32, # INT32 to int32 |
| } |
| node.attribute.extend([ |
| helper.make_attribute('to', map_dict[op.to]), |
| ]) |
| return node |
| |
| @classmethod |
| def _create_tile(cls, op, op_t): |
| """ |
| get a onnx node from singa tile |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| |
| node.input.append(op.name + ":repeats") |
| return node |
| |
| @classmethod |
| def _create_gather(cls, op, op_t): |
| """ |
| get a onnx node from singa gather |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| |
| node.attribute.extend([ |
| helper.make_attribute('axis', op.axis), |
| ]) |
| node.input.append(op.name + ":indices") |
| return node |
| |
| @classmethod |
| def _create_split(cls, op, op_t): |
| """ |
| get a onnx node from singa split |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| |
| node.attribute.extend([ |
| helper.make_attribute('axis', op.axis), |
| helper.make_attribute('split', op.parts), |
| ]) |
| return node |
| |
| @classmethod |
| def _create_slice(cls, op, op_t): |
| """ |
| get a onnx node from singa slice |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| for attr in ['starts', 'ends', 'axes', 'steps']: |
| node.input.append(op.name + ":" + attr) |
| return node |
| |
| @classmethod |
| def _create_squeeze(cls, op, op_t): |
| """ |
| get a onnx node from singa squeeze and unsqueeze |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| |
| node.attribute.extend([ |
| helper.make_attribute('axes', list(op.axis)), |
| ]) |
| return node |
| |
| @classmethod |
| def _create_reduceOp(cls, op, op_t): |
| """ |
| get a onnx node from singa ReduceSum, ReduceMean, ReduceMax, ReduceMin, etc. |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| |
| node.attribute.extend([ |
| helper.make_attribute('axes', list(op.axes)), |
| helper.make_attribute('keepdims', op.keepdims), |
| ]) |
| return node |
| |
| @classmethod |
| def _create_dropout(cls, op, op_t): |
| """ |
| get a onnx node from singa Dropout operator |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| |
| node.attribute.extend([ |
| helper.make_attribute('ratio', op.ratio), |
| ]) |
| return node |
| |
| @classmethod |
| def _create_constantOfShape(cls, op, op_t): |
| """ |
| get a onnx node from singa ConstantOfShape operator |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| tensor_type = onnx.TensorProto.FLOAT if isinstance( |
| op.value, float) else onnx.TensorProto.INT32 |
| tensor_value = onnx.helper.make_tensor("value", tensor_type, [1], |
| [op.value]) |
| node.attribute.extend([ |
| helper.make_attribute('value', tensor_value), |
| ]) |
| return node |
| |
| @classmethod |
| def _create_transpose(cls, op, op_t): |
| """ |
| get a onnx node from singa Transpose operator |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| |
| node.attribute.extend([ |
| helper.make_attribute('perm', op.perm), |
| ]) |
| return node |
| |
| @classmethod |
| def _create_clip(cls, op, op_t): |
| """ |
| get a onnx node from singa clip operator |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| if op.min is not None: |
| node.input.append(op.name + ":min") |
| else: |
| node.input.append("") |
| if op.max is not None: |
| node.input.append(op.name + ":max") |
| else: |
| node.input.append("") |
| return node |
| |
| @classmethod |
| def _create_hardsigmoid(cls, op, op_t): |
| """ |
| get a onnx node from singa HardSigmoid operator |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| |
| node.attribute.extend([ |
| helper.make_attribute('alpha', op.alpha), |
| helper.make_attribute('beta', op.gamma), |
| ]) |
| return node |
| |
| @classmethod |
| def _create_elu(cls, op, op_t): |
| """ |
| get a onnx node from singa elu operator |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| |
| node.attribute.extend([ |
| helper.make_attribute('alpha', op.alpha), |
| ]) |
| return node |
| |
| @classmethod |
| def _create_selu(cls, op, op_t): |
| """ |
| get a onnx node from singa SeLU operator |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| |
| node.attribute.extend([ |
| helper.make_attribute('alpha', op.alpha), |
| helper.make_attribute('gamma', op.gamma), |
| ]) |
| return node |
| |
| @classmethod |
| def _create_reshape(cls, op, op_t): |
| """ |
| get a onnx node from singa Concat operator |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| # make the shape node |
| # because the reshape in singa does not provide its shape as input tensor |
| shape_node_name = op.name + ":shape" |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| node.input.extend([shape_node_name]) |
| return node |
| |
| @classmethod |
| def _create_concat(cls, op, op_t): |
| """ |
| get a onnx node from singa Concat operator |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| |
| node.attribute.extend([ |
| helper.make_attribute('axis', op.axis), |
| ]) |
| return node |
| |
| @classmethod |
| def _create_softmax(cls, op, op_t): |
| """ |
| get a onnx node from singa Concat operator |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| |
| node.attribute.extend([ |
| helper.make_attribute('axis', op.axis), |
| ]) |
| return node |
| |
| @classmethod |
| def _create_flatten(cls, op, op_t): |
| """ |
| get a onnx node from singa flatten operator |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| |
| node.attribute.extend([ |
| helper.make_attribute('axis', op.axis), |
| ]) |
| return node |
| |
| @classmethod |
| def _create_gemm(cls, op, op_t): |
| """ |
| get a onnx node from singa gemm operator |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| |
| node.attribute.extend([ |
| helper.make_attribute('alpha', float(op.alpha)), |
| helper.make_attribute('beta', float(op.beta)), |
| helper.make_attribute('transA', op.transA), |
| helper.make_attribute('transB', op.transB), |
| ]) |
| |
| return node |
| |
| @classmethod |
| def _create_batchnorm(cls, op, op_t): |
| """ |
| get a onnx node from singa _BatchNorm2d operator |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| # first, we init batchnorm node |
| epsilon = 1e-5 # the epsilon value used in singa |
| bn_node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| bn_node.attribute.extend([ |
| helper.make_attribute('momentum', op.handle.factor), |
| helper.make_attribute('epsilon', epsilon), |
| ]) |
| # then we add nodes of scal, bias, mean, var |
| nodes = [] |
| running_values = {"mean": op.running_mean, "var": op.running_var} |
| for tmp_name, running_value in running_values.items(): |
| node_name = op.name + ":" + tmp_name |
| bn_node.input.append(node_name) |
| |
| nodes.append(bn_node) |
| return nodes |
| |
| @classmethod |
| def _create_conv_pool(cls, op, op_t): |
| """ |
| get a onnx node from singa _Conv2d and _Pooling2d operator |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| node = cls._common_singa_tensor_to_onnx_node(op, op_t) |
| |
| k = [op.handle.kernel_h, op.handle.kernel_w] |
| s = [op.handle.stride_h, op.handle.stride_w] |
| oddp = op.odd_padding |
| p = [ |
| op.handle.pad_h + oddp[0], |
| op.handle.pad_w + oddp[1], |
| op.handle.pad_w + oddp[2], |
| op.handle.pad_h + oddp[3], |
| ] |
| |
| node.attribute.extend([ |
| helper.make_attribute('kernel_shape', k), |
| helper.make_attribute('pads', p), |
| helper.make_attribute('strides', s), |
| ]) |
| if cls._get_singa_op_type(op) == '_Conv2d': |
| node.op_type = cls._rename_operators.get('_Conv2d') |
| node.attribute.extend([ |
| helper.make_attribute('group', op.handle.group), |
| helper.make_attribute('auto_pad', 'NOTSET'), |
| ]) |
| |
| elif op.handle.is_max_pooling: |
| node.op_type = cls._rename_operators.get('MaxPool2d') |
| else: |
| node.op_type = cls._rename_operators.get('AvgPool2d') |
| return node |
| |
| @classmethod |
| def _get_singa_op_inputs_outputs(cls, op): |
| """ |
| get inputs and outputs from a given operator |
| Args: |
| op: a given operator |
| Returns: |
| inputs and outputs of the op |
| """ |
| outputs = [op.output_name(idx) for _, idx in op.y_id2idx.items()] |
| inputs = [ |
| srcop.output_name(srcop.y_id2idx[yid]) |
| for (srcop, yid, _, _) in op.src |
| ] |
| return inputs, outputs |
| |
| @classmethod |
| def _get_singa_op_type(cls, op): |
| """ |
| get the operator type from a given operator |
| Args: |
| op: a given operator |
| Returns: |
| operator type |
| """ |
| return type(op).__name__ |
| |
| @classmethod |
| def _special_handle_batchnorm(cls, op, X, W): |
| """ |
| hanlde the special operators |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| onnx tensor list |
| """ |
| # for singa, x, scale, bias is input |
| # and mean and var is attribute |
| # so we add the mean and var to W |
| tensor_list = [] |
| append_inputs = {"mean": op.running_mean, "var": op.running_var} |
| for tmp_name, append_input in append_inputs.items(): |
| node_name = op.name + ":" + tmp_name |
| append_input = tensor.to_numpy(tensor.from_raw_tensor(append_input)) |
| tensor_list.append(numpy_helper.from_array(append_input, node_name)) |
| return tensor_list |
| |
| @classmethod |
| def _special_handle_reshape(cls, op, X, W): |
| """ |
| hanlde the special operators |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| onnx tensor list |
| """ |
| node_name = op.name + ":shape" |
| return [ |
| numpy_helper.from_array(np.array(op.shape, dtype=np.int64), |
| node_name) |
| ] |
| |
| @classmethod |
| def _special_handle_clip(cls, op, X, W): |
| """ |
| hanlde the special operators |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| onnx tensor list |
| """ |
| tensor_list = [] |
| # clip add min and max |
| append_inputs = {"min": op.min, "max": op.max} |
| for tmp_name, append_input in append_inputs.items(): |
| node_name = op.name + ":" + tmp_name |
| tensor_list.append( |
| helper.make_tensor(node_name, TensorProto.FLOAT, [], |
| [append_input])) |
| return tensor_list |
| |
| @classmethod |
| def _special_handle_slice(cls, op, X, W): |
| """ |
| hanlde the special operators |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| onnx tensor list |
| """ |
| tensor_list = [] |
| # slice add starts, ends, axes, steps |
| append_inputs = { |
| "starts": op.starts, |
| "ends": op.ends, |
| "axes": op.axes, |
| "steps": op.steps, |
| } |
| for tmp_name, append_input in append_inputs.items(): |
| node_name = op.name + ":" + tmp_name |
| tensor_list.append( |
| numpy_helper.from_array(np.array(append_input), node_name)) |
| return tensor_list |
| |
| @classmethod |
| def _special_handle_gather(cls, op, X, W): |
| """ |
| hanlde the special operators |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| onnx tensor list |
| """ |
| tensor_list = [] |
| append_inputs = { |
| "indices": op.indices, |
| } |
| for tmp_name, append_input in append_inputs.items(): |
| node_name = op.name + ":" + tmp_name |
| tensor_list.append( |
| numpy_helper.from_array(np.array(append_input), node_name)) |
| return tensor_list |
| |
| @classmethod |
| def _special_handle_tile(cls, op, X, W): |
| """ |
| hanlde the special operators |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| onnx tensor list |
| """ |
| tensor_list = [] |
| append_inputs = { |
| "repeats": op.repeats, |
| } |
| for tmp_name, append_input in append_inputs.items(): |
| node_name = op.name + ":" + tmp_name |
| tensor_list.append( |
| numpy_helper.from_array(np.array(append_input), node_name)) |
| return tensor_list |
| |
| @classmethod |
| def _special_handle_onehot(cls, op, X, W): |
| """ |
| hanlde the special operators |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| onnx tensor list |
| """ |
| tensor_list = [] |
| append_inputs = { |
| "depth": op.depth, |
| "values": op.values, |
| } |
| for tmp_name, append_input in append_inputs.items(): |
| node_name = op.name + ":" + tmp_name |
| tensor_list.append( |
| numpy_helper.from_array(np.array(append_input), node_name)) |
| return tensor_list |
| |
| @classmethod |
| def handle_special_ops(cls, op, X, W): |
| """ |
| hanlde the special operators, |
| because the inputs of batchnorm and reshape are differnet with onnx |
| we need to add these inputs into onnx model mannully |
| Args: |
| op: a given operator |
| Args: |
| X: onnx input list |
| Args: |
| X: onnx weight list |
| Returns: the onnx node |
| """ |
| optype = cls._get_singa_op_type(op) |
| translator = getattr(cls, cls._unhandled_operators[optype]) |
| tensor_list = translator(op, X, W) |
| for tensor in tensor_list: |
| X.append( |
| helper.make_tensor_value_info(tensor.name, tensor.data_type, |
| tensor.dims)) |
| W.append(tensor) |
| # return X, W |
| |
| @classmethod |
| def _common_singa_tensor_to_onnx_node(cls, op, op_t): |
| """ |
| get a onnx node from a singa operator, prepare its type, inputs and outputs |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: the onnx node |
| """ |
| node_def = NodeProto() |
| node_def.name = op.name |
| |
| optype = cls._get_singa_op_type(op) |
| node_def.op_type = cls._rename_operators.get(optype, optype) |
| |
| inputs, outputs = cls._get_singa_op_inputs_outputs(op) |
| node_def.input.extend(inputs) |
| node_def.output.extend(outputs) |
| |
| return node_def |
| |
| @classmethod |
| def singa_op_to_onnx_node(cls, op, op_t): |
| """ |
| get a onnx node from singa operator |
| Args: |
| op: a given operator |
| Args: |
| op_t: the tensor of the operator |
| Returns: |
| the onnx node |
| """ |
| optype = cls._get_singa_op_type(op) |
| # wether the operator needs special handler |
| if optype in cls._special_operators: |
| translator = getattr(cls, cls._special_operators[optype]) |
| else: |
| translator = cls._common_singa_tensor_to_onnx_node |
| nodes = translator(op, op_t) |
| if not isinstance(nodes, collections.Iterable): |
| nodes = [nodes] |
| nodes = [node for node in nodes if node is not None] |
| return nodes |
| |
| @classmethod |
| def singa_to_onnx_graph(cls, inputs, y, model_name="sonnx"): |
| """ |
| get onnx model from singa computational graph |
| Args: |
| inputs: a list of input tensors (each is initialized with a name) |
| Args: |
| y: a list of tensors, usually the outputs of the graph |
| Returns: |
| the onnx model |
| """ |
| assert len( |
| y |
| ) == 1, "Not support multiple output now." # assume there is only one output |
| y = y[0] |
| |
| graph_def = GraphProto() |
| graph_def.name = model_name |
| topol, ws, ins = utils.post_order_recursive(y.creator, y) |
| |
| # prepare the input |
| X = [] |
| for op_name, op_t in ins.items(): |
| op_t = inputs.pop(0) |
| dtype = TensorProto.INT32 if op_t.dtype == tensor.int32 else TensorProto.FLOAT |
| X.append(helper.make_tensor_value_info(op_name, dtype, op_t.shape)) |
| |
| # prepare the output |
| y_optype = cls._get_singa_op_type(y.creator) |
| if y_optype in cls._bool_operators: |
| y_dtype = cls._bool_operators[y_optype] |
| elif y.dtype == tensor.int32: |
| y_dtype = TensorProto.INT32 |
| else: |
| y_dtype = TensorProto.FLOAT |
| Y = [helper.make_tensor_value_info(y.name, y_dtype, y.shape)] |
| |
| # prepare the weight |
| W = [] |
| for op_name, op_t in ws.items(): |
| dtype = TensorProto.INT32 if op_t.dtype == tensor.int32 else TensorProto.FLOAT |
| wt = tensor.to_numpy(op_t) |
| wt = numpy_helper.from_array(wt) |
| wt.name = op_name |
| W.append(wt) |
| X.append(helper.make_tensor_value_info(op_name, dtype, op_t.shape)) |
| |
| # iterate the node graph |
| for op_name, op in topol.items(): |
| optype = cls._get_singa_op_type(op) |
| if optype in cls._unhandled_operators: |
| cls.handle_special_ops(op, X, W) |
| graph_def.node.extend(cls.singa_op_to_onnx_node(op, op_t)) |
| |
| graph_def.input.extend(X) |
| graph_def.output.extend(Y) |
| graph_def.initializer.extend(W) |
| return graph_def |
| |
| @classmethod |
| def singa_to_onnx_model(cls, inputs, y, model_name="sonnx"): |
| """ |
| get onnx model from singa computational graph |
| Args: |
| inputs: a list of input tensors (each is initialized with a name) |
| Args: |
| y: a list of tensors, usually the outputs of the graph |
| Returns: |
| the onnx model |
| """ |
| opset_id = OperatorSetIdProto() |
| opset_id.version = cls._target_opset_version |
| model = helper.make_model(cls.singa_to_onnx_graph(inputs, |
| y, |
| model_name="sonnx"), |
| producer_name='sonnx', |
| opset_imports=[opset_id]) |
| model = optimizer.optimize(model) |
| checker.check_model(model) |
| return model |
| |
| |
| class OnnxNode(object): |
| """ |
| Reimplementation of NodeProto from ONNX, but in a form |
| more convenient to work with from Python. |
| """ |
| |
| def __init__(self, node): |
| self.name = str(node.name) |
| self.op_type = str(node.op_type) |
| self.attrs = OnnxAttributes.from_onnx(node.attribute) |
| # there may some inputs which we regard as attribute, so we mark them there |
| self.consumed_inputs = list() |
| self.inputs = list(node.input) |
| self.outputs = list(node.output) |
| |
| def getattr(self, key, default=None): |
| return self.attrs[key] if key in self.attrs else default |
| |
| |
| class OnnxAttributes(dict): |
| """ |
| This is a more convenient way to work with ONNX attributes |
| that is not the protobuf representation. |
| """ |
| |
| @staticmethod |
| def from_onnx(args): |
| d = OnnxAttributes() |
| for arg in args: |
| d[arg.name] = helper.get_attribute_value(arg) |
| return d |
| |
| |
| class SingaBackend(Backend): |
| |
| # This number indicates the onnx operator set version |
| _known_opset_version = 11 |
| |
| # beceuase singa's operators are different from onnx. |
| # we define a dict for the name projection |
| _rename_operators = { |
| 'Relu': 'relu', |
| 'Softmax': 'SoftMax', |
| 'Sigmoid': 'sigmoid', |
| 'Add': 'add', |
| 'MatMul': 'matmul', |
| 'Conv': '_Conv2d', |
| 'MaxPool': '_Pooling2d', |
| 'AveragePool': '_Pooling2d', |
| 'BatchNormalization': 'batchnorm_2d', |
| 'Concat': 'Concat', |
| 'Flatten': 'Flatten', |
| 'Gemm': 'Gemm', |
| 'Reshape': 'Reshape', |
| 'Sum': 'sum', |
| 'Cos': 'cos', |
| 'Cosh': 'cosh', |
| 'Sin': 'sin', |
| 'Sinh': 'sinh', |
| 'Tan': 'tan', |
| 'Tanh': 'tanh', |
| 'Acos': 'acos', |
| 'Acosh': 'acosh', |
| 'Asin': 'asin', |
| 'Asinh': 'asinh', |
| 'Atan': 'atan', |
| 'Atanh': 'atanh', |
| 'Selu': 'SeLU', |
| 'Elu': 'Elu', |
| 'Equal': 'equal', |
| 'Less': 'less', |
| 'Sign': 'sign', |
| 'Div': 'div', |
| 'Sub': 'sub', |
| 'Sqrt': 'sqrt', |
| 'Log': 'log', |
| 'Greater': 'greater', |
| 'HardSigmoid': 'HardSigmoid', |
| 'Identity': 'identity', |
| 'Softplus': 'softplus', |
| 'Softsign': 'softsign', |
| 'Mean': 'mean', |
| 'Pow': 'pow', |
| 'Clip': 'Clip', |
| 'PRelu': 'prelu', |
| 'Mul': 'mul', |
| 'Transpose': 'Transpose', |
| 'Max': 'max', |
| 'Min': 'min', |
| 'Shape': 'shape', |
| 'And': '_and', |
| 'Or': '_or', |
| 'Xor': '_xor', |
| 'Not': '_not', |
| 'Neg': 'negative', |
| 'Reciprocal': 'reciprocal', |
| 'ConstantOfShape': 'ConstantOfShape', |
| 'Dropout': 'Dropout', |
| 'ReduceSum': 'ReduceSum', |
| 'ReduceMean': 'ReduceMean', |
| 'LeakyRelu': 'LeakyRelu', |
| 'GlobalAveragePool': 'GlobalAveragePool', |
| 'Squeeze': 'Squeeze', |
| 'Unsqueeze': 'Unsqueeze', |
| 'Slice': 'Slice', |
| 'Ceil': 'Ceil', |
| 'Split': 'Split', |
| 'Gather': 'Gather', |
| 'Tile': 'Tile', |
| 'NonZero': 'nonzero', |
| 'Cast': 'Cast', |
| 'OneHot': 'OneHot', |
| } |
| |
| # this dict indicates the operators that need extra handle |
| # each indicates a function name |
| _special_operators = { |
| 'Conv': '_create_conv', |
| 'MaxPool': '_create_max_avg_pool', |
| 'AveragePool': '_create_max_avg_pool', |
| 'BatchNormalization': '_create_batchnorm', |
| 'Concat': '_create_concat', |
| 'Flatten': '_create_flatten', |
| 'Gemm': '_create_gemm', |
| 'Reshape': '_create_reshape', |
| 'Softmax': '_create_softmax', |
| 'Selu': '_create_selu', |
| 'Elu': '_create_elu', |
| 'HardSigmoid': '_create_hardsigmoid', |
| 'Clip': '_create_clip', |
| 'Transpose': '_create_transpose', |
| 'ConstantOfShape': '_create_constantOfShape', |
| 'Dropout': '_create_dropout', |
| 'ReduceSum': '_create_reduceOp', |
| 'ReduceMean': '_create_reduceOp', |
| 'LeakyRelu': '_create_leakyrelu', |
| 'GlobalAveragePool': '_create_globalaveragepool', |
| 'Squeeze': '_create_squeeze', |
| 'Unsqueeze': '_create_squeeze', |
| 'Slice': '_create_slice', |
| 'Split': '_create_split', |
| 'Gather': '_create_gather', |
| 'Tile': '_create_tile', |
| 'Cast': '_create_cast', |
| 'OneHot': '_create_onehot', |
| 'Constant': "_create_constant" |
| } |
| |
| @classmethod |
| def _create_constant(cls, onnx_node, inputs, opset_version): |
| """ |
| parse onnx constatn node to weights |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| handle, the handle of singa operator |
| Returns: |
| forward, the autograd of singa operator |
| """ |
| tmp_tensor = onnx_node.getattr('value') |
| np_dtype = onnx.mapping.TENSOR_TYPE_TO_NP_TYPE[tmp_tensor.data_type] |
| np_tensor = np.frombuffer(tmp_tensor.raw_data, dtype=np_dtype) |
| if np_tensor.dtype == "int64": |
| np_tensor = np_tensor.astype(np.int32) |
| # todo, we cannot support scalar tensor |
| if np.ndim(np_tensor) == 0: |
| np_tensor = np.array(np_tensor, ndmin=1) |
| return None, np_tensor |
| |
| @classmethod |
| def _create_onehot(cls, onnx_node, inputs, opset_version): |
| """ |
| get the OneHot operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| handle, the handle of singa operator |
| Returns: |
| forward, the autograd of singa operator |
| """ |
| axis = onnx_node.getattr("axis", -1) |
| # we move several inputs to singa's attribuates |
| # and mark them so we don't use them when we run this operator |
| depth = tensor.to_numpy(inputs.pop(1)).astype(np.int32) |
| value = tensor.to_numpy(inputs.pop(1)) |
| onnx_node.consumed_inputs.extend(onnx_node.inputs[1:]) |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return _, forward(axis, depth, value) |
| |
| @classmethod |
| def _create_cast(cls, onnx_node, inputs, opset_version): |
| """ |
| get the Cast operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| handle, the handle of singa operator |
| Returns: |
| forward, the autograd of singa operator |
| """ |
| to = onnx_node.getattr("to") |
| # singa only supports float32 and int32 |
| map_dict = { |
| TensorProto.FLOAT: tensor.float32, # FLOAT to float32 |
| TensorProto.UINT8: None, # UINT8 |
| TensorProto.INT8: tensor.int32, # INT8 to int32 |
| TensorProto.UINT16: None, # UINT16 |
| TensorProto.INT16: tensor.int32, # INT16 to int32 |
| TensorProto.INT32: tensor.int32, # INT32 to int32 |
| TensorProto.INT64: tensor.int32, # INT64 to int32 |
| TensorProto.STRING: None, # stirng |
| TensorProto.BOOL: None, # bool |
| } |
| to = map_dict[to] |
| assert to != None, "not support cast type: {}".format(to) |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return _, forward(to) |
| |
| @classmethod |
| def _create_tile(cls, onnx_node, inputs, opset_version): |
| """ |
| get the Tile operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| handle, the handle of singa operator |
| Returns: |
| forward, the autograd of singa operator |
| """ |
| # we move several inputs to singa's attribuates |
| # and mark them so we don't use them when we run this operator |
| repeats = tensor.to_numpy(inputs.pop(1)).astype(np.int32).tolist() |
| onnx_node.consumed_inputs.append(onnx_node.inputs[1]) |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return _, forward(repeats) |
| |
| @classmethod |
| def _create_gather(cls, onnx_node, inputs, opset_version): |
| """ |
| get the Gather operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| handle, the handle of singa operator |
| Returns: |
| forward, the autograd of singa operator |
| """ |
| axis = onnx_node.getattr("axis", 0) |
| # we move several inputs to singa's attribuates |
| # and mark them so we don't use them when we run this operator |
| indices = tensor.to_numpy(inputs.pop(1)).astype(np.int32).tolist() |
| onnx_node.consumed_inputs.append(onnx_node.inputs[1]) |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return _, forward(axis, indices) |
| |
| @classmethod |
| def _create_split(cls, onnx_node, inputs, opset_version): |
| """ |
| get the Split operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| handle, the handle of singa operator |
| Returns: |
| forward, the autograd of singa operator |
| """ |
| axis = onnx_node.getattr("axis", 0) |
| split = onnx_node.getattr("split", None) |
| num_output = len(onnx_node.outputs) |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return _, forward(axis, split, num_output) |
| |
| @classmethod |
| def _create_slice(cls, onnx_node, inputs, opset_version): |
| """ |
| get the Slice operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| handle, the handle of singa operator |
| Returns: |
| forward, the autograd of singa operator |
| """ |
| # we move several inputs to singa's attribuates |
| # and mark them so we don't use them when we run this operator |
| starts = tensor.to_numpy(inputs.pop(1)).astype(np.int32).tolist() |
| ends = tensor.to_numpy(inputs.pop(1)).astype(np.int32).tolist() |
| # sometime onnx may ignore these two inputs, axes and step |
| if len(inputs) >= 2 and onnx_node.inputs[3] != '': |
| axes = tensor.to_numpy(inputs.pop(1)).astype(np.int32).tolist() |
| else: |
| axes = None |
| steps = tensor.to_numpy(inputs.pop(1)).astype( |
| np.int32).tolist() if len(inputs) >= 2 else None |
| onnx_node.consumed_inputs.extend(onnx_node.inputs[1:]) |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return _, forward(starts, ends, axes, steps) |
| |
| @classmethod |
| def _create_squeeze(cls, onnx_node, inputs, opset_version): |
| """ |
| get the Squeeze and Unsqueeze operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| handle, the handle of singa operator |
| Returns: |
| forward, the autograd of singa operator |
| """ |
| axes = onnx_node.getattr("axes") |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return _, forward(axes) |
| |
| @classmethod |
| def _create_globalaveragepool(cls, onnx_node, inputs, opset_version): |
| """ |
| get the GlobalAveragePool operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| handle, the handle of singa operator |
| Returns: |
| forward, the autograd of singa operator |
| """ |
| data_format = onnx_node.getattr("data_format", 'channels_first') |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return _, forward(data_format) |
| |
| @classmethod |
| def _create_leakyrelu(cls, onnx_node, inputs, opset_version): |
| """ |
| get the LeakyRelu operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| handle, the handle of singa operator |
| Returns: |
| forward, the autograd of singa operator |
| """ |
| alpha = onnx_node.getattr("alpha", 0.01) |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return _, forward(alpha) |
| |
| @classmethod |
| def _create_reduceOp(cls, onnx_node, inputs, opset_version): |
| """ |
| get the ReduceSum, ReduceMean, ReduceMax, ReduceMin, etc, operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| handle, the handle of singa operator |
| Returns: |
| forward, the autograd of singa operator |
| """ |
| axes = onnx_node.getattr("axes", None) |
| keepdims = onnx_node.getattr("keepdims", 1) |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return _, forward(axes, keepdims) |
| |
| @classmethod |
| def _create_dropout(cls, onnx_node, inputs, opset_version): |
| """ |
| get the Dropout operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| handle, the handle of singa operator |
| Returns: |
| forward, the autograd of singa operator |
| """ |
| ratio = onnx_node.getattr("ratio", 0) |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return _, forward(ratio) |
| |
| @classmethod |
| def _create_constantOfShape(cls, onnx_node, inputs, opset_version): |
| """ |
| get the ConstantOfShape operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| handle, the handle of singa operator |
| Returns: |
| forward, the autograd of singa operator |
| """ |
| value = onnx_node.getattr("value", 0) |
| if isinstance(value, onnx.TensorProto): |
| value = numpy_helper.to_array(value)[0].item() |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return _, forward(value) |
| |
| @classmethod |
| def _create_transpose(cls, onnx_node, inputs, opset_version): |
| """ |
| get the Transpose operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| handle, the handle of singa operator |
| Returns: |
| forward, the autograd of singa operator |
| """ |
| shape = inputs[0].shape |
| perm = onnx_node.getattr("perm", list(range(len(shape) - 1, -1, -1))) |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return _, forward(perm) |
| |
| @classmethod |
| def _create_clip(cls, onnx_node, inputs, opset_version): |
| """ |
| get the clip operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| handle, the handle of singa operator |
| Returns: |
| forward, the autograd of singa operator |
| """ |
| # sometime onnx may ignore these two inputs, min or max or both |
| if len(inputs) >= 2 and onnx_node.inputs[1] != '': |
| min_v = tensor.to_numpy(inputs.pop(1)).tolist()[0] |
| else: |
| min_v = None |
| if len(inputs) >= 2 and onnx_node.inputs[2] != '': |
| max_v = tensor.to_numpy(inputs.pop(1)).tolist()[0] |
| else: |
| max_v = None |
| onnx_node.consumed_inputs.extend(onnx_node.inputs[1:]) |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return _, forward(min_v, max_v) |
| |
| @classmethod |
| def _create_hardsigmoid(cls, onnx_node, inputs, opset_version): |
| """ |
| get the HardSigmoid operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| handle, the handle of singa operator |
| Returns: |
| forward, the autograd of singa operator |
| """ |
| alpha = onnx_node.getattr("alpha", 0.2) |
| beta = onnx_node.getattr("beta", 0.5) |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return _, forward(alpha, beta) |
| |
| @classmethod |
| def _create_elu(cls, onnx_node, inputs, opset_version): |
| """ |
| get the elu operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| handle, the handle of singa operator |
| Returns: |
| forward, the autograd of singa operator |
| """ |
| alpha = onnx_node.getattr("alpha", 1.) |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return _, forward(alpha) |
| |
| @classmethod |
| def _create_selu(cls, onnx_node, inputs, opset_version): |
| """ |
| get the selu operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| handle, the handle of singa operator |
| Returns: |
| forward, the autograd of singa operator |
| """ |
| alpha = onnx_node.getattr("alpha", 1.67326) |
| gamma = onnx_node.getattr("gamma", 1.0507) |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return _, forward(alpha, gamma) |
| |
| @classmethod |
| def _create_reshape(cls, onnx_node, inputs, opset_version): |
| """ |
| get the reshape operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| the handle of singa operator |
| Returns: |
| the autograd of singa operator |
| """ |
| shape = tensor.to_numpy(inputs.pop(1)).astype(np.int32).tolist() |
| onnx_node.consumed_inputs.append(onnx_node.inputs[1]) |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return _, forward(shape) |
| |
| @classmethod |
| def _create_conv(cls, onnx_node, inputs, opset_version): |
| """ |
| get the conv operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| handle, the handle of singa operator |
| Returns: |
| forward, the autograd of singa operator |
| """ |
| kernel = tuple(onnx_node.attrs["kernel_shape"]) |
| padding = tuple( |
| onnx_node.attrs["pads"]) if "pads" in onnx_node.attrs else (0, 0) |
| stride = tuple(onnx_node.getattr('strides', (1, 1))) |
| # default the odd_padding is 0, once there are same pad mode, we modify it |
| # for odd_padding, please refer the autegrade.py |
| odd_padding = (0, 0, 0, 0) |
| if "auto_pad" in onnx_node.attrs: |
| auto_pad = utils.force_unicode(onnx_node.attrs['auto_pad']) |
| if auto_pad in ('SAME_UPPER', 'SAME_LOWER'): |
| padding, odd_padding = utils.get_padding_shape( |
| auto_pad, inputs[0].shape[2:], kernel, stride) |
| |
| # not support dilation |
| dilation = onnx_node.getattr('dilations', 1) |
| if dilation != 1 and list(dilation) != [1, 1]: |
| raise ValueError("Not implemented yet for dilation") |
| group = onnx_node.getattr('group', 1) |
| |
| # only support 1d or 2d |
| if len(kernel) > 2: |
| raise ValueError("Only implemented for 1d or 2d") |
| |
| bias = len(inputs) == 3 |
| x = inputs[0] |
| x_shape = inputs[0].shape |
| in_channels = x_shape[1] |
| w_shape = inputs[1].shape |
| out_channels = w_shape[0] |
| assert w_shape[1] == in_channels // group |
| |
| if inputs[0].device.id() == -1: |
| if group != 1: |
| raise NotImplementedError |
| else: |
| handle = singa.ConvHandle(x.data, kernel, stride, padding, |
| in_channels, out_channels, bias, |
| group) |
| else: |
| handle = singa.CudnnConvHandle(x.data, kernel, stride, padding, |
| in_channels, out_channels, bias, |
| group) |
| |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return _, forward(handle, odd_padding) |
| |
| @classmethod |
| def _create_max_avg_pool(cls, onnx_node, inputs, opset_version): |
| """ |
| get the max or avg pool operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| handle, the handle of singa operator |
| Returns: |
| forward, the autograd of singa operator |
| """ |
| kernel = tuple(onnx_node.attrs["kernel_shape"]) |
| padding = tuple( |
| onnx_node.attrs["pads"]) if "pads" in onnx_node.attrs else (0, 0) |
| stride = tuple(onnx_node.getattr('strides', (1, 1))) |
| # default the odd_padding is 0, once there are same pad mode, we modify it |
| # for odd_padding, please refer the autegrade.py |
| odd_padding = (0, 0, 0, 0) |
| if "auto_pad" in onnx_node.attrs: |
| auto_pad = utils.force_unicode(onnx_node.attrs['auto_pad']) |
| if auto_pad in ('SAME_UPPER', 'SAME_LOWER'): |
| padding, odd_padding = utils.get_padding_shape( |
| auto_pad, inputs[0].shape[2:], kernel, stride) |
| |
| # not support count_include_pad and auto_pad |
| if "count_include_pad" in onnx_node.attrs or "ceil_mode" in onnx_node.attrs: |
| raise ValueError( |
| "Not implemented yet for count_include_pad or ceil_mode") |
| |
| # only support 2d |
| if len(kernel) != 2: |
| raise ValueError("Not implemented yet") |
| |
| is_max = onnx_node.op_type == 'MaxPool' |
| x = inputs[0] |
| if x.device.id() == -1: |
| handle = singa.PoolingHandle(x.data, kernel, stride, padding, |
| is_max) |
| else: |
| handle = singa.CudnnPoolingHandle(x.data, kernel, stride, padding, |
| is_max) |
| |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return _, forward(handle, odd_padding) |
| |
| @classmethod |
| def _create_batchnorm(cls, onnx_node, inputs, opset_version): |
| """ |
| get the batch norm operator from onnx node |
| Args:onnx_node: a given onnx node |
| Args:inputs: the input tensor |
| Args:opset_version: the opset version |
| Returns: the handle of singa operator |
| Returns: the autograd of singa operator |
| """ |
| x = inputs[0] |
| factor = onnx_node.getattr('momentum', 0.9) |
| if x.device.id() == -1: |
| handle = singa.BatchNormHandle(factor, x.data) |
| else: |
| handle = singa.CudnnBatchNormHandle(factor, x.data) |
| |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return handle, forward |
| |
| @classmethod |
| def _create_concat(cls, onnx_node, inputs, opset_version): |
| """ |
| get the concat operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| the handle of singa operator |
| Returns: |
| the autograd of singa operator |
| """ |
| factor = onnx_node.attrs["axis"] |
| if factor < 0: |
| factor = len(inputs[0].shape |
| ) + factor # in order to support the negative axis |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return None, forward(axis=factor) |
| |
| @classmethod |
| def _create_softmax(cls, onnx_node, inputs, opset_version): |
| """ |
| get the concat operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| the handle of singa operator |
| Returns: |
| the autograd of singa operator |
| """ |
| factor = onnx_node.getattr('axis', 1) |
| if factor < 0: |
| # in order to support the negative axis |
| factor = len(inputs[0].shape) + factor |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return None, forward(axis=factor) |
| |
| @classmethod |
| def _create_gemm(cls, onnx_node, inputs, opset_version): |
| """ |
| get the gemm operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| the handle of singa operator |
| Returns: |
| the autograd of singa operator |
| """ |
| x = inputs[0] |
| alpha = onnx_node.getattr('alpha', 1.) |
| beta = onnx_node.getattr('beta', 1.) |
| transA = onnx_node.getattr('transA', 0) |
| transB = onnx_node.getattr('transB', 0) |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return None, forward(alpha=alpha, |
| beta=beta, |
| transA=transA, |
| transB=transB) |
| |
| @classmethod |
| def _create_flatten(cls, onnx_node, inputs, opset_version): |
| """ |
| get the flatten operator from onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| the handle of singa operator |
| Returns: |
| the autograd of singa operator |
| """ |
| factor = onnx_node.getattr('axis', 1) |
| if factor < 0: |
| # in order to support the negative axis |
| factor = len(inputs[0].shape) + factor |
| |
| _, forward = cls._common_onnx_node_to_singa_op(onnx_node, inputs, |
| opset_version) |
| return None, forward(axis=factor) |
| |
| @classmethod |
| def _common_onnx_node_to_singa_op(cls, onnx_node, inputs, opset_version): |
| """ |
| get a common singa operator(only autograd) from a onnx node |
| other special operators also can call this func to get autograd |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| tensor_map: the input tensor |
| Args: |
| opset_version: the opset version |
| Returns: |
| a dict of tensors |
| Returns: |
| a list of SingaOps('name', 'op', 'handle', 'forward') |
| """ |
| onnx_op_type = onnx_node.op_type |
| assert onnx_op_type in cls._rename_operators, "not support operator: {}".format( |
| onnx_op_type) |
| autograd_op = getattr(autograd, cls._rename_operators[onnx_op_type]) |
| return None, autograd_op |
| |
| @classmethod |
| def _onnx_node_to_singa_op(cls, |
| onnx_node, |
| inputs, |
| opset_version=_known_opset_version): |
| """ |
| get a singa operator(handle and autograd) from a onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input list |
| Args: |
| opset_version: the opset version |
| Returns: |
| a dict of tensors |
| Returns: |
| a list of SingaOps('name', 'op', 'handle', 'forward') |
| """ |
| if onnx_node.op_type in cls._special_operators: |
| translator = getattr(cls, cls._special_operators[onnx_node.op_type]) |
| else: |
| translator = cls._common_onnx_node_to_singa_op |
| return translator(onnx_node, inputs, opset_version) |
| |
| @classmethod |
| def run_node(cls, onnx_node, inputs, opset_version=_known_opset_version): |
| """ |
| run a single singa operator from a onnx node |
| Args: |
| onnx_node: a given onnx node |
| Args: |
| inputs: the input tensor |
| Args: |
| device: the used device |
| Args: |
| opset_version: the opset version |
| Returns: |
| list, the output of the |
| """ |
| valid_inputs = [x for x in onnx_node.inputs if x != ""] |
| assert len(valid_inputs) == len( |
| inputs), "{}: expected {} but got {}".format( |
| onnx_node.op_type, len(valid_inputs), len(inputs)) |
| |
| tmp_inputs = [inputs[x] for x in onnx_node.inputs if x != ""] |
| handle, forward = cls._onnx_node_to_singa_op(onnx_node, tmp_inputs, |
| opset_version) |
| # only give the inputs it needs |
| # consumed_inputs are the inputs marked as attributes |
| # so we remove it here |
| tmp_inputs = [ |
| inputs[x] |
| for x in onnx_node.inputs |
| if x not in onnx_node.consumed_inputs |
| ] |
| return cls._run_node(onnx_node, tmp_inputs, handle, forward, |
| opset_version) |
| |
| @classmethod |
| def _run_node(cls, |
| onnx_node, |
| inputs, |
| handle, |
| forward, |
| opset_version=_known_opset_version): |
| """ |
| run a single singa operator from a onnx node |
| Args:inputs: |
| the input tensor |
| Args:handle: |
| the handle of singa operator |
| Args:forward: |
| the forward of singa operator |
| Args: |
| opset_version: the opset version |
| Returns: |
| list, the output of the |
| """ |
| outputs = forward(*inputs) if handle is None else forward( |
| handle, *inputs) |
| if not isinstance(outputs, collections.Iterable): |
| outputs = [outputs] |
| outputs_dict = OrderedDict() |
| for (key, val) in zip(onnx_node.outputs, outputs): |
| outputs_dict[key] = val |
| return outputs_dict |
| |
| @classmethod |
| def _init_graph_parameter(cls, graph, init_inputs, device): |
| """ |
| init the singa tensor from onnx infos |
| Args: |
| graph: a given onnx graph |
| Args: |
| init_inputs: a list of inputs, which used to init the operators |
| Args: |
| device: the used device |
| Returns: |
| a dict of tensors |
| """ |
| tensor_map = {} |
| # due to https://github.com/onnx/onnx/issues/2417 |
| # sometimes, input contains all initializer's info |
| # sometimes, may not |
| all_inputs = OrderedDict() |
| for t in graph.input: |
| all_inputs[t.name] = t |
| # so we refresh the input by the initializer |
| for t in graph.initializer: |
| all_inputs[t.name] = t |
| initializers = {t.name for t in graph.initializer} |
| inp_idx = 0 |
| for name, x in all_inputs.items(): |
| if name in initializers: |
| # if it has initializer, we use its value as the input |
| np_tensor = numpy_helper.to_array(x) |
| if np_tensor.dtype == "int64": |
| np_tensor = np_tensor.astype(np.int32) |
| # todo, we cannot support scalar tensor |
| if np.ndim(np_tensor) == 0: |
| np_tensor = np.array(np_tensor, ndmin=1) |
| else: |
| # if not, means it's a input rather than a inner weight |
| # so if the user gives values, we use these values |
| # if not, we just use the shape of input gived by onnx to init a random value |
| # HOWEVER, the random value may not be correct for some inputs, such as gather which needs indices |
| # so if have operators, the user must give inputs |
| x_shape = tuple( |
| dim.dim_value for dim in x.type.tensor_type.shape.dim) |
| if init_inputs is not None: |
| np_tensor = init_inputs[inp_idx] |
| inp_idx += 1 |
| else: |
| np_tensor = np.random.randn(*x_shape).astype(np.float32) |
| tmp_tensor = tensor.from_numpy(np_tensor) |
| tmp_tensor.to_device(device) |
| # todo, for backward |
| tmp_tensor.stores_grad = (name in initializers) |
| tensor_map[x.name] = tmp_tensor |
| return tensor_map |
| |
| @classmethod |
| def _onnx_model_to_singa_net(cls, model, init_inputs, device, |
| opset_version): |
| """ |
| get all intermediate tensors and operators from onnx model |
| Args: |
| model: a given onnx model |
| Args: |
| init_inputs: a list of inputs, which used to init the operators |
| Args: |
| device: the used device |
| Args: |
| opset_version: the opset version |
| Returns: |
| a dict of tensors |
| Returns: |
| a list of SingaOps('name', 'op', 'handle', 'forward') |
| """ |
| # init all tensor input and weight as a tensor map |
| tensor_map = cls._init_graph_parameter(model.graph, init_inputs, device) |
| # only weights tensor |
| weights = {x.name: tensor_map[x.name] for x in model.graph.initializer} |
| # the parsed operators queue |
| singa_ops = [] |
| singa_op = namedtuple('SingaOps', ['name', 'op', 'handle', 'forward']) |
| for node in model.graph.node: |
| node = OnnxNode(node) |
| # only give the inputs it needs |
| # consumed_inputs are the inputs marked as attributes |
| # so we remove it here |
| inputs = [ |
| tensor_map[x] |
| for x in node.inputs |
| if x not in node.consumed_inputs |
| ] |
| handle, forward = cls._onnx_node_to_singa_op( |
| node, inputs, opset_version) |
| # if it is Constant, we hanlde it as a weight |
| # otherwise, we run it and add its output into map for being used by later operators |
| if node.op_type == 'Constant': |
| tmp_tensor = tensor.from_numpy(forward) |
| tmp_tensor.to_device(device) |
| tmp_name = node.outputs.pop(0) |
| weights[tmp_name] = tmp_tensor |
| tensor_map[tmp_name] = tmp_tensor |
| else: |
| outputs = cls._run_node(node, inputs, handle, forward) |
| for key, val in outputs.items(): |
| tensor_map[key] = val |
| singa_ops.extend([singa_op(node.name, node, handle, forward)]) |
| return weights, singa_ops |
| |
| @classmethod |
| def prepare(cls, model, device, **kwargs): |
| """ |
| get the batch norm operator from onnx node |
| Args: |
| model: a given onnx node |
| Args: |
| device: the used device |
| Returns: |
| a list of output values |
| """ |
| super(SingaBackend, cls).prepare(model, device, **kwargs) |
| # when parsing graph, we use the shape of input gived by onnx to init a random value |
| # HOWEVER, the random value may not be correct for some inputs, such as gather which needs indices |
| # so if have operators, the user must give inputs |
| init_inputs = kwargs.get("init_inputs", None) |
| # whether initializers are moved into inputs, due to https://github.com/onnx/onnx/issues/2417 |
| # sometimes, input contains all initializer's info, sometimes, may not |
| cls.keep_initializers_as_inputs = kwargs.get( |
| 'keep_initializers_as_inputs', True) |
| # optimize and infer the shape of the model |
| try: |
| model = onnx.utils.polish_model(model) |
| except IndexError as err: |
| # due to https://github.com/onnx/onnx/issues/2417 |
| model = onnx.shape_inference.infer_shapes(model) |
| |
| # check the opset version and ir version |
| 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 singa 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 (singa requires it)" |
| ) |
| else: |
| opset_version = 1 |
| weights, singa_ops = cls._onnx_model_to_singa_net( |
| model, init_inputs, device, opset_version) |
| return SingaRep(model, weights, singa_ops, |
| cls.keep_initializers_as_inputs) |
| |
| |
| class SingaRep(BackendRep): |
| |
| def __init__(self, |
| model, |
| weights, |
| singa_ops, |
| keep_initializers_as_inputs=True): |
| """ |
| SingaRep provides the intermediate representation of Singa, |
| the user can run the forward of the singa model by run func, |
| or, the user can append more layers after the singa_ops to do |
| the transfer learning |
| Args: |
| model: a given operator |
| Args: |
| weights: the tensor of weights |
| Args: |
| singa_ops: the tensor of the operator |
| """ |
| super(SingaRep, self).__init__() |
| self.model = model |
| self.tensor_map = weights |
| self.keep_initializers_as_inputs = keep_initializers_as_inputs |
| # this each item of singa_ops is: ('name', 'op', 'handle', 'forward') |
| # the name is a string, op is OnnxNode, |
| # handle is Singa handle to store the tensor into singa operator |
| # the forward is singa autograd operator |
| self.singa_ops = singa_ops |
| |
| def run(self, inputs, **kwargs): |
| """ |
| run the forward of singa model |
| Args: |
| inputs: a given operator |
| Returns: |
| the onnx node |
| """ |
| graph = self.model.graph |
| # last_layers means we run this model until the last #N layers |
| last_layers = kwargs.get('last_layers', len(self.singa_ops)) |
| if last_layers != len(self.singa_ops): |
| final_outputs = self.singa_ops[last_layers-1].op.outputs |
| else: |
| final_outputs = [outp.name for outp in graph.output] |
| # whether return all outputs |
| all_outputs = kwargs.get('all_outputs', False) |
| # get a specific op by its name |
| op_name = kwargs.get('op_name', None) |
| # record the tensor we added from input |
| tmp_tensor_map = {name: val for name, val in self.tensor_map.items()} |
| |
| # the dict will be returned |
| ret_outputs = OrderedDict() |
| if self.keep_initializers_as_inputs: |
| require_input_len = len(graph.input) - len(graph.initializer) |
| actual_input_len = len(inputs) |
| else: |
| require_input_len = len(graph.input) |
| actual_input_len = len(inputs) |
| assert require_input_len == actual_input_len, "The length of graph input is different from the tensor input: %d, %d" % ( |
| require_input_len, actual_input_len) |
| # run the handle by the order of the list(the list is Topological Sorting) |
| for inp in graph.input: |
| if inp.name not in tmp_tensor_map: |
| tmp_tensor_map[inp.name] = inputs.pop(0) |
| |
| for _, op, handle, forward in self.singa_ops[:last_layers]: |
| if len(op.consumed_inputs) != 0: |
| # because if op has consumed_inputs, it means it moved some inputs into attributes |
| # so when running, we should update these attributes |
| handle, forward = get_op(op, |
| [tmp_tensor_map[x] for x in op.inputs]) |
| inputs = [ |
| tmp_tensor_map[x] |
| for x in op.inputs |
| if x not in op.consumed_inputs |
| ] |
| outputs = _run_node(op, inputs, handle, forward) |
| for key, val in outputs.items(): |
| tmp_tensor_map[key] = val |
| ret_outputs[key] = val |
| |
| if op_name is not None: |
| if op_name in outputs: |
| return outputs[op_name] |
| else: |
| raise RuntimeError( |
| "The op_name {} does not exist, please check. The available op_names are: {}" |
| .format(op_name, [val for key, val in op_name.items()])) |
| |
| # return all outputs if all_outputs==True |
| # else return last outputs |
| if all_outputs: |
| return ret_outputs |
| else: |
| return [ret_outputs[outp] for outp in final_outputs] |
| |
| |
| run_node = SingaBackend.run_node |
| _run_node = SingaBackend._run_node |
| prepare = SingaBackend.prepare |
| get_op = SingaBackend._onnx_node_to_singa_op |
| to_onnx = SingaFrontend.singa_to_onnx_model |
| save = onnx.save |
| load = onnx.load |