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

from collections import Counter, deque
import numpy as np

from singa import tensor
from singa import utils
from .tensor import Tensor
from . import singa_wrap as singa

CTensor = singa.Tensor
training = False


def axis_helper(y_shape, x_shape):
    """
    check which axes the x has been broadcasted
    Args:
        y_shape: the shape of result
        x_shape: the shape of x
    Return:
        a tuple refering the axes
    """
    res = []
    j = len(x_shape) - 1
    for i in range(len(y_shape) - 1, -1, -1):
        if j < 0 or x_shape[j] != y_shape[i]:
            res.append(i)
        j -= 1
    return tuple(res[::-1])


def back_broadcast(y_shape, x_shape, x):
    """
    for a brodcasted tensor, restore its shape of x from y_shape to x_shape
    Args:
        y_shape: the shape of result
        x_shape: the shape of x
        x: the input
    Return:
        a tensor
    """
    if y_shape != x_shape:
        x = tensor.from_raw_tensor(x)
        axis = axis_helper(y_shape, x_shape)
        x = tensor.sum(x, axis)
        x = tensor.reshape(x, x_shape)
        x = x.data
    return x


def infer_dependency(op):
    """
    Infer the dependency of all operations with the
    given op as the last operation.
    Operator A is depending on B if A uses the output(s) of B.

    Args:
        op: an Operator instance, e.g. the loss operation.

    Return:
        a Counter instance with the operation as the key,
        and the number of operations that are depending on it as the value;
        and a Counter instance with the id of the output tensor as the key, and
        the number of operations that are depending on it as the value.
    """

    # current op is not inserted into the dependency_count
    # if the current op is not a terminal op, then this function may just
    # count dependency of a branch.
    op_count = Counter()
    tensor_count = Counter()
    queue = deque([op])
    while len(queue) > 0:
        cur_op = queue.pop()
        for src_op, xid, _, _ in cur_op.src:
            if src_op not in op_count:
                op_count[src_op] = 1
                queue.append(src_op)
            else:
                op_count[src_op] += 1
            tensor_count[xid] += 1
    return op_count, tensor_count


def gradients(y, dy=None):
    """
    Compute the gradients of the output w.r.t the parameters

    Args:
        y: the output tensor, e.g., the loss
        dy: gradient of the target w.r.t y; None indicates the gradient is 1.0;
            it can be used to rescale the loss.

    Return:
        a dictionary storing the gradient tensors of all tensors
            whose stores_grad is true (e.g. parameter tensors)
    """
    grads = {}  # mapping: x->dx if x.stores_grad
    for p, dp in backward(y, dy):
        # TODO: this fn is only helper for test case for now.
        #   1. could implement __hash__ or
        #   2. make grad as a attribute of tensor class
        #      p.grad = dp
        grads[id(p)] = dp
    return grads


def backward(y, dy=None):
    """
    Run the backward propagation starting at y.
    Args:
        y: a Tensor instance, usually the loss
        dy: a number or a Tensor instance, for the gradient of the
            objective/loss w.r.t y, usually None, i.e., 1.0
    Return:
        yeild the parameter (tensor with stores_grad true) and the
            gradient tensors.
    """
    assert isinstance(y, Tensor), "wrong input type."
    op_dep, tensor_dep = infer_dependency(y.creator)
    assert y.size() == 1, ("y must be a Tensor with a single value;"
                           "size of y is % d" % y.size())

    # by default the dy is a tensor with 1.0 for each sample;
    if dy is None:
        dy = float(1.0)
    elif isinstance(dy, Tensor):
        dy = dy.data
    else:
        dy = float(dy)

    # ready is a queue of (operation, dy list)
    ready = deque([(y.creator, (dy,))])
    not_ready = {}  # mapping: op->[dy]

    if y.stores_grad:
        # gradients[y] = dy
        if isinstance(dy, float):
            g = np.array(dy)
        else:
            g = dy
        tg = Tensor(device=g.device(), data=g)
        yield (y, tg)

    while len(ready) > 0:
        op, dys = ready.pop()
        if not op.requires_grad or isinstance(op, Dummy):
            continue
        # if not isinstance(op, tensor.Dummy):
        dxs = op._do_backward(*dys)
        # TODO src and dx must match

        assert len(op.src) == len(dxs), (
            "the number of src ops (=%d) and dx (=%d) not match" %
            (len(op.src), len(dxs)))
        for (src_op, x_id, y, y_stores_grad), dx in zip(op.src, dxs):
            # prefix x is w.r.t op; prefix y is w.r.t src_op.
            # x_id is the python id of one input arg of src_op, denoted as x.
            # y_idx (below) is the index of x among the outputs of src_op.
            # not_ready[src_op][y_idx] records the intermediate gradient
            # of the y_idx'th output of src_op. 'intermediate gradient'
            # indicates that if this output is used in multiple children
            # operations, then we have to add the graident (dx) from all these
            # children operations. When src_op is ready, it means that
            # the gradient of all its outputs are available, i.e. all children
            # operations have been backwarded.
            # y is None if y.stores_grad is false; otherwise it is a Tensor

            if isinstance(src_op, Dummy) and (not src_op.stores_grad):
                continue

            y_idx = src_op.y_id2idx[x_id]
            if src_op not in not_ready:
                # src_op may have mulitple outputs
                not_ready[src_op] = [None for _ in src_op.y_id2idx]
                not_ready[src_op][y_idx] = dx
            else:
                dxs_ = not_ready[src_op]
                if dxs_[y_idx] is None:
                    dxs_[y_idx] = dx
                else:
                    # add the gradient from another children operation that
                    # uses y_idx'th output of src_op as input arg
                    dxs_[y_idx] += dx

            op_dep[src_op] -= 1
            tensor_dep[x_id] -= 1
            if y_stores_grad and tensor_dep[x_id] == 0:
                # store the gradient for final return, e.g. for parameters.
                # it may cause a delay to yield. Only after src_op's all
                # output tensors have received the gradients, then output
                g = not_ready[src_op][y_idx]
                tg = Tensor(device=g.device(),
                            data=g,
                            name=src_op.grad_name(y_idx))
                yield (y, tg)

            if op_dep[src_op] == 0:
                if src_op.requires_grad is True:
                    assert not isinstance(
                        src_op, Dummy), "Dummy op does not do backward()"
                    ready.append((src_op, not_ready[src_op]))
                del not_ready[src_op]
        del op  # delete the operation to free all tensors from this op


class Operator(object):
    """
    An operation includes the forward and backward function of
    tensor calculation.
    Steps to add a specific operation Xxxx:
    1. create a subclass of Operator, name it as Xxxx
    2. override the forward() and backward(); The arguments of forward()
       and backward() should only include CTensor;
    """

    op_count = 0

    def __init__(self, name=None):
        if name is None:
            self.name = "{}#{}".format(self.__class__.__name__,
                                       Operator.op_count)
            Operator.op_count += 1
        else:
            self.name = name

    def __call__(self, *xs):
        return self._do_forward(*xs)

    def output_name(self, idx):
        """
        Args:
            idx: index of the output among all outputs

        Return:
            the name of the output tensor
        """
        return "{}:{}".format(self.name, idx)

    def grad_name(self, idx):
        """
        Args:
            idx: index of the output among all outputs

        Return:
            the name of the gradient of the output tensor
        """
        return "{}_g".format(self.output_name(idx))

    def _do_forward(self, *xs):
        """
        Do not call this function from user code. It is called by __call__().
        Args:
            xs, Tensor instance(s)
        Returns:
            Tensor instance(s)
        """
        # TODO add the pre hook
        assert all([isinstance(x, Tensor) for x in xs
                   ]), "xs should include only Tensor instances"

        # need to do backward if any of its input arg needs gradient
        self.requires_grad = any([x.requires_grad for x in xs])

        self.src = []
        for x in xs:
            if x.stores_grad:
                # store the tensor whose gradient needs be returned in
                # backward(), e.g. if x is parameter
                self.src.append((x.creator, id(x), x, x.stores_grad))
            else:
                # for intermediate tensors, they will be released soon;
                # no need to store them --> use None
                self.src.append((x.creator, id(x), None, x.stores_grad))

        # get the CTensor (data) if the input arg is Tensor
        xs = tuple(x.data for x in xs)
        ys = self.forward(*xs)
        if not isinstance(ys, tuple):
            ys = (ys,)
        # create Tensor based on CTensor(data);
        # assume outputs are all Tensor instances
        ys = tuple(
            Tensor(
                device=y.device(),
                data=y,
                requires_grad=self.requires_grad,
                creator=self,
                name=self.output_name(idx),
            ) for idx, y in enumerate(ys))
        # map from python id to output index
        self.y_id2idx = {id(y): i for i, y in enumerate(ys)}
        # TODO add the post hook
        return ys

    def _do_backward(self, *dys):
        dxs = self.backward(*dys)
        if not isinstance(dxs, tuple):
            dxs = (dxs,)
        return dxs

    def forward(self, *xs):
        """Forward propagation.
        Args:
            xs: input args consisting of only CTensors.
        Returns:
            CTensor instance(s)
        """
        raise NotImplementedError

    def backward(self, *dys):
        """ Backward propagation.
        Args:
            dys: input args consisting of only CTensors.
        Returns:
            CTensor instance(s)
        """
        raise NotImplementedError

    def get_params(self):
        return []


class Dummy(Operator):
    """Dummy operation whice serves as a placehoder for autograd
    Args:
        name(string): set it for debug
    """

    def __init__(self, tensor, name=None):
        super(Dummy, self).__init__(name)
        self.src = []
        self.y_id2idx = {id(tensor): 0}
        self.tensor = tensor
        self.requires_grad = False

    def output_name(self, idx):
        return self.name

    def grad_name(self, idx):
        return "{}_g".format(self.name)

    def __getattr__(self, name):
        return self.tensor.__getattribute__(name)


class Mean(Operator):
    """
    Element-wise mean of each of the input CTensors.
    """

    def __init__(self):
        super(Mean, self).__init__()

    def forward(self, *l):
        """
        Args:
            l (a list of CTensor): a list of CTensor for element-wise mean.
        Returns:
            a new CTensor.
        """
        if training:
            self.l = len(l)
        assert (len(l) > 0)
        x = singa.Tensor(list(l[0].shape()), l[0].device())
        x.SetFloatValue(0.0)
        for i in range(len(l)):
            x += l[i]
        return singa.MultFloat(x, 1 / len(l))

    def backward(self, dy):
        """
        Args:
            dy (CTensor): dL / dy.
        Returns:
            a list of dx (CTensor).
        """
        return [singa.MultFloat(dy, 1 / self.l)] * self.l


def mean(*l):
    """
    Element-wise mean of each of the input tensors.
    Args:
        l (a list of Tensor): element-wise mean operator.
    Returns:
        a new Tensor.
    """
    return Mean()(*l)[0]


class ReLU(Operator):
    """
    Relu means rectified linear function, i.e, y = max(0, x) is applied to the
    CTensor elementwise.
    """

    def __init__(self):
        super(ReLU, self).__init__()

    def forward(self, x):
        """
        Args:
            x (CTensor): input tensor.
        Returns:
            a new CTensor whose element y = x if x >= 0; otherwise 0.
        """
        if training:
            self.input = x
        return singa.ReLU(x)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): dL / dy.
        Returns:
            dx (CTensor): dL / dx = dy if x >= 0; otherwise 0.
        """
        return singa.ReLUBackward(dy, self.input)


def relu(x):
    """
    Relu means rectified linear function, i.e, y = max(0, x) is applied to the
    CTensors elementwise.
    Args:
        x (Tensor): input tensor.
    Returns:
        a new Tensor whose element y = x if x >= 0; otherwise 0.
    """
    return ReLU()(x)[0]


class Less(Operator):
    """
    Returns the tensor resulted from performing the less logical operation
    elementwise on the input CTensors x and y.
    """

    def __init__(self):
        super(Less, self).__init__()

    def forward(self, x, y):
        """
        Return a<b, where a and b are CTensor.
        """
        cur = singa.LTFloat(singa.__sub__(x, y), 0)
        if training:
            self.cache = cur
        return cur

    def backward(self, dy):
        """
        Args:
            dy (CTensor): data for the dL / dy, L is the loss.
        Raises:
            AssertionError: no backward function for this operator.
        """
        assert False, ('no backward function for less')


def less(x, y):
    """
    Return a<b, where a and b are CTensor.
    """
    return Less()(x, y)[0]


class Clip(Operator):
    """
    Clip operator limits the given input within an interval. The interval
    is specified by the inputs 'min' and 'max'.
    """

    def __init__(self, min, max):
        """
        Args:
            min (float): min value, under which element is replaced by min.
            max (float): max value, above which element is replaced by max.
        """
        super(Clip, self).__init__()
        self.max = max
        self.min = min

    def forward(self, x):
        """
        Args:
            x (CTensor): input tensor
        Returns:
            a new CTensor with np.clip(x,min,max)
        """
        self.mask = singa.Tensor(list(x.shape()), x.device())
        self.mask.SetFloatValue(1.0)

        if self.min is not None:
            self.min = float(self.min)
            mask0 = singa.LTFloat(x, self.min)
            mask1 = singa.GEFloat(x, self.min)
            self.mask = singa.__mul__(mask1, self.mask)
            x = singa.__add__(singa.MultFloat(mask0, self.min),
                              singa.__mul__(mask1, x))

        if self.max is not None:
            self.max = float(self.max)
            mask0 = singa.GTFloat(x, self.max)
            mask1 = singa.LEFloat(x, self.max)
            self.mask = singa.__mul__(mask1, self.mask)
            x = singa.__add__(singa.MultFloat(mask0, self.max),
                              singa.__mul__(mask1, x))

        return x

    def backward(self, dy):
        """
        Args:
            dy (CTensor): dL / dy
        Returns:
            dx (CTensor): dL / dx
        """
        return singa.__mul__(dy, self.mask)


def clip(x, min=None, max=None):
    """
    Clip operator limits the given input within an interval. The interval
    is specified by the inputs 'min' and 'max'.
    Args:
        x (Tensor): input tensor
        min (float): Minimum value, under which element is replaced by min.
        max (float): Maximum value, above which element is replaced by max.
    Returns:
        a new Tensor with np.clip(x,min,max).
    """
    return Clip(min, max)(x)[0]


class Identity(Operator):
    """
    Init a identity operator
    """

    def __init__(self):
        super(Identity, self).__init__()

    def forward(self, x):
        """
        Args:
            x (CTensor): input tensor.
        Returns:
            the same CTensor x.
        """
        return x

    def backward(self, dy):
        """
        Args:
            dy (CTensor): dL / dy.
        Returns:
            dx (CTensor): dL / dx.
        """
        return dy


def identity(x):
    """
    Init a identity operator.
    Args:
        x (Tensor): input tensor.
    Returns:
        the same Tensor with x.
    """
    return Identity()(x)[0]


class Matmul(Operator):
    """
    Init matrix multiplication operator.
    """

    def __init__(self):
        super(Matmul, self).__init__()

    def forward(self, x, w):
        """
        Return `np.matmul(x,w)`, where x and w are CTensor.
        """
        # todo, cannot do Mult for dims more than 2
        if training:
            self.input = (x, w)
        res = singa.Mult(x, w)
        return res

    def backward(self, dy):
        """
        Args:
            dy (CTensor): data for the dL / dy, L is the loss.
        Returns:
            a tuple for (dx, dw).
        """
        return (
            singa.Mult(dy, singa.DefaultTranspose(self.input[1])),
            singa.Mult(singa.DefaultTranspose(self.input[0]), dy),
        )


def matmul(x, w):
    """
    Return `np.matmul(x,w)`, where x and w are Tensor.
    """
    return Matmul()(x, w)[0]


class Greater(Operator):
    """
    Returns the tensor resulted from performing the greater logical
    operation elementwise on the input tensors A and B.
    """

    def __init__(self):
        super(Greater, self).__init__()

    def forward(self, x, y):
        """
        Return a>b, where a and b are CTensor.
        """
        cur = singa.GTFloat(singa.__sub__(x, y), 0)
        if training:
            self.cache = cur
        return cur

    def backward(self, dy):
        """
        Args:
            dy (CTensor): data for the dL / dy, L is the loss.
        Raises:
            AssertionError: no backward function for this operator.
        """
        assert False, ('no backward function for greater')


def greater(x, y):
    """
    Return a>b, where a and b are Tensor.
    """
    return Greater()(x, y)[0]


class AddBias(Operator):
    """
    Add Bias to each row / column of the Tensor, depending on the axis arg.
    """

    def __init__(self, axis=0):
        """
        To indicate the calculation axis, 0 for row, 1 for column.
        Args:
            axis (int): 0 or 1, default is 0.
        """
        super(AddBias, self).__init__()
        self.axis = axis

    def forward(self, x, b):
        """
        Args:
            x (CTensor): matrix.
            b (CTensor): bias to be added.
        Return:
            the result Tensor
        """
        if self.axis == 0:
            singa.AddRow(b, x)
        elif self.axis == 1:
            singa.AddColumn(b, x)
        return x

    def backward(self, dy):
        """
        Args:
            dy (CTensor): data for the dL / dy, L is the loss.
        Return:
            a tuple for (db, dx), db is data for dL / db, dx is data
            for dL / dx.
        """
        dtype = dy.data_type()
        _dy = dy.AsType(tensor.float32)
        if self.axis == 0:
            return dy, singa.Sum(_dy, 0).AsType(dtype)
        elif self.axis == 1:
            return dy, singa.Sum(_dy, 0).AsType(dtype)


def add_bias(x, b, axis=0):
    """
    Add Bias to each row / column of the Tensor, depending on the axis arg.
    Args:
        x (Tensor): matrix.
        b (Tensor): bias to be added.
        axis (int): 0 or 1, default is 0.
    Return:
        the result Tensor
    """
    assert x.ndim() == 2, "1st arg required 2d tensor. got shape: %s" % (
        x.shape)
    assert b.ndim() == 1, "2nd arg required 1d tensor. got shape: %s" % (
        b.shape)
    assert axis in [0, 1], "allowed axis: 0 or 1"
    return AddBias(axis)(x, b)[0]


class Reshape(Operator):
    """
    Reshape the input tensor similar to np.reshape.
    """

    def __init__(self, shape):
        """
        Args:
            shape (list of int): Specified shape for output. At most one
                dimension of the new shape can be -1. In this case, the
                value is inferred from the size of the tensor and the
                remaining dimensions. A dimension could also be 0,
                in which case the actual dimension value is unchanged
                (i.e. taken from the input tensor).
        """
        super(Reshape, self).__init__()
        self.shape = shape

    def forward(self, x):
        """
        Args:
            x (CTensor): matrix.
        Return:
            the result CTensor
        """
        self._shape = x.shape()
        shape = list(self.shape)
        # handle the shape with 0
        shape = [
            self._shape[i]
            if i < len(self._shape) and shape[i] == 0 else shape[i]
            for i in range(len(shape))
        ]
        # handle the shape with -1
        hidden_shape = int(np.prod(self._shape) // np.abs(np.prod(shape)))
        self.cache = [int(s) if s != -1 else hidden_shape for s in shape]
        return singa.Reshape(x, self.cache)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): dL / dy
        Returns:
            dx (CTensor): dL / dx
        """
        return singa.Reshape(dy, self._shape)


