#
# 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
import onnx.utils
from onnx.backend.base import Backend, BackendRep
from onnx import (checker, helper, numpy_helper, GraphProto, NodeProto,
                  TensorProto, OperatorSetIdProto, optimizer, mapping,
                  shape_inference)
import warnings

from . import device
from . import autograd
from . import layer
from . import tensor
from . import model
from . import utils
from . import singa_wrap as singa

import collections
OrderedDict = collections.OrderedDict
namedtuple = collections.namedtuple

# singa only supports float32 and int32
NP_TYPE_TO_SINGA_SUPPORT_TYPE = {
    np.dtype('float32'): np.dtype('float32'),
    np.dtype('uint8'): None,
    np.dtype('int8'): np.dtype('int32'),
    np.dtype('uint16'): None,
    np.dtype('int16'): np.dtype('int32'),
    np.dtype('int32'): np.dtype('int32'),
    np.dtype('int64'): np.dtype('int32'),
    np.dtype('bool'): np.dtype('float32'),
    np.dtype('float16'): np.dtype('float32'),
    np.dtype('float64'): np.dtype('float32'),
    np.dtype('complex64'): None,
    np.dtype('complex128'): None,
    np.dtype('uint32'): None,
    np.dtype('uint64'): None,
    np.dtype(np.object): None
}


def onnx_type_to_singa_type(onnx_type):
    np_type = mapping.TENSOR_TYPE_TO_NP_TYPE[onnx_type]
    return NP_TYPE_TO_SINGA_SUPPORT_TYPE[np_type]


gpu_dev = None
if singa.USE_CUDA:
    gpu_dev = device.create_cuda_gpu()
