This is an automated email from the ASF dual-hosted git repository.

zha0q1 pushed a commit to branch v1.x
in repository https://gitbox.apache.org/repos/asf/incubator-mxnet.git


The following commit(s) were added to refs/heads/v1.x by this push:
     new b219ac2  [v1.x] Add more ONNX export support to operators (#19625)
b219ac2 is described below

commit b219ac2e72e8a2a3e2fa776683bf9a7cd1e5405a
Author: Joe Evans <[email protected]>
AuthorDate: Fri Dec 11 10:22:19 2020 -0800

    [v1.x] Add more ONNX export support to operators (#19625)
---
 ci/docker/runtime_functions.sh                     |   1 +
 .../mxnet/contrib/onnx/mx2onnx/_op_translations.py | 186 +++++++++++++++++++++
 tests/python-pytest/onnx/test_operators.py         | 134 +++++++++++++++
 3 files changed, 321 insertions(+)

diff --git a/ci/docker/runtime_functions.sh b/ci/docker/runtime_functions.sh
index 68ecf30..c387236 100755
--- a/ci/docker/runtime_functions.sh
+++ b/ci/docker/runtime_functions.sh
@@ -1281,6 +1281,7 @@ integrationtest_ubuntu_cpu_onnx() {
        pytest tests/python-pytest/onnx/mxnet_export_test.py
        pytest tests/python-pytest/onnx/test_models.py
        pytest tests/python-pytest/onnx/test_node.py
+       pytest tests/python-pytest/onnx/test_operators.py
        pytest tests/python-pytest/onnx/test_onnxruntime.py
 }
 
diff --git a/python/mxnet/contrib/onnx/mx2onnx/_op_translations.py 
b/python/mxnet/contrib/onnx/mx2onnx/_op_translations.py
index f03fabb..efb58c0 100644
--- a/python/mxnet/contrib/onnx/mx2onnx/_op_translations.py
+++ b/python/mxnet/contrib/onnx/mx2onnx/_op_translations.py
@@ -154,6 +154,29 @@ def create_basic_op_node(op_name, node, kwargs):
     )
     return [node]
 
+def create_const_scalar_node(input_name, value, kwargs):
+    """Helper function to create a tensor value node and a
+    initializer tensor node with constant value."""
+    from onnx.helper import make_tensor, make_tensor_value_info
+    initializer = kwargs["initializer"]
+    input_type = onnx.mapping.NP_TYPE_TO_TENSOR_TYPE[value.dtype]
+    value_node = make_tensor_value_info(input_name, input_type, ())
+    tensor_node = make_tensor(input_name, input_type, (), (value,))
+    initializer.append(tensor_node)
+    return value_node
+
+def create_const_node(input_name, value, kwargs):
+    """Helper function to create a tensor value node and a
+    initializer tensor node with constant value."""
+    from onnx.helper import make_tensor, make_tensor_value_info
+    initializer = kwargs["initializer"]
+    input_type = onnx.mapping.NP_TYPE_TO_TENSOR_TYPE[value.dtype]
+    input_shape = value.shape
+    value_node = make_tensor_value_info(input_name, input_type, input_shape)
+    tensor_node = make_tensor(input_name, input_type, input_shape, value)
+    initializer.append(tensor_node)
+    return value_node
+
 @mx_op.register("null")
 def convert_weights_and_inputs(node, **kwargs):
     """Helper function to convert weights and inputs.
@@ -802,6 +825,7 @@ def convert_leakyrelu(node, **kwargs):
     """Map MXNet's LeakyReLU operator attributes to onnx's Elu/LeakyRelu/PRelu 
operators
     based on the input node's attributes and return the created node.
     """
+    from onnx.helper import make_node
     name, input_nodes, attrs = get_inputs(node, kwargs)
 
     act_type = attrs.get("act_type", "leaky")
@@ -816,6 +840,19 @@ def convert_leakyrelu(node, **kwargs):
             inputs=input_nodes,
             outputs=[name],
             name=name)
+    elif act_type in ('gelu'):
+        sqrt2 = np.float32(1.4142135623730951)
+        nodes = [
+            create_const_scalar_node(name+"_sqrt2", sqrt2, kwargs),
+            make_node("Div", [input_nodes[0], name+"_sqrt2"], 
[name+"_div0_out"]),
+            make_node("Erf", [name+"_div0_out"], [name+"_erf0_out"]),
+            create_const_scalar_node(name+"_one", np.float32(1.0), kwargs),
+            create_const_scalar_node(name+"_half", np.float32(0.5), kwargs),
+            make_node("Add", [name+"_erf0_out", name+"_one"], 
[name+"_add0_out"]),
+            make_node("Mul", [input_nodes[0], name+"_add0_out"], 
[name+"_mul0_out"]),
+            make_node("Mul", [name+"_mul0_out", name+"_half"], [name])
+        ]
+        return nodes
     else:
         node = onnx.helper.make_node(
             act_name[act_type],
@@ -2214,3 +2251,152 @@ def convert_take(node, **kwargs):
         name=name,
     )
     return [node]
+
+
+@mx_op.register("LayerNorm")
+def convert_layer_norm(node, **kwargs):
+    """Map MXNet's LayerNorm operator attributes to onnx operators.
+    """
+    from onnx.helper import make_node
+    name, input_nodes, attrs = get_inputs(node, kwargs)
+
+    in_shape = kwargs['in_shape']
+    axes = [-i for i in range(len(in_shape[0]), 0, -1)]
+    eps = attrs.get('eps')
+    nodes = [
+        make_node("ReduceMean", [input_nodes[0]], [name+"_rm0_out"], 
axes=axes),
+        make_node("Sub", [input_nodes[0], name+"_rm0_out"], 
[name+"_sub0_out"]),
+        create_const_scalar_node(name+"_two", np.float32(2.), kwargs),
+        make_node("Pow", [name+"_sub0_out", name+"_two"], [name+"_pow0_out"]),
+        make_node("ReduceMean", [name+"_pow0_out"], [name+"_rm1_out"], 
axes=axes),
+        create_const_scalar_node(name+"_eps", np.float32(eps), kwargs),
+        make_node("Add", [name+"_rm1_out", name+"_eps"], [name+"_add0_out"]),
+        make_node("Sqrt", [name+"_add0_out"], [name+"_sqrt0_out"]),
+        make_node("Div", [name+"_sub0_out", name+"_sqrt0_out"], 
[name+"_div0_out"]),
+        make_node("Mul", [name+"_div0_out", input_nodes[1]], 
[name+"_mul0_out"]),
+        make_node("Add", [name+"_mul0_out", input_nodes[2]], [name], name)
+    ]
+
+    return nodes
+
+
+@mx_op.register("Embedding")
+def convert_embedding(node, **kwargs):
+    """Map MXNet's Embedding operator attributes to onnx's
+    Gather operator."""
+    name, input_nodes, attrs = get_inputs(node, kwargs)
+    axis = int(attrs.get('axis', 0))
+    node = onnx.helper.make_node(
+        "Gather",
+        input_nodes,
+        [name],
+        axis=axis,
+        name=name
+    )
+    return [node]
+
+
+@mx_op.register("stack")
+def convert_stack(node, **kwargs):
+    """Map MXNet's stack operator to onnx operators.
+    """
+    name, input_nodes, attrs = get_inputs(node, kwargs)
+    axis = int(attrs.get('axis', 0))
+    idx = 0
+    nodes = []
+    for input_node in input_nodes:
+        nodes.append(onnx.helper.make_node(
+            "Unsqueeze",
+            inputs=[input_node],
+            outputs=[name+"_unsqueeze"+str(idx)],
+            axes=[axis]
+        ))
+        idx += 1
+
+    nodes.append(onnx.helper.make_node(
+        "Concat",
+        inputs=[name+"_unsqueeze"+str(i) for i in range(len(nodes))],
+        outputs=[name],
+        name=name,
+        axis=axis
+    ))
+    return nodes
+
+
+@mx_op.register("slice")
+def convert_slice(node, **kwargs):
+    """Map MXNet's slice operator to onnx Slice operator."""
+    name, input_nodes, attrs = get_inputs(node, kwargs)
+    starts = convert_string_to_list(attrs.get("begin"))
+    ends = convert_string_to_list(attrs.get("end"))
+    steps = attrs.get("step", [])
+    nodes = [
+        create_const_node(name+"_begin", np.array(starts), kwargs),
+        create_const_node(name+"_end", np.array(ends), kwargs)
+    ]
+    inputs = [input_nodes[0], name+"_begin", name+"_end"]
+    if len(steps) > 0:
+        nodes.append(create_const_node(name+"_steps", np.array(steps, 
dtype='int64'), kwargs))
+        inputs.append(name+"_steps")
+    nodes.append(onnx.helper.make_node("Slice", inputs, [name], name=name))
+    return nodes
+
+
+@mx_op.register("zeros_like")
+def convert_zeros_like(node, **kwargs):
+    """Map MXNet's zeros_like operator attributes to onnx's ConstantOfShape 
operator.
+    """
+    from onnx.helper import make_node, make_tensor
+    name, _, _ = get_inputs(node, kwargs)
+
+    # create tensor with shape of input
+    create_const_node(name+"_shape", np.array(kwargs['in_shape'][0], 
dtype='int64'), kwargs)
+    tensor_value = make_tensor(name+"_zero", kwargs['in_type'], [1], [0])
+    nodes = [
+        make_node("ConstantOfShape", [name+"_shape"], [name], 
value=tensor_value)
+    ]
+    return nodes
+
+
+@mx_op.register("_contrib_arange_like")
+def convert_arange_like(node, **kwargs):
+    """Map MXNet's arange_like operator attributes to onnx's Range and Reshape 
operators.
+    """
+    from onnx.helper import make_node
+    name, _, attrs = get_inputs(node, kwargs)
+
+    opset_version = kwargs['opset_version']
+    if opset_version < 11:
+        raise AttributeError("ONNX opset 11 or greater is required to export 
this operator")
+
+    input_type = kwargs['in_type']
+    dtype = onnx.mapping.TENSOR_TYPE_TO_NP_TYPE[input_type]
+    in_shape = kwargs['in_shape']
+    axis = attrs.get('axis')
+
+    if axis is None:
+        # output will be same shape as input
+        output_shape = in_shape[0]
+    else:
+        # determine shape of axis
+        output_shape = [in_shape[0][int(axis)]]
+
+    start = np.array([attrs.get('start', 0.)], dtype=dtype)
+    step = np.array([attrs.get('step', 1.)], dtype=dtype)
+    repeat = np.array([attrs.get('repeat', 1)], dtype=dtype)
+    if repeat != 1:
+        raise NotImplementedError("arange_like operator with repeat != 1 not 
yet implemented.")
+
+    tot_elements = np.prod(output_shape)
+    limit = np.array([start + (tot_elements * step)], dtype=dtype)
+
+    # create constant inputs
+    nodes = [
+        create_const_scalar_node(name+"_start", start, kwargs),
+        create_const_scalar_node(name+"_limit", limit, kwargs),
+        create_const_scalar_node(name+"_step", step, kwargs),
+        create_const_node(name+"_shape", np.array(output_shape, 
dtype='int64'), kwargs),
+        make_node("Range", [name+"_start", name+"_limit", name+"_step"], 
[name+"_range0_out"]),
+        make_node("Reshape", [name+"_range0_out", name+"_shape"], [name])
+    ]
+    return nodes
diff --git a/tests/python-pytest/onnx/test_operators.py 
b/tests/python-pytest/onnx/test_operators.py
new file mode 100644
index 0000000..aa66858
--- /dev/null
+++ b/tests/python-pytest/onnx/test_operators.py
@@ -0,0 +1,134 @@
+# 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.
+
+import mxnet as mx
+from mxnet.gluon import HybridBlock, nn
+import numpy as np
+import onnxruntime as rt
+from mxnet.test_utils import assert_almost_equal
+import pytest
+import tempfile
+
+def op_export_test(op_name, Model, inputs, tmp_path):
+    def export_to_onnx(model, op_name, inputs):
+        model_path = '{}/{}'.format(tmp_path, op_name)
+        model.export(model_path, epoch=0)
+        sym_file = '{}-symbol.json'.format(model_path)
+        params_file = '{}-0000.params'.format(model_path)
+        dtype = inputs[0].dtype
+        onnx_file = '{}/{}.onnx'.format(tmp_path, op_name)
+        mx.contrib.onnx.export_model(sym_file, params_file, [i.shape for i in 
inputs],
+                                     dtype, onnx_file)
+        return onnx_file
+    def onnx_rt(onnx_file, inputs):
+        sess = rt.InferenceSession(onnx_file)
+        input_dict = dict((sess.get_inputs()[i].name, inputs[i].asnumpy()) for 
i in range(len(inputs)))
+        pred = sess.run(None, input_dict)[0]
+        return pred
+
+    # create a new model 
+    model = Model()
+    model.initialize(ctx=mx.cpu(0))
+    model.hybridize()
+    pred_nat = model(*inputs)
+    onnx_file = export_to_onnx(model, op_name, inputs)
+    pred_onx = onnx_rt(onnx_file, inputs)
+    assert_almost_equal(pred_nat, pred_onx)
+
+
+def test_onnx_export_abs():
+    with tempfile.TemporaryDirectory() as tmp_path:
+        class Model(HybridBlock):
+            def __init__(self, **kwargs):
+                super(Model, self).__init__(**kwargs)
+            def hybrid_forward(self, F, x):
+                out = F.abs(x)
+                return out
+        x = mx.nd.array([[-2, -1], [0, 99]], dtype='float32')
+        op_export_test('abs', Model, [x], tmp_path)
+
+def test_onnx_export_slice():
+    with tempfile.TemporaryDirectory() as tmp_path:
+        class Model(HybridBlock):
+            def __init__(self, **kwargs):
+                super(Model, self).__init__(**kwargs)
+            def hybrid_forward(self, F, x):
+                out = F.slice(x, begin=(0,1), end=(2,4))
+                return out
+        x = mx.nd.array([[1,2,3,4],[5,6,7,8],[9,10,11,12]], dtype='float32')
+        op_export_test('slice', Model, [x], tmp_path)
+
+def test_onnx_export_stack():
+    with tempfile.TemporaryDirectory() as tmp_path:
+        dtype = 'float32'
+        class Model(HybridBlock):
+            def __init__(self, **kwargs):
+                super(Model, self).__init__(**kwargs)
+            def hybrid_forward(self, F, x, y):
+                out = F.stack(x, y)
+                return out
+        x = mx.nd.array([1, 2], dtype=dtype)
+        y = mx.nd.array([3, 4], dtype=dtype)
+        op_export_test('stack', Model, [x, y], tmp_path)
+
+def test_onnx_export_zeros_like():
+    with tempfile.TemporaryDirectory() as tmp_path:
+        class Model(HybridBlock):
+            def __init__(self, **kwargs):
+                super(Model, self).__init__(**kwargs)
+            def hybrid_forward(self, F, x):
+                out = F.zeros_like(x)
+                return out
+        x = mx.nd.array([[-2,-1,0],[0,50,99],[4,5,6],[7,8,9]], dtype='float32')
+        op_export_test('zeros_like', Model, [x], tmp_path)
+
[email protected]("dtype", ["float32", "double"])
+def test_onnx_export_arange_like(dtype):
+    with tempfile.TemporaryDirectory() as tmp_path:
+        class Model(HybridBlock):
+            def __init__(self, **kwargs):
+                super(Model, self).__init__(**kwargs)
+            def hybrid_forward(self, F, x):
+                out = F.contrib.arange_like(x)
+                return out
+        x = mx.nd.array([[-2,-1,0],[0,50,99],[4,5,6],[7,8,9]], dtype=dtype)
+        op_export_test('arange_like', Model, [x], tmp_path)
+
+def test_onnx_export_layernorm():
+    with tempfile.TemporaryDirectory() as tmp_path:
+        dtype = 'float32'
+        class Model(HybridBlock):
+            def __init__(self, **kwargs):
+                super(Model, self).__init__(**kwargs)
+            def hybrid_forward(self, F, x, gamma, beta):
+                out = F.LayerNorm(x, gamma, beta, axis=1)
+                return out
+        x = mx.nd.array([[1,3],[2,4]], dtype=dtype)
+        gamma = mx.random.uniform(0, 1, x[0].shape).astype(dtype)
+        beta = mx.random.uniform(0, 1, x[0].shape).astype(dtype)
+        op_export_test('LayerNorm', Model, [x, gamma, beta], tmp_path)
+
+
+if __name__ == '__main__':
+    test_onnx_export_abs()
+    test_onnx_export_slice()
+    test_onnx_export_stack()
+    test_onnx_export_zeros_like()
+    test_onnx_export_arange_like('float32')
+    test_onnx_export_arange_like('double')
+    test_onnx_export_layernorm()
+

Reply via email to