def reshape(x, shape):
    """
    Reshape the input tensor similar to mp.reshape.
    Args:
        x (Tensor): matrix.
        shape (list of int): Specified shape for output. At most one
            dimension of the new shape can be -1. In this case, the
            value is inferred from the size of the tensor and the
            remaining dimensions. A dimension could also be 0,
            in which case the actual dimension value is unchanged
            (i.e. taken from the input tensor).
    Return:
        the result Tensor
    """
    return Reshape(shape)(x)[0]


class PRelu(Operator):
    """
    PRelu applies the function `f(x) = slope * x` for x < 0,
    `f(x) = x` for x >= 0 to the data tensor elementwise.
    """

    def __init__(self):
        super(PRelu, self).__init__()

    def forward(self, x, slope):
        """
        Args:
            x (CTensor): matrix.
        Return:
            the result CTensor
        """
        mask0 = singa.LTFloat(x, 0.0)
        res = singa.__mul__(x, mask0)
        res = singa.__mul__(res, slope)
        res += singa.ReLU(x)
        if training:
            self.input = x
            self.slope = slope
            self.mask0 = mask0
            self.shape0 = list(x.shape())
            self.shape1 = list(slope.shape())
            self.shape3 = list(res.shape())
        return res

    def backward(self, dy):
        """
        Args:
            dy (CTensor): dL / dy
        Returns:
            dx (CTensor): dL / dx
        """
        dx1mask = singa.GEFloat(self.input, 0.0)
        dx2 = singa.__mul__(self.mask0, self.slope)
        dx = singa.__add__(dx1mask, dx2)
        dx = singa.__mul__(dy, dx)
        dslope = singa.__mul__(dy, singa.__mul__(self.mask0, self.input))
        if (type(dy) == float) or self.shape0 == self.shape1:
            assert self.shape0 == self.shape1, ('should have same shape')
            return dx, dslope
        # handle broadcast
        dx = back_broadcast(self.shape3, self.shape0, dx)
        dslope = back_broadcast(self.shape3, self.shape1, dslope)
        return dx, dslope


def prelu(x, slope):
    """
    PRelu applies the function `f(x) = slope * x` for x < 0,
    `f(x) = x` for x >= 0 to the data tensor elementwise.
    Args:
        x (Tensor): matrix.
    Return:
        the result Tensor
    """
    return PRelu()(x, slope)[0]


class Add(Operator):
    """
    Performs element-wise binary addition.
    """

    def __init__(self):
        super(Add, self).__init__()

    def forward(self, a, b):
        """
        Return `a+b`, where a and b are CTensor.
        """
        res = singa.__add__(a, b)
        if training:
            self.shape0 = list(a.shape())
            self.shape1 = list(b.shape())
            self.shape3 = list(res.shape())
        return res

    def backward(self, dy):
        """
        Args:
            dy(CTensor): dL / dy
        Return:
            a tuple for (dx0, dx1), dx0 is data for dL / da, dx1 is data
            for dL / db.
        """
        dx0, dx1 = dy, dy
        if (type(dy) == float) or self.shape0 == self.shape1:
            assert self.shape0 == self.shape1, ('should have same shape')
            return dx0, dx1
        # handle broadcast
        dx0 = back_broadcast(self.shape3, self.shape0, dx0)
        dx1 = back_broadcast(self.shape3, self.shape1, dx1)
        return dx0, dx1


def add(a, b):
    """
    Return `a+b`, where a and b are Tensor.
    """
    return Add()(a, b)[0]


class Elu(Operator):
    """
    `f(x) = alpha * (exp(x) - 1.)` for x < 0, `f(x) = x` for x >= 0., is applied to
    the tensor elementwise.
    """

    def __init__(self, alpha=1.):
        """
        Args:
            alpha (float): Coefficient of ELU, default is 1.0
        """
        super(Elu, self).__init__()
        self.alpha = alpha

    def forward(self, x):
        """
        Args:
            x (CTensor): matrix
        Returns:
            a CTensor for the result
        """
        #f(x) = alpha * (exp(x) - 1.) for x < 0, f(x) = x for x >= 0
        if training:
            self.input = x
        x1 = singa.LTFloat(x, 0.0)
        x1 *= x
        x1 = singa.MultFloat(singa.SubFloat(singa.Exp(x1), 1.0), self.alpha)
        x2 = singa.ReLU(x)
        x1 += x2
        return x1

    def backward(self, dy):
        """
        Args:
            dy (CTensor): dL / dy
        Returns:
            dx (CTensor): dL / dx
        """
        dx1mask = singa.LTFloat(self.input, 0.0)
        dx = singa.MultFloat(singa.Exp(self.input), self.alpha)
        dx *= dx1mask

        dx2mask = singa.GEFloat(self.input, 0.0)

        dx += dx2mask
        dx *= dy
        return dx


def elu(x, alpha=1):
    """
    `f(x) = alpha * (exp(x) - 1.)` for x < 0, `f(x) = x` for x >= 0., is applied to
    the tensor elementwise.
    Args:
        x (Tensor): matrix
        alpha (float): Coefficient of ELU, default is 1.0
    Returns:
        a Tensor for the result
    """
    return Elu(alpha)(x)[0]


class Equal(Operator):
    """
    Returns the tensor resulted from performing the equal logical operation
    elementwise on the input tensors x and y.
    """

    def __init__(self):
        super(Equal, self).__init__()

    def forward(self, x, y):
        """
        Return `a=b`, where a and b are CTensor.
        """
        return singa.__eq__(x, y)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): data for the dL / dy, L is the loss
        Raises:
            AssertionError: no backward function for this operator
        """
        assert False, ('no backward function for equal')


def equal(x, y):
    """
    Return `a=b`, where a and b are Tensor.
    """
    return Equal()(x, y)[0]


class SeLU(Operator):
    """
    `y = gamma * (alpha * e^x - alpha)` for x <= 0, `y = gamma * x` for x > 0
    is applied to the tensor elementwise.
    """

    def __init__(self, alpha=1.67326, gamma=1.0507):
        """
        Args:
            alpha (float): Coefficient of SELU default to 1.67326
            gamma (float): Coefficient of SELU default to 1.0507
        """
        super(SeLU, self).__init__()
        self.alpha = alpha
        self.gamma = gamma

    def forward(self, x):
        """
        Args:
            x (CTensor): matrix
        Returns:
            a CTensor for the result
        """
        #y = gamma * (alpha * e^x - alpha) for x <= 0, y = gamma * x for x > 0
        if training:
            self.input = x
        x1 = singa.LEFloat(x, 0.0)
        x1 *= x
        x1 = singa.MultFloat(singa.SubFloat(singa.Exp(x1), 1.0),
                             self.alpha * self.gamma)
        x2 = singa.ReLU(x)
        x2 = singa.MultFloat(x2, self.gamma)
        x1 += x2
        return x1

    def backward(self, dy):
        """
        Args:
            dy (CTensor): dL / dy
        Returns:
            dx (CTensor): dL / dx
        """
        dx1mask = singa.LEFloat(self.input, 0.0)
        dx1 = singa.MultFloat(singa.Exp(self.input), self.gamma * self.alpha)
        dx1 = singa.__mul__(dx1mask, dx1)

        dx2mask = singa.GTFloat(self.input, 0.0)
        dx2 = singa.MultFloat(dx2mask, self.gamma)

        dx = singa.__add__(dx1, dx2)
        dx *= dy
        return dx


def selu(x, alpha=1.67326, gamma=1.0507):
    """
    `y = gamma * (alpha * e^x - alpha)` for x <= 0, `y = gamma * x` for x > 0
    is applied to the tensor elementwise.
    Args:
        x (Tensor): matrix
        alpha (float): Coefficient of SELU default to 1.67326
        gamma (float): Coefficient of SELU default to 1.0507
    Returns:
        a Tensor for the result
    """
    return SeLU(alpha, gamma)(x)[0]


class SoftMax(Operator):
    """
    Apply SoftMax for each row of the Tensor or each column of the Tensor
    according to the parameter axis.
    """

    def __init__(self, axis=1):
        """
        Args:
            axis (int): axis of softmax, default to 1
        """
        super(SoftMax, self).__init__()
        self.axis = axis

    def forward(self, x):
        """
        Args:
            x (CTensor): the input 1d or 2d tensor
        Returns:
            the result CTensor
        """
        self.output = singa.SoftMax(x, self.axis)
        return self.output

    def backward(self, dy):
        """
        Args:
            dy (CTensor): dL / dy
        Returns:
            dx (CTensor): dL / dx
        """
        return singa.SoftMaxBackward(dy, self.axis, self.output)


def softmax(x, axis=1):
    """
    Apply SoftMax for each row of the Tensor or each column of the Tensor
    according to the parameter axis.
    Args:
        x (Tensor): the input 1d or 2d tensor
        axis (int): axis of softmax, default to 1
    Returns:
        the result Tensor
    """
    return SoftMax(axis)(x)[0]


class Sum(Operator):
    """
    Element-wise sum of each of the input tensors
    """

    def __init__(self):
        super(Sum, self).__init__()

    def forward(self, *l):
        """
        Args:
            l (a list of CTensor): element-wise sum operator
        Returns:
            a CTensor for the result
        """
        if training:
            self.l = len(l)
        assert (len(l) > 0)
        x = singa.Tensor(list(l[0].shape()), l[0].device())
        x.SetFloatValue(0.0)
        for i in range(len(l)):
            x += l[i]
        return x

    def backward(self, dy):
        """
        Args:
            dy (CTensor): dL / dy
        Returns:
            dx (CTensor): dL / dx
        """
        return [dy] * self.l


def sum(*l):
    """
    Element-wise sum of each of the input tensors
    Args:
        l (a list of Tensor): element-wise sum operator
    Returns:
        a Tensor for the result
    """
    return Sum()(*l)[0]


class BinaryCrossEntropy(Operator):

    def __init__(self, t):
        super(BinaryCrossEntropy, self).__init__()
        self.t = t.data

    """
    Calculte negative log likelihood loss for a batch of training data.
    """

    def forward(self, x):
        """
        Args:
            x (CTensor): 1d or 2d tensor, the prediction data(output)
                         of current network.
            t (CTensor): 1d or 2d tensor, the target data for training.
        Returns:
            loss (CTensor): scalar.
        """
        posx = singa.AddFloat(x, 0.0001)
        loss = singa.SumAll(singa.__mul__(self.t, singa.Log(posx)))
        negt = singa.AddFloat(singa.MultFloat(self.t, -1.0), 1.0)
        negx = singa.AddFloat(singa.MultFloat(x, -1.0), 1.0001)
        negLoss = singa.SumAll(singa.__mul__(negt, singa.Log(negx)))
        loss += negLoss
        loss /= -x.shape()[0]
        self.x = singa.AddFloat(x, 0.0001)
        return loss

    def backward(self, dy=1.0):
        """
        Args:
            dy (float or CTensor): scalar, accumulate gradient from outside
                                of current network, usually equal to 1.0
        Returns:
            dx (CTensor): data for the dL /dx, L is the loss, x is the output
                          of current network. note that this is true for
                          dy = 1.0
        """

        dx = singa.__div__(self.t, self.x)
        negt = singa.AddFloat(self.t, -1.0)
        negx = singa.AddFloat(self.x, -0.9999)
        dx -= singa.__div__(negt, negx)
        dx *= float(-1.0 / self.x.shape()[0])
        if isinstance(dy, float):
            # dtype of dy: float
            dx *= dy
            return dx
        elif isinstance(dy, CTensor):
            pass  # TODO, broadcast elementwise multiply seems not support


def binary_cross_entropy(x, t):
    return BinaryCrossEntropy(t)(x)[0]


class CrossEntropy(Operator):

    def __init__(self, t):
        super(CrossEntropy, self).__init__()
        self.t = t.data

    """
    Calculte negative log likelihood loss for a batch of training data.
    """

    def forward(self, x):
        """
        Args:
            x (CTensor): 1d or 2d tensor, the prediction data(output)
                         of current network.
            t (CTensor): 1d or 2d tensor, the target data for training.
        Returns:
            loss (CTensor): scalar.
        """
        loss = singa.SumAll(singa.__mul__(self.t, singa.Log(x)))
        loss /= -x.shape()[0]
        self.x = x
        return loss

    def backward(self, dy=1.0):
        """
        Args:
            dy (float or CTensor): scalar, accumulate gradient from outside
                                of current network, usually equal to 1.0
        Returns:
            dx (CTensor): data for the dL /dx, L is the loss, x is the output
                          of current network. note that this is true for
                          dy = 1.0
        """

        dx = singa.__div__(self.t, self.x)
        dx *= float(-1.0 / self.x.shape()[0])
        if isinstance(dy, float):
            # dtype of dy: float
            dx *= dy
            return dx
        elif isinstance(dy, CTensor):
            pass  # TODO, broadcast elementwise multiply seems not support


def cross_entropy(x, t):
    assert x.ndim() == 2, "1st arg required 2d tensor. got shape: " + str(
        x.shape)
    assert t.ndim() <= 2, "2nd arg required <=2d tensor. got shape: " + str(
        t.shape)
    # x is the logits and t is the ground truth.
    return CrossEntropy(t)(x)[0]


class RankingLoss(Operator):

    def __init__(self, M=0.2):
        super().__init__()
        # margin
        self.M = M

    def forward(self, pos, neg):
        # L = max{0, M - fn(pos) + fn(neg)}
        zero = singa.Tensor(list(pos.shape()), pos.device())
        zero.SetFloatValue(0.0)
        val = singa.AddFloat(singa.__sub__(neg, pos), self.M)
        gt_zero = singa.__gt__(val, zero)
        if training:
            self.inputs = (gt_zero,)  # (BS,)
        all_loss = singa.__mul__(gt_zero, val)
        loss = singa.SumAll(all_loss)
        loss /= (pos.shape()[0])
        return loss

    def backward(self, dy=1.0):
        assert training, "enable training mode to do backward"
        # dpos = -1 if M-pos+neg > 0 else 0
        # dneg =  1 if M-pos+neg > 0 else 0
        gt_zero = self.inputs[0]
        dpos_factor = singa.Tensor(list(gt_zero.shape()), gt_zero.device())
        dpos_factor.SetFloatValue(-1.0 / gt_zero.Size())
        dneg_factor = singa.Tensor(list(gt_zero.shape()), gt_zero.device())
        dneg_factor.SetFloatValue(1.0 / gt_zero.Size())
        dpos = singa.__mul__(gt_zero, dpos_factor)
        dneg = singa.__mul__(gt_zero, dneg_factor)
        return dpos, dneg


def ranking_loss(pos, neg, M=0.2):
    assert pos.shape == neg.shape, "input and target shape different: %s, %s" % (
        pos.shape, neg.shape)
    return RankingLoss(M)(pos, neg)[0]


class SoftMaxCrossEntropy(Operator):

    def __init__(self, t):
        super(SoftMaxCrossEntropy, self).__init__()
        self.t = t.data

    def forward(self, x):
        self.p = singa.SoftMax(x)
        ret = singa.CrossEntropyFwd(self.p, self.t)
        loss = singa.SumAll(ret)
        loss /= x.shape()[0]
        return loss

    def backward(self, dy=1.0):
        dx = singa.SoftmaxCrossEntropyBwd(self.p, self.t)
        dx /= float(self.p.shape()[0])
        return dx


def softmax_cross_entropy(x, t):
    assert x.ndim() == 2, "1st arg required 2d tensor. got shape: " + str(
        x.shape)
    assert t.ndim() <= 2, "2nd arg required <=2d tensor. got shape: " + str(
        t.shape)
    # x is the logits and t is the ground truth.
    return SoftMaxCrossEntropy(t)(x)[0]


class MeanSquareError(Operator):

    def __init__(self, t):
        super(MeanSquareError, self).__init__()
        self.t = t.data

    def forward(self, x):
        self.err = singa.__sub__(x, self.t)
        sqr = singa.Square(self.err)
        loss = singa.SumAll(sqr)
        self.n = 1
        for s in x.shape():
            self.n *= s
        loss /= self.n
        return loss

    def backward(self, dy=1.0):
        dx = self.err
        dx *= float(2 / self.n)
        dx *= dy
        return dx


def mse_loss(x, t):
    assert x.shape == t.shape, "input and target shape different: %s, %s" % (
        x.shape, t.shape)
    return MeanSquareError(t)(x)[0]


def ctensor2numpy(x):
    """
    To be used in SoftMax Operator.
    Convert a singa_tensor to numpy_tensor.
    """
    np_array = x.GetFloatValue(int(x.Size()))
    return np_array.reshape(x.shape())


class Flatten(Operator):
    """
    Flattens the input tensor into a 2D matrix. If input tensor has shape
    `(d_0, d_1, ... d_n)` then the output will have shape `(d_0 X d_1 ...
    d_(axis-1), d_axis X d_(axis+1) ... X dn)`.
    """

    def __init__(self, axis=1):
        """
        Args:
            axis (int): Indicate up to which input dimensions (exclusive)
                should be flattened to the outer dimension of the output. The
                value for axis must be in the range [-r, r], where r is the
                rank of the input tensor. Negative value means counting
                dimensions from the back. When axis = 0, the shape of the
                output tensor is `(1, (d_0 X d_1 ... d_n)`, where the shape
                of the input tensor is `(d_0, d_1, ... d_n)`.
        Returns:
            the result CTensor
        """
        super(Flatten, self).__init__()
        self.axis = axis

    def forward(self, x):
        """
        Args:
            x (CTensor): the input tensor
        Returns:
            the result CTensor
        """
        self.shape = list(x.shape())
        shape, axis = self.shape, self.axis
        # the axis must be within this range (0, r-1)
        assert axis <= len(
            shape) - 1 or axis >= 0, "the axis must be within (0, %d-1)" % len(
                shape)
        # calculate the new shape
        new_shape = (1, int(np.prod(shape))) if axis == 0 else (
            int(np.prod(shape[0:axis]).astype(int)),
            int(np.prod(shape[axis:]).astype(int)))
        y = singa.Reshape(x, new_shape)
        return y

    def backward(self, dy):
        """
        Args:
            dy (CTensor): data for the dL / dy, L is the loss
        Returns:
            dx (CTensor): data for the dL / dx, L is the loss,
        """
        dx = singa.Reshape(dy, self.shape)
        return dx


def flatten(x, axis=1):
    """
    Flattens the input tensor into a 2D matrix. If input tensor has shape
    `(d_0, d_1, ... d_n)` then the output will have shape `(d_0 X d_1 ...
    d_(axis-1), d_axis X d_(axis+1) ... X dn)`.
    Args:
        x (Tensor): the input tensor
        axis (int): Indicate up to which input dimensions (exclusive)
            should be flattened to the outer dimension of the output. The
            value for axis must be in the range [-r, r], where r is the
            rank of the input tensor. Negative value means counting
            dimensions from the back. When axis = 0, the shape of the
            output tensor is `(1, (d_0 X d_1 ... d_n)`, where the shape
            of the input tensor is `(d_0, d_1, ... d_n)`.
    Returns:
        the result Tensor
    """
    return Flatten(axis)(x)[0]


class ScatterElements(Operator):
    """
    ScatterElements operator following ONNX Operator Schemas
    https://github.com/onnx/onnx/blob/master/docs/Changelog.md#ScatterElements-11

    Example usage:
    data = [
        [0.0, 0.0, 0.0],
        [0.0, 0.0, 0.0],
        [0.0, 0.0, 0.0],
    ]
    axis = 0
    indices = [
        [1, 0, 2],
        [0, 2, 1],
    ]
    updates = [
        [1.0, 1.1, 1.2],
        [2.0, 2.1, 2.2],
    ]
    output = [
        [2.0, 1.1, 0.0]
        [1.0, 0.0, 2.2]
        [0.0, 2.1, 1.2]
    ]

    """

    def __init__(self, indices, updates, axis=0):
        """
        Args:
            indices (Tensor): index tensor
            updates (Tensor): source tensor
            axis (int): Which axis to scatter on. A negative value means 
                counting dimension from the back. Accepted range is [-r,r-1]
                where r=rank(destination_tensor) 
        """
        super(ScatterElements, self).__init__()
        self.indices = indices
        self.updates = updates
        self.axis = axis

    def forward(self, x):
        x_shape = x.shape()
        x_rank = len(x_shape)
        if isinstance(self.indices, Tensor):
            self.indices = tensor.to_numpy(self.indices)
        elif isinstance(self.indices, (list, tuple)):
            self.indices = np.array(self.indices)
        if isinstance(self.updates, Tensor):
            self.updates = tensor.to_numpy(self.updates)
        elif isinstance(self.updates, (list, tuple)):
            self.updates = np.array(self.updates)
        self.updates.astype(np.int32)
        _x = tensor.to_numpy(tensor.from_raw_tensor(x))
        _x = _x.astype(np.float32)

        assert x_rank == 2, "Only support 2D input."
        assert x_rank == len(
            self.indices.shape
        ), "Index should have the same number of dimensions as output"
        assert -x_rank < self.axis <= x_rank, "Axis is out of range"
        assert np.logical_and(
            -_x.shape[self.axis] < self.indices,
            self.indices <= _x.shape[self.axis]).all(
            ), "The values of the indexes should be between %d and %d" % (
                -_x.shape[self.axis], _x.shape[self.axis] - 1)

        self.axis = self.axis % x_rank
        u_shape = self.updates.shape
        y = _x.copy()
        for i in range(u_shape[0]):
            for j in range(u_shape[1]):
                idx = int(self.indices[i][j])
                if self.axis == 0:
                    y[idx][j] = self.updates[i][j]
                else:
                    y[i][idx] = self.updates[i][j]
        y = tensor.from_numpy(y)
        y.to_device(x.device())
        return y.data

    def backward(self, dy):
        mask = np.ones(dy.shape(), dtype=np.float32)
        u_shape = self.updates.shape
        for i in range(u_shape[0]):
            for j in range(u_shape[1]):
                idx = int(self.indices[i][j])
                if self.axis == 0:
                    mask[idx][j] = 0.
                else:
                    mask[i][idx] = 0.
        mask = tensor.from_numpy(mask)
        mask.to_device(dy.device())
        return singa.__mul__(dy, mask.data)


def scatter_elements(x, indices, updates, axis=0):
    """
    Produces a ScatterElements operator
    Args:
        x (Tensor): input tensor.
        indices (Tensor): index tensor
        updates (Tensor): source tensor
        axis (int): Which axis to scatter on. A negative value means 
            counting dimension from the back. Accepted range is [-r,r-1]
            where r=rank(destination_tensor) 
    Returns:
        the output Tensor.
    """
    return ScatterElements(indices, updates, axis)(x)[0]


class Concat(Operator):
    """
    Concatenate a list of tensors into a single tensor. All input tensors must
    have the same shape, except for the dimension size of the axis to
    concatenate on.
    """

    def __init__(self, axis=0):
        """
        Args:
            axis (int): Which axis to concat on. A negative value means
                counting dimensions from the back. Accepted range is [-r, r-1]
                where r = rank(inputs).
        Returns:
            the result CTensor
        """
        super(Concat, self).__init__()
        self.axis = axis

    def forward(self, *xs):
        """
        Args:
            xs (a list of CTensor): List of tensors for concatenation
        Returns:
            a CTensor for the result
        """
        if self.axis < 0:
            self.axis = self.axis % len(xs[0].shape())
        if training:
            offset = 0
            self.slice_point = []
            for t in xs:
                offset += t.shape()[self.axis]
                self.slice_point.append(offset)
        x = singa.VecTensor(list(xs))
        return singa.ConcatOn(x, self.axis)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): data for the dL / dy, L is the loss
        Returns:
            dxs (a tuple of CTensor): data for the dL / dxs, L is the loss,
        """
        assert hasattr(
            self, "slice_point"), "Please set training as True before do BP. "
        assert self.slice_point[-1] == dy.shape()[self.axis], "Shape mismatch."
        dxs = []
        last_offset = 0
        for p in self.slice_point:
            dxs.append(singa.SliceOn(dy, last_offset, p, self.axis))
            last_offset = p
        return tuple(dxs)