cpu_dev = device.get_default_device()


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 = TensorProto.FLOAT if isinstance(
            op.value, float) else TensorProto.INT32
        tensor_value = 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 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).replace(".", "_")
        self.op_type = str(node.op_type)
        self.attrs = OnnxAttributes.from_onnx(node.attribute)
        # inputs as attributes in singa
        self.attr_inputs = {}
        # inputs as weights in singa
        self.weight_inputs = {}
        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

    def set_attr_inputs(self, key, name):
        self.attr_inputs[key] = name

    def del_attr_inputs(self, key):
        del self.attr_inputs[key]

    def set_weight_inputs(self, key, name):
        self.weight_inputs[key] = name

    def del_weight_inputs(self, key):
        del self.weight_inputs[key]


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
    _opset_version = 11

    _ir_version = 0x0000000000000006

    # beceuase singa's operators are different from onnx.
    # we define a dict for the name projection
    _rename_operators = {
        # common op
        'Relu': 'ReLU',
        'Sigmoid': 'Sigmoid',
        'Add': 'Add',
        'MatMul': 'Matmul',
        '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',
        'Equal': 'Equal',
        'Less': 'Less',
        'Sign': 'Sign',
        'Div': 'Div',
        'Sub': 'Sub',
        'Sqrt': 'Sqrt',
        'Log': 'Log',
        'Greater': 'Greater',
        'Identity': 'Identity',
        'Softplus': 'SoftPlus',
        'Softsign': 'SoftSign',
        'Mean': 'Mean',
        'Pow': 'Pow',
        'PRelu': 'PRelu',
        'Mul': 'Mul',
        'Max': 'Max',
        'Min': 'Min',
        'Shape': 'Shape',
        'And': 'And',
        'Or': 'Or',
        'Xor': 'Xor',
        'Not': 'Not',
        'Neg': 'Negative',
        'Reciprocal': 'Reciprocal',
        'Unsqueeze': 'Unsqueeze',
        'NonZero': 'NonZero',
        'Ceil': 'Ceil',
        'Floor': 'Floor',
        'Abs': 'Abs',
        # special op
        'ScatterElements': 'ScatterElements',
        'Cast': 'Cast',
        'Split': 'Split',
        'Squeeze': 'Squeeze',
        'GlobalAveragePool': 'GlobalAveragePool',
        'LeakyRelu': 'LeakyRelu',
        'ReduceSum': 'ReduceSum',
        'ReduceMean': 'ReduceMean',
        'Dropout': 'Dropout',
        'ConstantOfShape': 'ConstantOfShape',
        'Transpose': 'Transpose',
        'HardSigmoid': 'HardSigmoid',
        'Elu': 'Elu',
        'Selu': 'SeLU',
        'Concat': 'Concat',
        'Softmax': 'SoftMax',
        'Flatten': 'Flatten',
        'OneHot': 'OneHot',
        'Tile': 'Tile',
        'Gather': 'Gather',
        'Reshape': 'Reshape',
        'Slice': 'Slice',
        'Clip': 'Clip',
        'Expand': 'Expand',
        'Pad': 'Pad',
        'Upsample': 'UpSample',
        'DepthToSpace': 'DepthToSpace',
        'SpaceToDepth': 'SpaceToDepth',
        'Where': 'Where',
        'Erf': 'Erf',
        'Gemm': 'layer.Gemm',  # layer
        'BatchNormalization': 'layer.BatchNorm2d',  # layer
        'Conv': 'layer.Conv2d',  # layer
        'MaxPool': 'layer.Pooling2d',  # layer
        'AveragePool': 'layer.Pooling2d',  # layer
    }

    # this dict indicates the operators that need extra handle
    # each indicates a function name
    _special_operators = {
        'Cast': '_create_cast',
        'Split': '_create_split',
        'Squeeze': '_create_squeeze_unsqueeze',
        'Unsqueeze': '_create_squeeze_unsqueeze',
        'GlobalAveragePool': '_create_global_average_pool',
        'LeakyRelu': '_create_leakyrelu',
        'ReduceSum': '_create_reduce_ops',
        'ReduceMean': '_create_reduce_ops',
        'Dropout': '_create_dropout',
        'ConstantOfShape': '_create_constant_of_shape',
        'Transpose': '_create_transpose',
        'HardSigmoid': '_create_hardsigmoid',
        'Elu': '_create_elu',
        'Selu': '_create_selu',
        'Concat': '_create_concat',
        'Softmax': '_create_softmax',
        'Gemm': '_create_gemm',
        'Flatten': '_create_flatten',
        'OneHot': '_create_onehot',
        'Tile': '_create_tile',
        'Gather': '_create_gather',
        'Reshape': '_create_reshape',
        'Slice': '_create_slice',
        'Clip': '_create_clip',
        'BatchNormalization': '_create_batch_norm',
        'Conv': '_create_conv',
        'MaxPool': '_create_max_avg_pool',
        'AveragePool': '_create_max_avg_pool',
        'Expand': '_create_expand',
        'Pad': '_create_pad',
        'Upsample': '_create_upsample',
        'DepthToSpace': '_create_depth_space',
        'SpaceToDepth': '_create_depth_space',
        'ScatterElements': '_create_scatter_elements',
        'Where': '_create_where',
    }

    @classmethod
    def _create_depth_space(cls, onnx_node, operator, opset_version=_opset_version):
        """
        get the DepthToSpace and SpaceToDepth operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        blocksize = onnx_node.getattr("blocksize")
        mode = utils.force_unicode(onnx_node.getattr("mode", "DCR"))
        return operator(blocksize, mode)

    @classmethod
    def _create_where(cls, onnx_node, operator, opset_version=_opset_version):
        """
        get the Where operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        onnx_node.set_attr_inputs(onnx_node.inputs[0], 'condition')
        return operator(None)

    @classmethod
    def _create_pad(cls, onnx_node, operator, opset_version=_opset_version):
        """
        get the Pad operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        mode = utils.force_unicode(onnx_node.getattr("mode", "constant"))
        onnx_node.set_attr_inputs(onnx_node.inputs[1], 'pads')
        if len(onnx_node.inputs) == 3:
            onnx_node.set_attr_inputs(onnx_node.inputs[2], 'constant')
        return operator(mode, None, None)

    @classmethod
    def _create_upsample(cls,
                         onnx_node,
                         operator,
                         opset_version=_opset_version):
        """
        get the UpSample operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        mode = utils.force_unicode(onnx_node.getattr("mode", None))
        onnx_node.set_attr_inputs(onnx_node.inputs[1], 'scales')
        return operator(mode, None)

    @classmethod
    def _create_expand(cls, onnx_node, operator, opset_version=_opset_version):
        """
        get the Expand operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        onnx_node.set_attr_inputs(onnx_node.inputs[1], 'shape')
        return operator(None)

    @classmethod
    def _create_cast(cls, onnx_node, operator, opset_version=_opset_version):
        """
        get the Cast operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        to_type = onnx_type_to_singa_type(onnx_node.getattr("to"))
        assert to_type != None, "not support cast type: {}".format(to_type)
        if to_type == np.dtype('float32'):
            return operator(tensor.float32)
        else:
            return operator(tensor.int32)

    @classmethod
    def _create_split(cls, onnx_node, operator, opset_version=_opset_version):
        """
        get the Split operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        axis = onnx_node.getattr("axis", 0)
        split = onnx_node.getattr("split", None)
        num_output = len(onnx_node.outputs)
        return operator(axis, split, num_output)

    @classmethod
    def _create_squeeze_unsqueeze(cls,
                                  onnx_node,
                                  operator,
                                  opset_version=_opset_version):
        """
        get the Squeeze and Unsqueeze operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        axes = onnx_node.getattr("axes")
        return operator(axes)

    @classmethod
    def _create_global_average_pool(cls,
                                    onnx_node,
                                    operator,
                                    opset_version=_opset_version):
        """
        get the GlobalAveragePool operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        data_format = onnx_node.getattr("data_format", 'channels_first')
        return operator(data_format)

    @classmethod
    def _create_leakyrelu(cls,
                          onnx_node,
                          operator,
                          opset_version=_opset_version):
        """
        get the LeakyRelu operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        alpha = onnx_node.getattr("alpha", 0.01)
        return operator(alpha)

    @classmethod
    def _create_reduce_ops(cls,
                           onnx_node,
                           operator,
                           opset_version=_opset_version):
        """
        get the ReduceSum, ReduceMean, ReduceMax, ReduceMin, etc, operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        axes = onnx_node.getattr("axes", None)
        keepdims = onnx_node.getattr("keepdims", 1)
        return operator(axes, keepdims)

    @classmethod
    def _create_dropout(cls, onnx_node, operator, opset_version=_opset_version):
        """
        get the Dropout operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        seed = onnx_node.getattr("seed", 0)
        ratio = onnx_node.getattr("ratio", 0)
        return operator(seed, ratio)

    @classmethod
    def _create_constant_of_shape(cls,
                                  onnx_node,
                                  operator,
                                  opset_version=_opset_version):
        """
        get the ConstantOfShape operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        value = onnx_node.getattr("value", 0)
        if isinstance(value, onnx.TensorProto):
            value = numpy_helper.to_array(value)[0].item()
        return operator(value)

    @classmethod
    def _create_transpose(cls,
                          onnx_node,
                          operator,
                          opset_version=_opset_version):
        """
        get the Transpose operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        perm = onnx_node.getattr("perm")
        return operator(perm)

    @classmethod
    def _create_hardsigmoid(cls,
                            onnx_node,
                            operator,
                            opset_version=_opset_version):
        """
        get the hardsigmoid operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        alpha = onnx_node.getattr("alpha", 0.2)
        beta = onnx_node.getattr("beta", 0.5)
        return operator(alpha, beta)

    @classmethod
    def _create_elu(cls, onnx_node, operator, opset_version=_opset_version):
        """
        get the elu operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        alpha = onnx_node.getattr("alpha", 1.)
        return operator(alpha)

    @classmethod
    def _create_selu(cls, onnx_node, operator, opset_version=_opset_version):
        """
        get the selu operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        alpha = onnx_node.getattr("alpha", 1.67326)
        gamma = onnx_node.getattr("gamma", 1.0507)
        return operator(alpha, gamma)

    @classmethod
    def _create_concat(cls, onnx_node, operator, opset_version=_opset_version):
        """
        get the concat operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        factor = onnx_node.getattr('axis')
        return operator(axis=factor)

    @classmethod
    def _create_softmax(cls, onnx_node, operator, opset_version=_opset_version):
        """
        get the softmax operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        factor = onnx_node.getattr('axis', 1)
        return operator(axis=factor)

    @classmethod
    def _create_gemm(cls, onnx_node, operator, opset_version=_opset_version):
        """
        get the gemm operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        alpha = onnx_node.getattr('alpha', 1.)
        beta = onnx_node.getattr('beta', 1.)
        transA = onnx_node.getattr('transA', 0)
        transB = onnx_node.getattr('transB', 0)
        onnx_node.set_weight_inputs(onnx_node.inputs[1], 'W')
        bias = False
        if len(onnx_node.inputs) == 3:
            onnx_node.set_weight_inputs(onnx_node.inputs[2], 'b')
            bias = True
        return operator(None,
                        alpha=alpha,
                        beta=beta,
                        transA=transA,
                        transB=transB,
                        bias=bias)

    @classmethod
    def _create_flatten(cls, onnx_node, operator, opset_version=_opset_version):
        """
        get the flatten operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        factor = onnx_node.getattr('axis', 1)
        return operator(axis=factor)

    @classmethod
    def _create_onehot(cls, onnx_node, operator, opset_version=_opset_version):
        """
        get the OneHot operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        axis = onnx_node.getattr("axis", -1)
        onnx_node.set_attr_inputs(onnx_node.inputs[1], 'depth')
        onnx_node.set_attr_inputs(onnx_node.inputs[2], 'values')
        return operator(axis, None, None)

    @classmethod
    def _create_tile(cls, onnx_node, operator, opset_version=_opset_version):
        """
        get the Tile operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        onnx_node.set_attr_inputs(onnx_node.inputs[1], 'repeats')
        return operator(None)

    @classmethod
    def _create_gather(cls, onnx_node, operator, opset_version=_opset_version):
        """
        get the Gather operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        axis = onnx_node.getattr("axis", 0)
        onnx_node.set_attr_inputs(onnx_node.inputs[1], 'indices')
        return operator(axis, None)

    @classmethod
    def _create_reshape(cls, onnx_node, operator, opset_version=_opset_version):
        """
        get the reshape operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        onnx_node.set_attr_inputs(onnx_node.inputs[1], 'shape')
        return operator(None)

    @classmethod
    def _create_slice(cls, onnx_node, operator, opset_version=_opset_version):
        """
        get the Slice operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        onnx_node.set_attr_inputs(onnx_node.inputs[1], 'starts')
        onnx_node.set_attr_inputs(onnx_node.inputs[2], 'ends')
        if len(onnx_node.inputs) >= 4 and onnx_node.inputs[3] != '':
            onnx_node.set_attr_inputs(onnx_node.inputs[3], 'axes')
        if len(onnx_node.inputs) == 5 and onnx_node.inputs[4] != '':
            onnx_node.set_attr_inputs(onnx_node.inputs[4], 'steps')
        return operator(None, None, None, None)

    @classmethod
    def _create_clip(cls, onnx_node, operator, opset_version=_opset_version):
        """
        get the clip operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        if len(onnx_node.inputs) >= 2 and onnx_node.inputs[1] != '':
            onnx_node.set_attr_inputs(onnx_node.inputs[1], 'min')
        if len(onnx_node.inputs) == 3 and onnx_node.inputs[2] != '':
            onnx_node.set_attr_inputs(onnx_node.inputs[2], 'max')
        return operator(None, None)

    @classmethod
    def _create_batch_norm(cls,
                           onnx_node,
                           operator,
                           opset_version=_opset_version):
        """
        get the clip operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        factor = onnx_node.getattr('momentum', 0.9)
        onnx_node.set_weight_inputs(onnx_node.inputs[1], 'scale')
        onnx_node.set_weight_inputs(onnx_node.inputs[2], 'bias')
        onnx_node.set_weight_inputs(onnx_node.inputs[3], 'running_mean')
        onnx_node.set_weight_inputs(onnx_node.inputs[4], 'running_var')
        return operator(factor)

    @classmethod
    def _create_conv(cls, onnx_node, operator, opset_version=_opset_version):
        """
        get the clip operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        kernel_size = tuple(onnx_node.getattr('kernel_shape'))
        padding = tuple(onnx_node.getattr('pads', (0, 0)))
        stride = tuple(onnx_node.getattr('strides', (1, 1)))
        auto_pad = utils.force_unicode(onnx_node.getattr('auto_pad', 'NOTSET'))

        # 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_size) > 2:
            raise ValueError("Only implemented for 1d or 2d")

        onnx_node.set_weight_inputs(onnx_node.inputs[1], 'W')
        bias = False
        if len(onnx_node.inputs) == 3:
            onnx_node.set_weight_inputs(onnx_node.inputs[2], 'b')
            bias = True
        return operator(None,
                        kernel_size,
                        stride=stride,
                        padding=padding,
                        dilation=dilation,
                        group=group,
                        bias=bias,
                        pad_mode=auto_pad)

    @classmethod
    def _create_max_avg_pool(cls,
                             onnx_node,
                             operator,
                             opset_version=_opset_version):
        """
        get the clip operator from onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        kernel_size = tuple(onnx_node.getattr('kernel_shape'))
        padding = tuple(onnx_node.getattr('pads', (0, 0)))
        stride = tuple(onnx_node.getattr('strides', (1, 1)))
        auto_pad = utils.force_unicode(onnx_node.getattr('auto_pad', 'NOTSET'))

        # not support count_include_pad and auto_pad
        ceil_mode = onnx_node.getattr('ceil_mode', 0)
        count_include_pad = onnx_node.getattr('count_include_pad', 0)
        if ceil_mode != 0 or count_include_pad != 0:
            raise ValueError(
                "Not implemented yet for count_include_pad or ceil_mode")

        # only support 1d or 2d
        if len(kernel_size) > 2:
            raise ValueError("Only implemented for 1d or 2d")

        is_max = onnx_node.op_type == 'MaxPool'
        return operator(kernel_size, stride, padding, is_max, auto_pad)

    @classmethod
    def _create_scatter_elements(cls,
                                 onnx_node,
                                 operator,
                                 opset_version=_opset_version):
        """
        get the ScatterElements from the onnx node
        Args:
            onnx_node(OnnxNode): a given onnx node
            operator (Operator Class): a singa operator class
            opset_version(int): the opset version
        Returns: 
            singa operator instance      
        """
        axis = onnx_node.getattr("axis", 0)
        onnx_node.set_attr_inputs(onnx_node.inputs[1], 'indices')
        onnx_node.set_attr_inputs(onnx_node.inputs[2], 'updates')
        return operator(None, None, axis)

    @classmethod
    def _onnx_constant_to_np(cls, onnx_node, opset_version=_opset_version):
        """
        parse onnx constatn node to numpy array
        Args:
            onnx_node (OnnxNode): a given onnx node
            opset_version (int): the opset version
        Returns: 
            a numpy ndarray
        """
        onnx_tensor = onnx_node.getattr('value')
        np_dtype = mapping.TENSOR_TYPE_TO_NP_TYPE[onnx_tensor.data_type]
        return np.frombuffer(onnx_tensor.raw_data, dtype=np_dtype)

    @classmethod
    def _onnx_node_to_singa_op(cls, onnx_node, opset_version=_opset_version):
        """
        get singa operator from a onnx node
        Args:
            onnx_node (OnnxNode): a given onnx node
            opset_version (int): the opset version
        Returns: 
            singa operator instance
        """
        onnx_op_type = onnx_node.op_type
        assert onnx_op_type in cls._rename_operators, "not support operator: {}".format(
            onnx_op_type)
        renamed_op = cls._rename_operators[onnx_op_type]
        if renamed_op.startswith('layer.'):
            op_class = getattr(layer, renamed_op[6:])
        else:
            op_class = getattr(autograd, renamed_op)
        if onnx_node.op_type in cls._special_operators:
            translator = getattr(cls, cls._special_operators[onnx_node.op_type])
            op = translator(onnx_node, op_class, opset_version)
            op.name = onnx_node.name
        else:
            op = op_class()
        # refine the ONNXNode
        onnx_node.inputs = [inp for inp in onnx_node.inputs if inp != '']
        return op

    @classmethod
    def run_node(cls, node, inputs, device='CPU', opset_version=_opset_version):
        """
        run a single singa operator from a onnx node
        Args:
            node (NodeProto): a given onnx node
            inputs (ndarray[]): a list of numpy ndarray
            device (string): CPU or CUDA
            opset_version (int): the opset version
        Returns:
            list, the output
        """
        node = OnnxNode(node)
        valid_inputs = [x for x in node.inputs if x != ""]
        assert len(valid_inputs) == len(
            inputs), "{}: expected {} inputs, but got {}. ".format(
                node.op_type, len(valid_inputs), len(inputs))

        operator = cls._onnx_node_to_singa_op(node, opset_version)
        # separate weights with inputs, and init inputs as Tensor
        weights = {}
        _inputs = []
        for (key, val) in zip(valid_inputs, inputs):
            val = val.astype(onnx_type_to_singa_type(val.dtype))
            if key in node.weight_inputs:
                weights[key] = val
            else:
                x = tensor.from_numpy(val)
                if device != 'CPU':
                    assert singa.USE_CUDA, "Your SINGA doesn't compile GPU module."
                    dev = device.create_cuda_gpu()
                else:
                    dev = device.get_default_device()
                x.to_device(dev)
                _inputs.append(x)
        inputs = _inputs
        # set params
        params = {}
        for key, name in node.weight_inputs.items():
            params[name] = weights[key]
        operator.set_params(params)
        outputs = cls._run_node(operator, inputs)
        outputs_dict = OrderedDict()
        for (key, val) in zip(node.outputs, outputs):
            outputs_dict[key] = val
        return outputs_dict

    @classmethod
    def _run_node(cls, operator, inputs):
        """
        run a single singa operator from singa operator
        Args:
            operator (Operator): the Operator instance
            inputs (Tensor[]): a list of SINGA Tensor
        Returns:
            list, the output
        """
        outputs = operator(*inputs)
        if not isinstance(outputs, collections.Iterable):
            outputs = [outputs]
        return outputs

    @classmethod
    def _parse_graph_params(cls, graph, device):
        """
        parse the parameters from onnx graph
        Args:
            graph (Graph): a given onnx graph
            device (string): CPU or CUDA
        Returns:
            a dict of numpy ndarray
        """
        params = {}
        for tp in graph.initializer:
            val = numpy_helper.to_array(tp)
            val = val.astype(onnx_type_to_singa_type(tp.data_type))
            params[tp.name] = val
        return params

    @classmethod
    def _parse_graph_inputs_outputs(cls, graph, params, device):
        """
        parse the inits, outputs from onnx graph
        Args:
            graph (Graph): a given onnx graph
            device (string): # CPU or CUDA
        Returns:
            a dict of ValueInfo
            a dict of ValueInfo
        """
        inputs = []
        outputs = []
        info_tuple = namedtuple('info_tuple', ['name', 'dtype', 'shape'])
        for t in graph.input:
            if t.name not in params:
                dtype = t.type.tensor_type.elem_type
                shape = [dim.dim_value for dim in t.type.tensor_type.shape.dim]
                inputs.extend([info_tuple(t.name, dtype, shape)])
        for t in graph.output:
            dtype = t.type.tensor_type.elem_type
            shape = [dim.dim_value for dim in t.type.tensor_type.shape.dim]
            outputs.extend([info_tuple(t.name, dtype, shape)])
        return inputs, outputs

    @classmethod
    def _onnx_model_to_singa_ops(cls,
                                 graph,
                                 device,
                                 opset_version=_opset_version):
        """
        get all intermediate params, operators, and input info from onnx model
        Args:
            graph (Graph): the loaded ONNX graph
            device (string): CPU or CUDA
            opset_version (int): the opset version
        Returns:
            a dict of weights
            a dict of ValueInfo
            a dict of ValueInfo
            a list of SingaOps('node', 'forward')
        """
        # init all tensor input and params as a tensor map
        params = cls._parse_graph_params(graph, device)
        inputs, outputs = cls._parse_graph_inputs_outputs(graph, params, device)
        # the parsed operators queue
        operators = []
        operator_tuple = namedtuple('operator_tuple', ['node', 'operator'])
        for node in graph.node:
            if not node.name:
                node.name = "%s_%d" % (str(node.op_type), len(operators))
            node = OnnxNode(node)
            # convert Constant to param
            if node.op_type == 'Constant':
                params[node.outputs[0]] = cls._onnx_constant_to_np(node)
            else:
                op = cls._onnx_node_to_singa_op(node, opset_version)
                operators.append(operator_tuple(node, op))
        return params, inputs, outputs, operators

    @classmethod
    def prepare(cls, model, device='CPU', **kwargs):
        """
        parse the ONNX and to create layers
        Args:
            model (ModelProto): the loaded ONNX model
            device (string): CPU or CUDA
        Returns:
            a SingaRep instance to stores the layers and weights
        """
        super(SingaBackend, cls).prepare(model, device, **kwargs)
        # optimize and infer the shape of the model
        try:
            model = onnx.utils.polish_model(model)
        except IndexError as err:
            model = shape_inference.infer_shapes(model)

        # check the opset version and ir version
        # SINGA supports opset version(11), ir version(1.6.0 -> 6)
        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._opset_version:
                    warnings.warn(
                        "The imported opertor set verion {} is larger than the supported version {}."
                        .format(imp.version, cls._opset_version))
            else:
                warnings.warn("Unrecognized operator set {}".format(imp.domain))

        if model.ir_version > cls._ir_version:
            warnings.warn(
                "The imported ir verion {} is larger than the supported version {}."
                .format(cls._ir_version, imp.version))

        graph = model.graph
        params, inputs, outputs, layers = cls._onnx_model_to_singa_ops(
            graph, device, opset_version)
        return SingaRep(params, inputs, outputs, layers, device)