def cat(xs, axis=0):
    """
    Concatenate a list of tensors into a single tensor. All input tensors must
    have the same shape, except for the dimension size of the axis to
    concatenate on.
    Args:
        xs (a list of Tensor): List of tensors for concatenation
        axis (int): Which axis to concat on. A negative value means
            counting dimensions from the back. Accepted range is [-r, r-1]
            where r = rank(inputs).
    Returns:
        a Tensor for the result
    """
    return Concat(axis)(*xs)[0]


"""
def make_slice(arr, axis, i):  # type: ignore
        slc = [slice(None)] * arr.ndim
        slc[axis] = i
        return slc
"""


class _Conv2d(Operator):
    """
    Init a conv 2d operator
    """

    def __init__(self, handle, odd_padding=(0, 0, 0, 0)):
        """
        Args:
            handle (object): ConvHandle for cpu or CudnnConvHandle for gpu
            odd_padding (tuple of four ints):, the odd paddding is the value
                that cannot be handled by the tuple padding (w, h) mode so
                we need to firstly handle the input, then use the nomal padding
                method.
        """
        super(_Conv2d, self).__init__()
        self.handle = handle
        self.odd_padding = odd_padding

    def forward(self, x, W, b=None):
        """
        Args:
            x (CTensor): input
            W (CTensor): weight
            b (CTensor): bias
        Returns:
            CTensor
        """
        assert x.nDim() == 4, "The dimensions of input should be 4D."
        if self.odd_padding != (0, 0, 0, 0):
            x = utils.handle_odd_pad_fwd(x, self.odd_padding)

        if training:
            if self.handle.bias_term:
                self.inputs = (x, W, b)
            else:
                self.inputs = (x, W)

        if not self.handle.bias_term:
            # create empty bias tensor for Cpp API
            b = CTensor((self.handle.num_filters,), x.device())
            b.SetFloatValue(0.0)

        if (type(self.handle) != singa.ConvHandle):
            return singa.GpuConvForward(x, W, b, self.handle)
        else:
            return singa.CpuConvForward(x, W, b, self.handle)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): dL / dy
        Returns:
            dx (CTensor): dL / dx
        """
        assert training is True and hasattr(
            self, "inputs"), "Please set training as True before do BP. "

        if (type(self.handle) != singa.ConvHandle):
            dx = singa.GpuConvBackwardx(dy, self.inputs[1], self.inputs[0],
                                        self.handle)
            dW = singa.GpuConvBackwardW(dy, self.inputs[0], self.inputs[1],
                                        self.handle)
            db = singa.GpuConvBackwardb(
                dy, self.inputs[2],
                self.handle) if self.handle.bias_term else None
        else:
            dx = singa.CpuConvBackwardx(dy, self.inputs[1], self.inputs[0],
                                        self.handle)
            dW = singa.CpuConvBackwardW(dy, self.inputs[0], self.inputs[1],
                                        self.handle)
            db = singa.CpuConvBackwardb(
                dy, self.inputs[2],
                self.handle) if self.handle.bias_term else None
        if self.odd_padding != (0, 0, 0, 0):
            dx = utils.handle_odd_pad_bwd(dx, self.odd_padding)

        if db:
            return dx, dW, db

        else:
            return dx, dW


def conv2d(handle, x, W, b=None, odd_padding=(0, 0, 0, 0)):
    """
    Conv 2d operator
    Args:
        handle (object): ConvHandle for cpu or CudnnConvHandle for gpu
        x (Tensor): input
        W (Tensor): weight
        b (Tensor): bias
        odd_padding (tuple of four ints):, the odd paddding is the value
            that cannot be handled by the tuple padding (w, h) mode so
            we need to firstly handle the input, then use the nomal padding
            method.
    """
    if b is None:
        return _Conv2d(handle, odd_padding)(x, W)[0]
    else:
        return _Conv2d(handle, odd_padding)(x, W, b)[0]


class _BatchNorm2d(Operator):
    """
    Carries out batch normalization as described in the paper
    https://arxiv.org/abs/1502.03167.
    """

    def __init__(self, handle, running_mean, running_var, name=None):
        """
        Args:
            handle (object): BatchNormHandle for cpu and CudnnBatchNormHandle
                for gpu
            running_mean (float): the running_mean
            running_var (float): the running_var
            name (string): the name assigned to this operator
        """
        super(_BatchNorm2d, self).__init__(name)
        self.handle = handle
        self.running_mean = running_mean.data
        self.running_var = running_var.data

    def forward(self, x, scale, bias):
        """
        Args:
            x (CTensor): the input tensor
            scale (CTensor): the bias tensor
            bias (CTensor): the bias tensor
        Returns:
            the result CTensor
        """
        if training:
            if (type(self.handle) == singa.BatchNormHandle):
                y, mean, var = singa.CpuBatchNormForwardTraining(
                    self.handle, x, scale, bias, self.running_mean,
                    self.running_var)

                self.cache = (x, scale, mean, var, y, bias)
            else:
                y, mean, var = singa.GpuBatchNormForwardTraining(
                    self.handle, x, scale, bias, self.running_mean,
                    self.running_var)

                self.cache = (x, scale, mean, var)

        else:

            if (type(self.handle) == singa.BatchNormHandle):
                y = singa.CpuBatchNormForwardInference(
                    self.handle,
                    x,
                    scale,
                    bias,
                    self.running_mean,
                    self.running_var,
                )
            else:
                y = singa.GpuBatchNormForwardInference(
                    self.handle,
                    x,
                    scale,
                    bias,
                    self.running_mean,
                    self.running_var,
                )
        return y

    def backward(self, dy):
        """
        Args:
            dy (CTensor): data for the dL / dy, L is the loss
        Returns:
            dx (CTensor): data for the dL / dx, L is the loss
            ds (CTensor): data for the dL / ds, L is the loss
            db (CTensor): data for the dL / db, L is the loss
        """
        assert training is True and hasattr(
            self, "cache"), "Please set training as True before do BP. "

        if (type(self.handle) == singa.BatchNormHandle):
            x, scale, mean, var, y, bias = self.cache
            dx, ds, db = singa.CpuBatchNormBackwardx(self.handle, y, dy, x,
                                                     scale, bias, mean, var)
        else:
            x, scale, mean, var = self.cache
            dx, ds, db = singa.GpuBatchNormBackward(self.handle, dy, x, scale,
                                                    mean, var)

        return dx, ds, db


def batchnorm_2d(handle, x, scale, bias, running_mean, running_var):
    """
    Carries out batch normalization as described in the paper
    https://arxiv.org/abs/1502.03167.
    Args:
        handle (object): BatchNormHandle for cpu and CudnnBatchNormHandle
            for gpu
        x (Tensor): the input tensor
        scale (Tensor): the bias tensor
        bias (Tensor): the bias tensor
        running_mean (float): the running_mean
        running_var (float): the running_var
    Returns:
        the result Tensor
    """
    return _BatchNorm2d(handle, running_mean, running_var)(x, scale, bias)[0]


class _Pooling2d(Operator):
    """
    Init a pool 2d operator
    """

    def __init__(self, handle, odd_padding=(0, 0, 0, 0)):
        """
        Args:
            handle (object): PoolingHandle for cpu or CudnnPoolingHandle for
                gpu
            odd_padding (tuple of four int): the odd paddding is the value
                that cannot be handled by the tuple padding (w, h) mode so
                it needs to firstly handle the input, then use the normal
                padding method.
        """
        super(_Pooling2d, self).__init__()
        self.handle = handle
        self.odd_padding = odd_padding

    def forward(self, x):
        """
        Args:
            x (CTensor): the input tensor
        Returns:
            the result CTensor
        """
        assert x.nDim() == 4, "The dimensions of input should be 4D."
        if self.odd_padding != (0, 0, 0, 0):
            x = utils.handle_odd_pad_fwd(x, self.odd_padding, True)

        if (type(self.handle) != singa.PoolingHandle):
            y = singa.GpuPoolingForward(self.handle, x)
        else:
            y = singa.CpuPoolingForward(self.handle, x)
        if training:
            self.cache = (x, y)
        return y

    def backward(self, dy):
        """
        Args:
            dy (CTensor): data for the dL / dy, L is the loss
        Returns:
            dx (CTensor): data for the dL / dx, L is the loss,
        """
        if (type(self.handle) != singa.PoolingHandle):
            dx = singa.GpuPoolingBackward(self.handle, dy, self.cache[0],
                                          self.cache[1])
        else:
            dx = singa.CpuPoolingBackward(self.handle, dy, self.cache[0],
                                          self.cache[1])
        if self.odd_padding != (0, 0, 0, 0):
            dx = utils.handle_odd_pad_bwd(dx, self.odd_padding)

        return dx


def pooling_2d(handle, x, odd_padding=(0, 0, 0, 0)):
    """
    Pooling 2d operator
    Args:
        handle (object): PoolingHandle for cpu or CudnnPoolingHandle for
            gpu
        x (Tensor): input
        odd_padding (tuple of four int): the odd paddding is the value
            that cannot be handled by the tuple padding (w, h) mode so
            it needs to firstly handle the input, then use the normal
            padding method.
    Returns:
        the result Tensor
    """
    return _Pooling2d(handle, odd_padding)(x)[0]


class Tanh(Operator):
    """
    Calculates the hyperbolic tangent of the given input tensor element-wise.
    """

    def __init__(self):
        super(Tanh, self).__init__()

    def forward(self, x):
        """
        Args:
            x (CTensor): Input tensor
        Returns:
            CTensor, the output
        """
        out = singa.Tanh(x)
        if training:
            self.cache = (out,)
        return out

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        dx = singa.__mul__(self.cache[0], self.cache[0])
        dx = singa.MultFloat(dx, -1.0)
        dx = singa.AddFloat(dx, 1.0)
        dx *= dy
        return dx


def tanh(x):
    """
    Calculates the hyperbolic tangent of the given input tensor element-wise.
    Args:
        x (Tensor): Input tensor
    Returns:
        Tensor, the output
    """
    return Tanh()(x)[0]


class Cos(Operator):
    """
    Calculates the cosine of the given input tensor, element-wise.
    """

    def __init__(self):
        super(Cos, self).__init__()

    def forward(self, x):
        """
        Args:
            x (CTensor): Input tensor
        Returns:
            CTensor, the output
        """
        if training:
            self.input = x
        return singa.Cos(x)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        dx = singa.Sin(self.input)
        dx = singa.MultFloat(dx, -1.0)
        dx *= dy
        return dx


def cos(x):
    """
    Calculates the cosine of the given input tensor, element-wise.
    Args:
        x (Tensor): Input tensor
    Returns:
        Tensor, the output
    """

    return Cos()(x)[0]


class Cosh(Operator):
    """
    Calculates the hyperbolic cosine of the given input tensor element-wise.
    """

    def __init__(self):
        super(Cosh, self).__init__()

    def forward(self, x):
        """
        Args:
            x (CTensor): Input tensor
        Returns:
            CTensor, the output
        """
        if training:
            self.input = x
        return singa.Cosh(x)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        dx = singa.Sinh(self.input)
        dx *= dy
        return dx


def cosh(x):
    """
    Calculates the hyperbolic cosine of the given input tensor element-wise.
    Args:
        x (Tensor): Input tensor
    Returns:
        Tensor, the output
    """
    return Cosh()(x)[0]


class Acos(Operator):
    """
    Calculates the arccosine (inverse of cosine) of the given input tensor,
    element-wise.
    """

    def __init__(self):
        super(Acos, self).__init__()

    def forward(self, x):
        """
        Args:
            x (CTensor): Input tensor
        Returns:
            CTensor, the output
        """
        if training:
            self.input = x
        return singa.Acos(x)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        dx = singa.Square(self.input)
        dx = singa.MultFloat(dx, -1.0)
        dx = singa.AddFloat(dx, 1.0)
        dx = singa.PowFloat(dx, -0.5)
        dx = singa.MultFloat(dx, -1.0)
        dx *= dy
        return dx


def acos(x):
    """
    Calculates the arccosine (inverse of cosine) of the given input tensor,
    element-wise.
    Args:
        x (Tensor): Input tensor
    Returns:
        Tensor, the output
    """
    return Acos()(x)[0]


class Acosh(Operator):
    """
    Calculates the hyperbolic arccosine of the given input tensor element-wise.
    """

    def __init__(self):
        super(Acosh, self).__init__()

    def forward(self, x):
        """
        Args:
            x (CTensor): Input tensor
        Returns:
            CTensor, the output
        """
        if training:
            self.input = x
        return singa.Acosh(x)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        dx = singa.SubFloat(self.input, 1.0)
        dx = singa.Sqrt(dx)
        temp = singa.AddFloat(self.input, 1.0)
        temp = singa.Sqrt(temp)
        dx = singa.__mul__(dx, temp)
        dx = singa.PowFloat(dx, -1.0)
        dx *= dy
        return dx


def acosh(x):
    """
    Calculates the hyperbolic arccosine of the given input tensor element-wise.
    Args:
        x (Tensor): Input tensor
    Returns:
        Tensor, the output
    """
    return Acosh()(x)[0]


class Sin(Operator):
    """
    Calculates the sine of the given input tensor, element-wise.
    """

    def __init__(self):
        super(Sin, self).__init__()

    def forward(self, x):
        """
        Args:
            x (CTensor): Input tensor
        Returns:
            CTensor, the output
        """
        if training:
            self.input = x
        return singa.Sin(x)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        dx = singa.Cos(self.input)
        dx *= dy
        return dx


def sin(x):
    """
    Calculates the sine of the given input tensor, element-wise.
    Args:
        x (Tensor): Input tensor
    Returns:
        Tensor, the output
    """
    return Sin()(x)[0]


class Sinh(Operator):
    """
    Calculates the hyperbolic sine of the given input tensor element-wise.
    """

    def __init__(self):
        super(Sinh, self).__init__()

    def forward(self, x):
        """
        Args:
            x (CTensor): Input tensor
        Returns:
            CTensor, the output
        """
        if training:
            self.input = x
        return singa.Sinh(x)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        dx = singa.Cosh(self.input)
        dx *= dy
        return dx


def sinh(x):
    """
    Calculates the hyperbolic sine of the given input tensor element-wise.
    Args:
        x (Tensor): Input tensor
    Returns:
        Tensor, the output
    """
    return Sinh()(x)[0]


class Asin(Operator):
    """
    Calculates the arcsine (inverse of sine) of the given input tensor, element-wise.
    """

    def __init__(self):
        super(Asin, self).__init__()

    def forward(self, x):
        """
        Args:
            x (CTensor): Input tensor
        Returns:
            CTensor, the output
        """
        if training:
            self.input = x
        return singa.Asin(x)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        dx = singa.Square(self.input)
        dx = singa.MultFloat(dx, -1.0)
        dx = singa.AddFloat(dx, 1.0)
        dx = singa.PowFloat(dx, -0.5)
        dx *= dy
        return dx


def asin(x):
    """
    Calculates the arcsine (inverse of sine) of the given input tensor, element-wise.
    Args:
        x (Tensor): Input tensor
    Returns:
        Tensor, the output
    """

    return Asin()(x)[0]


class Asinh(Operator):
    """
    Calculates the hyperbolic arcsine of the given input tensor element-wise.
    """

    def __init__(self):
        super(Asinh, self).__init__()

    def forward(self, x):
        """
        Args:
            x (CTensor): Input tensor
        Returns:
            CTensor, the output
        """
        if training:
            self.input = x
        return singa.Asinh(x)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        dx = singa.Square(self.input)
        dx = singa.AddFloat(dx, 1.0)
        dx = singa.PowFloat(dx, -0.5)
        dx *= dy
        return dx


def asinh(x):
    """
    Calculates the hyperbolic arcsine of the given input tensor element-wise.
    Args:
        x (Tensor): Input tensor
    Returns:
        Tensor, the output
    """
    return Asinh()(x)[0]


class Tan(Operator):
    """
    Insert single-dimensional entries to the shape of an input tensor (data).
    """

    def __init__(self):
        super(Tan, self).__init__()

    def forward(self, x):
        """
        Args:
            x (CTensor): Input tensor
        Returns:
            CTensor, the output
        """
        if training:
            self.input = x
        return singa.Tan(x)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        dx = singa.Cos(self.input)
        dx = singa.Square(dx)
        dx = singa.PowFloat(dx, -1.0)
        dx *= dy
        return dx


def tan(x):
    """
    Calculates the tangent of the given input tensor, element-wise.
    Args:
        x (Tensor): Input tensor
    Returns:
        Tensor, the output
    """
    return Tan()(x)[0]


class Atan(Operator):
    """
    Calculates the arctangent (inverse of tangent) of the given input tensor, element-wise.
    """

    def __init__(self):
        super(Atan, self).__init__()

    def forward(self, x):
        """
        Args:
            x (CTensor): Input tensor
        Returns:
            CTensor, the output
        """
        if training:
            self.input = x
        return singa.Atan(x)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        dx = singa.Square(self.input)
        dx = singa.AddFloat(dx, 1.0)
        dx = singa.PowFloat(dx, -1.0)
        dx *= dy
        return dx


def atan(x):
    """
    Calculates the arctangent (inverse of tangent) of the given input tensor, element-wise.
    Args:
        x (Tensor): Input tensor
    Returns:
        Tensor, the output
    """
    return Atan()(x)[0]


class Atanh(Operator):
    """
    Calculates the hyperbolic arctangent of the given input tensor element-wise.
    """

    def __init__(self):
        super(Atanh, self).__init__()

    def forward(self, x):
        """
        Args:
            x (CTensor): Input tensor
        Returns:
            CTensor, the output
        """
        if training:
            self.input = x
        return singa.Atanh(x)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        dx = singa.Square(self.input)
        dx = singa.MultFloat(dx, -1.0)
        dx = singa.AddFloat(dx, 1.0)
        dx = singa.PowFloat(dx, -1.0)
        dx *= dy
        return dx


def atanh(x):
    """
    Calculates the hyperbolic arctangent of the given input tensor element-wise.
    Args:
        x (Tensor): Input tensor
    Returns:
        Tensor, the output
    """
    return Atanh()(x)[0]


class Sigmoid(Operator):
    """
    `y = 1 / (1 + exp(-x))`, is applied to the tensor elementwise.
    """

    def __init__(self):
        super(Sigmoid, self).__init__()

    def forward(self, x):
        """
        Args:
            x (CTensor): Input tensor
        Returns:
            CTensor, the output
        """
        out = singa.Sigmoid(x)
        if training:
            self.cache = (out,)
        return out

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        dx = singa.MultFloat(self.cache[0], -1.0)
        dx = singa.AddFloat(dx, 1.0)
        dx = singa.__mul__(self.cache[0], dx)
        dx *= dy
        return dx


def sigmoid(x):
    """
    `y = 1 / (1 + exp(-x))`, is applied to the tensor elementwise.
    Args:
        x (Tensor): Input tensor
    Returns:
        Tensor, the output
    """
    return Sigmoid()(x)[0]


class Mul(Operator):
    """
    Performs element-wise binary multiplication (with Numpy-style broadcasting
    support).
    """

    def __init__(self):
        super(Mul, self).__init__()

    def forward(self, a, b):
        """
        Return `np.multiply(a,b)`, where a and b are CTensor.
        """
        # todo we cannot support mul op for int tensors
        _a, _b = a, b
        dtype0 = _a.data_type()
        dtype1 = _b.data_type()
        if dtype0 == singa.kInt or dtype1 == singa.kInt:
            _a = a.AsType(singa.kFloat32)
            _b = b.AsType(singa.kFloat32)
            res = singa.__mul__(_a, _b)
            res = res.AsType(singa.kInt)
        else:
            res = singa.__mul__(_a, _b)
        if training:
            self.input = (_a, _b)
            self.shape0 = list(_a.shape())
            self.shape1 = list(_b.shape())
            self.shape3 = list(res.shape())
        return res

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            a tuple for (da, db), da is data for dL / da, db is data
                for dL / db.
        """
        dx0 = singa.__mul__(dy, self.input[1])
        dx1 = singa.__mul__(dy, self.input[0])
        if (type(dy) == float) or self.shape0 == self.shape1:
            assert self.shape0 == self.shape1, ('should have same shape')
            return dx0, dx1
        # handle broadcast
        dx0 = back_broadcast(self.shape3, self.shape0, dx0)
        dx1 = back_broadcast(self.shape3, self.shape1, dx1)
        return dx0, dx1


def mul(x, y):
    """
    Return `np.multiply(x,y)`, where a and b are Tensor.
    """
    return Mul()(x, y)[0]


class Unsqueeze(Operator):
    """
    Insert single-dimensional entries to the shape of an input tensor (data).
    """

    def __init__(self, axis):
        """
        Args:
            axis (list of int): the dimensions to be inserted.
        """
        super(Unsqueeze, self).__init__()
        if (type(axis) is int):
            self.axis = list(axis)
        else:
            self.axis = axis

    def forward(self, x):
        """
        Args:
            x (CTensor): Input tensor
        Returns:
            CTensor, the output
        """
        self.cache = x.shape()
        cur = list(self.cache)
        # todo, need optimize after we have scalar tensor
        if len(self.cache) == 1 and self.axis == [0]:
            return x
        for i in self.axis:
            cur.insert(i, 1)
        return singa.Reshape(x, cur)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        return singa.Reshape(dy, self.cache)


def unsqueeze(x, axis=-1):
    """
    Insert single-dimensional entries to the shape of an input tensor (data).
    Args:
        x (Tensor): Input tensor
        axis (list of int): the dimensions to be inserted.
    Returns:
        Tensor, the output
    """
    return Unsqueeze(axis)(x)[0]


class Transpose(Operator):
    """
    Transpose the input tensor similar to numpy.transpose.
    """

    def __init__(self, perm):
        """
        Args:
            perm (list of ints): A list of integers. By default, reverse the
                dimensions, otherwise permute the axes according to the values given.
        """
        super(Transpose, self).__init__()
        self.perm = list(perm)

    def forward(self, x):
        """
        Args:
            x (CTensor): Input tensor
        Returns:
            CTensor, the output
        """
        return singa.Transpose(x, self.perm)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        cur = []
        for i in range(len(self.perm)):
            cur += [self.perm.index(i)]
        return singa.Transpose(dy, cur)


def transpose(x, shape):
    """
    Transpose the input tensor similar to numpy.transpose.
    Args:
        x (Tensor): Input tensor
        perm (list of ints): A list of integers. By default, reverse the
            dimensions, otherwise permute the axes according to the values given.
    Returns:
        Tensor, the output
    """
    return Transpose(shape)(x)[0]


def add_all(*xs):
    assert len(xs) > 2
    y = add(xs[0], xs[1])
    for x in xs[2:]:
        y = add(y, x)
    return


class Abs(Operator):
    """
    `y = abs(x)`, is applied to the tensor elementwise.
    """

    def forward(self, a):
        """
        Return `abs(a)`, where a is CTensor.
        """
        if training:
            self.input = a
        return singa.Abs(a)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        dx = singa.Sign(self.input)
        dx *= dy
        return dx


def abs(a):
    """
    Return abs(a), where a is Tensor.
    """
    return Abs()(a)[0]


class Exp(Operator):
    """
    `y = exp(x)`, is applied to the tensor elementwise.
    """

    def forward(self, a):
        """
        Return `exp(a)`, where a is Tensor.
        """
        if training:
            self.input = a
        return singa.Exp(a)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        dx = singa.Exp(self.input)
        dx *= dy
        return dx


def exp(a):
    """
    Return `exp(a)`, where a is Tensor.
    """
    return Exp()(a)[0]


class LeakyRelu(Operator):
    """
    `f(x) = alpha * x` for x < 0, `f(x) = x` for x >= 0, is applied to the tensor elementwise.
    """

    def __init__(self, a):
        """
        Args:
            a (float): Coefficient of leakage.
        """
        super(LeakyRelu, self).__init__()
        self.a = a

    def forward(self, x):
        """
        Args:
            x (CTensor): Input tensor
        Returns:
            CTensor, the output
        """
        if training:
            self.input = x
        x1 = singa.LTFloat(x, 0.0)
        x1 = singa.__mul__(x, x1)
        x1 = singa.MultFloat(x1, self.a)
        x2 = singa.ReLU(x)
        x1 = singa.__add__(x1, x2)
        return x1

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        # TODO(wangwei) check the correctness
        dx1 = singa.GTFloat(self.input, 0.0)
        dx2 = singa.LTFloat(self.input, 0.0)
        dx2 = singa.MultFloat(dx2, self.a)
        dx = singa.__add__(dx1, dx2)
        dx *= dy
        return dx


def leakyrelu(x, a=0.01):
    """
    `f(x) = alpha * x` for x < 0, `f(x) = x` for x >= 0 is applied to the tensor
    elementwise.
    Args:
        x (Tensor): Input tensor
        a (float): Coefficient of leakage, default to 0.01.
    Returns:
        Tensor, the output
    """
    return LeakyRelu(a)(x)[0]


class Sign(Operator):
    """
    Calculate the sign of the given input tensor element-wise. If input > 0,
    output 1. if input < 0, output -1. if input == 0, output 0.
    """

    def __init__(self):
        super(Sign, self).__init__()

    def forward(self, a):
        """
        Args:
            a (CTensor): Input tensor
        Returns:
            CTensor, the output
        """
        if training:
            self.input = a
        return singa.Sign(a)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        dx = singa.MultFloat(dy, 0.0)
        return dx


def sign(a):
    """
    Calculate the sign of the given input tensor element-wise. If input > 0,
    output 1. if input < 0, output -1. if input == 0, output 0.
    Args:
        a (Tensor): Input tensor
    Returns:
        Tensor, the output
    """
    return Sign()(a)[0]


class Pow(Operator):
    """
    `f(x) = a^b`, is applied to the tensor elementwise.
    """

    def __init__(self):
        super(Pow, self).__init__()

    def forward(self, a, b):
        """
        Return `a^b`, where a and b are CTensor.
        """
        res = singa.Pow(a, b)
        if training:
            self.input = (a, b)
            self.shape0 = list(a.shape())
            self.shape1 = list(b.shape())
            self.shape3 = list(res.shape())
        return res

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            a tuple for (da, db), da is data for dL / da, db is data
                for dL / db.
        """
        da1 = singa.__mul__(
            self.input[1],
            singa.Pow(self.input[0], singa.SubFloat(self.input[1], 1.0)))
        dx0 = singa.__mul__(da1, dy)
        db1 = singa.__mul__(singa.Pow(self.input[0], self.input[1]),
                            singa.Log(self.input[0]))
        dx1 = singa.__mul__(db1, dy)
        if (type(dy) == float) or self.shape0 == self.shape1:
            assert self.shape0 == self.shape1, ('should have same shape')
            return dx0, dx1
        # handle broadcast
        dx0 = back_broadcast(self.shape3, self.shape0, dx0)
        dx1 = back_broadcast(self.shape3, self.shape1, dx1)
        return dx0, dx1


def pow(a, b):
    """
    Return `a^b`, where a and b are Tensor.
    """
    return Pow()(a, b)[0]


class SoftSign(Operator):
    """
    Calculates the softsign `(x/(1+|x|))` of the given input tensor element-wise.
    """

    def __init__(self):
        super(SoftSign, self).__init__()

    def forward(self, x):
        """
        Return `(x/(1+|x|))`, where x is CTensor.
        """
        # y = x / (1 + np.abs(x))
        if training:
            self.input = x
        x1 = singa.AddFloat(singa.Abs(x), 1.0)
        y = singa.__div__(x, x1)

        return y

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        dx = singa.AddFloat(singa.Abs(self.input), 1.0)
        dx = singa.PowFloat(singa.Square(dx), -1.0)
        dx = singa.__mul__(dy, dx)
        return dx


def softsign(x):
    """
    Return `(x/(1+|x|))`, where x is Tensor.
    """
    return SoftSign()(x)[0]


class Sqrt(Operator):
    """
    `y = x^0.5`, is applied to the tensor elementwise.
    """

    def __init__(self):
        super(Sqrt, self).__init__()

    def forward(self, x):
        """
        Return `x^0.5`, where x is CTensor.
        """
        if training:
            self.input = x
        return singa.Sqrt(x)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        dx = singa.PowFloat(self.input, -0.5)
        dx = singa.MultFloat(dx, 0.5)
        dx = singa.__mul__(dy, dx)
        return dx


def sqrt(x):
    """
    Return `x^0.5`, where x is Tensor.
    """
    return Sqrt()(x)[0]


class SoftPlus(Operator):
    """
    `y = ln(exp(x) + 1)` is applied to the tensor elementwise.
    """

    def __init__(self):
        super(SoftPlus, self).__init__()

    def forward(self, x):
        """
        Return `ln(exp(x) + 1)`, where x is CTensor.
        """
        #f(x) = ln(exp(x) + 1)
        if training:
            self.input = x
        x1 = singa.AddFloat(singa.Exp(x), 1.0)
        y = singa.Log(x1)
        return y

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        dx = singa.Exp(singa.MultFloat(self.input, -1.0))
        dx = singa.PowFloat(singa.AddFloat(dx, 1.0), -1.0)
        dx = singa.__mul__(dy, dx)
        return dx


def softplus(x):
    """
    Return `ln(exp(x) + 1)`, where x is Tensor.
    """
    return SoftPlus()(x)[0]


class Sub(Operator):
    """
    Performs element-wise binary subtraction (with Numpy-style broadcasting
    support).
    """

    def __init__(self):
        super(Sub, self).__init__()

    def forward(self, a, b):
        """
        Return `a-b`, where x is CTensor.
        """
        ori_type = None
        if a.data_type() != singa.kFloat32:
            ori_type = a.data_type()
            a = a.AsType(singa.kFloat32)
            b = b.AsType(singa.kFloat32)
        res = singa.__sub__(a, b)
        if ori_type is not None:
            res = res.AsType(ori_type)
        if training:
            self.shape0 = list(a.shape())
            self.shape1 = list(b.shape())
            self.shape3 = list(res.shape())
        return res

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            a tuple for (da, db), da is data for dL / da, db is data
                for dL / db.
        """
        dx0 = dy
        dx1 = singa.MultFloat(dy, -1.0)
        if (type(dy) == float) or self.shape0 == self.shape1:
            assert self.shape0 == self.shape1, ('should have same shape')
            return dx0, dx1
        # handle broadcast
        dx0 = back_broadcast(self.shape3, self.shape0, dx0)
        dx1 = back_broadcast(self.shape3, self.shape1, dx1)
        return dx0, dx1


def sub(a, b):
    """
    Return a-b, where a and b are Tensor.
    """
    return Sub()(a, b)[0]