class SingaRep(BackendRep):

    def __init__(self, params, inputs, outputs, layers, device):
        """
        https://github.com/onnx/onnx/blob/master/docs/ImplementingAnOnnxBackend.md
        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:
            params (dict{}): a dict of params, data type is numpy ndarray
            inputs (ValueInfo): a dict of inputs
            outputs (ValueInfo): a dict of outputs
            layers (namedtuple('operator_tuple', ['node', 'operator'])[]): a list of singa operator
            device (string): CPU or CUDA
        """
        super(SingaRep, self).__init__()
        self.inputs = inputs
        self.states = params
        self.outputs = outputs
        self.dev = cpu_dev if device == "CPU" else gpu_dev
        self.layers = layers
        self.tensor_count = {}
        self.has_initialized = False
        self.is_graph = False

    def initialize(self):
        """
        Init the instance
        """
        self.outputs_info = {outp.name: outp for outp in self.outputs}
        _layers = []  # layers by topo order
        for node, operator in self.layers:
            _del_keys = []
            for key, name in node.weight_inputs.items():
                if key not in self.states:
                    # cannot find the weights, try to find it from input
                    node.set_attr_inputs(key, name)
                    _del_keys.append(key)
            for key in _del_keys:
                node.del_weight_inputs(key)
            self.__dict__[node.name] = operator
            _layers.append(node)
        self._layers = _layers

    def init_tensor_count(self):
        """
        Init the tensor count dict
        """
        self.tensor_count = {}
        for node, operator in self.layers:
            # init the tensor count
            all_possible_inputs = node.inputs + list(
                node.attr_inputs.keys()) + list(node.weight_inputs.keys())
            for inp in all_possible_inputs:
                if inp not in self.tensor_count:
                    self.tensor_count[inp] = 1
                else:
                    self.tensor_count[inp] += 1

    def to_input_tensor(self, x):
        """
        convert the input to tensors
        Args:
            x (np.ndarray[]): a list of numpy ndarray as inputs
        Returns: 
            a dict of SINGA Tensors
        """
        tensor_dict = {}
        # init inputs as Tensor
        for (key, val) in zip(self.inputs, x):
            if not self.is_graph:
                val = val.astype(onnx_type_to_singa_type(key.dtype))
                # todo, scalar
                val = np.atleast_1d(val)
                val = tensor.from_numpy(val)
                val.to_device(self.dev)
            tensor_dict[key.name] = val
        return tensor_dict

    def to_output_tensor(self, y, out_name):
        """
        convert the tensors to input
        Args:
            x (np.ndarray[]): a list of numpy ndarray as inputs
        Returns: 
            a dict of SINGA Tensors
        """
        if not self.is_graph:
            y = tensor.to_numpy(y)
            if out_name in self.outputs_info:
                np_dtyp = mapping.TENSOR_TYPE_TO_NP_TYPE[
                    self.outputs_info[out_name].dtype]
                y = y.astype(np_dtyp)
        return y

    def get_s(self, name, node, tensor_dict):
        """
        get state from the node's weights or tensor_dict
        Args:
            name (str): name of the state
            node (ONNXNode): ONNX node
            tensor_dict ({}): tensor dict
        Returns: 
            the states
        """
        if name in node.attr_inputs:
            return tensor_dict[name]
        else:
            return self.states[name]

    def handle_special_ops(self, node, op, tensor_dict):
        """
        hanlde some special operations
        Args:
            name (str): name of the state
            node (ONNXNode): ONNX node
            tensor_dict ({}): tensor dict
        Returns: 
            the states
        """
        # todo, hard code
        # Conv2d nb_kernels
        if node.op_type == "Conv":
            shape = self.get_s(node.inputs[1], node, tensor_dict).shape
            op.nb_kernels = shape[0]
        # Gemm nb_kernels and bias_shape
        elif node.op_type == "Gemm":
            nb_kernels_flag = 0 if op.transB == 1 else -1
            shape = self.get_s(node.inputs[1], node, tensor_dict).shape
            op.nb_kernels = shape[nb_kernels_flag]
            if op.bias:
                shape = self.get_s(node.inputs[2], node, tensor_dict).shape
                op.bias_shape = shape

    def run(self, x, **kwargs):
        """
        run the forward of singa model
        Args:
            x (np.ndarray[]): a list of numpy ndarray as inputs
        Returns: 
            a list of outputs
        """
        if not self.has_initialized:
            self.initialize()
            if isinstance(x[0], tensor.Tensor):
                self.dev = x[0].device

        outputs_dict = OrderedDict([])

        # last_layers means we run this model until the last #N layers
        last_layers = kwargs.get('last_layers', len(self._layers) - 1)
        last_layers = last_layers if last_layers >= 0 else (
            last_layers + 1) % len(self._layers)
        if last_layers != len(self._layers) - 1:
            for outp in self._layers[last_layers].outputs:
                outputs_dict[outp] = None
        else:
            for outp in self.outputs:
                outputs_dict[outp.name] = None

        aux_output = kwargs.get('aux_output', ())
        for outp in aux_output:
            outputs_dict[outp] = None

        tensor_dict = self.to_input_tensor(x)
        self.init_tensor_count()

        # run the layer by the topo order
        for node in self._layers[:last_layers + 1]:
            op = self.__dict__[node.name]
            self.handle_special_ops(node, op, tensor_dict)
            # make input
            inputs = []
            for inp in node.inputs:
                if inp not in node.weight_inputs and inp not in node.attr_inputs:
                    if inp in tensor_dict:
                        inputs.append(tensor_dict[inp])
                    elif inp in self.states:
                        # todo, scalar
                        val = np.atleast_1d(self.states[inp])
                        val = tensor.from_numpy(val)
                        val.to_device(self.dev)
                        inputs.append(val)
                    else:
                        raise KeyError(
                            "Not found the input {} for operation {}".format(
                                inp, node.name))
            states = {}
            if callable(getattr(op, "initialize",
                                None)) and not op._initialized:
                # init the operator
                op.initialize(*inputs)
                op._initialized = True
                for key, name in node.weight_inputs.items():
                    if key not in node.attr_inputs:
                        # find the weights and not in the inputs
                        states[name] = self.states[key]

            # replace attrs by inputs
            for key, name in node.attr_inputs.items():
                if key in tensor_dict:
                    ts = tensor_dict[key]
                elif key in self.states:
                    ts = self.states[key]
                if isinstance(ts, tensor.Tensor):
                    ts = tensor.to_numpy(ts)
                states[name] = ts
            # set states
            if states:
                if callable(getattr(op, "set_states", None)):
                    # rename the layer's states
                    states = {
                        getattr(op, key).name: val
                        for (key, val) in states.items()
                    }
                    if self.is_graph and not self.has_initialized:
                        prev_state = self.dev.graph_enabled()
                        self.dev.EnableGraph(False)
                        op.set_states(states)
                        self.dev.EnableGraph(prev_state)
                    else:
                        op.set_states(states)
                else:
                    for key, value in states.items():
                        setattr(op, key, value)
            # run the node
            outputs = _run_node(op, inputs)
            # release the input tensor
            for inp in node.inputs:
                if inp in self.tensor_count:
                    self.tensor_count[inp] -= 1
                if self.tensor_count[inp] == 0:
                    if inp in tensor_dict:
                        del tensor_dict[inp]
                    del self.tensor_count[inp]
            # store the output
            for (outp, val) in zip(node.outputs, outputs):
                tensor_dict[outp] = val
                if outp in outputs_dict:
                    outputs_dict[outp] = self.to_output_tensor(val, outp)
        self.has_initialized = True
        return list(outputs_dict.values())


class SONNXModel(model.Model):

    def __init__(self, onnx_model):
        """
        Init a SIGNA Model
        Args:
            onnx_model (ModelProto): a loaded onnx model
        """
        super(SONNXModel, self).__init__()
        self.sg_ir = prepare(onnx_model)
        for node, operator in self.sg_ir.layers:
            self.__dict__[node.name] = operator
        self.sg_ir.is_graph = True

    def forward(self, *input, aux_output=(), **kwargs):
        """
        The forward of the SINGA model
        Args:
            input (Tensors[]): a list of Tensor
            aux_output (string()): a set of required output name

        Returns:
            a OrderedDict of Tensor
        """
        return self.sg_ir.run(input, aux_output=aux_output, **kwargs)


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