# optimize min to support multi inputs
class Min(Operator):
    """
    Element-wise min of each of the input tensors (with Numpy-style
    broadcasting support).
    """

    def __init__(self):
        super(Min, self).__init__()
        self.masks = []

    def _min(self, a, b):
        """
        Args:
            a (CTensor): First operand
            b (CTensor): Second operand
        Returns:
            CTensor, the output
            tuple of CTensor, mask tensor
        """
        m = singa.__sub__(a, b)
        mask0 = singa.LEFloat(m, 0)
        mask1 = singa.GTFloat(m, 0)
        res = singa.__add__(singa.__mul__(mask0, a), singa.__mul__(mask1, b))
        return res, (mask0, mask1)

    def forward(self, *x):
        """
        Args:
            *x (a list of CTensor): List of tensors for max.
        Returns:
            CTensor, the output
        """
        assert (len(x) > 0)
        self.l = len(x)
        if len(x) == 1:
            res, masks = self._min(x[0], x[0])
            self.masks.append(masks)
            return x[0]
        res, masks = self._min(x[0], x[1])
        self.masks.append(masks)
        for i in range(2, len(x)):
            res, masks = self._min(res, x[i])
            self.masks.append(masks)
        return res

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            a tuple for (*dx), dx is data for dL / dx.
        """
        if self.l == 1:
            return self.masks[0][0]
        else:
            ret = []
            cumulation = None
            for mask0, mask1 in self.masks[::-1]:
                if not cumulation:
                    ret.insert(0, mask1)
                    cumulation = mask0
                else:
                    ret.insert(0, singa.__mul__(cumulation, mask1))
                    cumulation = singa.__mul__(cumulation, mask0)
            ret.insert(0, cumulation)
            return tuple(ret)


def min(*l):
    """
    Element-wise min of each of the input tensors (with Numpy-style
    broadcasting support).
    Args:
        *x (a list of Tensor): List of tensors for max.
    Returns:
        Tensor, the output
    """
    return Min()(*l)[0]


class Log(Operator):
    """
    `y = log(x)`, is applied to the tensor elementwise.
    """

    def __init__(self):
        super(Log, self).__init__()

    def forward(self, x):
        """
        Return `log(x)`, where x is CTensor.
        """
        if training:
            self.input = x
        return singa.Log(x)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        dx = singa.PowFloat(self.input, -1)
        dx = singa.__mul__(dy, dx)
        return dx


def log(x):
    """
    Return log(x), where x is Tensor.
    """
    return Log()(x)[0]


class HardSigmoid(Operator):
    """
    `y = max(0, min(1, alpha * x + beta))`, is applied to the tensor elementwise.
    """

    def __init__(self, alpha=0.2, gamma=0.5):
        """
        Args:
            alpha (float): Value of alpha.
            gamma (float): Value of beta.
        """
        super(HardSigmoid, self).__init__()
        self.alpha = alpha
        self.gamma = gamma

    def forward(self, x):
        """
        Args:
            x (CTensor): matrix
        Returns:
            a CTensor for the result
        """
        x = singa.AddFloat(singa.MultFloat(x, self.alpha), self.gamma)
        if training:
            self.cache = x

        x = singa.ReLU(x)
        mask1 = singa.LTFloat(x, 1.0)
        mask2 = singa.GEFloat(x, 1.0)

        ans = singa.__add__(singa.__mul__(x, mask1), mask2)
        return singa.ReLU(ans)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        mask0 = singa.GTFloat(self.cache, 0.0)
        mask1 = singa.LTFloat(self.cache, 1.0)
        mask = singa.__mul__(mask0, mask1)
        return singa.__mul__(singa.MultFloat(mask, self.alpha), dy)


def hardsigmoid(x, alpha=0.2, gamma=0.5):
    """
    `y = max(0, min(1, alpha * x + beta))`, is applied to the tensor elementwise.
    Args:
        x (Tensor): matrix
        alpha (float): Value of alpha.
        gamma (float): Value of beta.
    Returns:
        a Tensor for the result
    """
    return HardSigmoid(alpha, gamma)(x)[0]


class Squeeze(Operator):
    """
    Remove single-dimensional entries from the shape of a tensor. Takes a
    parameter axes with a list of axes to squeeze. If axes is not provided,
    all the single dimensions will be removed from the shape. If an axis is
    selected with shape entry not equal to one, an error is raised.
    """

    def __init__(self, axis=[]):
        """
        Args:
            axis (list of ints): List of integers indicating the dimensions
                to squeeze. Negative value means counting dimensions from
                the back. Accepted range is [-r, r-1] where r = rank(data).
        """
        super(Squeeze, self).__init__()
        self.axis = axis

    def forward(self, x):
        """
        Args:
            x (CTensor): Input tensor
        Returns:
            CTensor, the output
        """
        self.cache = x.shape()
        newshape = []
        if (self.axis == []):
            newshape = list(filter(lambda i: i != 1, self.cache))
        else:
            for id, i in enumerate(self.axis):
                assert i < len(self.cache)
                self.axis[id] = i % len(self.cache)
                assert self.cache[
                    i] == 1, "the length of axis {} is {}, which should be 1".format(
                        i, self.cache[i])
            for ind, v in enumerate(self.cache):
                if ind not in self.axis:
                    newshape.append(v)
        # todo, need optimize after we have scalar tensor
        if newshape == []:
            return x
        return singa.Reshape(x, newshape)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        return singa.Reshape(dy, self.cache)


def squeeze(x, axis=[]):
    """
    Remove single-dimensional entries from the shape of a tensor. Takes a
    parameter axes with a list of axes to squeeze. If axes is not provided,
    all the single dimensions will be removed from the shape. If an axis is
    selected with shape entry not equal to one, an error is raised.
    Args:
        x (Tensor): Input tensor
        axis (list of ints): List of integers indicating the dimensions
            to squeeze. Negative value means counting dimensions from
            the back. Accepted range is [-r, r-1] where r = rank(data).
    Returns:
        Tensor, the output
    """
    return Squeeze(axis)(x)[0]


class Div(Operator):
    """
    Performs element-wise binary division (with Numpy-style broadcasting support).
    """

    def __init__(self):
        super(Div, self).__init__()

    def forward(self, a, b):
        """
        Return `np.div(a,b)`, where a and b are CTensor.
        """
        ori_type = None
        if a.data_type() != singa.kFloat32:
            ori_type = a.data_type()
            a = a.AsType(singa.kFloat32)
            b = b.AsType(singa.kFloat32)
        res = singa.__mul__(a, singa.PowFloat(b, -1.0))
        # res = singa.__div__(a, b)
        if ori_type is not None:
            res = res.AsType(ori_type)
        if training:
            self.input = (singa.MultFloat(a, -1.0), singa.PowFloat(b, -1.0)
                         )  # -a, 1/b
            self.shape0 = list(a.shape())
            self.shape1 = list(b.shape())
            self.shape3 = list(res.shape())
        return res

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            a CTensor tuple for (da, db), da is data for dL / da, db is data
                for dL / db.
        """
        #dy/dx_0 = b^(-1)
        #dy/dx_1 = (-a)*b^(-2)
        dx0 = singa.__mul__(dy, self.input[1])
        dx1 = singa.__mul__(self.input[0], singa.PowFloat(self.input[1], 2.0))
        dx1 = singa.__mul__(dy, dx1)
        if (type(dy) == float) or self.shape0 == self.shape1:
            assert self.shape0 == self.shape1, ('should have same shape')
            return dx0, dx1
        # handle broadcast
        dx0 = back_broadcast(self.shape3, self.shape0, dx0)
        dx1 = back_broadcast(self.shape3, self.shape1, dx1)
        return dx0, dx1


def div(a, b):
    """
    Return `np.div(a,b)`, where a and b are Tensor.
    """
    return Div()(a, b)[0]


class Shape(Operator):
    """
    Takes a tensor as input and outputs a tensor containing the shape of the
    input tensor.
    """

    def __init__(self):
        super(Shape, self).__init__()

    def forward(self, x):
        """
        Args:
            x (CTensor): Input tensor
        Returns:
            CTensor, the output
        """
        cur = list(x.shape())
        cur = tensor.from_numpy(np.array(cur))
        cur.to_device(x.device())
        return cur.data

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            list of int, the shape of dy
        """
        return list(dy.shape())


def shape(x):
    """
    Takes a tensor as input and outputs a tensor containing the shape of the
    input tensor.
    Args:
        x (Tensor): Input tensor
    Returns:
        Tensor, the output
    """
    return Shape()(x)[0]


# optimize max to support multi inputs
class Max(Operator):
    """
    Element-wise max of each of the input tensors (with Numpy-style
    broadcasting support).
    """

    def __init__(self):
        super(Max, self).__init__()
        self.masks = []

    def _max(self, a, b):
        """
        Args:
            a (CTensor): First operand
            b (CTensor): Second operand
        Returns:
            CTensor, the output
            tuple of CTensor, mask tensor
        """
        m = singa.__sub__(a, b)
        mask0 = singa.GEFloat(m, 0)
        mask1 = singa.LTFloat(m, 0)
        res = singa.__add__(singa.__mul__(mask0, a), singa.__mul__(mask1, b))
        return res, (mask0, mask1)

    def forward(self, *x):
        """
        Args:
            *x (a list of CTensor): List of tensors for max.
        Returns:
            CTensor, the output
        """
        assert (len(x) > 0)
        self.l = len(x)
        if len(x) == 1:
            res, masks = self._max(x[0], x[0])
            self.masks.append(masks)
            return x[0]
        res, masks = self._max(x[0], x[1])
        self.masks.append(masks)
        for i in range(2, len(x)):
            res, masks = self._max(res, x[i])
            self.masks.append(masks)
        return res

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            a tuple for (*dx), dx is data for dL / dx.
        """
        if self.l == 1:
            return self.masks[0][0]
        else:
            ret = []
            cumulation = None
            for mask0, mask1 in self.masks[::-1]:
                if not cumulation:
                    ret.insert(0, mask1)
                    cumulation = mask0
                else:
                    ret.insert(0, singa.__mul__(cumulation, mask1))
                    cumulation = singa.__mul__(cumulation, mask0)
            ret.insert(0, cumulation)
            return tuple(ret)


def max(*l):
    """
    Element-wise max of each of the input tensors (with Numpy-style broadcasting support).
    Args:
        *x (a list of Tensor): List of tensors for max.
    Returns:
        Tensor, the output
    """
    return Max()(*l)[0]


class And(Operator):
    """
    Returns the tensor resulted from performing the and logical operation elementwise on the input tensors A and B (with Numpy-style broadcasting support).
    """

    def __init__(self):
        super(And, self).__init__()

    def forward(self, a, b):
        """
        Return `np.logical_and(a,b)`, where a and b are CTensor.
        """
        m = singa.__mul__(a, b)
        cur = singa.PowFloat(singa.Sign(m), 2)

        return cur

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Raises:
            AssertionError: no backward function for this operator
        """
        assert False, ('no gradient for backward function')


def _and(a, b):
    """
    Return `np.logical_and(a,b)`, where a and b are Tensor.
    """
    return And()(a, b)[0]


class Or(Operator):
    """
    Returns the tensor resulted from performing the or logical operation elementwise on the input tensors A and B (with Numpy-style broadcasting support).
    """

    def __init__(self):
        super(Or, self).__init__()

    def forward(self, a, b):
        """
        Return `np.logical_or(a,b)`, where a and b are CTensor.
        """
        m = singa.__add__(singa.PowFloat(singa.Sign(a), 2.0),
                          singa.PowFloat(singa.Sign(b), 2.0))
        cur = singa.Sign(m)

        return cur

    def backward(self, dy):
        """
        Args:
            dy (CTensor): data for the `dL / dy`, L is the loss.
        Raises:
            AssertionError: no backward function for this operator
        """
        assert False, ('no gradient for backward function')


def _or(a, b):
    """
    Return np.logical_or(a,b), where a and b are Tensor.
    """
    return Or()(a, b)[0]


class Not(Operator):
    """
    Returns the negation of the input tensor element-wise.
    """

    def __init__(self):
        super(Not, self).__init__()

    def forward(self, x):
        """
        Return `np.logical_not(x)`, where x is CTensor.
        """
        mask0 = singa.GEFloat(x, 0)
        mask1 = singa.LEFloat(x, 0)
        cur = singa.__mul__(mask0, mask1)

        return cur

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Raises:
            AssertionError: no backward function for this operator
        """
        assert False, ('no gradient for backward function')


def _not(x):
    """
    Return `np.logical_not(x)`, where x is Tensor.
    """
    return Not()(x)[0]


class Xor(Operator):
    """
    Performing the xor logical operation elementwise on the input tensors A and B (with Numpy-style broadcasting support).
    """

    def __init__(self):
        super(Xor, self).__init__()

    def forward(self, a, b):
        """
        Return `np.logical_xor(a,b)`, where a and b are CTensor.
        """
        m = singa.__sub__(singa.PowFloat(singa.Sign(a), 2.0),
                          singa.PowFloat(singa.Sign(b), 2.0))
        cur = singa.PowFloat(singa.Sign(m), 2.0)

        return cur

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Raises:
            AssertionError: no backward function for this operator
        """
        assert False, ('no gradient for backward function')


def _xor(a, b):
    """
    Return `np.logical_xor(a,b)`, where a and b are Tensor.
    """
    return Xor()(a, b)[0]


class Negative(Operator):
    """
    `y = -x`, is applied to the tensor elementwise.
    """

    def __init__(self):
        super(Negative, self).__init__()

    def forward(self, x):
        """
        Return `-x`, where x is CTensor.
        """
        #y=-x
        return singa.MultFloat(x, -1)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        return singa.MultFloat(dy, -1)


def negative(x):
    """
    Return `-x`, where x is Tensor.
    """
    return Negative()(x)[0]


class Reciprocal(Operator):
    """
    `y = 1/x`, is applied to the tensor elementwise.
    """

    def __init__(self):
        super(Reciprocal, self).__init__()

    def forward(self, x):
        """
        Return `1/x`, where x is CTensor.
        """
        #y=1/x elementwise
        if training:
            self.input = x

        return singa.PowFloat(x, -1)

    def backward(self, dy):
        """
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        #dy/dx = -1/x**2
        dx = singa.MultFloat(singa.PowFloat(self.input, -2), -1)
        return singa.__mul__(dy, dx)


def reciprocal(x):
    """
    Return 1/x, where x is Tensor.
    """
    return Reciprocal()(x)[0]


class Gemm(Operator):
    """
    Init a General Matrix multiplication(Gemm) operator. Compute `Y = alpha *
    A' * B' + beta * C`, where input tensor A has shape (M, K) or (K, M), input
    tensor B has shape (K, N) or (N, K), input tensor C is broadcastable to
    shape (M, N), and output tensor Y has shape (M, N).
    `A' = transpose(A)` if transA else A
    `B' = transpose(B)` if transB else B
    """

    def __init__(self, alpha=1.0, beta=1.0, transA=0, transB=0):
        """
        Args:
            alpha (float): Scalar multiplier for the product of input tensors
                A * B.
            beta (float): Scalar multiplier for input tensor C.
            ransA (int): Whether A should be transposed
            transB (int): Whether B should be transposed
        Returns:
            CTensor, the output
        """
        super(Gemm, self).__init__()
        self.alpha = alpha
        self.beta = beta
        self.transA = transA
        self.transB = transB

    def forward(self, A, B, C=None):
        """
        forward propogation of Gemm
        Args:
            A (CTensor): The shape of A should be (M, K) if transA is 0, or
                (K, M) if transA is non-zero.
            B (CTensor): The shape of B should be (K, N) if transB is 0, or
                (N, K) if transB is non-zero.
            C (CTensor): (optional), Optional input tensor C. If not specified,
                the computation is done as if C is a scalar 0. The shape of C
                should be unidirectional broadcastable to (M, N).
        Returns:
            tensor, the output
        """
        _A = singa.DefaultTranspose(A) if self.transA == 1 else A
        _B = singa.DefaultTranspose(B) if self.transB == 1 else B
        if training:
            self.inputs = (_A, _B, C)
        tmpM = singa.MultFloat(singa.Mult(_A, _B), self.alpha)
        if C:
            tmpM = singa.__add__(tmpM, singa.MultFloat(C, self.beta))
        return tmpM

    def backward(self, dy):
        """
        backward propogation of Gemm
        Args:
            dy (CTensor): The shape of A should be (M, K) if transA is 0, or (K, M) if transA is non-zero.
        Returns:
            CTensor, the gradient over A
            CTensor, the gradient over B
            CTensor(optional), the gradient over C
        """
        _A, _B, C = self.inputs
        # y = alpha * A  * B  => da = alpha * dy * BT
        # y = alpha * A  * BT => da = alpha * dy * B
        # y = alpha * AT * B  => da = alpha * B * dyT = alpha * (dy * BT)T
        # y = alpha * AT * BT => da = alpha * BT * dyT = alpha * (dy * B)T
        da = singa.MultFloat(singa.Mult(dy, singa.DefaultTranspose(_B)),
                             self.alpha)
        if self.transA:
            da = singa.DefaultTranspose(da)

        # y = alpha * A  * B  => db = alpha * AT * dy
        # y = alpha * AT * B  => db = alpha * A * dy
        # y = alpha * A  * BT => db = alpha * dyT * A = alpha * (AT * dy)T
        # y = alpha * AT * BT => db = alpha * dyT * AT = alpha * (A * dy)T
        db = singa.MultFloat(singa.Mult(singa.DefaultTranspose(_A), dy),
                             self.alpha)
        if self.transB:
            db = singa.DefaultTranspose(db)
        if C:
            dc = back_broadcast(dy.shape(), C.shape(),
                                singa.MultFloat(dy, self.beta))
            return da, db, dc
        else:
            return da, db


def gemm(A, B, C=None, alpha=1.0, beta=1.0, transA=0, transB=0):
    """
    Init a General Matrix multiplication(Gemm) operator. Compute `Y = alpha *
    A' * B' + beta * C`, where input tensor A has shape (M, K) or (K, M), input
    tensor B has shape (K, N) or (N, K), input tensor C is broadcastable to
    shape (M, N), and output tensor Y has shape (M, N).
    `A' = transpose(A)` if transA else A
    `B' = transpose(B)` if transB else B
    Args:
        A (Tensor): The shape of A should be (M, K) if transA is 0, or
            (K, M) if transA is non-zero.
        B (Tensor): The shape of B should be (K, N) if transB is 0, or
            (N, K) if transB is non-zero.
        C (Tensor): (optional), Optional input tensor C. If not specified,
            the computation is done as if C is a scalar 0. The shape of C
            should be unidirectional broadcastable to (M, N).
        alpha (float): Scalar multiplier for the product of input tensors A * B.
        beta (float): Scalar multiplier for input tensor C.
        ransA (int): Whether A should be transposed
        transB (int): Whether B should be transposed
    Returns:
        Tensor, the output
    """
    if C:
        return Gemm(alpha, beta, transA, transB)(A, B, C)[0]
    else:
        return Gemm(alpha, beta, transA, transB)(A, B)[0]


class GlobalAveragePool(Operator):
    """
    Init a GlobalAveragePool operator
    """

    def __init__(self, data_format='channels_first'):
        """
        Args:
            data_format (string): A string, we support two formats:
                channels_last and channels_first, default is channels_first.
                channels_first means the format of input is (N x C x H x W)
                channels_last means the format of input is (N x H x W x C)
        """
        super(GlobalAveragePool, self).__init__()
        self.data_format = data_format

    def forward(self, x):
        """
        forward propogation of GlobalAveragePool
        Args:
            x (CTensor): the input tensor
        Returns:
            CTensor, the output
        """
        if training:
            self.mask = singa.Tensor(x.shape(), x.device())

        shape = list(x.shape())

        # (N x C x H x W) for channels_first
        if self.data_format == 'channels_first':
            axes = tuple(i for i in range(2, len(shape)))
            self.shape_divisor = 1 / np.prod(shape[2:])
        else:  # (N x H x W x C) for channels_last
            axes = tuple(i for i in range(1, len(shape) - 1))
            self.shape_divisor = 1 / np.prod(shape[1:-1])

        # output shape
        # (N x C x 1 x 1) for channels_first
        # (N x 1 x 1 x C) for channels_last
        for i in axes:
            shape[i] = 1

        x = tensor.from_raw_tensor(x)
        x = tensor.sum(x, axis=axes)
        x = tensor.reshape(x, shape)
        return singa.MultFloat(x.data, self.shape_divisor)

    def backward(self, dy):
        """
        backward propogation of GlobalAveragePool
        Args:
            dy (CTensor): the gradient tensor from upper operations
        Returns:
            CTensor, the gradient over input
        """
        self.mask.SetFloatValue(self.shape_divisor)
        return singa.__mul__(self.mask, dy)


def globalaveragepool(x, data_format='channels_first'):
    """
    GlobalAveragePool operator
    Args:
        x (Tensor): the input tensor
        data_format (string): A string, we support two formats:
            channels_last and channels_first, default is channels_first.
            channels_first means the format of input is (N x C x H x W)
            channels_last means the format of input is (N x H x W x C)
    Returns:
        Tensor, the output
    """
    return GlobalAveragePool(data_format)(x)[0]


class ConstantOfShape(Operator):
    """
    Init a ConstantOfShape, generate a tensor with given value and shape.
    """

    def __init__(self, value=0.):
        """
        Args:
            value (float): (Optional) The value of the output elements. Should
                be a one-element value. If not specified, it defaults to 0 and
                datatype float32
        """
        super(ConstantOfShape, self).__init__()
        self.value = value

    def forward(self, x):
        """
        forward of ConstantOfShape
        Args:
            x: CTensor, 1D tensor. The shape of the expected output tensor.
                All values must be >= 0.
        Returns:
            the output CTensor. If attribute 'value' is specified, the value
                and datatype of the output tensor is taken from 'value'. If
                attribute 'value' is not specified, the value in the output
                defaults to 0, and the datatype defaults to float32.
        """
        x_shape = tensor.to_numpy(tensor.from_raw_tensor(x)).astype(
            np.int64).tolist()
        assert np.min(x_shape) >= 0, ('shape cannot be negative')
        x = CTensor(x_shape, x.device())
        x.SetFloatValue(self.value)
        return x

    def backward(self, dy):
        """
        backward of ConstantOfShape
        Args:
            dy (CTensor): gradient tensor.
        Raises:
            AssertionError: no backward function for this operator
        """
        assert False, ('no gradient for backward function')


def constant_of_shape(x, value=0):
    """
    Init a ConstantOfShape, generate a tensor with given value and shape.
    Args:
        x: Tensor, 1D tensor. The shape of the expected output tensor.
            All values must be >= 0.
        value (float): (Optional) The value of the output elements. Should
            be a one-element value. If not specified, it defaults to 0 and
            datatype float32
    Returns:
        the output Tensor. If attribute 'value' is specified, the value
            and datatype of the output tensor is taken from 'value'. If
            attribute 'value' is not specified, the value in the output
            defaults to 0, and the datatype defaults to float32.
    """
    return ConstantOfShape(value)(x)[0]


class Dropout(Operator):
    """
    Init a Dropout, which scales the masked input data by the following equation:
    `output = scale * data * mask`, `scale = 1. / (1. - ratio)`.
    """

    def __init__(self, seed=0, ratio=0.5):
        """
        Args:
            seed (int): the random seed
            ratio (float): the ratio of random dropout, with value in [0, 1).
        """
        super(Dropout, self).__init__()
        self.ratio = ratio
        self.seed = int(seed)
        self.init_seed = False

    def forward(self, x):
        """
        forward of Dropout
        Args:
            x (CTensor): input tensor.
        Returns:
            the output CTensor.
        """
        if not self.init_seed:
            x.device().SetRandSeed(self.seed)
            self.init_seed = True
        if training:
            self.scale = 1 / 1 - self.ratio
            self.mask = singa.Tensor(list(x.shape()), x.device())
            singa.Bernoulli(1 - self.ratio, self.mask)
            x = singa.MultFloat(singa.__mul__(self.mask, x), self.scale)
        return x

    def backward(self, dy):
        """
        backward of Dropout
        Args:
            dy (CTensor): gradient tensor.
        Returns:
            the gradient tensor over input tensor.
        """
        if training:
            dy = singa.MultFloat(singa.__mul__(self.mask, dy), self.scale)
        return dy


def dropout(x, seed=0, ratio=0.5):
    """
    Init a Dropout, which scales the masked input data by the following
    equation: `output = scale * data * mask`, `scale = 1. / (1. - ratio)`.
    Args:
        x (Tensor): input tensor.
        ratio (float): the ratio of random dropout, with value in [0, 1).
    Returns:
        the output Tensor.
    """
    return Dropout(seed, ratio)(x)[0]


class ReduceSum(Operator):
    """
    Init a ReduceSum, computes the sum of the input tensor's element along
    the provided axes.
    """

    def __init__(self, axes=None, keepdims=1):
        """
        Args:
            axes (list of int): A list of integers, along which to reduce.
                Accepted range is [-r, r-1] where r = rank(data). The default
                is None, which reduces over all the dimensions of the input tensor.
            keepdims (int): Keep the reduced dimension or not, default 1 mean
                keep reduced dimension.
        """
        super(ReduceSum, self).__init__()
        self.axes = axes
        self.keepdims = keepdims

    def forward(self, x):
        """
        forward of ReduceSum
        Args:
            x (CTensor): input tensor.
        Returns:
            the output CTensor.
        """
        _x = tensor.from_raw_tensor(x)
        x_shape = list(_x.shape)
        # handle the special axes
        if self.axes is None:
            self.axes = [i for i in range(len(x_shape))]  # axes = None
        else:
            self.axes = [i if i >= 0 else len(x_shape) + i for i in self.axes
                        ]  # axes has negative
        self.axes.sort(reverse=True)
        for axis in self.axes:
            _x = tensor.sum(_x, axis)
            x_shape[axis] = 1
        if self.keepdims == 1:
            _x = tensor.reshape(_x, x_shape)
        self.cache = (x_shape, x)
        return _x.data

    def backward(self, dy):
        """
        backward of ReduceSum
        Args:
            dy (CTensor): gradient tensor.
        Returns:
            the gradient tensor over input tensor.
        """
        x_shape, x = self.cache
        dy = singa.Reshape(dy, x_shape)
        scale = np.prod(x_shape) / np.prod(x.shape())
        mask = singa.Tensor(list(x.shape()), x.device())
        mask.SetFloatValue(scale)
        dy = singa.__mul__(mask, dy)
        return dy


def reduce_sum(x, axes=None, keepdims=1):
    """
    Init a ReduceSum, computes the sum of the input tensor's element along
    the provided axes.
    Args:
        x (Tensor): input tensor.
        axes (list of int): A list of integers, along which to reduce.
            Accepted range is [-r, r-1] where r = rank(data). The default
            is None, which reduces over all the dimensions of the input tensor.
        keepdims (int): Keep the reduced dimension or not, default 1 mean
            keep reduced dimension.
    Returns:
        the output Tensor.
    """
    return ReduceSum(axes, keepdims)(x)[0]


class ReduceMean(Operator):
    """
    Init a ReduceMean, computes the mean of the input tensor's element along
    the provided axes.
    """

    def __init__(self, axes=None, keepdims=1):
        """
        Args:
            axes (list of int): A list of integers, along which to reduce.
                Accepted range is [-r, r-1] where r = rank(data). The default
                is None, which reduces over all the dimensions of the input tensor.
            keepdims (int): Keep the reduced dimension or not, default 1 mean
                keep reduced dimension.
        """
        super(ReduceMean, self).__init__()
        self.axes = axes
        self.keepdims = keepdims

    def forward(self, x):
        """
        forward of ReduceMean
        Args:
            x (CTensor): input tensor.
        Returns:
            the output CTensor.
        """
        _x = tensor.from_raw_tensor(x)
        x_shape = list(_x.shape)
        # handle the special axes
        if self.axes is None:
            self.axes = [i for i in range(len(x_shape))]  # axes = None
        else:
            self.axes = [i if i >= 0 else len(x_shape) + i for i in self.axes
                        ]  # axes has negative
        self.axes.sort(reverse=True)
        for axis in self.axes:
            _x = tensor.sum(_x, axis)
            x_shape[axis] = 1
        if self.keepdims == 1:
            _x = tensor.reshape(_x, x_shape)
        self.cache = (x_shape, x)
        scale = np.prod(x_shape) / np.prod(x.shape())
        self.scale = scale
        _x = singa.MultFloat(_x.data, scale)
        return _x

    def backward(self, dy):
        """
        backward of ReduceMean
        Args:
            dy (CTensor): gradient tensor.
        Returns:
            the gradient tensor over input tensor.
        """
        x_shape, x = self.cache
        dy = singa.Reshape(dy, x_shape)
        mask = singa.Tensor(list(x.shape()), x.device())
        mask.SetFloatValue(1.0)
        dy = singa.__mul__(mask, dy)
        dy = singa.MultFloat(dy, self.scale)
        return dy


def reduce_mean(x, axes=None, keepdims=1):
    """
    Init a ReduceMean, computes the mean of the input tensor's element along
    the provided axes.
    Args:
        x (Tensor): input tensor.
        axes (list of int): A list of integers, along which to reduce.
            Accepted range is [-r, r-1] where r = rank(data). The default
            is None, which reduces over all the dimensions of the input tensor.
        keepdims (int): Keep the reduced dimension or not, default 1 mean
            keep reduced dimension.
    Returns:
        the output Tensor.
    """
    return ReduceMean(axes, keepdims)(x)[0]


class Slice(Operator):
    """
    Init a Slice, Produces a slice of the input tensor along multiple axes.
    Similar to numpy: https://docs.scipy.org/doc/numpy/reference/arrays.indexing.html
    """

    def __init__(self, starts, ends, axes=None, steps=None):
        """
        Args:
            starts (list of int): starting indices of corresponding axis
            ends (list of int): ending indices of corresponding axis
            axes (list of int): axes that `starts` and `ends` apply to.
                Negative value means counting dimensions from the back.
                Accepted range is [-r, r-1] where r = rank(data).
            steps (list of int): slice step of corresponding axis in `axes`.
                Negative value means slicing backward. 'steps' cannot be 0.
                Defaults to 1.
        """
        super(Slice, self).__init__()
        self.starts = starts
        self.ends = ends
        self.axes = axes
        self.steps = steps

    def forward(self, x):
        """
        forward of Slice
        Args:
            x (CTensor): input tensor.
        Returns:
            the output CTensor.
        """
        x_shape = list(x.shape())
        # handle the special axes
        if self.axes is None:
            self.axes = [i for i in range(len(x_shape))]  # axes = None
        else:
            self.axes = [i if i >= 0 else len(x_shape) + i for i in self.axes
                        ]  # axes has negative
        self.cache = []
        # handle the special steps
        if self.steps is None:
            self.steps = [1] * len(x_shape)  # steps = None
        for idx, axis in enumerate(self.axes):
            axis = int(axis)
            start, end, step = self.starts[idx], self.ends[idx], self.steps[idx]
            if end > x_shape[axis]:
                end = x_shape[axis]
            self.cache.append((axis, x_shape[axis], start, end, step))
            xs = []
            for step_idx in range(x_shape[axis])[start:end:step]:
                xs.append(singa.SliceOn(x, step_idx, step_idx + 1, axis))
            assert len(xs) > 0, "Cannot support empty tensor"
            x = singa.VecTensor(xs)
            x = singa.ConcatOn(x, axis)
        return x

    def backward(self, dy):
        """
        backward of Slice
        Args:
            dy (CTensor): gradient tensor.
        Returns:
            the gradient tensor over input tensor.
        """
        for axis, shape, start, end, step in self.cache[::-1]:
            data_idxes = tuple(range(shape)[start:end:step])
            dys = []
            data_idx = 0
            for step_idx in range(shape):
                if step_idx in data_idxes:
                    tmp_tensor = singa.SliceOn(dy, data_idx, data_idx + 1, axis)
                    data_idx += 1
                else:
                    tmp_shape = list(dy.shape())
                    tmp_shape[axis] = 1
                    tmp_tensor = singa.Tensor(tmp_shape, dy.device())
                    tmp_tensor.SetFloatValue(0.)
                dys.append(tmp_tensor)
            dys = singa.VecTensor(dys)
            dy = singa.ConcatOn(dys, axis)
        return dy


def slice(x, starts, ends, axes=None, steps=None):
    """
    Init a Slice, Produces a slice of the input tensor along multiple axes.
    Similar to numpy: https://docs.scipy.org/doc/numpy/reference/arrays.indexing.html
    Args:
        x (Tensor): input tensor.
        starts (list of int): starting indices of corresponding axis
        ends (list of int): ending indices of corresponding axis
        axes (list of int): axes that `starts` and `ends` apply to.
            Negative value means counting dimensions from the back.
            Accepted range is [-r, r-1] where r = rank(data).
        steps (list of int): slice step of corresponding axis in `axes`.
            Negative value means slicing backward. 'steps' cannot be 0.
            Defaults to 1.
    Returns:
        the output Tensor.
    """
    return Slice(starts, ends, axes, steps)(x)[0]


class Ceil(Operator):
    """
    Ceil takes one input data (Tensor) and produces one output data (Tensor)
    where the ceil is, `y = ceil(x)`, is applied to the tensor elementwise.
    """

    def __init__(self):
        super(Ceil, self).__init__()

    def forward(self, x):
        """
        forward of Ceil
        Args:
            x (CTensor): input tensor.
        Returns:
            the output CTensor.
        """
        return singa.Ceil(x)

    def backward(self, dy):
        """
        backward of Ceil
        Args:
            dy (CTensor): gradient tensor.
        Returns:
            the gradient tensor over input tensor.
        """
        dy = singa.Tensor(dy.shape(), dy.device())
        dy.SetFloatValue(0.)
        return dy


def ceil(x):
    """
    Ceil takes one input data (Tensor) and produces one output data (Tensor)
    where the ceil is, `y = ceil(x)`, is applied to the tensor elementwise.
    Args:
        x (Tensor): input tensor.
    Returns:
        the output Tensor.
    """
    return Ceil()(x)[0]


class Floor(Operator):
    """
    Floor takes one input data (Tensor) and produces one output data (Tensor), 
    where the floor is, `y = floor(x)`, is applied to the tensor elementwise
    """

    def __init__(self):
        super(Floor, self).__init__()

    def forward(self, x):
        """
        forward of floor
        Args: 
            x (CTensor): input tensor
        Returns:
            the output CTensor    
        """
        return singa.Floor(x)

    def backward(self, dy):
        """
        backward of floor. Derivative of floor is 0
        Args: 
            dy (CTensor): gradient tensor
        Returns:
            the gradient tensor over the input tensor. 
        """
        dy = singa.Tensor(dy.shape(), dy.device())
        dy.SetFloatValue(0.)
        return dy


def floor(x):
    """
    floor takes one input data (Tensor) and produces one output data (Tensor)
    the value of floor is `y = floor(x)`, is applied to the tensor elementwise. 
    Args: 
        x(Tensor): input tensor.
    Returns: 
        the output tensor    
    """
    return Floor()(x)[0]


class Split(Operator):
    """
    Init a Split, Split a tensor into a list of tensors, along the specified
    'axis'.
    """

    def __init__(self, axis, parts, num_output=None):
        """
        Args:
            axis (int): which axis to split on. A negative value means
                counting dimensions from the back. Accepted range is
                [-rank, rank-1] where r = rank(input).
            parts (list of int): length of each output, which can be specified
                using argument 'parts'. Otherwise, the tensor is parts to equal
                sized parts.
            num_output (bool): once parts is none, the tensor is split to equal
                sized parts for each output.
        """
        super(Split, self).__init__()
        self.axis = axis
        self.parts = parts
        self.num_output = num_output
        if self.parts is None:
            assert self.num_output is not None, "For (parts, num_output), it at least requires one."

    def forward(self, x):
        """
        forward of Split
        Args:
            x (CTensor): input tensor.
        Returns:
            the output CTensor.
        """
        x_shape = list(x.shape())
        self.axis = self.axis % len(x_shape)
        if self.parts is None:
            self.parts = [x_shape[self.axis] // self.num_output
                         ] * self.num_output
        xs = []
        _s = 0
        for _l in self.parts:
            xs.append(singa.SliceOn(x, _s, _s + _l, self.axis))
            _s += _l
        return tuple(xs)

    def backward(self, *dys):
        """
        backward of Split
        Args:
            dys: list of CTensor, gradient tensor.
        Returns:
            the gradient tensor over input tensor.
        """
        dys = singa.VecTensor(dys)
        dy = singa.ConcatOn(dys, self.axis)
        return dy


def split(x, axis, parts, num_output=None):
    """
    Init a Split, Split a tensor into a list of tensors, along the specified
    'axis'.
    Args:
        x (Tensor): input tensor.
        axis (int): which axis to split on. A negative value means
            counting dimensions from the back. Accepted range is
            [-rank, rank-1] where r = rank(input).
        parts (list of int): length of each output, which can be specified
            using argument 'parts'. Otherwise, the tensor is parts to equal
            sized parts.
        num_output (bool): once parts is none, the tensor is split to equal
            sized parts for each output.
    Returns:
        the output Tensor.
    """
    return Split(axis, parts, num_output)(x)


class Gather(Operator):
    """
    Init a Gather, Given data tensor of rank r >= 1, and indices tensor of
    rank q, gather entries of the axis dimension of data (by default outer-most
    one as axis=0) indexed by indices, and concatenates them in an output tensor of rank `q + (r - 1)`.
    """

    def __init__(self, axis, indices):
        """
        Args:
            axis (int): which axis to slice on. A negative value means counting
                dimensions from the back. Accepted range is [-rank, rank-1]
                where r = rank(input).
            indices (list of int): entries of the axis dimension of data.
        """
        super(Gather, self).__init__()
        self.axis = axis
        self.indices = indices

    def forward(self, x):
        """
        forward of Gather
        Args:
            x (CTensor): input tensor.
        Returns:
            the output CTensor.
        """
        self.x_shape = list(x.shape())
        self.axis = self.axis % len(self.x_shape)  # handle the negative value
        _shape = self.x_shape[self.axis]
        xs = []
        for indice in self.indices:
            # each indice is a sub-indice
            if isinstance(indice, (tuple, list, np.ndarray)):
                sub_xs = []
                for idx in indice:
                    idx = int(idx % _shape)
                    tmp_tensor = singa.SliceOn(x, idx, idx + 1, self.axis)
                    sub_xs.append(tmp_tensor)
                sub_xs = singa.VecTensor(sub_xs)
                tmp_tensor = singa.ConcatOn(sub_xs, self.axis)
                _slice_shape = list(tmp_tensor.shape())
                _slice_shape.insert(self.axis, 1)  # add a new axis to concat
                tmp_tensor = singa.Reshape(tmp_tensor, _slice_shape)
            else:
                indice = int(indice % _shape)
                tmp_tensor = singa.SliceOn(x, indice, indice + 1, self.axis)
            xs.append(tmp_tensor)
        xs = singa.VecTensor(xs)
        return singa.ConcatOn(xs, self.axis)

    def backward(self, dy):
        """
        backward of Gather
        Args:
            dy (CTensor): gradient tensor.
        Returns:
            the gradient tensor over input tensor.
        """
        _shape = self.x_shape[self.axis]

        def construct_dx(dy, axis, indices, _shape):
            dys = []
            data_idx = 0
            data_idxes = tuple(indices)
            for step_idx in range(_shape):
                if step_idx in data_idxes:
                    tmp_tensor = singa.SliceOn(dy, data_idx, data_idx + 1, axis)
                    data_idx += 1
                else:
                    tmp_shape = list(dy.shape())
                    tmp_shape[axis] = 1
                    tmp_tensor = singa.Tensor(tmp_shape, dy.device())
                    tmp_tensor.SetFloatValue(0.)
                dys.append(tmp_tensor)
            dys = singa.VecTensor(dys)
            dy = singa.ConcatOn(dys, axis)
            return dy

        if isinstance(self.indices[0], tuple) or isinstance(
                self.indices[0], list):
            dx = singa.Tensor(self.x_shape, dy.device())
            dx.SetFloatValue(0.)
            for data_idx in range(len(self.indices)):
                # get a piece of the dy and remove its new axis added at forward
                tmp_tensor = singa.SliceOn(dy, data_idx, data_idx + 1,
                                           self.axis)
                _slice_shape = list(tmp_tensor.shape())
                del _slice_shape[self.axis]
                tmp_tensor = singa.Reshape(tmp_tensor, _slice_shape)
                # construct dx and sum them together
                tmp_tensor = construct_dx(tmp_tensor, self.axis,
                                          self.indices[data_idx],
                                          self.x_shape[self.axis])
                dx = singa.__add__(dx, tmp_tensor)
            return dx
        else:
            return construct_dx(dy, self.axis, self.indices, _shape)


def gather(x, axis, indices):
    """
    Init a Gather, Given data tensor of rank r >= 1, and indices tensor of
    rank q, gather entries of the axis dimension of data (by default outer-most
    one as axis=0) indexed by indices, and concatenates them in an output tensor of rank `q + (r - 1)`.
    Args:
        x (Tensor): input tensor.
        axis (int): which axis to slice on. A negative value means counting
            dimensions from the back. Accepted range is [-rank, rank-1]
            where r = rank(input).
        indices (list of int): entries of the axis dimension of data.
    Returns:
        the output Tensor.
    """
    return Gather(axis, indices)(x)[0]


class Tile(Operator):
    """
    Init a Tile, Constructs a tensor by tiling a given tensor. This is the same
    as function tile in Numpy: https://docs.scipy.org/doc/numpy/reference/generated/numpy.tile.html
    """

    def __init__(self, repeats):
        """
        Args:
            repeats (list of int): 1D int matrix of the same length as input's
                dimension number, includes numbers of repeated copies along
                input's dimensions.
        """
        super(Tile, self).__init__()
        self.repeats = [repeats] if isinstance(repeats, int) else repeats

    def forward(self, x):
        """
        forward of Tile
        Args:
            x (CTensor): input tensor.
        Returns:
            the output CTensor.
        """
        self.x_shape = list(x.shape())
        # add new axis from head
        if len(self.x_shape) < len(self.repeats):
            append_len = len(self.repeats) - len(self.x_shape)
            new_shape = [1] * append_len + self.x_shape
            x = singa.Reshape(x, new_shape)
        for axis, rp in enumerate(self.repeats):
            if rp == 1:
                continue
            xs = []
            for idx in range(rp):
                xs.append(x.Clone())
            xs = singa.VecTensor(xs)
            x = singa.ConcatOn(xs, axis)
        return x

    def backward(self, dy):
        """
        backward of Tile
        Args:
            dy (CTensor): gradient tensor.
        Returns:
            the gradient tensor over input tensor.
        """
        for axis, rp in enumerate(self.repeats):
            if rp == 1:
                continue
            _slice_shape = list(dy.shape())
            ori_len = _slice_shape[axis] // rp
            _slice_shape[axis] = ori_len
            _dy = singa.Tensor(_slice_shape, dy.device())
            _dy.SetFloatValue(0.)

            for idx in range(rp):
                tmp_tensor = singa.SliceOn(dy, ori_len * idx,
                                           ori_len * (idx + 1), axis)
                _dy = singa.__add__(_dy, tmp_tensor)
            dy = _dy
        # remove the new axis we added at forward
        if len(self.x_shape) < len(self.repeats):
            dy = singa.Reshape(dy, self.x_shape)
        return dy


def tile(x, repeats):
    """
    Init a Tile, Constructs a tensor by tiling a given tensor. This is the same
    as function tile in Numpy: https://docs.scipy.org/doc/numpy/reference/generated/numpy.tile.html
    Args:
        x (Tensor): input tensor.
        repeats (list of int): 1D int matrix of the same length as input's
            dimension number, includes numbers of repeated copies along
            input's dimensions.
    Returns:
        the output Tensor.
    """
    return Tile(repeats)(x)[0]


class NonZero(Operator):
    """
    Init a NonZero, Constructs a tensor by tiling a given tensor. This is the same
    as function tile in Numpy: https://docs.scipy.org/doc/numpy/reference/generated/numpy.tile.html
    """

    def __init__(self):
        super(NonZero, self).__init__()

    def forward(self, x):
        """
        forward of NonZero
        Args:
            x (CTensor): input tensor.
        Returns:
            the output CTensor.
        """
        y = tensor.to_numpy(tensor.from_raw_tensor(x))
        y = np.array((np.nonzero(y))).astype(np.int32)
        y = tensor.from_numpy(y)
        y.to_device(x.device())
        return y.data

    def backward(self, dy):
        """
        backward of NonZero
        Args:
            dy (CTensor): gradient tensor.
        Raises:
            AssertionError: no backward function for this operator
        """
        assert False, ('no gradient for backward function')


def nonzero(x):
    """
    Init a NonZero, Constructs a tensor by tiling a given tensor. This is the same
    as function tile in Numpy: https://docs.scipy.org/doc/numpy/reference/generated/numpy.tile.html
    Args:
        x (Tensor): input tensor.
    Returns:
        the output Tensor.
    """
    return NonZero()(x)[0]


class Cast(Operator):
    """
    The operator casts the elements of a given input tensor to a data type
    specified by the 'to' argument and returns an output tensor of the same
    size in the converted type.
    """

    def __init__(self, to):
        """
        Args:
            to (int): data type, float32 = 0; int = 2.
        """
        super(Cast, self).__init__()
        self.to = to

    def forward(self, x):
        """
        forward of Cast
        Args:
            x (CTensor): input tensor.
        Returns:
            the output CTensor.
        """
        if x.data_type() != self.to:
            x = x.AsType(self.to)
        return x

    def backward(self, dy):
        """
        backward of Cast
        Args:
            dy (CTensor), gradient tensor.
        Raises:
            AssertionError: no backward function for this operator
        """
        assert False, ('no gradient for backward function')


def cast(x, to):
    """
    The operator casts the elements of a given input tensor to a data type
    specified by the 'to' argument and returns an output tensor of the same
    size in the converted type.
    Args:
        x (Tensor): input tensor.
        to (int): data type, float32 = 0; int = 2.
    Returns:
        the output Tensor.
    """
    return Cast(to)(x)[0]


class OneHot(Operator):
    """
    Produces a one-hot tensor based on inputs.
    """

    def __init__(self, axis, depth, values):
        """
        Args:
            axis (int): Axis along which one-hot representation in added.
                Default: axis=-1. axis=-1 means that the additional dimension
                will be inserted as the innermost/last dimension in the output
                tensor.
            depth (int): Scalar specifying the number of classes in one-hot
                tensor. This is also the size of the one-hot dimension
                (specified by 'axis' attribute) added on in the output tensor.
                The values in the 'indices' input tensor are expected to be in
                the range [-depth, depth-1].
            values (float): Rank 1 tensor containing exactly two elements, in
                the format [off_value, on_value], where 'on_value' is the
                value used for filling locations specified in 'indices' input
                tensor,
        """
        super(OneHot, self).__init__()
        self.axis = axis
        self.depth = depth
        self.values = values

    def forward(self, indices):
        """
        forward of OneHot, we borrow this function from onnx
        Args:
            indices (CTensor): Scalar specifying the number of classes in
                one-hot tensor. The values in the 'indices' input tensor are
                expected to be in the range [-depth, depth-1].
        Returns:
            the output CTensor.
        """
        values = tensor.to_numpy(tensor.from_raw_tensor(indices))
        rank = len(values.shape)
        depth_range = np.arange(self.depth)
        if self.axis < 0:
            self.axis += (rank + 1)
        ls = values.shape[0:self.axis]
        rs = values.shape[self.axis:rank]
        targets = np.reshape(depth_range, (1,) * len(ls) + depth_range.shape +
                             (1,) * len(rs))
        values = np.reshape(np.mod(values, self.depth), ls + (1,) + rs)
        np_tensor = np.asarray(targets == values, dtype=np.float32)
        np_tensor = np_tensor * (self.values[1] -
                                 self.values[0]) + self.values[0]
        tmp_tensor = tensor.from_numpy(np_tensor)
        tmp_tensor.to_device(indices.device())
        return tmp_tensor.data

    def backward(self, dy):
        """
        backward of OneHot
        Args:
            dy (CTensor):gradient tensor.
        Raises:
            AssertionError: no backward function for this operator
        """
        assert False, ('no gradient for backward function')


def onehot(axis, indices, depth, values):
    """
    Produces a one-hot tensor based on inputs.
    Args:
        axis (int): Axis along which one-hot representation in added.
            Default: axis=-1. axis=-1 means that the additional dimension
            will be inserted as the innermost/last dimension in the output
            tensor.
        indices (Tensor): Scalar specifying the number of classes in
            one-hot tensor. The values in the 'indices' input tensor are
            expected to be in the range [-depth, depth-1].
        depth (int): Scalar specifying the number of classes in one-hot
            tensor. This is also the size of the one-hot dimension
            (specified by 'axis' attribute) added on in the output tensor.
            The values in the 'indices' input tensor are expected to be in
            the range [-depth, depth-1].
        values (float): Rank 1 tensor containing exactly two elements, in
            the format [off_value, on_value], where 'on_value' is the
            value used for filling locations specified in 'indices' input
            tensor,
    Returns:
        the output Tensor.
    """
    return OneHot(axis, depth, values)(indices)[0]


class _RNN(Operator):
    """ RNN operation with c++ backend
    """

    def __init__(
            self,
            handle,
            return_sequences=False,
            #  batch_first=True,
            use_mask=False,
            seq_lengths=None):
        assert singa.USE_CUDA, "Not able to run without CUDA"
        super(_RNN, self).__init__()
        self.handle = handle
        self.return_sequences = return_sequences
        self.use_mask = use_mask
        if use_mask:
            assert type(seq_lengths) == Tensor, "wrong type for seq_lengths"
        self.seq_lengths = seq_lengths

    def forward(self, x, hx, cx, w):
        if training:
            if self.use_mask:
                (y, hy,
                 cy) = singa.GpuRNNForwardTrainingEx(x, hx, cx, w,
                                                     self.seq_lengths.data,
                                                     self.handle)
            else:
                (y, hy,
                 cy) = singa.GpuRNNForwardTraining(x, hx, cx, w, self.handle)
            self.inputs = {
                'x': x,
                'hx': hx,
                'cx': cx,
                'w': w,
                'y': y,
                'hy': hy,
                'cy': cy
            }
        else:
            if self.use_mask:
                (y, hy,
                 cy) = singa.GpuRNNForwardInferenceEx(x, hx, cx, w,
                                                      self.seq_lengths.data,
                                                      self.handle)
            else:
                (y, hy,
                 cy) = singa.GpuRNNForwardInference(x, hx, cx, w, self.handle)

        if self.return_sequences:
            # (seq, bs, data)
            return y
        else:
            # return last time step of y
            # (seq, bs, data)[-1] -> (bs, data)
            last_y_shape = (y.shape()[1], y.shape()[2])
            last_y = singa.Tensor(list(last_y_shape), x.device())

            src_offset = y.Size() - last_y.Size()
            # def copy_data_to_from(dst, src, size, dst_offset=0, src_offset=0):
            singa.CopyDataToFrom(last_y, y, last_y.Size(), 0, src_offset)
            return last_y

    def backward(self, grad):
        assert training is True and hasattr(
            self, "inputs"), "Please set training as True before do BP. "

        # (seq, bs, hid)
        dy = None
        if self.return_sequences:
            assert grad.shape() == self.inputs['y'].shape(), (
                "grad shape %s != y shape %s" %
                (grad.shape(), self.inputs['y'].shape()))
            dy = grad
        else:
            # grad (bs, directions*hidden) -> dy (seq, bs, directions*hidden)
            #   empty space filled by zeros
            assert grad.shape() == (self.inputs['y'].shape()[1],
                                    self.inputs['y'].shape()[2]), (
                                        "grad y shape %s != last y shape %s" %
                                        (grad.shape(),
                                         (self.inputs['y'].shape()[1],
                                          self.inputs['y'].shape()[2])))
            dy = singa.Tensor(list(self.inputs['y'].shape()), grad.device())
            dy.SetFloatValue(0.0)
            dst_offset = dy.Size() - grad.Size()
            singa.CopyDataToFrom(dy, grad, grad.Size(), dst_offset, 0)

        # states grad are zeros, since states are not used in forward pass
        dhy = singa.Tensor(list(self.inputs['hy'].shape()), grad.device())
        dhy.SetFloatValue(0.0)
        dcy = singa.Tensor(list(self.inputs['cy'].shape()), grad.device())
        dcy.SetFloatValue(0.0)

        if self.use_mask:
            (dx, dhx,
             dcx) = singa.GpuRNNBackwardxEx(self.inputs['y'], dy, dhy, dcy,
                                            self.inputs['w'], self.inputs['hx'],
                                            self.inputs['cx'],
                                            self.seq_lengths.data, self.handle)
            dW = singa.GpuRNNBackwardWEx(self.inputs['x'], self.inputs['hx'],
                                         self.inputs['y'],
                                         self.seq_lengths.data, self.handle)
        else:
            (dx, dhx,
             dcx) = singa.GpuRNNBackwardx(self.inputs['y'], dy, dhy, dcy,
                                          self.inputs['w'], self.inputs['hx'],
                                          self.inputs['cx'], self.handle)
            dW = singa.GpuRNNBackwardW(self.inputs['x'], self.inputs['hx'],
                                       self.inputs['y'], self.handle)

        return dx, dhx, dcx, dW


class CosSim(Operator):
    """
    Init a cos similarity operator
    """

    def __init__(self):
        super(CosSim, self).__init__()

    @classmethod
    def dot(cls, a, b):
        """
        dot multiply
        Args:
            a (CTensor): 2d input tensor.
            b (CTensor): 2d input tensor.
        Returns:
            CTensor: the output CTensor.
        """
        batch_size = a.shape()[0]
        ret = []
        for indice in range(batch_size):
            tmp_a = singa.SliceOn(a, indice, indice + 1, 0)  # 1 * d
            tmp_b = singa.SliceOn(b, indice, indice + 1, 0)  # 1 * d
            tmp_b = singa.DefaultTranspose(tmp_b)
            tmp_tensor = singa.Mult(tmp_a, tmp_b)  # 1 * d * d * 1
            ret.append(tmp_tensor)
        ret = singa.VecTensor(ret)
        ret = singa.ConcatOn(ret, 0)  # b * 1
        return singa.Reshape(ret, [ret.shape()[0]])  # b

    def forward(self, a, b):
        """
        forward of CosSim
        Args:
            a (CTensor): input tensor.
            b (CTensor): input tensor.
        Returns:
            the output CTensor.
        """
        ad = CosSim.dot(a, a)
        bd = CosSim.dot(b, b)
        ap = singa.PowFloat(ad, 0.5)
        bp = singa.PowFloat(bd, 0.5)
        ret = singa.__div__(CosSim.dot(a, b), singa.__mul__(ap, bp))
        if training:
            self.cache = (a, b, ad, bd, ap, bp, ret)
        return ret

    def backward(self, dy):
        """
        backward of CosSim
        follow https://math.stackexchange.com/a/1923705
        Args:
            dy (CTensor): gradient tensor.
        Return:
            the gradient tensor over input tensor.
        """
        a, b, ad, bd, ap, bp, ret = self.cache
        ab = singa.__mul__(ap, bp)
        ab = singa.Reshape(ab, list(ab.shape()) + [1])  # b * 1
        ad = singa.Reshape(ad, list(ad.shape()) + [1])  # b * 1
        bd = singa.Reshape(bd, list(bd.shape()) + [1])  # b * 1
        ret = singa.Reshape(ret, list(ret.shape()) + [1])  # b * 1
        dy = singa.Reshape(dy, list(dy.shape()) + [1])  # boardcast
        da = singa.__sub__(singa.__div__(b, ab),
                           singa.__div__(singa.__mul__(ret, a), ad))
        db = singa.__sub__(singa.__div__(a, ab),
                           singa.__div__(singa.__mul__(ret, b), bd))
        da = singa.__mul__(dy, da)
        db = singa.__mul__(dy, db)
        return da, db


def cossim(a, b):
    """
    Produces a cos similarity operator
    Args:
        a (CTensor): input tensor.
        b (CTensor): input tensor.
    Returns:
        the output Tensor.
    """
    assert a.shape == b.shape, "shape not match for cossim"
    assert a.ndim() == 2, "shape should be in 2d for cossim"
    assert b.ndim() == 2, "shape should be in 2d for cossim"
    return CosSim()(a, b)[0]


class Expand(Operator):
    """
    Expand operator following ONNX Operator Schemas
    https://github.com/onnx/onnx/blob/master/docs/Operators.md#Expand

    Example usage::
    data = [[1.], [2.], [3.]]

    # dim_changed
    shape = [2, 1, 6]
    output = [[[1., 1., 1., 1., 1., 1.], 
               [2., 2., 2., 2., 2., 2.],
               [3., 3., 3., 3., 3., 3.]],
              [[1., 1., 1., 1., 1., 1.],
               [2., 2., 2., 2., 2., 2.],
               [3., 3., 3., 3., 3., 3.]]]

    # dim_unchanged
    shape = [3, 4]
    output = [[1., 1., 1., 1.],
              [2., 2., 2., 2.],
              [3., 3., 3., 3.]]
    """

    def __init__(self, shape):
        """
        Args:
            shape (list[int]: indicates the shape you want to expand to, 
                following the broadcast rule
        """
        super(Expand, self).__init__()
        self.shape = shape

    def forward(self, x):
        if isinstance(self.shape, np.ndarray):
            self.shape = self.shape.tolist()
        else:
            self.shape = list(self.shape)
        self.x_shape = list(x.shape())
        x_shape = self.x_shape.copy()
        self.dim_changed = True if len(self.shape) != len(x_shape) else False
        if self.dim_changed:
            tmp_tensor = singa.Tensor(self.shape, x.device())
            tmp_tensor.SetFloatValue(1.)
            x = singa.__mul__(x, tmp_tensor)
        else:
            for axis, s_1, s_2 in zip(range(len(self.shape)), self.shape,
                                      x_shape):
                if s_1 == s_2:
                    continue
                xs = [x] * (s_1 // s_2)
                x = singa.VecTensor(xs)
                x = singa.ConcatOn(x, axis)
        return x

    def backward(self, dy):
        x_shape = self.x_shape
        if self.dim_changed:
            dy = tensor.from_raw_tensor(dy)
            if len(self.shape) > len(x_shape):
                x_shape = [1] * (len(self.shape) - len(x_shape)) + x_shape
            for axis, s in zip(range(len(self.shape))[::-1], x_shape[::1]):
                if s == 1:
                    dy = tensor.sum(dy, axis)
            dy = dy.data
        else:
            for axis, s_1, s_2 in zip(
                    range(len(self.shape))[::-1], self.shape[::-1],
                    x_shape[::-1]):
                if s_1 > s_2:
                    duplic = s_1 // s_2
                    dxs = []
                    for i in range(s_2):
                        tmp_tensor = None
                        for j in range(duplic):
                            if not tmp_tensor:
                                tmp_tensor = singa.SliceOn(
                                    dy, j * s_2 + i, j * s_2 + i + 1, axis)
                            else:
                                tmp_tensor += singa.SliceOn(
                                    dy, j * s_2 + i, j * s_2 + i + 1, axis)
                        dxs.append(tmp_tensor)
                    dxs = singa.VecTensor(dxs)
                    dy = singa.ConcatOn(dxs, axis)
        dy = singa.Reshape(dy, self.x_shape)
        return dy


def expand(x, shape):
    """
    Produces a Expand operator
    Args:
        x (Tensor): input tensor.
        shape (list[int]: indicates the shape you want to expand to, 
            following the broadcast rule
    Returns:
        the output Tensor.
    """
    return Expand(shape)(x)[0]


class Pad(Operator):
    """
    Pad operator following ONNX Operator Schemas
    https://github.com/onnx/onnx/blob/master/docs/Operators.md#Pad

    Example usage::
        data = 
        [
            [1.0, 1.2],
            [2.3, 3.4],
            [4.5, 5.7],
        ] 
        pads = [0, 2, 0, 0]

        # constant mode
        mode = 'constant'
        constant_value = 0.0
        output = 
        [
            [
                [0.0, 0.0, 1.0, 1.2],
                [0.0, 0.0, 2.3, 3.4],
                [0.0, 0.0, 4.5, 5.7],
            ],
        ]

        # reflect mode
        mode = 'reflect'
        output = 
        [
            [
                [1.0, 1.2, 1.0, 1.2],
                [2.3, 3.4, 2.3, 3.4],
                [4.5, 5.7, 4.5, 5.7],
            ],
        ]

        # edge mode
        mode = 'edge'
        output = 
        [
            [
                [1.0, 1.0, 1.0, 1.2],
                [2.3, 2.3, 2.3, 3.4],
                [4.5, 4.5, 4.5, 5.7],
            ],
        ]
    """

    def __init__(self, mode, pads, constant=0.):
        """
        Args:
            mode (string): Supported modes: `constant`(default), `reflect`, `edge`.
            pads (list[int]): list of integers indicating the number of padding elements 
                to add at the beginning each axis.
            constant (float): A scalar value to be used if the mode chosen is 
                `constant`
        """
        super(Pad, self).__init__()
        self.mode = mode
        if self.mode not in ("constant", "reflect", "edge"):
            assert False, ('Only support three modes: constant, reflect, edge')
        self.constant = constant
        self.pads = pads
        self.pad_width = ()

    def forward(self, x):
        if not self.pad_width:
            half_width = len(self.pads) // 2
            for i in range(half_width):
                self.pad_width += ((self.pads[i], self.pads[i + half_width])),

        for axis, pads in zip(range(len(x.shape())), self.pad_width):
            for pad, is_left in zip(pads, (True, False)):
                if pad == 0:
                    continue
                pad_shape = list(x.shape())
                if self.mode == "constant":
                    pad_shape[axis] = pad
                    padding = singa.Tensor(list(pad_shape), x.device())
                    padding.SetFloatValue(self.constant)
                    if is_left:
                        x = singa.ConcatOn(singa.VecTensor([padding, x]), axis)
                    else:
                        x = singa.ConcatOn(singa.VecTensor([x, padding]), axis)
                elif self.mode == "reflect":
                    axis_shape = pad_shape[axis]
                    if is_left:
                        padding = singa.SliceOn(x, 0, pad, axis)
                        x = singa.ConcatOn(singa.VecTensor([padding, x]), axis)
                    else:
                        padding = singa.SliceOn(x, axis_shape - pad, axis_shape,
                                                axis)
                        x = singa.ConcatOn(singa.VecTensor([x, padding]), axis)
                elif self.mode == "edge":
                    axis_shape = pad_shape[axis]
                    if is_left:
                        padding = []
                        for _ in range(pad):
                            padding.append(singa.SliceOn(x, 0, 1, axis))
                        padding.append(x)
                        padding = singa.VecTensor(padding)
                        x = singa.ConcatOn(padding, axis)
                    else:
                        padding = [x]
                        for _ in range(pad):
                            padding.append(
                                singa.SliceOn(x, axis_shape - 1, axis_shape,
                                              axis))
                        padding = singa.VecTensor(padding)
                        x = singa.ConcatOn(padding, axis)
        return x

    def backward(self, dy):
        for axis, pads in zip(range(len(dy.shape())), self.pad_width):
            for pad, is_left in zip(pads, (True, False)):
                if pad == 0:
                    continue
                axis_shape = list(dy.shape())[axis]
                if is_left:
                    dy = singa.SliceOn(dy, pad, axis_shape, axis)
                else:
                    dy = singa.SliceOn(dy, 0, axis_shape - pad, axis)
        return dy


def pad(x, mode, pads, constant=0.):
    """
    Produces a pad operator
    Args:
        x (Tensor): input tensor.
        mode (string): Supported modes: `constant`(default), `reflect`, `edge`.
        pads (list[int]): list of integers indicating the number of padding elements 
            to add at the beginning each axis.
        constant (float): A scalar value to be used if the mode chosen is 
            `constant`
    Returns:
        the output Tensor.
    """
    return Pad(mode, pads, constant)(x)[0]


class UpSample(Operator):
    """
    UpSample operator following ONNX Operator Schemas
    https://github.com/onnx/onnx/blob/master/docs/Operators.md#upsample

    Example usage::
    data = [[[[1, 2],
              [3, 4],]]]

    # nearest
    scales = [1.0, 1.0, 2.0, 3.0]
    output = [[[[1, 1, 1, 2, 2, 2],
                [1, 1, 1, 2, 2, 2],
                [3, 3, 3, 4, 4, 4],
                [3, 3, 3, 4, 4, 4],]]]
    """

    def __init__(self, mode, scales):
        """
        Args:
            scales (list[int]): The scale array along each dimension. It takes 
                value greater than or equal to 1. 
        """
        super(UpSample, self).__init__()
        self.scales = scales
        self.mode = mode.lower()
        if self.mode != "nearest":
            assert False, "only support nearest mode."

    def forward(self, x):
        if isinstance(self.scales, np.ndarray):
            self.scales = self.scales.tolist()
        else:
            self.scales = list(self.scales)
        self.x_shape = list(x.shape())
        for axis, s in zip(range(len(self.scales)), self.scales):
            s = int(s)
            if s == 1:
                continue
            x = x.Repeat([
                s,
            ], axis)
        return x

    def backward(self, dy):
        x_shape = self.x_shape.copy()
        for axis, s_1, s_2 in zip(
                range(len(self.scales))[::-1], self.scales[::-1],
                x_shape[::-1]):
            s_1 = int(s_1)
            if s_1 != 1:
                duplic = s_1
                dxs = []
                for i in range(s_2):
                    tmp_tensor = None
                    for j in range(duplic):
                        if not tmp_tensor:
                            tmp_tensor = singa.SliceOn(dy, i * duplic + j,
                                                       i * duplic + j + 1, axis)
                        else:
                            tmp_tensor += singa.SliceOn(dy, i * duplic + j,
                                                        i * duplic + j + 1,
                                                        axis)
                    dxs.append(tmp_tensor)
                dxs = singa.VecTensor(dxs)
                dy = singa.ConcatOn(dxs, axis)
        dy = singa.Reshape(dy, self.x_shape)
        return dy


def upsample(x, mode, scales):
    """
    Produces a upsample operator
    Args:
        x (Tensor): input tensor.
        scales (list[int]): The scale array along each dimension. It takes 
                value greater than or equal to 1. 
    Returns:
        the output Tensor.
    """
    return UpSample(mode, scales)(x)[0]


class DepthToSpace(Operator):
    """
    DepthToSpace operator following ONNX Operator Schemas
    https://github.com/onnx/onnx/blob/master/docs/Operators.md#DepthToSpace

    Example usage::
    blocksize = 2
    # (1, 8, 2, 3) input tensor
    data = [[[[0., 1., 2.],
            [3., 4., 5.]],
            [[9., 10., 11.],
            [12., 13., 14.]],
            [[18., 19., 20.],
            [21., 22., 23.]],
            [[27., 28., 29.],
            [30., 31., 32.]],
            [[36., 37., 38.],
            [39., 40., 41.]],
            [[45., 46., 47.],
            [48., 49., 50.]],
            [[54., 55., 56.],
            [57., 58., 59.]],
            [[63., 64., 65.],
            [66., 67., 68.]]]]

    # DCR mode
    # (1, 2, 4, 6) output tensor
    output = [[[[0., 18., 1., 19., 2., 20.],
                [36., 54., 37., 55., 38., 56.],
                [3., 21., 4., 22., 5., 23.],
                [39., 57., 40., 58., 41., 59.]],
               [[9., 27., 10., 28., 11., 29.],
                [45., 63., 46., 64., 47., 65.],
                [12., 30., 13., 31., 14., 32.],
                [48., 66., 49., 67., 50., 68.]]]]

    # CRD mode
    # (1, 2, 4, 6) output tensor
    output = [[[[0., 9., 1., 10., 2., 11.],
                [18., 27., 19., 28., 20., 29.],
                [3., 12., 4., 13., 5., 14.],
                [21., 30., 22., 31., 23., 32.]],
               [[36., 45., 37., 46., 38., 47.],
                [54., 63., 55., 64., 56., 65.],
                [39., 48., 40., 49., 41., 50.],
                [57., 66., 58., 67., 59., 68.]]]]
    """

    def __init__(self, blocksize, mode="DCR"):
        """
        Args:
            blocksize (int): Blocks of [blocksize, blocksize] are moved.
            mode (string): DCR (default) for depth-column-row order re-
                arrangement. Use CRD for column-row-depth order.
        """
        super(DepthToSpace, self).__init__()
        self.blocksize = blocksize
        self.mode = mode.upper()

    def forward(self, x):
        if training:
            self.x_shape = x.shape()
        b, c, h, w = x.shape()
        blocksize = self.blocksize
        if self.mode == "DCR":
            x = singa.Reshape(
                x, [b, blocksize, blocksize, c // (blocksize**2), h, w])
            x = singa.Transpose(x, [0, 3, 4, 1, 5, 2])
            x = singa.Reshape(
                x, [b, c // (blocksize**2), h * blocksize, w * blocksize])
        elif self.mode == "CRD":
            x = singa.Reshape(
                x, [b, c // (blocksize**2), blocksize, blocksize, h, w])
            x = singa.Transpose(x, [0, 1, 4, 2, 5, 3])
            x = singa.Reshape(
                x, [b, c // (blocksize**2), h * blocksize, w * blocksize])
        else:
            assert False, ("only support two methods: DCR and CRD.")
        return x

    def backward(self, dy):
        b, c, h, w = self.x_shape
        blocksize = self.blocksize
        dy = singa.Reshape(
            dy, [b, c // (blocksize**2), h, blocksize, w, blocksize])
        if self.mode == "DCR":
            dy = singa.Transpose(dy, [0, 3, 5, 1, 2, 4])
        elif self.mode == "CRD":
            dy = singa.Transpose(dy, [0, 1, 3, 5, 2, 4])
        else:
            assert False, ("only support two methods: DCR and CRD.")
        dy = singa.Reshape(dy, self.x_shape)
        return dy


def depth_to_space(x, blocksize, mode="DCR"):
    """
    Produces a DepthToSpace operator
    Args:
        x (Tensor): input tensor.
        blocksize (int): Blocks of [blocksize, blocksize] are moved.
        mode (string): DCR (default) for depth-column-row order re-
            arrangement. Use CRD for column-row-depth order.
    Returns:
        the output Tensor.
    """
    return DepthToSpace(blocksize, mode)(x)[0]


class SpaceToDepth(Operator):
    """
    SpaceToDepth operator following ONNX Operator Schemas, reverse of DepthToSpace
    https://github.com/onnx/onnx/blob/master/docs/Operators.md#SpaceToDepth
    """

    def __init__(self, blocksize, mode="DCR"):
        """
        Args:
            blocksize (int): Blocks of [blocksize, blocksize] are moved.
            mode (string): DCR (default) for depth-column-row order re-
                arrangement. Use CRD for column-row-depth order.
        """
        super(SpaceToDepth, self).__init__()
        self.blocksize = blocksize
        self.mode = mode.upper()

    def forward(self, x):
        blocksize = self.blocksize
        b, c, h, w = x.shape()
        b, c, h, w = b, c * (blocksize**2), h // blocksize, w // blocksize
        if training:
            self.x_shape = (b, c, h, w)
        x = singa.Reshape(
            x, [b, c // (blocksize**2), h, blocksize, w, blocksize])
        if self.mode == "DCR":
            x = singa.Transpose(x, [0, 3, 5, 1, 2, 4])
        elif self.mode == "CRD":
            x = singa.Transpose(x, [0, 1, 3, 5, 2, 4])
        else:
            assert False, ("only support two methods: DCR and CRD.")
        x = singa.Reshape(x, self.x_shape)
        return x

    def backward(self, dy):
        b, c, h, w = self.x_shape
        blocksize = self.blocksize
        if self.mode == "DCR":
            dy = singa.Reshape(
                dy, [b, blocksize, blocksize, c // (blocksize**2), h, w])
            dy = singa.Transpose(dy, [0, 3, 4, 1, 5, 2])
            dy = singa.Reshape(
                dy, [b, c // (blocksize**2), h * blocksize, w * blocksize])
        elif self.mode == "CRD":
            dy = singa.Reshape(
                dy, [b, c // (blocksize**2), blocksize, blocksize, h, w])
            dy = singa.Transpose(dy, [0, 1, 4, 2, 5, 3])
            dy = singa.Reshape(
                dy, [b, c // (blocksize**2), h * blocksize, w * blocksize])
        else:
            assert False, ("only support two methods: DCR and CRD.")
        return dy


def space_to_depth(x, blocksize, mode="DCR"):
    """
    Produces a SpaceToDepth operator
    Args:
        x (Tensor): input tensor.
        blocksize (int): Blocks of [blocksize, blocksize] are moved.
        mode (string): DCR (default) for depth-column-row order re-
            arrangement. Use CRD for column-row-depth order.
    Returns:
        the output Tensor.
    """
    return SpaceToDepth(blocksize, mode)(x)[0]


class Where(Operator):
    """
    Where operator following ONNX Operator Schemas
    https://github.com/onnx/onnx/blob/master/docs/Operators.md#Where
    and Numpy
    https://numpy.org/doc/stable/reference/generated/numpy.where.html
    Example usage::
    condition = [[True, False], 
              [True, True]]
    x = [[1, 2], 
        [3, 4]]
    y =  [[9, 8], 
        [7, 6]]

    output = [[1, 8],
            [3, 4]]
    """

    def __init__(self, condition):
        """
        Args:
            condition (Tensor): When True (nonzero), yield X, otherwise yield Y
        """
        super(Where, self).__init__()
        self.condition = condition

    def forward(self, a, b):
        if isinstance(self.condition, list):
            self.condition = np.array(self.condition)
        if isinstance(self.condition, np.ndarray):
            self.condition = self.condition.astype(np.float32)
            self.condition = tensor.from_numpy(self.condition)
            self.condition.to_device(a.device())
            self.condition = self.condition.data
        self.neg_condition = singa.AddFloat(
            singa.MultFloat(self.condition, -1.), 1.)
        _a, _b = a, b
        dtype0 = _a.data_type()
        dtype1 = _b.data_type()
        if dtype0 == singa.kInt or dtype1 == singa.kInt:
            _a = a.AsType(singa.kFloat32)
            _b = b.AsType(singa.kFloat32)
            res = singa.__add__(singa.__mul__(self.condition, _a),
                                singa.__mul__(self.neg_condition, _b))
            res = res.AsType(singa.kInt)
        else:
            res = singa.__add__(singa.__mul__(self.condition, _a),
                                singa.__mul__(self.neg_condition, _b))
        return res

    def backward(self, dy):
        da = singa.__mul__(self.condition, dy)
        db = singa.__mul__(self.neg_condition, dy)
        return da, db


def where(x, y, condition):
    """
    Produces a Where operator
    Args:
        x (Tensor): input tensor.
        y (Tensor): input tensor.
        condition (Tensor): When True (nonzero), yield X, otherwise yield Y
    Returns:
        the output Tensor.
    """
    return Where(condition)(x, y)[0]


class Round(Operator):
    """
    Element-wise round the input
    """

    def __init__(self):
        super(Round, self).__init__()

    def forward(self, x):
        return singa.Round(x)

    def backward(self, dy):
        dy = singa.Tensor(dy.shape(), dy.device())
        dy.SetFloatValue(0.)
        return dy


def round(x):
    """
    Element-wise round the input
    Args:
        x (Tensor): input tensor.
    Returns:
        the output Tensor.
    """
    return Round()(x)[0]


class Rounde(Operator):
    """
    Element-wise round the input, In case of halfs, round to the nearest even integer
    """

    def __init__(self):
        super(Rounde, self).__init__()

    def forward(self, x):
        return singa.RoundE(x)

    def backward(self, dy):
        dy = singa.Tensor(dy.shape(), dy.device())
        dy.SetFloatValue(0.)
        return dy


def rounde(x):
    """
    Element-wise round the input, In case of halfs, round to the nearest even integer
    Args:
        x (Tensor): input tensor.
    Returns:
        the output Tensor.
    """
    return Rounde()(x)[0]


class Embedding(Operator):
    """
    Init an embedding operator
    """

    def __init__(self):
        super(Embedding, self).__init__()

    def forward(self, x, w):
        """
        forward of embedding
        Args:
            x (CTensor): input tensor.
            w (CTensor): weight tensor.
        Returns:
            the output CTensor.
        """
        x = tensor.to_numpy(tensor.from_raw_tensor(x))
        if training:
            self.cache = (x, w.shape())

        xs = []
        x = x.tolist()
        for indice in x:
            sub_xs = []
            for idx in indice:
                idx = int(idx)
                tmp_tensor = singa.SliceOn(w, idx, idx + 1, 0)
                sub_xs.append(tmp_tensor)
            sub_xs = singa.VecTensor(sub_xs)
            tmp_tensor = singa.ConcatOn(sub_xs, 0)
            tmp_tensor = singa.Reshape(tmp_tensor,
                                       [1] + list(tmp_tensor.shape()))

            xs.append(tmp_tensor)
        xs = singa.VecTensor(xs)
        xs = singa.ConcatOn(xs, 0)
        return xs

    def backward(self, dy):
        """
        backward of embedding
        Args:
            dy (CTensor): gradient tensor.
        Raises:
            the gradient tensor over input tensor.
        """
        x, w_shape = self.cache
        dy_shape = dy.shape()
        # construct the dx
        dx = tensor.sum(tensor.from_raw_tensor(dy), axis=2)

        # construct the dw
        dws = []
        for idx in range(w_shape[0]):
            tmp_tensor = singa.Tensor((1, w_shape[1]), dy.device())
            tmp_tensor.SetFloatValue(0.0)
            dws.append(tmp_tensor)
        dy = singa.Reshape(dy, [dy_shape[0] * dy_shape[1], dy_shape[2]])
        x = x.reshape(-1)
        for idx, val in enumerate(x):
            tmp_tensor = singa.SliceOn(dy, idx, idx + 1, 0)
            dws[val] = singa.__add__(dws[val], tmp_tensor)
        dws = singa.VecTensor(dws)
        return dx.data, singa.ConcatOn(dws, 0)


def embedding(x, w):
    """
    Produces an embedding operator.
    Args:
    Returns:
        the output Tensor.
    """
    return Embedding()(x, w)[0]


class Erf(Operator):
    """
    Apply element-wise math.erf to the input
    """

    def __init__(self):
        super(Erf, self).__init__()

    def forward(self, x):
        return singa.Erf(x)

    def backward(self, dy):
        dx = singa.MultFloat(singa.PowFloat(dy, 2.0), -1.0)
        dx = singa.MultFloat(singa.Exp(dx), 2. / np.pi ** 0.5)
        return dx


def erf(x):
    """
    Apply element-wise math.erf to the input
    Args:
        x (Tensor): input tensor.
    Returns:
        the output Tensor.
    """
    return Erf()(x)[0]


''' alias for Operator and Layers
'''
Operation = Operator
''' import layer at the end to resolve circular import
'''
from singa import layer
Linear = layer.Linear
Conv2d = layer.Conv2d
SeparableConv2d = layer.SeparableConv2d
BatchNorm2d = layer.BatchNorm2d
Pooling2d = layer.Pooling2d
MaxPool2d = layer.MaxPool2d
AvgPool2d = layer.AvgPool2d
MaxPool1d = layer.MaxPool1d
AvgPool1d = layer.AvgPool1d
RNN_Base = layer.RNN_Base
RNN = layer.RNN
LSTM = layer.LSTM
