From b662a699e95acfe45f8fb82b99e17dc8c7f0c88a Mon Sep 17 00:00:00 2001 From: Acharya Date: Mon, 26 Feb 2018 01:00:17 -0800 Subject: [PATCH 01/14] import module --- python/mxnet/contrib/__init__.py | 1 + python/mxnet/contrib/serde/__init__.py | 2 + .../mxnet/contrib/serde/_export/__init__.py | 4 + .../mxnet/contrib/serde/_import/__init__.py | 26 ++ python/mxnet/contrib/serde/_import/backend.py | 129 +++++++ .../contrib/serde/_import/backend_rep.py | 68 ++++ python/mxnet/contrib/serde/_import/common.py | 142 ++++++++ .../contrib/serde/_import/import_helper.py | 244 +++++++++++++ .../contrib/serde/_import/import_onnx.py | 327 ++++++++++++++++++ .../contrib/serde/_import/tests/__init__.py | 9 + .../serde/_import/tests/onnx_backend_test.py | 73 ++++ .../serde/_import/tests/test_models.py | 89 +++++ .../_import/tests/test_super_resolution.py | 53 +++ 13 files changed, 1167 insertions(+) create mode 100644 python/mxnet/contrib/serde/__init__.py create mode 100644 python/mxnet/contrib/serde/_export/__init__.py create mode 100644 python/mxnet/contrib/serde/_import/__init__.py create mode 100644 python/mxnet/contrib/serde/_import/backend.py create mode 100644 python/mxnet/contrib/serde/_import/backend_rep.py create mode 100644 python/mxnet/contrib/serde/_import/common.py create mode 100644 python/mxnet/contrib/serde/_import/import_helper.py create mode 100644 python/mxnet/contrib/serde/_import/import_onnx.py create mode 100644 python/mxnet/contrib/serde/_import/tests/__init__.py create mode 100644 python/mxnet/contrib/serde/_import/tests/onnx_backend_test.py create mode 100644 python/mxnet/contrib/serde/_import/tests/test_models.py create mode 100644 python/mxnet/contrib/serde/_import/tests/test_super_resolution.py diff --git a/python/mxnet/contrib/__init__.py b/python/mxnet/contrib/__init__.py index 21c77719b70b..096e893ac745 100644 --- a/python/mxnet/contrib/__init__.py +++ b/python/mxnet/contrib/__init__.py @@ -28,3 +28,4 @@ from . import tensorboard from . import text +from . import serde \ No newline at end of file diff --git a/python/mxnet/contrib/serde/__init__.py b/python/mxnet/contrib/serde/__init__.py new file mode 100644 index 000000000000..30e72ec6d134 --- /dev/null +++ b/python/mxnet/contrib/serde/__init__.py @@ -0,0 +1,2 @@ +from . import _import +from . import _export \ No newline at end of file diff --git a/python/mxnet/contrib/serde/_export/__init__.py b/python/mxnet/contrib/serde/_export/__init__.py new file mode 100644 index 000000000000..3d09eb53d3c0 --- /dev/null +++ b/python/mxnet/contrib/serde/_export/__init__.py @@ -0,0 +1,4 @@ +import onnx + +def export_model(sym, params): + pass \ No newline at end of file diff --git a/python/mxnet/contrib/serde/_import/__init__.py b/python/mxnet/contrib/serde/_import/__init__.py new file mode 100644 index 000000000000..3df0384885e1 --- /dev/null +++ b/python/mxnet/contrib/serde/_import/__init__.py @@ -0,0 +1,26 @@ +# coding: utf-8 +"""import function""" +import onnx +from .import_onnx import GraphProto + +def import_model(model_file): + """Imports the supplied ONNX model file into MXNet symbol and parameters. + + Parameters + ---------- + model_file : ONNX model file name + + Returns + ------- + sym : mx.symbol + Compatible mxnet symbol + + params : dict of str to mx.ndarray + Dict of converted parameters stored in mx.ndarray format + """ + graph = GraphProto() + + # loads model file and returns ONNX protobuf object + model_proto = onnx.load(model_file) + sym, params = graph.from_onnx(model_proto.graph) + return sym, params \ No newline at end of file diff --git a/python/mxnet/contrib/serde/_import/backend.py b/python/mxnet/contrib/serde/_import/backend.py new file mode 100644 index 000000000000..b2eaca76d0e7 --- /dev/null +++ b/python/mxnet/contrib/serde/_import/backend.py @@ -0,0 +1,129 @@ +# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# http://www.apache.org/licenses/LICENSE-2.0 +# or in the "license" file accompanying this file. This file 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. + +# coding: utf-8 +# pylint: disable=too-many-locals,invalid-name +"""backend wrapper for onnx test infrastructure""" +from collections import namedtuple +import mxnet as mx +from onnx.backend.base import Backend +from .import_onnx import GraphProto +from .backend_rep import MXNetBackendRep + +# Using these functions for onnx test infrastructure. +# Implemented by following onnx docs guide: +# https://github.com/onnx/onnx/blob/master/docs/Implementing%20an%20ONNX%20backend.md +# MXNetBackend class will take an ONNX model with inputs, perform a computation, +# and then return the output. + +class MXNetBackend(Backend): + """MXNet backend for ONNX""" + @classmethod + def run_node(cls, node, inputs, device='CPU'): + """Running individual node inference on mxnet engine and + return the result to onnx test infrastructure. + + Parameters + ---------- + node : onnx node object + loaded onnx node (individual layer) + inputs : numpy array + input to run a node on + device : 'CPU' + device to run a node on + + Returns + ------- + params : numpy array + result obtained after running the operator + """ + graph = GraphProto() + sym = graph.run_node(node) + data_names = [i for i in node.input] + data_shapes = [] + reduce_op_types = set(['ReduceMin', 'ReduceMax', 'ReduceMean', + 'ReduceProd', 'ReduceSum', 'Slice', 'Pad', + 'Squeeze', 'Upsample', 'Reshape']) + + # Adding extra dimension of batch_size 1 if the batch_size is different for multiple inputs. + for idx, input_name in enumerate(data_names): + batch_size = 1 + if len(inputs[idx].shape) < 4 and len(inputs) > 1 and \ + len(set(x.shape[0] for x in inputs)) != 1: + tuples = ((batch_size,), inputs[idx].shape) + new_shape = sum(tuples, ()) + data_shapes.append((input_name, new_shape)) + else: + data_shapes.append((input_name, inputs[idx].shape)) + + # create module, passing cpu context + if device == 'CPU': + ctx = mx.cpu() + else: + raise NotImplementedError("Only CPU context is supported for now") + + # create a module + mod = mx.mod.Module(symbol=sym, data_names=data_names, context=ctx, label_names=None) + mod.bind(for_training=False, data_shapes=data_shapes, label_shapes=None) + + # initializing parameters for calculating result of each individual node + mod.init_params() + + batch = namedtuple('Batch', ['data']) + + data_forward = [] + for val in inputs: + # slice and pad operator tests needs 1 less dimension in forward pass + # otherwise it will throw an error. + # for squeeze operator, need to retain shape of input as provided + if node.op_type in reduce_op_types: + data_forward.append(mx.nd.array(val)) + else: + data_forward.append(mx.nd.array([val])) + + mod.forward(batch(data_forward)) + result = mod.get_outputs()[0].asnumpy() + if node.op_type in reduce_op_types: + return [result] + return result + + @classmethod + def prepare(cls, model, device='CPU', **kwargs): + """For running end to end model(used for onnx test backend) + + Parameters + ---------- + model : onnx ModelProto object + loaded onnx graph + device : 'CPU' + specifying device to run test on + kwargs : + other arguments + + Returns + ------- + MXNetBackendRep : object + Returns object of MXNetBackendRep class which will be in turn + used to run inference on the input model and return the result for comparison. + """ + graph = GraphProto() + sym, params = graph.from_onnx(model.graph) + return MXNetBackendRep(sym, params, device) + + @classmethod + def supports_device(cls, device): + """Supports only CPU for testing""" + return device == 'CPU' + +prepare = MXNetBackend.prepare + +run_node = MXNetBackend.run_node + +supports_device = MXNetBackend.supports_device diff --git a/python/mxnet/contrib/serde/_import/backend_rep.py b/python/mxnet/contrib/serde/_import/backend_rep.py new file mode 100644 index 000000000000..d22924c65cc7 --- /dev/null +++ b/python/mxnet/contrib/serde/_import/backend_rep.py @@ -0,0 +1,68 @@ +# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# http://www.apache.org/licenses/LICENSE-2.0 +# or in the "license" file accompanying this file. This file 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. + +# coding: utf-8 +# pylint: disable=too-few-public-methods +"""backend rep for onnx test infrastructure""" +from collections import namedtuple +import mxnet as mx +import numpy as np +from onnx.backend.base import BackendRep + +# Using these functions for onnx test infrastructure. +# Implemented by following onnx docs guide: +# https://github.com/onnx/onnx/blob/master/docs/Implementing%20an%20ONNX%20backend.md +# MXNetBackendRep object will be returned by MXNetBackend's prepare method which is used to +# execute a model repeatedly. +# Inputs will be passed to the run method of MXNetBackendRep class, it will perform computation and +# retrieve the corresponding results for comparison to the onnx backend. +# https://github.com/onnx/onnx/blob/master/onnx/backend/test/runner/__init__.py. + +class MXNetBackendRep(BackendRep): + """Running model inference on mxnet engine and return the result + to onnx test infrastructure for comparison.""" + def __init__(self, symbol, params, device): + self.symbol = symbol + self.params = params + self.device = device + + def run(self, inputs, **kwargs): + """Run model inference and return the result + + Parameters + ---------- + inputs : numpy array + input to run a layer on + + Returns + ------- + params : numpy array + result obtained after running the inference on mxnet + """ + input_data = np.asarray(inputs[0], dtype='f') + + # create module, passing cpu context + if self.device == 'CPU': + ctx = mx.cpu() + else: + raise NotImplementedError("Only CPU context is supported for now") + + mod = mx.mod.Module(symbol=self.symbol, data_names=['input_0'], context=ctx, + label_names=None) + mod.bind(for_training=False, data_shapes=[('input_0', input_data.shape)], + label_shapes=None) + mod.set_params(arg_params=self.params, aux_params=None) + + # run inference + batch = namedtuple('Batch', ['data']) + + mod.forward(batch([mx.nd.array(input_data)])) + result = mod.get_outputs()[0].asnumpy() + return [result] diff --git a/python/mxnet/contrib/serde/_import/common.py b/python/mxnet/contrib/serde/_import/common.py new file mode 100644 index 000000000000..3e415324856a --- /dev/null +++ b/python/mxnet/contrib/serde/_import/common.py @@ -0,0 +1,142 @@ +# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# http://www.apache.org/licenses/LICENSE-2.0 +# or in the "license" file accompanying this file. This file 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. + +# Derived from Apache 2.0 licensed common.py file from DMLC NNVM: +# https://github.com/dmlc/nnvm/blob/3da53e46db57c438b05fbebe8aa332ee8c5994d1/python/nnvm/frontend/common.py + +# coding: utf-8 +# pylint: disable=invalid-name,no-self-use,too-many-branches,too-few-public-methods,too-many-arguments +"""Shared functions and classes for frontends.""" +from __future__ import absolute_import as _abs +from mxnet.base import string_types + +class Renamer(object): + """A simple renamer for operators. + + Parameters + ---------- + new_name : str + The new name for the operator + """ + def __init__(self, new_name): + self._new_name = new_name + + def __call__(self, attrs): + return self._new_name, attrs + + +class AttributeConverter(object): + """Common attribute converter. An AttributeConverter instance is a callable: + ``` + attr_converter = AttributeConverter(op_name, transforms={'a':'b', 'c':('d', 1)}) + new_op_name, new_attr = attr_converter(attrs) + ``` + + Parameters + ---------- + op_name : str or callable + If set as str, returned operator name is the str. + If set as callable, returned operator is the str returned by calling: + `op_name = func(attr)` + transforms : dict of `new_name, or (new_name, default_value, transform function)` + If only a new_name is provided, it's like renaming the attribute name. + If default_value if provided, then the attribute is considered as optional. + If transform function is provided, the original attribute value is handled + by transform function. + excludes : list + A list of excluded attributes that should `NOT` appear. + Raise NotImplementedError if occurred. + disables : list + A list of attributes that is disabled in mxnet. Raise warnings. + ignores : list + A list of attributes that is ignored in mxnet. Silent. + extras : dict + A series of additional attributes should be added anyway to the returned + attribute dict. + custom_check : callable + A custom function takes attribute, and return True/False. + Raise RuntimeError if not bool(True) returned. + """ + def __init__(self, op_name, transforms=None, + excludes=None, disables=None, ignores=None, + extras=None, custom_check=None): + self._op_name = op_name + self._transforms = transforms if transforms else {} + self._excludes = excludes if excludes else [] + self._disables = disables if disables else [] + self._ignores = ignores if ignores else [] + self._extras = extras if extras else {} + self._custom_check = custom_check + + def __call__(self, attrs): + # apply custom check + if self._custom_check: + func, msg = self._custom_check + if not func(attrs): + raise RuntimeError("Check failed: {}".format(msg)) + # get new op_name + if isinstance(self._op_name, string_types): + op_name = self._op_name + else: + assert callable(self._op_name), "op_name can either be string or callable" + op_name = self._op_name(attrs) + # convert attributes + new_attrs = {} + for k in attrs.keys(): + if k in self._excludes: + raise NotImplementedError("Attribute {} not supported yet.".format(k)) + elif k in self._ignores: + pass + elif k in self._transforms: + new_name, defaults, transform = self._parse_default(self._transforms[k]) + if defaults is None: + new_attr = self._required_attr(attrs, k) + else: + new_attr = attrs.get(k, None) + if new_attr is None: + new_attrs[new_name] = defaults + else: + new_attrs[new_name] = transform(new_attr) + else: + # copy + new_attrs[k] = attrs[k] + # add extras + new_attrs.update(self._extras) + return op_name, new_attrs + + def _parse_default(self, target): + """Helper function to parse default values.""" + if not isinstance(target, (list, tuple)): + k, v, t = target, None, lambda x: x + elif len(target) == 1: + k, v, t = target[0], None, lambda x: x + elif len(target) == 2: + k, v, t = target[0], target[1], lambda x: x + elif len(target) > 2: + k, v, t = target[0], target[1], target[2] + else: + k = None # should raise + if not isinstance(k, string_types): + msg = "{} is not a valid target, (name, default) expected.".format(target) + raise ValueError(msg) + return k, v, t + + def _parse_bool(self, value): + """Helper function to parse default boolean values.""" + if isinstance(value, string_types): + return value.strip().lower() in ['true', '1', 't', 'y', 'yes'] + return bool(value) + + def _required_attr(self, attr, key): + """Wrapper for getting required attributes.""" + assert isinstance(attr, dict) + if key not in attr: + raise AttributeError("Required attribute {} not found.".format(key)) + return attr[key] diff --git a/python/mxnet/contrib/serde/_import/import_helper.py b/python/mxnet/contrib/serde/_import/import_helper.py new file mode 100644 index 000000000000..b41ac1221645 --- /dev/null +++ b/python/mxnet/contrib/serde/_import/import_helper.py @@ -0,0 +1,244 @@ +# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# http://www.apache.org/licenses/LICENSE-2.0 +# or in the "license" file accompanying this file. This file 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. + +# Derived from Apache 2.0 licensed onnx.py file from DMLC NNVM: +# https://github.com/dmlc/nnvm/blob/3da53e46db57c438b05fbebe8aa332ee8c5994d1/python/nnvm/frontend/onnx.py + +# coding: utf-8 +# pylint: disable=invalid-name +"""Operator attributes conversion""" +from onnx_mxnet.common import Renamer, AttributeConverter as AttrCvt + +def _revert_caffe2_pad(attr): + """Removing extra padding from Caffe2.""" + if len(attr) == 4: + attr = attr[:2] + elif len(attr) == 2: + pass + else: + raise ValueError("Invalid caffe2 type padding: {}".format(attr)) + return attr + +def _math_name_picker(surfix): + def _impl(attr): + if attr.get('broadcast', 0): + return 'broadcast_' + surfix + return 'elemwise_' + surfix + return _impl + +def _broadcast_constraint(): + def _broadcast_check(attrs): + if attrs.get('axis', None): + return False + return True + return _broadcast_check, "Specifying broadcast axis not allowed." + +def _dimension_constraint(): + """checking dimensions for conv, deconv, pooling operators""" + def _dim_check(attrs): + if len(attrs['kernel_shape']) == 2: + return True + return False + return _dim_check, "Only 2d kernel supported." + +def _elemwise(name): + """converting attributes for add operator""" + return AttrCvt( + op_name=_math_name_picker(name), + disables=['axis'], + ignores=['broadcast']) + +def _pooling(name): + """converting attributes for pooling operator""" + return AttrCvt( + op_name='Pooling', + transforms={ + 'kernel_shape': 'kernel', + 'strides': 'stride', + 'pads': 'pad'}, + # pooling convention full to match caffe2 + extras={'pool_type': name, 'pooling_convention':'valid'}, + custom_check=_dimension_constraint()) + +def _conv(): + """converting attributes for convolution operator""" + return AttrCvt( + op_name='Convolution', + transforms={ + 'kernel_shape': 'kernel', + 'strides': 'stride', + 'dilations': ('dilate', (0, 0)), + 'pads': ('pad', (0, 0), _revert_caffe2_pad), + 'group': ('num_group', 1)}, + custom_check=_dimension_constraint()) + +def _conv_transpose(): + """converting attributes for deconvolution operator""" + return AttrCvt( + op_name='Deconvolution', + transforms={ + 'kernel_shape': 'kernel', + 'strides': 'stride', + 'dilations': ('dilate', (0, 0)), + 'pads': ('pad', (0, 0), _revert_caffe2_pad), + 'group': ('num_group', 1)}, + disables=['output_shape'], + custom_check=_dimension_constraint()) + +def _batch_norm(): + """converting attributes for BatchNorm operator""" + return AttrCvt( + op_name='BatchNorm', + transforms={'epsilon': 'eps'}, + extras={'cudnn_off': 1}, + ignores=['spatial', 'is_test', 'consumed_inputs']) + +def _activation(name): + """converting attributes for LeakyRelu operator""" + return AttrCvt( + op_name='LeakyReLU', + transforms={ + 'alpha':'slope'}, + extras={'act_type': name}) + +def _pad_sequence_fix(attr, kernelDim): + """Changing onnx's pads sequence to match with mxnet's pad_width + mxnet: (x1_begin, x1_end, ... , xn_begin, xn_end) + onnx: (x1_begin, x2_begin, ... , xn_end, xn_end)""" + new_attr = () + if len(attr) % 2 == 0: + for index in range(int(len(attr) / 2)): + new_attr = new_attr + attr[index::int(len(attr) / 2)] + # Making sure pad values are in the attr for all axes. + while len(new_attr) < kernelDim*2: + new_attr = new_attr + (0, 0) + + return new_attr + +def _pad(): + """converting attributes for Pad operator""" + return AttrCvt( + op_name='pad', + transforms={ + 'pads': ('pad_width', (0, 0, 0, 0, 0, 0, 0, 0), _pad_sequence_fix), + 'value': 'constant_value'}) + +def _global_pooling(name): + """Requires kernel attribute which is not present in onnx currently. + So for now giving default kernel.""" + return AttrCvt( + op_name='Pooling', + extras={'global_pool': True, + 'kernel': (1, 1), + 'pool_type': name}) + +def _upsample_scale_fix(attr): + """Scale attribute conversion from float to int""" + return int(attr) + +def _upsample_restrict_mode(attr): + """Mxnet's current UpSampling operator doesn't work well in bilinear mode. + New operator is coming in this PR https://github.com/apache/incubator-mxnet/pull/9688/ + Issue to track this: https://github.com/onnx/onnx-mxnet/issues/33 + For now, only nearest mode is enabled.""" + if attr.decode() != 'nearest': + raise ValueError("Only nearest mode is supported: {}".format(attr)) + return attr.decode() + +def _upsample(name): + """converting attributes for UpSampling operator""" + return AttrCvt( + op_name=name, + transforms={'height_scale': ('scale', 1, _upsample_scale_fix), + 'mode': ('sample_type', 'nearest', _upsample_restrict_mode), + 'width_scale': ('scale', 1, _upsample_scale_fix)}) + +# compatible operators that do NOT require any conversion. +_identity_list = [] + +# _convert_map defines maps of name to converter functor(callable) +_convert_map = { + # defs/experimental + 'FC' : AttrCvt('FullyConnected', ignores=['axis', 'axis_w']), + + # defs/generator + 'Constant': Renamer('identity'), + 'RandomUniform' : AttrCvt('random_uniform', ignores=['seed']), + 'RandomNormal' : AttrCvt('random_normal', {'mean':'loc'}, ignores=['seed']), + 'RandomUniformLike' : AttrCvt('random_uniform', ignores=['seed']), + 'RandomNormalLike': AttrCvt('random_normal', {'mean':'loc'}, ignores=['seed']), + + # defs/logical + + # defs/math + 'Add' : _elemwise('add'), + 'Sub' : _elemwise('sub'), + 'Mul' : _elemwise('mul'), + 'Div' : _elemwise('div'), + 'Neg' : Renamer('negative'), + 'Abs' : Renamer('abs'), + 'Reciprocal' : Renamer('reciprocal'), + 'Floor' : Renamer('floor'), + 'Ceil' : Renamer('ceil'), + 'Sqrt' : Renamer('sqrt'), + 'Gemm' : AttrCvt('linalg_gemm', {'transA':'transpose_a', 'transB':'transpose_b'}, + ignores=['broadcast']), + 'Relu' : Renamer('relu'), + 'LeakyRelu' : AttrCvt('LeakyReLU', {'alpha': 'slope'}), + # 'Selu' + 'Elu' : _activation('elu'), + 'Exp' : Renamer('exp'), + 'Log' : Renamer('log'), + 'Tanh' : Renamer('tanh'), + 'Pow' : AttrCvt('pow', {'exponent':'exp'}), + 'Dot' : Renamer('dot'), + 'MatMul' : Renamer('linalg_gemm2'), + # 'PRelu' + 'Sigmoid' : Renamer('sigmoid'), + 'Max' : Renamer('maximum'), #elemwise maximum + 'Min' : Renamer('minimum'), #elemwise minimum + 'Sum' : Renamer('add_n'), #elemwise sum + # softmax default axis is different in onnx + 'Softmax' : AttrCvt('softmax', extras={'axis': 1}), + + # defs/nn + 'AveragePool' : _pooling('avg'), + 'MaxPool' : _pooling('max'), + 'Conv' : _conv(), + 'ConvTranspose' : _conv_transpose(), + 'GlobalAveragePool': _global_pooling('avg'), + 'GlobalMaxPool' : _global_pooling('max'), + 'BatchNormalization': _batch_norm(), + 'SpatialBN' : _batch_norm(), + 'Dropout' : AttrCvt('Dropout', {'ratio': 'p'}, ignores=['is_test']), + 'Flatten' : Renamer('flatten'), + 'LRN' : AttrCvt('LRN', {'bias': 'knorm', 'size' : 'nsize'}), + # defs/reduction + 'ReduceMax' : AttrCvt('max', {'axes': 'axis'}), + 'ReduceMin' : AttrCvt('min', {'axes': 'axis'}), + 'ReduceSum' : AttrCvt('sum', {'axes': 'axis'}), + 'ReduceMean' : AttrCvt('mean', {'axes': 'axis'}), + 'ReduceProd' : AttrCvt('prod', {'axes': 'axis'}), + # 'ReduceLogSumExp' + 'ArgMax' : Renamer('argmax'), + 'ArgMin' : Renamer('argmin'), + + # defs/tensor + 'Cast' : AttrCvt('cast', {'to': 'dtype'}), + 'Reshape' : Renamer('reshape'), + 'Concat' : AttrCvt('concat', {'axis': 'dim'}), + 'Split' : AttrCvt('split', {'split': 'num_outputs'}), + 'Pad' : _pad(), + 'Slice' : AttrCvt('slice_axis', {'axes': 'axis', 'ends': 'end', 'starts': 'begin'}), + 'Transpose' : AttrCvt('transpose', {'perm': 'axes'}), + 'Squeeze' : AttrCvt('split', {'axes': 'axis'}), + # 'Gather' + 'Upsample' : _upsample('UpSampling') +} diff --git a/python/mxnet/contrib/serde/_import/import_onnx.py b/python/mxnet/contrib/serde/_import/import_onnx.py new file mode 100644 index 000000000000..a45bb0e183db --- /dev/null +++ b/python/mxnet/contrib/serde/_import/import_onnx.py @@ -0,0 +1,327 @@ +# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# http://www.apache.org/licenses/LICENSE-2.0 +# or in the "license" file accompanying this file. This file 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. + +# Derived from Apache 2.0 licensed onnx.py file from DMLC NNVM: +# https://github.com/dmlc/nnvm/blob/3da53e46db57c438b05fbebe8aa332ee8c5994d1/python/nnvm/frontend/onnx.py + +# coding: utf-8 +# pylint: disable=invalid-name,too-many-locals,no-self-use +""" Support import export formats.""" +from __future__ import absolute_import as _abs +import mxnet as mx +from onnx_mxnet.import_helper import _identity_list, _convert_map, _pad_sequence_fix + +def _convert_operator(op_name, attrs, identity_list=None, convert_map=None): + """Convert from onnx operator to mxnet operator. + The converter must specify conversions explicitly for incompatible name, and + apply handlers to operator attributes. + + Parameters + ---------- + op_name : str + Operator name, such as Convolution, FullyConnected + attrs : dict + Dict of operator attributes + identity_list : list + List of operators that don't require conversion + convert_map : dict + Dict of name : callable, where name is the op's name that + require conversion to mxnet, callable are functions which + take attrs and return (new_op_name, new_attrs) + + Returns + ------- + (op_name, attrs) + Converted (op_name, attrs) for mxnet. + """ + identity_list = identity_list if identity_list else _identity_list + convert_map = convert_map if convert_map else _convert_map + if op_name in identity_list: + pass + elif op_name in convert_map: + op_name, attrs = convert_map[op_name](attrs) + else: + raise NotImplementedError("Operator {} not implemented.".format(op_name)) + op = getattr(mx.sym, op_name, None) + if not op: + raise RuntimeError("Unable to map op_name {} to sym".format(op_name)) + return op, attrs + +class GraphProto(object): + """A helper class for handling mxnet symbol copying from pb2.GraphProto. + Definition: https://github.com/onnx/onnx/blob/master/onnx/onnx.proto + """ + def __init__(self): + self._nodes = {} + self._params = {} + self._renames = {} + self._num_input = 0 + self._num_param = 0 + + def from_onnx(self, graph): + """Construct symbol from onnx graph. + The inputs from onnx graph is vague, only providing "1", "2"... + For convenience, we rename the `real` input names to "input_0", + "input_1"... And renaming parameters to "param_0", "param_1"... + + Parameters + ---------- + graph : onnx protobuf object + The loaded onnx graph + + Returns + ------- + sym :mx.sym + The returned mxnet symbol + params : dict + A dict of name: mx.nd.array pairs, used as pretrained weights + """ + # parse network inputs, aka parameters + for init_tensor in graph.initializer: + if not init_tensor.name.strip(): + raise ValueError("Tensor's name is required.") + self._params[init_tensor.name] = self._parse_array(init_tensor) + + # converting GraphProto message + for i in graph.input: + if i.name in self._params: + # i is a param instead of input + name_param = 'param_{}'.format(self._num_param) + self._num_param += 1 + self._params[name_param] = self._params.pop(i.name) + self._nodes[name_param] = mx.sym.Variable(name=name_param, + shape=self._params[name_param].shape) + self._renames[i.name] = name_param + else: + name_input = 'input_{}'.format(self._num_input) + self._num_input += 1 + self._nodes[name_input] = mx.sym.Variable(name=name_input) + self._renames[i.name] = name_input + + # constructing nodes, nodes are stored as directed acyclic graph + # converting NodeProto message + for node in graph.node: + op_name = node.op_type + node_name = node.name.strip() + node_name = node_name if node_name else None + onnx_attr = self._parse_attr(node.attribute) + new_op, mx_attr = _convert_operator(op_name, onnx_attr) + inputs = [self._nodes[self._renames.get(i, i)] for i in node.input] + + # some workarounds for inconsistencies in onnx and mxnet conventions. + mx_attr = self._fix_bias(new_op, mx_attr, len(inputs)) + mx_attr = self._fix_channels(new_op, mx_attr, list(node.input)) + self._fix_bias_shape(node.op_type, node.input, onnx_attr) + + # calling again to get new symbols after some workarounds + inputs = [self._nodes[self._renames.get(i, i)] for i in node.input] + + # onnx's Gemm operator also supports broadcasting C input which + # mxnet's equivalent linalg_gemm doesn't. So using combination of + # transpose and FullyConnected operators. + if op_name == 'Gemm': + new_op, inputs, mx_attr = self._fix_gemm('FullyConnected', inputs, onnx_attr) + + # onnx slice works on multiple axes whereas mxnet's slice_axis is for single axis + if op_name == 'Slice': + op = self._fix_slice(inputs, mx_attr) + elif op_name == 'AveragePool' and onnx_attr.get('pads') is not None or \ + op_name == 'MaxPool' and onnx_attr.get('pads') is not None: + op = self._fix_pooling(op_name, inputs, onnx_attr) + elif op_name == 'Squeeze': + op = self._fix_squeeze(inputs, mx_attr) + else: + op = new_op(name=node_name, *inputs, **mx_attr) + + node_output = self._fix_outputs(op_name, node.output) + + assert len(node_output) == len(op.list_outputs()), ( + "Number of output mismatch {} vs {} in {}.".format( + len(node_output), len(op.list_outputs()), op_name)) + for k, i in zip(list(node_output), range(len(node_output))): + self._nodes[k] = op[i] + # now return the outputs + out = [self._nodes[i.name] for i in graph.output] + if len(out) > 1: + out = mx.sym.Group(out) + else: + out = out[0] + return out, self._params + + def run_node(self, node, device='CPU'): # pylint: disable=unused-argument + """Construct symbol from individual node. + Mainly using this function for unittests""" + op_name = node.op_type + attr = self._parse_attr(node.attribute) + new_op, new_attr = _convert_operator(op_name, attr) + sym_list = [mx.sym.Variable(node_name) for node_name in node.input] + + # some workarounds for onnx problem + new_attr = self._fix_bias(new_op, new_attr, len(sym_list)) + new_attr = self._fix_channels(new_op, new_attr, list(node.input)) + + # calling again to get new symbols after some workarounds + sym_list = [mx.sym.Variable(node_name) for node_name in node.input] + + # onnx slice works on multiple axes whereas mxnet's slice_axis is for single axis + if op_name == 'Slice': + op = self._fix_slice(sym_list, new_attr) + elif op_name == 'Squeeze': + op = self._fix_squeeze(sym_list, new_attr) + else: + op = new_op(*sym_list, **new_attr) + + node_output = self._fix_outputs(op_name, node.output) + for k, i in zip(list(node_output), range(len(node_output))): + self._nodes[k] = op[i] + + # now return the outputs + return op + + def _fix_pooling(self, op_name, inputs, new_attr): + """onnx pooling operator supports asymmetrical padding + Adding pad operator before pooling in mxnet to work with onnx""" + pool_type = 'avg' if op_name == 'AveragePool' else 'max' + stride = new_attr.get('strides') + kernel = new_attr.get('kernel_shape') + padding = new_attr.get('pads') + pad_width = (0, 0, 0, 0) + _pad_sequence_fix(padding, len(kernel)) + new_pad_op = mx.sym.pad(inputs[0], mode='constant', pad_width=pad_width) + new_pooling_op = mx.sym.Pooling(new_pad_op, pool_type=pool_type, + stride=stride, kernel=kernel) + return new_pooling_op + + def _fix_slice(self, inputs, new_attr): + """onnx slice provides slicing on multiple axis. Adding multiple slice_axis operator + for multiple axes from mxnet""" + begin = new_attr.get('begin') + end = new_attr.get('end') + axes = new_attr.get('axis', tuple(range(len(begin)))) + slice_op = mx.sym.slice_axis(inputs[0], axis=axes[0], begin=begin[0], end=end[0]) + if len(axes) > 1: + for i, axis in enumerate(axes): + slice_op = mx.sym.slice_axis(slice_op, axis=axis, begin=begin[i], end=end[i]) + return slice_op + + def _fix_squeeze(self, inputs, new_attr): + """ + MXNet doesnt have a squeeze operator. + Using "split" to perform similar operation. + "split" can be slower compared to "reshape". + This can have performance impact. + TODO: Remove this implementation once mxnet adds the support. + """ + axes = new_attr.get('axis') + op = mx.sym.split(inputs[0], axis=axes[0], num_outputs=1, squeeze_axis=1) + for i in axes[1:]: + op = mx.sym.split(op, axis=i-1, num_outputs=1, squeeze_axis=1) + return op + + def _fix_gemm(self, op_name, inputs, old_attr): + """Using FullyConnected operator in place of linalg_gemm to perform same operation""" + op = getattr(mx.sym, op_name, None) + alpha = float(old_attr.get('alpha', 1.0)) + beta = float(old_attr.get('beta', 1.0)) + transA = int(old_attr.get('transA', 0)) + transB = int(old_attr.get('transB', 0)) + if transA: + inputs[0] = mx.sym.transpose(inputs[0], axes=(1, 0)) + if not transB: + inputs[1] = mx.sym.transpose(inputs[1], axes=(1, 0)) + new_inputs = [alpha*inputs[0], inputs[1], beta*inputs[2]] + new_attr = {'num_hidden' : self._params[inputs[2].name].shape[0]} + return op, new_inputs, new_attr + + def _parse_array(self, tensor_proto): + """Grab data in TensorProto and convert to numpy array.""" + try: + from onnx.numpy_helper import to_array + except ImportError as e: + raise ImportError("Unable to import onnx which is required {}".format(e)) + np_array = to_array(tensor_proto).reshape(tuple(tensor_proto.dims)) + return mx.nd.array(np_array) + + def _parse_attr(self, attr_proto): + """Convert a list of AttributeProto to a dict, with names as keys.""" + attrs = {} + for a in attr_proto: + for f in ['f', 'i', 's']: + if a.HasField(f): + attrs[a.name] = getattr(a, f) + for f in ['floats', 'ints', 'strings']: + if list(getattr(a, f)): + assert a.name not in attrs, "Only one type of attr is allowed" + attrs[a.name] = tuple(getattr(a, f)) + for f in ['t', 'g']: + if a.HasField(f): + attrs[a.name] = getattr(a, f) + for f in ['tensors', 'graphs']: + if list(getattr(a, f)): + raise NotImplementedError("Filed {} is not supported in mxnet.".format(f)) + if a.name not in attrs: + raise ValueError("Cannot parse attribute: \n{}\n.".format(a)) + return attrs + + def _fix_outputs(self, op, outputs): + """A workaround to handle dropout or similar operator that have more than one out + in ONNX. + """ + if op == 'Dropout': + assert len(outputs) == 2, "ONNX have two outputs for dropout layer." + outputs = outputs[:-1] + return outputs + + def _fix_bias(self, op, attrs, num_inputs): + """A workaround for 'use_bias' attribute since onnx don't provide this attribute, + we have to check the number of inputs to decide it.""" + if op not in [mx.sym.Convolution, mx.sym.Deconvolution, mx.sym.FullyConnected]: + return attrs + if num_inputs == 3: + attrs['no_bias'] = False + elif num_inputs == 2: + attrs['no_bias'] = True + else: + raise ValueError("Unexpected number of inputs for: {}".format(op)) + return attrs + + + def _fix_bias_shape(self, op_name, inputs, attrs): + """A workaround to reshape bias term to (1, num_channel).""" + if (op_name == 'Add' or op_name == 'Mul') and \ + ('broadcast' in attrs and attrs['broadcast'] == 1): + assert len(list(inputs)) == 2 + bias_name = self._renames.get(inputs[1], inputs[1]) + bias = self._params[bias_name] + assert len(bias.shape) == 1 + # reshape to (1, n) + bias = mx.nd.array(bias.asnumpy().reshape((1, -1, 1, 1))) + # broadcast_add expects shape with sym.variable + self._nodes[bias_name] = mx.sym.Variable(name=bias_name, shape=bias.shape) + self._params[bias_name] = bias + + + def _fix_channels(self, op, attrs, inputs): + """A workaround for getting 'channels' or 'units' since onnx don't provide + these attributes. We check the shape of weights provided to get the number. + """ + if op not in [mx.sym.Convolution, mx.sym.Deconvolution, mx.sym.FullyConnected]: + return attrs + weight_name = self._renames[inputs[1]] + if not weight_name in self._params: + raise ValueError("Unable to get channels/units attr from onnx graph.") + else: + wshape = self._params[weight_name].shape + assert len(wshape) >= 2, "Weights shape is invalid: {}".format(wshape) + channels = wshape[0] + if op in [mx.sym.FullyConnected]: + attrs['num_hidden'] = channels + else: + attrs['num_filter'] = channels + return attrs diff --git a/python/mxnet/contrib/serde/_import/tests/__init__.py b/python/mxnet/contrib/serde/_import/tests/__init__.py new file mode 100644 index 000000000000..dfd786a881b3 --- /dev/null +++ b/python/mxnet/contrib/serde/_import/tests/__init__.py @@ -0,0 +1,9 @@ +# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# http://www.apache.org/licenses/LICENSE-2.0 +# or in the "license" file accompanying this file. This file 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. diff --git a/python/mxnet/contrib/serde/_import/tests/onnx_backend_test.py b/python/mxnet/contrib/serde/_import/tests/onnx_backend_test.py new file mode 100644 index 000000000000..1716ed8f7359 --- /dev/null +++ b/python/mxnet/contrib/serde/_import/tests/onnx_backend_test.py @@ -0,0 +1,73 @@ +# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# http://www.apache.org/licenses/LICENSE-2.0 +# or in the "license" file accompanying this file. This file 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. +"""onnx test backend wrapper""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import unittest + +import onnx.backend.test +from onnx_mxnet import backend as mxnet_backend + +# This is a pytest magic variable to load extra plugins +pytest_plugins = 'onnx.backend.test.report' + +backend_test = onnx.backend.test.BackendTest(mxnet_backend, __name__) + +# will add model tests later +backend_test.exclude('test_bvlc_alexnet') +backend_test.exclude('test_densenet121') +backend_test.exclude('test_inception_v1') +backend_test.exclude('test_inception_v2') +backend_test.exclude('test_resnet50') +backend_test.exclude('test_shufflenet') + +# Not implemented +unimplemented_operators = [ + 'test_and*', + 'test_clip*', + 'test_equal*', + 'test_gather*', + 'test_greater*', + 'test_hardmax*', + 'test_hardsigmoid*', + 'test_less*', + 'test_logsoftmax*', + 'test_mean*', + 'test_not*', + 'test_or*', + 'test_selu*', + 'test_shape*', + 'test_size*', + 'test_softplus*', + 'test_softsign*', + 'test_thresholdedrelu*', + 'test_top*', + 'test_unsqueeze*', + 'test_xor*', + 'test_Embedding*', + 'test_PReLU*', + 'test_Softplus*', + #'test_Upsample*', + 'test_operator*', + 'test_constant_cpu' + ] +for op_test in unimplemented_operators: + backend_test.exclude(op_test) + +# import all test cases at global scope to make them visible to python.unittest +globals().update(backend_test + .enable_report() + .test_cases) + +if __name__ == '__main__': + unittest.main() diff --git a/python/mxnet/contrib/serde/_import/tests/test_models.py b/python/mxnet/contrib/serde/_import/tests/test_models.py new file mode 100644 index 000000000000..14f64b2afbaa --- /dev/null +++ b/python/mxnet/contrib/serde/_import/tests/test_models.py @@ -0,0 +1,89 @@ +# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# http://www.apache.org/licenses/LICENSE-2.0 +# or in the "license" file accompanying this file. This file 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. + +# coding: utf-8 +"""Testing model conversions from onnx/models repo""" +from __future__ import absolute_import as _abs +from __future__ import print_function +from collections import namedtuple +import os +import tarfile +import mxnet as mx +from mxnet.test_utils import download +import numpy as np +import numpy.testing as npt +import onnx_mxnet + +URLS = { + 'squeezenet_onnx' : 'https://s3.amazonaws.com/download.onnx/models/squeezenet.tar.gz', + 'shufflenet_onnx' : 'https://s3.amazonaws.com/download.onnx/models/shufflenet.tar.gz', + 'inception_v1_onnx' : 'https://s3.amazonaws.com/download.onnx/models/inception_v1.tar.gz', + 'inception_v2_onnx' : 'https://s3.amazonaws.com/download.onnx/models/inception_v2.tar.gz', + 'bvlc_alexnet_onnx' : 'https://s3.amazonaws.com/download.onnx/models/bvlc_alexnet.tar.gz', + 'densenet121_onnx' : 'https://s3.amazonaws.com/download.onnx/models/densenet121.tar.gz', + 'resnet50_onnx' : 'https://s3.amazonaws.com/download.onnx/models/resnet50.tar.gz', + 'vgg16_onnx' : 'https://s3.amazonaws.com/download.onnx/models/vgg16.tar.gz', + 'vgg19_onnx' : 'https://s3.amazonaws.com/download.onnx/models/vgg19.tar.gz' +} + +def extract_file(model_tar): + """Extract tar file and returns model path and input, output data""" + # extract tar file + tar = tarfile.open(model_tar, "r:*") + tar.extractall() + tar.close() + path = model_tar.rsplit('_', 1)[0] + # return model, inputs, outputs path + cur_dir = os.path.abspath(os.path.dirname(__file__)) + model_path = os.path.join(cur_dir, path, 'model.onnx') + npz_path = os.path.join(cur_dir, path, 'test_data_0.npz') + sample = np.load(npz_path, encoding='bytes') + input_data = list(sample['inputs']) + output_data = list(sample['outputs']) + return model_path, input_data, output_data + +def verify_onnx_forward_impl(model_path, input_data, output_data): + """Verifies result after inference""" + print("Converting onnx format to mxnet's symbol and params...") + sym, params = onnx_mxnet.import_model(model_path) + + # create module + mod = mx.mod.Module(symbol=sym, data_names=['input_0'], context=mx.cpu(), label_names=None) + mod.bind(for_training=False, data_shapes=[('input_0', input_data.shape)], label_shapes=None) + mod.set_params(arg_params=params, aux_params=params, allow_missing=True, allow_extra=True) + # run inference + Batch = namedtuple('Batch', ['data']) + + mod.forward(Batch([mx.nd.array(input_data)]), is_train=False) + + # Run the model with an onnx backend and verify the results + npt.assert_equal(mod.get_outputs()[0].shape, output_data.shape) + npt.assert_almost_equal(output_data, mod.get_outputs()[0].asnumpy(), decimal=3) + print("Conversion Successful") + +def verify_model(name): + """Testing models from onnx model zoo""" + print("Testing model ", name) + download(URLS.get(name), name) + model_path, inputs, outputs = extract_file(name) + input_data = np.asarray(inputs[0], dtype=np.float32) + output_data = np.asarray(outputs[0], dtype=np.float32) + verify_onnx_forward_impl(model_path, input_data, output_data) + +if __name__ == '__main__': + verify_model('squeezenet_onnx') # working + verify_model('bvlc_alexnet_onnx') # working + verify_model('vgg16_onnx') # working + verify_model('vgg19_onnx') # working + #verify_model('inception_v1_onnx') # working, accuracy is different 1.4 + #verify_model('inception_v2_onnx') # working, accuracy is different 7.4 + #verify_model('shufflenet_onnx') # working, accuracy is different 10.2 + verify_model('densenet121_onnx') # working + #verify_model('resnet50_onnx') # working, accuracy is different 18.1 diff --git a/python/mxnet/contrib/serde/_import/tests/test_super_resolution.py b/python/mxnet/contrib/serde/_import/tests/test_super_resolution.py new file mode 100644 index 000000000000..b96666f7f610 --- /dev/null +++ b/python/mxnet/contrib/serde/_import/tests/test_super_resolution.py @@ -0,0 +1,53 @@ +# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# http://www.apache.org/licenses/LICENSE-2.0 +# or in the "license" file accompanying this file. This file 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. +"""Testing super_resolution model conversion""" +from __future__ import absolute_import as _abs +from __future__ import print_function +from collections import namedtuple +import mxnet as mx +from mxnet.test_utils import download +import numpy as np +from PIL import Image +import onnx_mxnet + +model_url = 'https://s3.amazonaws.com/onnx-mxnet/examples/super_resolution.onnx' + +download(model_url, 'super_resolution.onnx') + +print("Converting onnx format to mxnet's symbol and params...") +sym, params = onnx_mxnet.import_model('super_resolution.onnx') + +# Load test image +input_image_dim = 224 +output_image_dim = 672 +img_url = 'https://s3.amazonaws.com/onnx-mxnet/examples/super_res_input.jpg' +download(img_url, 'super_res_input.jpg') +img = Image.open('super_res_input.jpg').resize((input_image_dim, input_image_dim)) +img_ycbcr = img.convert("YCbCr") +img_y, img_cb, img_cr = img_ycbcr.split() +x = np.array(img_y)[np.newaxis, np.newaxis, :, :] + +# create module +mod = mx.mod.Module(symbol=sym, data_names=['input_0'], label_names=None) +mod.bind(for_training=False, data_shapes=[('input_0', x.shape)]) +mod.set_params(arg_params=params, aux_params=None) + +# run inference +Batch = namedtuple('Batch', ['data']) +mod.forward(Batch([mx.nd.array(x)])) + +# Save the result +img_out_y = Image.fromarray(np.uint8(mod.get_outputs()[0][0][0].asnumpy().clip(0, 255)), mode='L') + +result_img = Image.merge( + "YCbCr", [img_out_y, + img_cb.resize(img_out_y.size, Image.BICUBIC), + img_cr.resize(img_out_y.size, Image.BICUBIC)]).convert("RGB") +result_img.save("super_res_output.jpg") From 1b0c5164596c47467de1923f8975a4503085dbcc Mon Sep 17 00:00:00 2001 From: Acharya Date: Mon, 26 Feb 2018 16:30:49 -0800 Subject: [PATCH 02/14] fixing dependencies --- python/mxnet/contrib/serde/_import/backend.py | 12 +++-- .../contrib/serde/_import/backend_rep.py | 10 ++-- python/mxnet/contrib/serde/_import/common.py | 2 +- .../contrib/serde/_import/import_onnx.py | 49 ++++++++++--------- .../serde/_import/tests/test_models.py | 10 ++-- .../_import/tests/test_super_resolution.py | 10 ++-- python/setup.py | 2 +- 7 files changed, 52 insertions(+), 43 deletions(-) diff --git a/python/mxnet/contrib/serde/_import/backend.py b/python/mxnet/contrib/serde/_import/backend.py index b2eaca76d0e7..f1fe6456900a 100644 --- a/python/mxnet/contrib/serde/_import/backend.py +++ b/python/mxnet/contrib/serde/_import/backend.py @@ -12,10 +12,12 @@ # pylint: disable=too-many-locals,invalid-name """backend wrapper for onnx test infrastructure""" from collections import namedtuple -import mxnet as mx from onnx.backend.base import Backend from .import_onnx import GraphProto from .backend_rep import MXNetBackendRep +from .... import context +from .... import module +from .... import ndarray as nd # Using these functions for onnx test infrastructure. # Implemented by following onnx docs guide: @@ -65,12 +67,12 @@ def run_node(cls, node, inputs, device='CPU'): # create module, passing cpu context if device == 'CPU': - ctx = mx.cpu() + ctx = context.cpu() else: raise NotImplementedError("Only CPU context is supported for now") # create a module - mod = mx.mod.Module(symbol=sym, data_names=data_names, context=ctx, label_names=None) + mod = module.Module(symbol=sym, data_names=data_names, context=ctx, label_names=None) mod.bind(for_training=False, data_shapes=data_shapes, label_shapes=None) # initializing parameters for calculating result of each individual node @@ -84,9 +86,9 @@ def run_node(cls, node, inputs, device='CPU'): # otherwise it will throw an error. # for squeeze operator, need to retain shape of input as provided if node.op_type in reduce_op_types: - data_forward.append(mx.nd.array(val)) + data_forward.append(nd.array(val)) else: - data_forward.append(mx.nd.array([val])) + data_forward.append(nd.array([val])) mod.forward(batch(data_forward)) result = mod.get_outputs()[0].asnumpy() diff --git a/python/mxnet/contrib/serde/_import/backend_rep.py b/python/mxnet/contrib/serde/_import/backend_rep.py index d22924c65cc7..53db24ea07f2 100644 --- a/python/mxnet/contrib/serde/_import/backend_rep.py +++ b/python/mxnet/contrib/serde/_import/backend_rep.py @@ -12,9 +12,11 @@ # pylint: disable=too-few-public-methods """backend rep for onnx test infrastructure""" from collections import namedtuple -import mxnet as mx import numpy as np from onnx.backend.base import BackendRep +from .... import context +from .... import module +from .... import ndarray as nd # Using these functions for onnx test infrastructure. # Implemented by following onnx docs guide: @@ -50,11 +52,11 @@ def run(self, inputs, **kwargs): # create module, passing cpu context if self.device == 'CPU': - ctx = mx.cpu() + ctx = context.cpu() else: raise NotImplementedError("Only CPU context is supported for now") - mod = mx.mod.Module(symbol=self.symbol, data_names=['input_0'], context=ctx, + mod = module.Module(symbol=self.symbol, data_names=['input_0'], context=ctx, label_names=None) mod.bind(for_training=False, data_shapes=[('input_0', input_data.shape)], label_shapes=None) @@ -63,6 +65,6 @@ def run(self, inputs, **kwargs): # run inference batch = namedtuple('Batch', ['data']) - mod.forward(batch([mx.nd.array(input_data)])) + mod.forward(batch([nd.array(input_data)])) result = mod.get_outputs()[0].asnumpy() return [result] diff --git a/python/mxnet/contrib/serde/_import/common.py b/python/mxnet/contrib/serde/_import/common.py index 3e415324856a..5aa70cbbd734 100644 --- a/python/mxnet/contrib/serde/_import/common.py +++ b/python/mxnet/contrib/serde/_import/common.py @@ -15,7 +15,7 @@ # pylint: disable=invalid-name,no-self-use,too-many-branches,too-few-public-methods,too-many-arguments """Shared functions and classes for frontends.""" from __future__ import absolute_import as _abs -from mxnet.base import string_types +from ....base import string_types class Renamer(object): """A simple renamer for operators. diff --git a/python/mxnet/contrib/serde/_import/import_onnx.py b/python/mxnet/contrib/serde/_import/import_onnx.py index a45bb0e183db..274dbe0544fb 100644 --- a/python/mxnet/contrib/serde/_import/import_onnx.py +++ b/python/mxnet/contrib/serde/_import/import_onnx.py @@ -15,7 +15,8 @@ # pylint: disable=invalid-name,too-many-locals,no-self-use """ Support import export formats.""" from __future__ import absolute_import as _abs -import mxnet as mx +from .... import symbol +from .... import ndarray as nd from onnx_mxnet.import_helper import _identity_list, _convert_map, _pad_sequence_fix def _convert_operator(op_name, attrs, identity_list=None, convert_map=None): @@ -49,7 +50,7 @@ def _convert_operator(op_name, attrs, identity_list=None, convert_map=None): op_name, attrs = convert_map[op_name](attrs) else: raise NotImplementedError("Operator {} not implemented.".format(op_name)) - op = getattr(mx.sym, op_name, None) + op = getattr(symbol, op_name, None) if not op: raise RuntimeError("Unable to map op_name {} to sym".format(op_name)) return op, attrs @@ -78,10 +79,10 @@ def from_onnx(self, graph): Returns ------- - sym :mx.sym + sym :symbol.Symbol The returned mxnet symbol params : dict - A dict of name: mx.nd.array pairs, used as pretrained weights + A dict of name: nd.array pairs, used as pretrained weights """ # parse network inputs, aka parameters for init_tensor in graph.initializer: @@ -96,13 +97,13 @@ def from_onnx(self, graph): name_param = 'param_{}'.format(self._num_param) self._num_param += 1 self._params[name_param] = self._params.pop(i.name) - self._nodes[name_param] = mx.sym.Variable(name=name_param, + self._nodes[name_param] = symbol.Variable(name=name_param, shape=self._params[name_param].shape) self._renames[i.name] = name_param else: name_input = 'input_{}'.format(self._num_input) self._num_input += 1 - self._nodes[name_input] = mx.sym.Variable(name=name_input) + self._nodes[name_input] = symbol.Variable(name=name_input) self._renames[i.name] = name_input # constructing nodes, nodes are stored as directed acyclic graph @@ -150,7 +151,7 @@ def from_onnx(self, graph): # now return the outputs out = [self._nodes[i.name] for i in graph.output] if len(out) > 1: - out = mx.sym.Group(out) + out = symbol.Group(out) else: out = out[0] return out, self._params @@ -161,14 +162,14 @@ def run_node(self, node, device='CPU'): # pylint: disable=unused-argument op_name = node.op_type attr = self._parse_attr(node.attribute) new_op, new_attr = _convert_operator(op_name, attr) - sym_list = [mx.sym.Variable(node_name) for node_name in node.input] + sym_list = [symbol.Variable(node_name) for node_name in node.input] # some workarounds for onnx problem new_attr = self._fix_bias(new_op, new_attr, len(sym_list)) new_attr = self._fix_channels(new_op, new_attr, list(node.input)) # calling again to get new symbols after some workarounds - sym_list = [mx.sym.Variable(node_name) for node_name in node.input] + sym_list = [symbol.Variable(node_name) for node_name in node.input] # onnx slice works on multiple axes whereas mxnet's slice_axis is for single axis if op_name == 'Slice': @@ -193,8 +194,8 @@ def _fix_pooling(self, op_name, inputs, new_attr): kernel = new_attr.get('kernel_shape') padding = new_attr.get('pads') pad_width = (0, 0, 0, 0) + _pad_sequence_fix(padding, len(kernel)) - new_pad_op = mx.sym.pad(inputs[0], mode='constant', pad_width=pad_width) - new_pooling_op = mx.sym.Pooling(new_pad_op, pool_type=pool_type, + new_pad_op = symbol.pad(inputs[0], mode='constant', pad_width=pad_width) + new_pooling_op = symbol.Pooling(new_pad_op, pool_type=pool_type, stride=stride, kernel=kernel) return new_pooling_op @@ -204,10 +205,10 @@ def _fix_slice(self, inputs, new_attr): begin = new_attr.get('begin') end = new_attr.get('end') axes = new_attr.get('axis', tuple(range(len(begin)))) - slice_op = mx.sym.slice_axis(inputs[0], axis=axes[0], begin=begin[0], end=end[0]) + slice_op = symbol.slice_axis(inputs[0], axis=axes[0], begin=begin[0], end=end[0]) if len(axes) > 1: for i, axis in enumerate(axes): - slice_op = mx.sym.slice_axis(slice_op, axis=axis, begin=begin[i], end=end[i]) + slice_op = symbol.slice_axis(slice_op, axis=axis, begin=begin[i], end=end[i]) return slice_op def _fix_squeeze(self, inputs, new_attr): @@ -219,22 +220,22 @@ def _fix_squeeze(self, inputs, new_attr): TODO: Remove this implementation once mxnet adds the support. """ axes = new_attr.get('axis') - op = mx.sym.split(inputs[0], axis=axes[0], num_outputs=1, squeeze_axis=1) + op = symbol.split(inputs[0], axis=axes[0], num_outputs=1, squeeze_axis=1) for i in axes[1:]: - op = mx.sym.split(op, axis=i-1, num_outputs=1, squeeze_axis=1) + op = symbol.split(op, axis=i-1, num_outputs=1, squeeze_axis=1) return op def _fix_gemm(self, op_name, inputs, old_attr): """Using FullyConnected operator in place of linalg_gemm to perform same operation""" - op = getattr(mx.sym, op_name, None) + op = getAttr(symbol, op_name, None) alpha = float(old_attr.get('alpha', 1.0)) beta = float(old_attr.get('beta', 1.0)) transA = int(old_attr.get('transA', 0)) transB = int(old_attr.get('transB', 0)) if transA: - inputs[0] = mx.sym.transpose(inputs[0], axes=(1, 0)) + inputs[0] = symbol.transpose(inputs[0], axes=(1, 0)) if not transB: - inputs[1] = mx.sym.transpose(inputs[1], axes=(1, 0)) + inputs[1] = symbol.transpose(inputs[1], axes=(1, 0)) new_inputs = [alpha*inputs[0], inputs[1], beta*inputs[2]] new_attr = {'num_hidden' : self._params[inputs[2].name].shape[0]} return op, new_inputs, new_attr @@ -246,7 +247,7 @@ def _parse_array(self, tensor_proto): except ImportError as e: raise ImportError("Unable to import onnx which is required {}".format(e)) np_array = to_array(tensor_proto).reshape(tuple(tensor_proto.dims)) - return mx.nd.array(np_array) + return nd.array(np_array) def _parse_attr(self, attr_proto): """Convert a list of AttributeProto to a dict, with names as keys.""" @@ -281,7 +282,7 @@ def _fix_outputs(self, op, outputs): def _fix_bias(self, op, attrs, num_inputs): """A workaround for 'use_bias' attribute since onnx don't provide this attribute, we have to check the number of inputs to decide it.""" - if op not in [mx.sym.Convolution, mx.sym.Deconvolution, mx.sym.FullyConnected]: + if op not in [symbol.Convolution, symbol.Deconvolution, symbol.FullyConnected]: return attrs if num_inputs == 3: attrs['no_bias'] = False @@ -301,9 +302,9 @@ def _fix_bias_shape(self, op_name, inputs, attrs): bias = self._params[bias_name] assert len(bias.shape) == 1 # reshape to (1, n) - bias = mx.nd.array(bias.asnumpy().reshape((1, -1, 1, 1))) + bias = nd.array(bias.asnumpy().reshape((1, -1, 1, 1))) # broadcast_add expects shape with sym.variable - self._nodes[bias_name] = mx.sym.Variable(name=bias_name, shape=bias.shape) + self._nodes[bias_name] = symbol.Variable(name=bias_name, shape=bias.shape) self._params[bias_name] = bias @@ -311,7 +312,7 @@ def _fix_channels(self, op, attrs, inputs): """A workaround for getting 'channels' or 'units' since onnx don't provide these attributes. We check the shape of weights provided to get the number. """ - if op not in [mx.sym.Convolution, mx.sym.Deconvolution, mx.sym.FullyConnected]: + if op not in [symbol.Convolution, symbol.Deconvolution, symbol.FullyConnected]: return attrs weight_name = self._renames[inputs[1]] if not weight_name in self._params: @@ -320,7 +321,7 @@ def _fix_channels(self, op, attrs, inputs): wshape = self._params[weight_name].shape assert len(wshape) >= 2, "Weights shape is invalid: {}".format(wshape) channels = wshape[0] - if op in [mx.sym.FullyConnected]: + if op in [symbol.FullyConnected]: attrs['num_hidden'] = channels else: attrs['num_filter'] = channels diff --git a/python/mxnet/contrib/serde/_import/tests/test_models.py b/python/mxnet/contrib/serde/_import/tests/test_models.py index 14f64b2afbaa..354c49a4eae4 100644 --- a/python/mxnet/contrib/serde/_import/tests/test_models.py +++ b/python/mxnet/contrib/serde/_import/tests/test_models.py @@ -15,8 +15,10 @@ from collections import namedtuple import os import tarfile -import mxnet as mx -from mxnet.test_utils import download +from ..... import context +from ..... import module +from ..... import ndarray as nd +from .....test_utils import download import numpy as np import numpy.testing as npt import onnx_mxnet @@ -55,13 +57,13 @@ def verify_onnx_forward_impl(model_path, input_data, output_data): sym, params = onnx_mxnet.import_model(model_path) # create module - mod = mx.mod.Module(symbol=sym, data_names=['input_0'], context=mx.cpu(), label_names=None) + mod = module.Module(symbol=sym, data_names=['input_0'], context=context.cpu(), label_names=None) mod.bind(for_training=False, data_shapes=[('input_0', input_data.shape)], label_shapes=None) mod.set_params(arg_params=params, aux_params=params, allow_missing=True, allow_extra=True) # run inference Batch = namedtuple('Batch', ['data']) - mod.forward(Batch([mx.nd.array(input_data)]), is_train=False) + mod.forward(Batch([nd.array(input_data)]), is_train=False) # Run the model with an onnx backend and verify the results npt.assert_equal(mod.get_outputs()[0].shape, output_data.shape) diff --git a/python/mxnet/contrib/serde/_import/tests/test_super_resolution.py b/python/mxnet/contrib/serde/_import/tests/test_super_resolution.py index b96666f7f610..28cb364a692e 100644 --- a/python/mxnet/contrib/serde/_import/tests/test_super_resolution.py +++ b/python/mxnet/contrib/serde/_import/tests/test_super_resolution.py @@ -11,8 +11,10 @@ from __future__ import absolute_import as _abs from __future__ import print_function from collections import namedtuple -import mxnet as mx -from mxnet.test_utils import download +from ..... import context +from ..... import module +from ..... import ndarray as nd +from .....test_utils import download import numpy as np from PIL import Image import onnx_mxnet @@ -35,13 +37,13 @@ x = np.array(img_y)[np.newaxis, np.newaxis, :, :] # create module -mod = mx.mod.Module(symbol=sym, data_names=['input_0'], label_names=None) +mod = module.Module(symbol=sym, data_names=['input_0'], label_names=None) mod.bind(for_training=False, data_shapes=[('input_0', x.shape)]) mod.set_params(arg_params=params, aux_params=None) # run inference Batch = namedtuple('Batch', ['data']) -mod.forward(Batch([mx.nd.array(x)])) +mod.forward(Batch([nd.array(x)])) # Save the result img_out_y = Image.fromarray(np.uint8(mod.get_outputs()[0][0][0].asnumpy().clip(0, 255)), mode='L') diff --git a/python/setup.py b/python/setup.py index cf94adf982d5..1ef14d95a0d1 100644 --- a/python/setup.py +++ b/python/setup.py @@ -28,7 +28,7 @@ else: from setuptools import setup from setuptools.extension import Extension - kwargs = {'install_requires': ['numpy<=1.13.3,>=1.8.2', 'requests==2.18.4', 'graphviz==0.8.1'], 'zip_safe': False} + kwargs = {'install_requires': ['numpy<=1.13.3,>=1.8.2', 'requests==2.18.4', 'graphviz==0.8.1', 'onnx>=1.0.1'], 'zip_safe': False} from setuptools import find_packages with_cython = False From ada45fb259974afca135cb86ec6bf084644e23dd Mon Sep 17 00:00:00 2001 From: thinksanky <31976455+thinksanky@users.noreply.github.com> Date: Tue, 27 Feb 2018 02:50:30 -0800 Subject: [PATCH 03/14] Updated build_doc.sh to build on the new release tag found (#9896) * temporary fix to update the verionsioning of 1.1.0 that is skipped during build process * updated build_doc.sh * testing new updates of build_doc.sh * fixed comments and syntax * removed test data and comments --- docs/build_version_doc/build_doc.sh | 67 ++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/docs/build_version_doc/build_doc.sh b/docs/build_version_doc/build_doc.sh index eefc81e362e8..2e4ed063d5ed 100755 --- a/docs/build_version_doc/build_doc.sh +++ b/docs/build_version_doc/build_doc.sh @@ -19,10 +19,22 @@ set -e set -x +# This script is run on a nightly basis. Refer to Job: http://jenkins.mxnet-ci.amazon-ml.com/job/incubator-mxnet-build-site/ +# Job should pass in paramters: +# web_url=https://github.com/apache/incubator-mxnet-site +# web_branch=asf-site +# release_branch=v1.1.0 (example). This needs to come from the job config + +# First parameter sent by job configuration: https://github.com/apache/incubator-mxnet-site web_url="$1" + +# Second parameter sent by job configuration: asf-site +web_branch="$2" + web_folder="VersionedWeb" + local_build="latest" -web_branch="$2" + git clone $web_url $web_folder cd $web_folder git checkout $web_branch @@ -37,28 +49,64 @@ while read -r line do tag_list+=("$line") done < "$tag_list_file" + +# This is the first tag found in tag.txt latest_tag=${tag_list[0]} -echo "latest_tag is: $latest_tag" +echo "++++ LATEST TAG found in tag.txt file is : $latest_tag ++++" + commit_id=$(git rev-parse HEAD) + +# Find the current TAG in GIT curr_tag=${TAG} curr_tag=${curr_tag:5} -echo "Current tag is $curr_tag" + +echo "++++ CURRENT TAG IN GIT is $curr_tag ++++" + +# If current tag in git is newer than latest tag found in tag.txt if [[ "$curr_tag" != 'master' ]] && [ $curr_tag != $latest_tag ] then + echo "++++ Found a git TAG $curr_tag newer than mxnet repo tag $latest_tag , we need to build a new release ++++" + echo "assigning curr_tag to latest_tag" latest_tag=$curr_tag fi # Build new released tag if [ $latest_tag != ${tag_list[0]} ] then - echo "Building new tag" + echo " ****************************************** " + echo " Building new release on: $latest_tag " + echo " ****************************************** " git submodule update + + # checkout the latest release tag. + echo "++++ Checking out and building new tag $latest_tag ++++" + git checkout tags/$latest_tag make docs || exit 1 - echo -e "$latest_tag\n$(cat $tag_list_file)" > "$tag_list_file" - cat $tag_list_file + tests/ci_build/ci_build.sh doc python docs/build_version_doc/AddVersion.py --file_path "docs/_build/html/" --current_version "$latest_tag" tests/ci_build/ci_build.sh doc python docs/build_version_doc/AddPackageLink.py \ --file_path "docs/_build/html/install/index.html" --current_version "$latest_tag" + + # Update the tag_list (tag.txt). + ###### content of tag.txt######## + # + # 1.0.0 + # 0.12.1 + # 0.12.0 + # 0.11.0 + echo "++++ Adding $latest_tag to the top of the $tag_list_file ++++" + echo -e "$latest_tag\n$(cat $tag_list_file)" > "$tag_list_file" + cat $tag_list_file + + # The following block does the following: + # a. copies the static html that was built from new tag to a local sandbox folder. + # b. copies the $tag_list_file into local sandbox tag.txt + # c. removes .git in VersionedWeb folder + # d. copies VersionedWeb/versions to local sandbox versions folder. + # e. makes a new directory with the previous TAG version. N-1 version name (example current: 1.1.0, Previous: 1.0.0) + # f. Copies ReadMe.md to the local sandbox build. + # g. removes the content of VersionedWeb completely. + # f. Adds new content from local sandbox build to VersionedWeb. cp -a "docs/_build/html/." "$local_build" cp $tag_list_file "$local_build/tag.txt" rm -rf "$web_folder/.git" @@ -69,13 +117,18 @@ then rm -rf "$local_build/versions/${tag_list[0]}/versions" rm -rf "$web_folder/*" cp -a "$local_build/." "$web_folder" + + echo " ****************************************** " + echo " Successfully built new release $latest_tag " + echo " ****************************************** " fi # Build latest master +echo " ********** Building Master ************ " git checkout master git checkout -- . git submodule update -echo "Building master" + make docs || exit 1 rm -rfv $web_folder/versions/master/* From 8ee8fa2085052ff8f15e2cf2e3c75acfe86a0635 Mon Sep 17 00:00:00 2001 From: Aaron Markham Date: Tue, 27 Feb 2018 07:55:05 -0800 Subject: [PATCH 04/14] Docs build all versions refactor (#9878) * changed url references from dmlc to apache/incubator-mxnet * refactored build all docs * Created dockerfile and modified scripts to support it. * add license header * used license header tool this time * updated scripts to take arguments for flexible use --- docs/build_version_doc/Dockerfile | 44 ++++++++ docs/build_version_doc/build_all_version.sh | 86 ++++++++-------- docs/build_version_doc/build_site_tag.sh | 65 ++++++++++++ docs/build_version_doc/setup_docs_ubuntu.sh | 62 ++++++++++++ docs/build_version_doc/update_all_version.sh | 100 +++++++++++++++++++ 5 files changed, 313 insertions(+), 44 deletions(-) create mode 100755 docs/build_version_doc/Dockerfile create mode 100755 docs/build_version_doc/build_site_tag.sh create mode 100755 docs/build_version_doc/setup_docs_ubuntu.sh create mode 100755 docs/build_version_doc/update_all_version.sh diff --git a/docs/build_version_doc/Dockerfile b/docs/build_version_doc/Dockerfile new file mode 100755 index 000000000000..204320e32611 --- /dev/null +++ b/docs/build_version_doc/Dockerfile @@ -0,0 +1,44 @@ +FROM ubuntu:16.04 +LABEL maintainer="markhama@amazon.com" + +# Install dependencies +RUN apt-get update && apt-get install -y \ + apt-transport-https \ + build-essential \ + ca-certificates \ + curl \ + doxygen \ + git \ + libatlas-base-dev \ + liblapack-dev \ + libopenblas-dev \ + libopencv-dev \ + pandoc \ + python-numpy \ + python-pip \ + software-properties-common \ + unzip \ + wget + +# Setup Scala +RUN echo "deb https://dl.bintray.com/sbt/debian /" | tee -a /etc/apt/sources.list.d/sbt.list +RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2EE0EA64E40A89B84B2DF73499E82A75642AC823 +RUN apt-get update && apt-get install -y \ + sbt \ + scala + +RUN pip install --upgrade pip && pip install \ + beautifulsoup4 \ + breathe \ + CommonMark==0.5.4 \ + h5py \ + mock==1.0.1 \ + pypandoc \ + recommonmark==0.4.0 \ + sphinx==1.5.6 + + +COPY *.sh / +COPY *.py / +RUN /build_all_version.sh "1.1.0 1.0.0 0.12.1 0.12.0 0.11.0 master" +RUN /update_all_version.sh "1.1.0 1.0.0 0.12.1 0.12.0 0.11.0 master" 1.1.0 http://mxnet.incubator.apache.org/ diff --git a/docs/build_version_doc/build_all_version.sh b/docs/build_version_doc/build_all_version.sh index 6a37815fd50e..4db9326a4464 100755 --- a/docs/build_version_doc/build_all_version.sh +++ b/docs/build_version_doc/build_all_version.sh @@ -19,67 +19,65 @@ # This script is for locally building website for all versions # Built files are stored in $built -# Version numbers are stored in $tag_list. -# Version numbers are ordered from latest to old and final one is master. + +# Takes one argument: +# * tag list - space delimited list of Github tags; Example: "1.1.0 1.0.0 master" +# Example Usage: +# ./build_all_version.sh "1.1.0 1.0.0 master" + set -e set -x -tag_list="1.1.0 1.0.0 0.12.1 0.12.0 0.11.0 master" +if [ -z "$1" ] + then + echo "Please provide a list of version tags you wish to run." + exit 1 + else + tag_list="$1" + echo "Using these tags: $1" +fi mxnet_url="https://github.com/apache/incubator-mxnet.git" mxnet_folder="apache_mxnet" built="VersionedWeb" -mkdir $built -mkdir "$built/versions" -git clone $mxnet_url $mxnet_folder --recursive -cd "$mxnet_folder/docs" -tag_file="tag_list.txt" +if [ ! -d "$mxnet_folder" ]; then + mkdir $mxnet_folder + git clone $mxnet_url $mxnet_folder --recursive +fi -# Write all version numbers into $tag_file -for tag in $tag_list; do - if [ $tag != 'master' ] - then - echo "$tag" >> "$tag_file" - fi -done +if [ ! -d "$built" ]; then + mkdir $built + mkdir "$built/versions" +fi # Build all versions and use latest version(First version number in $tag_list) as landing page. -version_num=0 for tag in $tag_list; do + cd "$mxnet_folder" + git fetch if [ $tag == 'master' ] - then - git checkout master - else - git checkout "tags/$tag" + then + git checkout master + git pull + else + git checkout "tags/$tag" + fi + if [ $tag == '0.11.0' ] + then + git checkout master -- docs/mxdoc.py fi - git submodule update || exit 1 - cd .. make clean cd docs make clean - make html USE_OPENMP=0 || exit 1 - python build_version_doc/AddVersion.py --file_path "_build/html/" --current_version "$tag" || exit 1 - - if [ $tag != 'master' ] - then - python build_version_doc/AddPackageLink.py --file_path "_build/html/get_started/install.html" \ - --current_version "$tag" || exit 1 - fi - - if [ $version_num == 0 ] - then - cp -a _build/html/. "../../$built" - else - file_loc="../../$built/versions/$tag" - mkdir "$file_loc" - cp -a _build/html/. "$file_loc" + make html USE_OPENMP=1 || exit 1 + cd ../../ + file_loc="$built/versions/$tag" + if [ -d "$file_loc" ] ; then + rm -rf "$file_loc" fi - - ((++version_num)) + mkdir "$file_loc" + cp -a "$mxnet_folder/docs/_build/html/." "$file_loc" done - -mv "$tag_file" "../../$built/tag.txt" -cd ../.. -rm -rf "$mxnet_folder" + +echo "Now you may want to run update_all_version.sh to create the production layout with the versions dropdown and other per-version corrections." diff --git a/docs/build_version_doc/build_site_tag.sh b/docs/build_version_doc/build_site_tag.sh new file mode 100755 index 000000000000..d453e0cc9734 --- /dev/null +++ b/docs/build_version_doc/build_site_tag.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# 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. + + +# How this script works: +# 1. Receive tag list +# Looks like: tag_list="1.1.0 1.0.0 0.12.1 0.12.0 0.11.0 master" +# 2. Receive default tag (for main website view) +# 3. Receive root URL +# 4. Call build and then update scripts + +# Take user input or check env var for tag list +if [ -z "$1" ] + then + echo "No tag list supplied... trying environment variable $TAG_LIST" + else + tag_list="${TAG_LIST:-"$1"}" + echo "Using these tags: $1" +fi + +if [ -z "$tag_list" ] + then + echo "No tags defined" + exit 1 +fi + +if [ -z "$2" ] + then + echo "Please pick a version to use as a default for the website. Ex: 1.1.0" + exit 1 + else + tag_default=$2 +fi + +if [ -z "$3" ] + then + echo "Please provide the root url for the site. Ex: http://mxnet.incubator.apache.org/" + exit 1 + else + root_url=$3 +fi + +# Pass params to build and update scripts +for tag in $tag_list; do + ./build_all_version.sh $tag || exit 1 +done + +./update_all_version.sh "$tag_list" $tag_default $root_url || exit 1 + diff --git a/docs/build_version_doc/setup_docs_ubuntu.sh b/docs/build_version_doc/setup_docs_ubuntu.sh new file mode 100755 index 000000000000..98b9259c6966 --- /dev/null +++ b/docs/build_version_doc/setup_docs_ubuntu.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# 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. + + +# If you need to build <= v0.12.0 then use a Python 2 environment +# mxdoc.py - a sphinx extension, was not Python 3 compatible in the old versions +# source activate mxnet_p27 + +# Install dependencies +sudo apt-get update +sudo apt-get install -y \ + apt-transport-https \ + ca-certificates \ + curl \ + doxygen \ + software-properties-common + +pip install --user \ + beautifulsoup4 \ + breathe \ + CommonMark==0.5.4 \ + h5py \ + mock==1.0.1 \ + pypandoc \ + recommonmark==0.4.0 \ + sphinx==1.5.6 + +# Recommonmark/Sphinx errors: https://github.com/sphinx-doc/sphinx/issues/3800 + + +# Setup scala +echo "deb https://dl.bintray.com/sbt/debian /" | sudo tee -a /etc/apt/sources.list.d/sbt.list +sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2EE0EA64E40A89B84B2DF73499E82A75642AC823 +sudo apt-get update +sudo apt-get install -y \ + sbt \ + scala + +# Cleanup +sudo apt autoremove -y + +# Make docs using the manual way +# cd .. && make html USE_OPENMP=0 +# using the docker way +# sudo make docs + diff --git a/docs/build_version_doc/update_all_version.sh b/docs/build_version_doc/update_all_version.sh new file mode 100755 index 000000000000..e79b97117c5a --- /dev/null +++ b/docs/build_version_doc/update_all_version.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +# 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. + +# This script will update the html content from building +# different tags. +# It assumes you have already run build_all_version.sh for +# the tags you want to update. + +# Takes three arguments: +# * tag list - space delimited list of Github tags; Example: "1.1.0 1.0.0 master" +# * default tag - which version should the site default to; Example: 1.0.0 +# * root URL - for the versions dropdown to change to production or dev server; Example: http://mxnet.incubator.apache.org/ + +# Example Usage: +# ./update_all_version.sh "1.1.0 1.0.0 master" 1.0.0 http://mxnet.incubator.apache.org/ + +set -e +set -x + +if [ -z "$1" ] + then + echo "Please provide a list of version tags you wish to run. Ex : \"1.1.0 1.0.0 master\"" + exit 1 + else + tag_list=$1 +fi + +if [ -z "$2" ] + then + echo "Please pick a version to use as a default for the website. Ex: 1.1.0" + exit 1 + else + tag_default=$2 +fi + +if [ -z "$3" ] + then + echo "Please provide the root url for the site. Ex: http://mxnet.incubator.apache.org/" + exit 1 + else + root_url=$3 +fi + +mxnet_folder="apache_mxnet" +built="VersionedWeb" +tag_file="tag_list.txt" + +if [ -f "$tag_file" ]; then + rm $tag_file +fi + +# Write all version numbers into $tag_file for AddVersion.py to use later +# Master is added by that script by default +for tag in $tag_list; do + if [ $tag != 'master' ] + then + echo "$tag" >> "$tag_file" + fi +done + +# Update the specified tags with the Versions dropdown +for tag in $tag_list; do + # This Python script is expecting the tag_list.txt and it will use that as the entries to populate + python AddVersion.py --root_url "$root_url" --file_path "$built/versions/$tag" --current_version "$tag" || exit 1 + + if [ $tag != 'master' ] + then + python AddPackageLink.py --file_path "$built/versions/master/install/index.html" \ + --current_version "$tag" || exit 1 + fi + + if [ $tag == $tag_default ] + then + cp -a "$built/versions/$tag/." "$built" + else + file_loc="$built/versions/$tag" + #rm -rf "$file_loc" + #mkdir "$file_loc" + #cp -a $mxnet_folder/docs/_build/html/. "$file_loc" + fi +done + +echo "The output of this process can be found in the VersionedWeb folder." + From dc393437703db24d6c386f4de3c0a972d2c8ffe2 Mon Sep 17 00:00:00 2001 From: Marco de Abreu Date: Tue, 27 Feb 2018 17:36:14 +0100 Subject: [PATCH 05/14] Make sure build_doc only builds target version (#9900) --- docs/build_version_doc/build_doc.sh | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/docs/build_version_doc/build_doc.sh b/docs/build_version_doc/build_doc.sh index 2e4ed063d5ed..5e1a2b700668 100755 --- a/docs/build_version_doc/build_doc.sh +++ b/docs/build_version_doc/build_doc.sh @@ -121,19 +121,16 @@ then echo " ****************************************** " echo " Successfully built new release $latest_tag " echo " ****************************************** " -fi - -# Build latest master -echo " ********** Building Master ************ " -git checkout master -git checkout -- . -git submodule update +else + # Build latest master + echo " ********** Building Master ************ " -make docs || exit 1 + make docs || exit 1 -rm -rfv $web_folder/versions/master/* -cp -a "docs/_build/html/." "$web_folder/versions/master" -tests/ci_build/ci_build.sh doc python docs/build_version_doc/AddVersion.py --file_path "$web_folder/versions/master" + rm -rfv $web_folder/versions/master/* + cp -a "docs/_build/html/." "$web_folder/versions/master" + tests/ci_build/ci_build.sh doc python docs/build_version_doc/AddVersion.py --file_path "$web_folder/versions/master" +fi # Update version list for all previous version website if [ $latest_tag != ${tag_list[0]} ] From 3545697210edebe02ef68bad7465c3054d34313d Mon Sep 17 00:00:00 2001 From: Tianqi Chen Date: Tue, 27 Feb 2018 08:41:28 -0800 Subject: [PATCH 06/14] TVM bridge support to JIT NDArray Function by TVM (#9880) * TVM bridge support. Support wrap TVM compiled function as a NDArray function. * Testcases and CI to include TVM as dependency * address review comments * Add more comments, change to constexpr * change to log warn * update comment on the type code --- CMakeLists.txt | 2 +- Jenkinsfile | 10 +- Makefile | 4 +- dlpack | 2 +- include/mxnet/tensor_blob.h | 10 + nnvm | 2 +- python/mxnet/ndarray/ndarray.py | 7 + src/nnvm/tvm_bridge.cc | 180 ++++++++++++++++++ tests/ci_build/Dockerfile.gpu | 6 + tests/ci_build/install/ubuntu_install_llvm.sh | 28 +++ tests/ci_build/install/ubuntu_install_tvm.sh | 44 +++++ tests/python/gpu/test_tvm_bridge.py | 64 +++++++ 12 files changed, 349 insertions(+), 10 deletions(-) create mode 100644 src/nnvm/tvm_bridge.cc create mode 100755 tests/ci_build/install/ubuntu_install_llvm.sh create mode 100755 tests/ci_build/install/ubuntu_install_tvm.sh create mode 100644 tests/python/gpu/test_tvm_bridge.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 16d365355ceb..d229cb0847d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -234,6 +234,7 @@ include_directories("include") include_directories("mshadow") include_directories("3rdparty/cub") include_directories("nnvm/include") +include_directories("nnvm/tvm/include") include_directories("dmlc-core/include") include_directories("dlpack/include") @@ -696,4 +697,3 @@ endif() set(LINT_DIRS "include src plugin cpp-package tests") set(EXCLUDE_PATH "src/operator/contrib/ctc_include") add_custom_target(mxnet_lint COMMAND ${CMAKE_COMMAND} -DMSVC=${MSVC} -DPYTHON_EXECUTABLE=${PYTHON_EXECUTABLE} -DLINT_DIRS=${LINT_DIRS} -DPROJECT_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR} -DPROJECT_NAME=mxnet -DEXCLUDE_PATH=${EXCLUDE_PATH} -P ${CMAKE_CURRENT_SOURCE_DIR}/dmlc-core/cmake/lint.cmake) - diff --git a/Jenkinsfile b/Jenkinsfile index c23bbbfe5a50..7016c0f03157 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -38,12 +38,12 @@ def init_git() { deleteDir() retry(5) { try { - // Make sure wait long enough for api.github.com request quota. Important: Don't increase the amount of + // Make sure wait long enough for api.github.com request quota. Important: Don't increase the amount of // retries as this will increase the amount of requests and worsen the throttling timeout(time: 15, unit: 'MINUTES') { checkout scm - sh 'git submodule update --init' - sh 'git clean -d -f' + sh 'git submodule update --init --recursive' + sh 'git clean -d -f' } } catch (exc) { deleteDir() @@ -61,8 +61,8 @@ def init_git_win() { // retries as this will increase the amount of requests and worsen the throttling timeout(time: 15, unit: 'MINUTES') { checkout scm - bat 'git submodule update --init' - bat 'git clean -d -f' + bat 'git submodule update --init --recursive' + bat 'git clean -d -f' } } catch (exc) { deleteDir() diff --git a/Makefile b/Makefile index cb3e63ba13b0..5d81c7fbb160 100644 --- a/Makefile +++ b/Makefile @@ -91,7 +91,7 @@ ifeq ($(DEBUG), 1) else CFLAGS += -O3 -DNDEBUG=1 endif -CFLAGS += -I$(ROOTDIR)/mshadow/ -I$(ROOTDIR)/dmlc-core/include -fPIC -I$(NNVM_PATH)/include -I$(DLPACK_PATH)/include -Iinclude $(MSHADOW_CFLAGS) +CFLAGS += -I$(ROOTDIR)/mshadow/ -I$(ROOTDIR)/dmlc-core/include -fPIC -I$(NNVM_PATH)/include -I$(DLPACK_PATH)/include -I$(NNVM_PATH)/tvm/include -Iinclude $(MSHADOW_CFLAGS) LDFLAGS = -pthread $(MSHADOW_LDFLAGS) $(DMLC_LDFLAGS) ifeq ($(DEBUG), 1) NVCCFLAGS += -std=c++11 -Xcompiler -D_FORCE_INLINES -g -G -O0 -ccbin $(CXX) $(MSHADOW_NVCCFLAGS) @@ -356,7 +356,7 @@ ifeq ($(USE_CUDA), 1) LDFLAGS += -lcuda -lnvrtc CFLAGS += -DMXNET_ENABLE_CUDA_RTC=1 endif - # Make sure to add stubs as fallback in order to be able to build + # Make sure to add stubs as fallback in order to be able to build # without full CUDA install (especially if run without nvidia-docker) LDFLAGS += -L/usr/local/cuda/lib64/stubs SCALA_PKG_PROFILE := $(SCALA_PKG_PROFILE)-gpu diff --git a/dlpack b/dlpack index a6e09b58dc00..10892ac964f1 160000 --- a/dlpack +++ b/dlpack @@ -1 +1 @@ -Subproject commit a6e09b58dc00ee0065f5b7879800e646fbb01d1e +Subproject commit 10892ac964f1af7c81aae145cd3fab78bbccd297 diff --git a/include/mxnet/tensor_blob.h b/include/mxnet/tensor_blob.h index 168ddcca24b7..59c1eacb2c58 100755 --- a/include/mxnet/tensor_blob.h +++ b/include/mxnet/tensor_blob.h @@ -36,8 +36,18 @@ #include #include #include "./base.h" + namespace mxnet { +// redefine DLPack enumeration to be backward compatible. +constexpr const int kCPU = kDLCPU; +constexpr const int kGPU = kDLGPU; +// extension type code under TVM function. +// Currently NNVM reserved 16 to 19 type code from TVM +// 16, 17, 18 is used by NNVM compiler already. +// Pick code 19 for MXNet NDArray +constexpr const int kTVMNDArrayTypeCode = 19; + /* Forward declaration for friend declaration in TBlob */ class NDArray; diff --git a/nnvm b/nnvm index 7a052d678455..c342da72271c 160000 --- a/nnvm +++ b/nnvm @@ -1 +1 @@ -Subproject commit 7a052d678455f1c96538c1cc5a25f11115363558 +Subproject commit c342da72271c85e477480323f1d91997c6101ac0 diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index 59389ddb220d..5ac279635a1d 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -174,8 +174,15 @@ class NDArray(NDArrayBase): __slots__ = [] # make numpy functions return NDArray instead of numpy object array __array_priority__ = 1000.0 + # Extension type code for TVM function. + # See C++ side of definition(kTVMNDArrayTypeCode) at include/mxmet/tensor_blob.h + _tvm_tcode = 19 # pylint: disable= no-member, undefined-variable + @property + def _tvm_handle(self): + return self.handle.value + def __repr__(self): """Returns a string representation of the array.""" shape_info = 'x'.join(['%d' % x for x in self.shape]) diff --git a/src/nnvm/tvm_bridge.cc b/src/nnvm/tvm_bridge.cc new file mode 100644 index 000000000000..06929984640d --- /dev/null +++ b/src/nnvm/tvm_bridge.cc @@ -0,0 +1,180 @@ +/* + * 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. + */ + +/*! + * \file tvm_bridge.cc + * \brief Bridge to run TVM's PackedFunc in MXNet's async engine. + * + * This bridge is mainly used to expose MXNet's async engine push to + * TVM. It only uses TVM runtime in aheader only mode, which means + * there is no link dependencies. + * + * Support for TVM is optional even when this code + * is always compiled and built with the project. + * We choose this strategy because we do not yet want + * llvm as dependency(which TVM uses). So instead we expose hook + * to TVM and let user use this feature when they have TVM installed. + * + * We do require TVM and MXNet to be built with same C++ ABI of std::function + */ +#define TVM_RUNTIME_HEADER_ONLY 1 +#include +#include +#include +#include + +#include + +namespace mxnet { + +using tvm::runtime::PackedFunc; +using tvm::runtime::TVMArgs; +using tvm::runtime::TVMRetValue; + +/*! + * \brief Async functor object + * calling argument of the function. + */ +class TVMFunctor { + public: + // constructor + explicit TVMFunctor(PackedFunc func, PackedFunc fset_stream) + : func_(func), fset_stream_(fset_stream) {} + + void Init(const TVMArgs& args, + const std::vector& const_loc, + std::vector* const_vars, + std::vector* mutate_vars) { + values_.clear(); + type_codes_.clear(); + values_.insert(values_.end(), args.values, args.values + args.size()); + type_codes_.insert( + type_codes_.end(), args.type_codes, args.type_codes + args.size()); + + size_t const_loc_ptr = 0; + for (int i = 0; i < args.size(); ++i) { + if (args.type_codes[i] == kTVMNDArrayTypeCode) { + const NDArray& nd = + static_cast(args.values[i].v_handle)[0]; + // We cannot set the value until + type_codes_[i] = kArrayHandle; + array_data_.push_back(nd); + array_loc_.push_back(i); + // check if there is read or mutate + // by default assume we mutate the array. + if (const_loc_ptr < const_loc.size() && + i == const_loc[const_loc_ptr]) { + const_vars->push_back(nd.var()); + ++const_loc_ptr; + } else { + mutate_vars->push_back(nd.var()); + } + } else { + CHECK_LT(args.type_codes[i], kTVMType) + << "Only allow POD type in mxnet async call"; + } + } + } + + Context ctx() { + return array_data_[0].ctx(); + } + + void Run(const RunContext& rctx) { + // setup DLTensor + for (size_t i = 0; i < array_loc_.size(); ++i) { + values_[array_loc_[i]].v_handle = + const_cast(&(array_data_[i].data().dltensor())); + } + // run the packed function + TVMRetValue rv; + TVMArgs args(&values_[0], &type_codes_[0], values_.size()); + if (ctx().dev_type == Context::kGPU) { +#if MXNET_USE_CUDA + // pass stream via last argument. + void* strm = static_cast(rctx.get_stream()->stream_); + int dev_type = kDLGPU; + fset_stream_(dev_type, rctx.ctx.dev_id, strm); + func_.CallPacked(args, &rv); + fset_stream_(dev_type, rctx.ctx.dev_id, nullptr); +#else + LOG(FATAL) << "Please compile with CUDA enabled for cuda features"; +#endif + } else { + func_.CallPacked(args, &rv); + } + } + + private: + /*! \brief The function */ + PackedFunc func_; + /*! \brief Set stream */ + PackedFunc fset_stream_; + /*! \brief Values field */ + std::vector values_; + /*! \brief type code field */ + std::vector type_codes_; + /*! \brief arrays field */ + std::vector array_data_; + /*! \brief position of array in arguments */ + std::vector array_loc_; +}; + + +// Wrap a TVM function to a function that invokes MXNet's Engine +// It does two things: call the engine properly +// set up the NDArray to DLTensor during invocation. +void WrapAsyncCall(TVMArgs wrap_args, TVMRetValue* wrap_rv) { + PackedFunc f = wrap_args[0]; + PackedFunc fset_stream = wrap_args[1]; + int num_const = wrap_args[2]; + + // sorted position of constant arguments + std::vector const_loc; + for (int i = 0; i < num_const; ++i) { + const_loc.push_back(wrap_args[i + 3].operator int()); + } + std::sort(const_loc.begin(), const_loc.end()); + // wrapped function + // This is the function that called by the user. + auto wrapped = [f, fset_stream, const_loc](TVMArgs args, TVMRetValue* rv) { + std::shared_ptr func = + std::make_shared(f, fset_stream); + std::vector const_vars, mutate_vars; + func->Init(args, const_loc, &const_vars, &mutate_vars); + Engine *engine = Engine::Get(); + engine->DeduplicateVarHandle(&const_vars, &mutate_vars); + engine->PushSync([func](RunContext ctx) { + func->Run(ctx); + }, func->ctx(), const_vars, mutate_vars); + }; + *wrap_rv = PackedFunc(wrapped); +} + +} // namespace mxnet + +// C callback that can be used by TVM to extract +// the WrapAsyncCall function. +extern "C" MXNET_DLL int MXTVMBridge(TVMFunctionHandle pregister) { + using tvm::runtime::PackedFunc; + const PackedFunc& fregister = + *static_cast(pregister); + fregister("WrapAsyncCall", PackedFunc(mxnet::WrapAsyncCall)); + return 0; +} diff --git a/tests/ci_build/Dockerfile.gpu b/tests/ci_build/Dockerfile.gpu index 2483e62b99b5..bd1a00839167 100644 --- a/tests/ci_build/Dockerfile.gpu +++ b/tests/ci_build/Dockerfile.gpu @@ -12,3 +12,9 @@ COPY install/ubuntu_install_r.sh /install/ RUN /install/ubuntu_install_r.sh COPY install/ubuntu_install_perl.sh /install/ RUN /install/ubuntu_install_perl.sh + +COPY install/ubuntu_install_llvm.sh /install/ +RUN /install/ubuntu_install_llvm.sh + +COPY install/ubuntu_install_tvm.sh /install/ +RUN /install/ubuntu_install_tvm.sh diff --git a/tests/ci_build/install/ubuntu_install_llvm.sh b/tests/ci_build/install/ubuntu_install_llvm.sh new file mode 100755 index 000000000000..d3282e7a5fce --- /dev/null +++ b/tests/ci_build/install/ubuntu_install_llvm.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +# 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. + + + +echo deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-5.0 main\ + >> /etc/apt/sources.list.d/llvm.list +echo deb-src http://apt.llvm.org/xenial/ llvm-toolchain-xenial-5.0 main\ + >> /etc/apt/sources.list.d/llvm.list + +wget -O - http://apt.llvm.org/llvm-snapshot.gpg.key|sudo apt-key add - +apt-get update && apt-get install -y --force-yes llvm-5.0 diff --git a/tests/ci_build/install/ubuntu_install_tvm.sh b/tests/ci_build/install/ubuntu_install_tvm.sh new file mode 100755 index 000000000000..2729c7fe3bee --- /dev/null +++ b/tests/ci_build/install/ubuntu_install_tvm.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +# 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. + +# Build and install TVM +cd /tmp +git clone https://github.com/dmlc/tvm/ --recursive +cd tvm + +# This is a stable tag that support MXNet TVM bridge. +# We use this since support for mxnet bridge just checked +# into master and there is yet a version tag +git checkout 30eaf463e34d7c301357c31a010945d11df16537 + +cp make/config.mk +echo USE_CUDA=1 >> config.mk +echo LLVM_CONFIG=llvm-config-5.0 >> config.mk +echo USE_RPC=1 >> config.mk +echo USE_GRAPH_RUNTIME=1 >> config.mk +echo CUDA_PATH=/usr/local/cuda >> config.mk +make -j`nproc` + +cd python +python setup.py install +cd - + +cd topi/python +python setup.py install +cd - diff --git a/tests/python/gpu/test_tvm_bridge.py b/tests/python/gpu/test_tvm_bridge.py new file mode 100644 index 000000000000..292b9d91e5f7 --- /dev/null +++ b/tests/python/gpu/test_tvm_bridge.py @@ -0,0 +1,64 @@ +# 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. + +"""Test TVM bridge, only enable this when TVM is available""" +import logging +import mxnet as mx +import numpy as np + +def test_tvm_bridge(): + # only enable test if TVM is available + try: + import tvm + import tvm.contrib.mxnet + import topi + except ImportError: + logging.warn("TVM bridge test skipped because TVM is missing...") + return + + def check(target): + shape = (20,) + scale = tvm.var("scale", dtype="float32") + x = tvm.placeholder(shape) + y = tvm.placeholder(shape) + z = tvm.compute(shape, lambda i: x[i] + y[i]) + zz = tvm.compute(shape, lambda *i: z(*i) * scale) + ctx = mx.gpu(0) if target == "cuda" else mx.cpu(0) + target = tvm.target.create(target) + + # build the function + with target: + s = topi.generic.schedule_injective(zz) + f = tvm.build(s, [x, y, zz, scale]) + + # get a mxnet version + mxf = tvm.contrib.mxnet.to_mxnet_func(f, const_loc=[0, 1]) + xx = mx.nd.uniform(shape=shape, ctx=ctx) + yy = mx.nd.uniform(shape=shape, ctx=ctx) + zz = mx.nd.empty(shape=shape, ctx=ctx) + # invoke myf: this runs in mxnet engine + mxf(xx, yy, zz, 10.0) + np.testing.assert_allclose( + zz.asnumpy(), (xx.asnumpy() + yy.asnumpy()) * 10) + + check("llvm") + check("cuda") + + + +if __name__ == "__main__": + test_tvm_bridge() From 6005ad5adb0fec894bbef7c399b8c122ad96f2bd Mon Sep 17 00:00:00 2001 From: Marco de Abreu Date: Tue, 27 Feb 2018 19:01:09 +0100 Subject: [PATCH 07/14] Update website version dropdown on master (#9901) * Update website version dropdown on master * Update --- docs/build_version_doc/build_doc.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/build_version_doc/build_doc.sh b/docs/build_version_doc/build_doc.sh index 5e1a2b700668..427f40c592a0 100755 --- a/docs/build_version_doc/build_doc.sh +++ b/docs/build_version_doc/build_doc.sh @@ -136,9 +136,13 @@ fi if [ $latest_tag != ${tag_list[0]} ] then total=${#tag_list[*]} - for (( i=0; i<=$(( $total -1 )); i++ )) + for (( i=0; i<=$(( $total - 1 )); i++ )) + do tests/ci_build/ci_build.sh doc python docs/build_version_doc/AddVersion.py --file_path "$web_folder/versions/${tag_list[$i]}" \ --current_version "${tag_list[$i]}" done + + # Update master version dropdown + tests/ci_build/ci_build.sh doc python docs/build_version_doc/AddVersion.py --file_path "$web_folder/versions/master" fi From 7a0509d6f5ee19b0d3530fb2a4cb944e4f743b33 Mon Sep 17 00:00:00 2001 From: chsin Date: Tue, 27 Feb 2018 15:05:57 -0500 Subject: [PATCH 08/14] fix optimizer bug in CPP-Package (#9809) * opt register only once * lint pass * opt register only once lint pass * add test_optimizer.cpp * fix warning * typo --- Jenkinsfile | 2 ++ cpp-package/example/test_optimizer.cpp | 32 +++++++++++++++++++++ cpp-package/include/mxnet-cpp/optimizer.hpp | 17 ++++++----- cpp-package/tests/ci_test.sh | 3 ++ 4 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 cpp-package/example/test_optimizer.cpp diff --git a/Jenkinsfile b/Jenkinsfile index 7016c0f03157..78af1cf021d4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -332,6 +332,7 @@ try { make('build_cuda', flag) pack_lib('gpu') stash includes: 'build/cpp-package/example/test_score', name: 'cpp_test_score' + stash includes: 'build/cpp-package/example/test_optimizer', name: 'cpp_test_optimizer' } } }, @@ -676,6 +677,7 @@ try { init_git() unpack_lib('gpu') unstash 'cpp_test_score' + unstash 'cpp_test_optimizer' timeout(time: max_time, unit: 'MINUTES') { sh "${docker_run} gpu --dockerbinary nvidia-docker cpp-package/tests/ci_test.sh" } diff --git a/cpp-package/example/test_optimizer.cpp b/cpp-package/example/test_optimizer.cpp new file mode 100644 index 000000000000..bf465b786988 --- /dev/null +++ b/cpp-package/example/test_optimizer.cpp @@ -0,0 +1,32 @@ +/* + * 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. + */ +#include "mxnet-cpp/MxNetCpp.h" + +using namespace std; +using namespace mxnet::cpp; + +int main(int argc, char** argv) { + // Confirm >1 optimizers can be created w/o error + Optimizer* opt = OptimizerRegistry::Find("sgd"); + opt = OptimizerRegistry::Find("adam"); + int ret = (opt == 0) ? 1 : 0; + + MXNotifyShutdown(); + return ret; +} diff --git a/cpp-package/include/mxnet-cpp/optimizer.hpp b/cpp-package/include/mxnet-cpp/optimizer.hpp index e3d47d1161c6..26fd00f3a162 100644 --- a/cpp-package/include/mxnet-cpp/optimizer.hpp +++ b/cpp-package/include/mxnet-cpp/optimizer.hpp @@ -125,13 +125,16 @@ inline float Optimizer::GetWD_(int index) { } inline Optimizer* OptimizerRegistry::Find(const std::string& name) { - MXNETCPP_REGISTER_OPTIMIZER(sgd, SGDOptimizer); - MXNETCPP_REGISTER_OPTIMIZER(ccsgd, SGDOptimizer); // For backward compatibility - MXNETCPP_REGISTER_OPTIMIZER(rmsprop, RMSPropOptimizer); - MXNETCPP_REGISTER_OPTIMIZER(adam, AdamOptimizer); - MXNETCPP_REGISTER_OPTIMIZER(adagrad, AdaGradOptimizer); - MXNETCPP_REGISTER_OPTIMIZER(adadelta, AdaDeltaOptimizer); - MXNETCPP_REGISTER_OPTIMIZER(signum, SignumOptimizer); + if (cmap().empty()) { + // Optimizers should only be registered once + MXNETCPP_REGISTER_OPTIMIZER(sgd, SGDOptimizer); + MXNETCPP_REGISTER_OPTIMIZER(ccsgd, SGDOptimizer); // For backward compatibility + MXNETCPP_REGISTER_OPTIMIZER(rmsprop, RMSPropOptimizer); + MXNETCPP_REGISTER_OPTIMIZER(adam, AdamOptimizer); + MXNETCPP_REGISTER_OPTIMIZER(adagrad, AdaGradOptimizer); + MXNETCPP_REGISTER_OPTIMIZER(adadelta, AdaDeltaOptimizer); + MXNETCPP_REGISTER_OPTIMIZER(signum, SignumOptimizer); + } auto it = cmap().find(name); if (it == cmap().end()) return nullptr; diff --git a/cpp-package/tests/ci_test.sh b/cpp-package/tests/ci_test.sh index 3b2af35bf1be..2042529ace01 100755 --- a/cpp-package/tests/ci_test.sh +++ b/cpp-package/tests/ci_test.sh @@ -22,6 +22,9 @@ export LD_LIBRARY_PATH=$(readlink -f ../../lib):$LD_LIBRARY_PATH echo $LD_LIBRARY_PATH ls -l ../../lib/ +cp ../../build/cpp-package/example/test_optimizer . +./test_optimizer + cp ../../build/cpp-package/example/test_score . ./get_mnist.sh ./test_score 0.93 From 34af930dcdc6f4b06ebffc07c903696d0092bacb Mon Sep 17 00:00:00 2001 From: Ziyue Huang Date: Wed, 28 Feb 2018 04:06:34 +0800 Subject: [PATCH 09/14] add infer_type for regression ops, fix issues#9847 (#9848) * add infer_type for regression ops * trigger CI --- src/operator/regression_output.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/operator/regression_output.cc b/src/operator/regression_output.cc index 7b0fbae3bccb..0b8ce69062bd 100644 --- a/src/operator/regression_output.cc +++ b/src/operator/regression_output.cc @@ -23,6 +23,8 @@ */ #include "./regression_output-inl.h" +#include "./elemwise_op_common.h" + #define MXNET_OPERATOR_REGISTER_REGRESSION_FWD(__name$, __kernel$, __bwdop$) \ NNVM_REGISTER_OP(__name$) \ @@ -33,6 +35,7 @@ return std::vector{"data", "label"}; \ }) \ .set_attr("FInferShape", RegressionOpShape) \ + .set_attr("FInferType", ElemwiseType<2, 1>) \ .set_attr("FGradient", RegressionOpGrad{__bwdop$}) \ .set_attr("FInplaceOption", \ [](const NodeAttrs& attrs){ \ @@ -48,6 +51,7 @@ .set_num_inputs(2) \ .set_num_outputs(2) \ .set_attr_parser(ParamParser) \ + .set_attr("FInferType", ElemwiseType<2, 2>) \ .set_attr("TIsBackward", true) \ .set_attr("FInplaceOption", \ [](const NodeAttrs& attrs){ \ From b8ae967b3c7b34f0e4b7cb8ac651ae5b282c43e2 Mon Sep 17 00:00:00 2001 From: Xingjian Shi Date: Tue, 27 Feb 2018 12:33:42 -0800 Subject: [PATCH 10/14] [Metric] Accelerate the calculation of F1 (#9833) * Accelerate the calculation of F1 * cache the mid results * trigger CI --- python/mxnet/metric.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/python/mxnet/metric.py b/python/mxnet/metric.py index 0a02b80a1c06..ddffc01bd238 100644 --- a/python/mxnet/metric.py +++ b/python/mxnet/metric.py @@ -510,28 +510,27 @@ def update_binary_stats(self, label, pred): if len(numpy.unique(label)) > 2: raise ValueError("%s currently only supports binary classification." % self.__class__.__name__) + pred_true = (pred_label == 1) + pred_false = 1 - pred_true + label_true = (label == 1) + label_false = 1 - label_true - for y_pred, y_true in zip(pred_label, label): - if y_pred == 1 and y_true == 1: - self.true_positives += 1. - elif y_pred == 1 and y_true == 0: - self.false_positives += 1. - elif y_pred == 0 and y_true == 1: - self.false_negatives += 1. - else: - self.true_negatives += 1. + self.true_positives += (pred_true * label_true).sum() + self.false_positives += (pred_true * label_false).sum() + self.false_negatives += (pred_false * label_true).sum() + self.true_negatives += (pred_false * label_false).sum() @property def precision(self): if self.true_positives + self.false_positives > 0: - return self.true_positives / (self.true_positives + self.false_positives) + return float(self.true_positives) / (self.true_positives + self.false_positives) else: return 0. @property def recall(self): if self.true_positives + self.false_negatives > 0: - return self.true_positives / (self.true_positives + self.false_negatives) + return float(self.true_positives) / (self.true_positives + self.false_negatives) else: return 0. From 9761f212788455429e9110847ca8d0d1c0f34164 Mon Sep 17 00:00:00 2001 From: thinksanky <31976455+thinksanky@users.noreply.github.com> Date: Tue, 27 Feb 2018 17:34:28 -0800 Subject: [PATCH 11/14] updated version to 1.1.0 (#9895) --- docs/_static/mxnet-theme/index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/_static/mxnet-theme/index.html b/docs/_static/mxnet-theme/index.html index d22e2541903c..3b48832a03cd 100644 --- a/docs/_static/mxnet-theme/index.html +++ b/docs/_static/mxnet-theme/index.html @@ -21,9 +21,9 @@
-

Apache MXNet 1.0 Released

-

We're excited to announce the release of MXNet 1.0! Check out the release notes for latest updates.

- Learn More +

Apache MXNet 1.1.0 Released

+

We're excited to announce the release of MXNet 1.1.0! Check out the release notes for latest updates.

+ Learn More

MXNet Model Server

From 48749a5d43864a41653ccd8746cdccf1477b2ae4 Mon Sep 17 00:00:00 2001 From: Aaron Markham Date: Tue, 27 Feb 2018 17:41:34 -0800 Subject: [PATCH 12/14] workaround for install page display issue (#9902) --- docs/build_version_doc/AddVersion.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/build_version_doc/AddVersion.py b/docs/build_version_doc/AddVersion.py index 2c9ee22bf42e..c4d088a4b0f4 100755 --- a/docs/build_version_doc/AddVersion.py +++ b/docs/build_version_doc/AddVersion.py @@ -57,6 +57,9 @@ for name in files: if not name.endswith('.html'): continue + if 'install' in path: + print("Skipping this path: {}".format(path)) + continue with open(os.path.join(path, name), 'r') as html_file: content = bs(html_file, 'html.parser') navbar = content.find(id="main-nav") @@ -74,7 +77,7 @@ outstr = str(content).replace('<', '<').replace('>', '>') # Fix link if args.current_version == tag_list[0]: - print("Fixing" + os.path.join(path, name)) + print("Fixing " + os.path.join(path, name)) outstr = outstr.replace('https://mxnet.io', 'https://mxnet.incubator.apache.org') outstr = outstr.replace('http://mxnet.io', 'https://mxnet.incubator.apache.org') else: From 17a9c6ad440139d3f87924a8e989d4da252504be Mon Sep 17 00:00:00 2001 From: Shufan <33112206+juliusshufan@users.noreply.github.com> Date: Wed, 28 Feb 2018 13:01:34 +0800 Subject: [PATCH 13/14] Using "uniform" Xavier strategy to initialize the weight for VGG network (a trial solution to issue#9866) (#9867) * Enable the reporting of cross-entropy or nll loss value during training * Set the default value of loss as a '' to avoid a Python runtime issue when loss argument is not set * Applying the Xavier with "uniform" type to initialize weight when network is VGG --- example/image-classification/common/fit.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/example/image-classification/common/fit.py b/example/image-classification/common/fit.py index 0e0cd521f28d..9412b6f9371b 100755 --- a/example/image-classification/common/fit.py +++ b/example/image-classification/common/fit.py @@ -237,6 +237,9 @@ def fit(args, network, data_loader, **kwargs): if args.network == 'alexnet': # AlexNet will not converge using Xavier initializer = mx.init.Normal() + # VGG will not trend to converge using Xavier-Gaussian + elif 'vgg' in args.network: + initializer = mx.init.Xavier() else: initializer = mx.init.Xavier( rnd_type='gaussian', factor_type="in", magnitude=2) From 6f712874a8cd5cd88e29b4f889a3ab8b63b1bdfd Mon Sep 17 00:00:00 2001 From: Acharya Date: Thu, 1 Mar 2018 12:01:28 -0800 Subject: [PATCH 14/14] Addressing comments and refactoring test code. - Change package name to onnx. - remove Renamer class - Add apache license to files. - refactor test files to unittests folder. - remove export folder. Signed-off-by: Acharya --- python/mxnet/contrib/__init__.py | 2 +- python/mxnet/contrib/onnx/__init__.py | 18 ++++ python/mxnet/contrib/onnx/_import/__init__.py | 43 ++++++++ .../contrib/{serde => onnx}/_import/common.py | 43 +++----- .../{serde => onnx}/_import/import_helper.py | 83 +++++++------- .../{serde => onnx}/_import/import_onnx.py | 71 ++++-------- python/mxnet/contrib/serde/__init__.py | 2 - .../mxnet/contrib/serde/_export/__init__.py | 4 - .../mxnet/contrib/serde/_import/__init__.py | 26 ----- .../contrib/serde/_import/tests/__init__.py | 9 -- .../serde/_import/tests/onnx_backend_test.py | 73 ------------- .../serde/_import/tests/test_models.py | 91 ---------------- .../python/onnx_test_utils}/backend.py | 102 +++++++++++++----- .../python/onnx_test_utils}/backend_rep.py | 35 +++--- tests/python/unittest/onnx_backend_test.py | 60 +++++++++++ .../python/unittest}/test_super_resolution.py | 38 ++++--- 16 files changed, 320 insertions(+), 380 deletions(-) create mode 100644 python/mxnet/contrib/onnx/__init__.py create mode 100644 python/mxnet/contrib/onnx/_import/__init__.py rename python/mxnet/contrib/{serde => onnx}/_import/common.py (82%) rename python/mxnet/contrib/{serde => onnx}/_import/import_helper.py (79%) rename python/mxnet/contrib/{serde => onnx}/_import/import_onnx.py (83%) delete mode 100644 python/mxnet/contrib/serde/__init__.py delete mode 100644 python/mxnet/contrib/serde/_export/__init__.py delete mode 100644 python/mxnet/contrib/serde/_import/__init__.py delete mode 100644 python/mxnet/contrib/serde/_import/tests/__init__.py delete mode 100644 python/mxnet/contrib/serde/_import/tests/onnx_backend_test.py delete mode 100644 python/mxnet/contrib/serde/_import/tests/test_models.py rename {python/mxnet/contrib/serde/_import => tests/python/onnx_test_utils}/backend.py (53%) rename {python/mxnet/contrib/serde/_import => tests/python/onnx_test_utils}/backend_rep.py (67%) create mode 100644 tests/python/unittest/onnx_backend_test.py rename {python/mxnet/contrib/serde/_import/tests => tests/python/unittest}/test_super_resolution.py (58%) diff --git a/python/mxnet/contrib/__init__.py b/python/mxnet/contrib/__init__.py index 096e893ac745..ad1010443f9e 100644 --- a/python/mxnet/contrib/__init__.py +++ b/python/mxnet/contrib/__init__.py @@ -28,4 +28,4 @@ from . import tensorboard from . import text -from . import serde \ No newline at end of file +from . import onnx diff --git a/python/mxnet/contrib/onnx/__init__.py b/python/mxnet/contrib/onnx/__init__.py new file mode 100644 index 000000000000..e5402002bd80 --- /dev/null +++ b/python/mxnet/contrib/onnx/__init__.py @@ -0,0 +1,18 @@ +# 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 . import _import diff --git a/python/mxnet/contrib/onnx/_import/__init__.py b/python/mxnet/contrib/onnx/_import/__init__.py new file mode 100644 index 000000000000..748c1df19a54 --- /dev/null +++ b/python/mxnet/contrib/onnx/_import/__init__.py @@ -0,0 +1,43 @@ +# 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. + +# coding: utf-8 +"""import function""" +import onnx +from .import_onnx import GraphProto + +def import_model(model_file): + """Imports the supplied ONNX model file into MXNet symbol and parameters. + + Parameters + ---------- + model_file : ONNX model file name + + Returns + ------- + sym : mx.symbol + Compatible mxnet symbol + + params : dict of str to mx.ndarray + Dict of converted parameters stored in mx.ndarray format + """ + graph = GraphProto() + + # loads model file and returns ONNX protobuf object + model_proto = onnx.load(model_file) + sym, params = graph.from_onnx(model_proto.graph) + return sym, params diff --git a/python/mxnet/contrib/serde/_import/common.py b/python/mxnet/contrib/onnx/_import/common.py similarity index 82% rename from python/mxnet/contrib/serde/_import/common.py rename to python/mxnet/contrib/onnx/_import/common.py index 5aa70cbbd734..9154f009ffdd 100644 --- a/python/mxnet/contrib/serde/_import/common.py +++ b/python/mxnet/contrib/onnx/_import/common.py @@ -1,15 +1,19 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# http://www.apache.org/licenses/LICENSE-2.0 -# or in the "license" file accompanying this file. This file 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. - -# Derived from Apache 2.0 licensed common.py file from DMLC NNVM: -# https://github.com/dmlc/nnvm/blob/3da53e46db57c438b05fbebe8aa332ee8c5994d1/python/nnvm/frontend/common.py +# 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. # coding: utf-8 # pylint: disable=invalid-name,no-self-use,too-many-branches,too-few-public-methods,too-many-arguments @@ -17,21 +21,6 @@ from __future__ import absolute_import as _abs from ....base import string_types -class Renamer(object): - """A simple renamer for operators. - - Parameters - ---------- - new_name : str - The new name for the operator - """ - def __init__(self, new_name): - self._new_name = new_name - - def __call__(self, attrs): - return self._new_name, attrs - - class AttributeConverter(object): """Common attribute converter. An AttributeConverter instance is a callable: ``` diff --git a/python/mxnet/contrib/serde/_import/import_helper.py b/python/mxnet/contrib/onnx/_import/import_helper.py similarity index 79% rename from python/mxnet/contrib/serde/_import/import_helper.py rename to python/mxnet/contrib/onnx/_import/import_helper.py index b41ac1221645..f67dcc3c05d6 100644 --- a/python/mxnet/contrib/serde/_import/import_helper.py +++ b/python/mxnet/contrib/onnx/_import/import_helper.py @@ -1,20 +1,24 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# http://www.apache.org/licenses/LICENSE-2.0 -# or in the "license" file accompanying this file. This file 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. - -# Derived from Apache 2.0 licensed onnx.py file from DMLC NNVM: -# https://github.com/dmlc/nnvm/blob/3da53e46db57c438b05fbebe8aa332ee8c5994d1/python/nnvm/frontend/onnx.py +# 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. # coding: utf-8 # pylint: disable=invalid-name """Operator attributes conversion""" -from onnx_mxnet.common import Renamer, AttributeConverter as AttrCvt +from .common import AttributeConverter as AttrCvt def _revert_caffe2_pad(attr): """Removing extra padding from Caffe2.""" @@ -108,7 +112,7 @@ def _activation(name): 'alpha':'slope'}, extras={'act_type': name}) -def _pad_sequence_fix(attr, kernelDim): +def _pad_sequence_fix(attr, kernelDim=None): """Changing onnx's pads sequence to match with mxnet's pad_width mxnet: (x1_begin, x1_end, ... , xn_begin, xn_end) onnx: (x1_begin, x2_begin, ... , xn_end, xn_end)""" @@ -117,9 +121,9 @@ def _pad_sequence_fix(attr, kernelDim): for index in range(int(len(attr) / 2)): new_attr = new_attr + attr[index::int(len(attr) / 2)] # Making sure pad values are in the attr for all axes. - while len(new_attr) < kernelDim*2: - new_attr = new_attr + (0, 0) - + if kernelDim is not None: + while len(new_attr) < kernelDim*2: + new_attr = new_attr + (0, 0) return new_attr def _pad(): @@ -160,16 +164,13 @@ def _upsample(name): 'mode': ('sample_type', 'nearest', _upsample_restrict_mode), 'width_scale': ('scale', 1, _upsample_scale_fix)}) -# compatible operators that do NOT require any conversion. -_identity_list = [] - # _convert_map defines maps of name to converter functor(callable) _convert_map = { # defs/experimental 'FC' : AttrCvt('FullyConnected', ignores=['axis', 'axis_w']), # defs/generator - 'Constant': Renamer('identity'), + 'Constant': AttrCvt('identity'), 'RandomUniform' : AttrCvt('random_uniform', ignores=['seed']), 'RandomNormal' : AttrCvt('random_normal', {'mean':'loc'}, ignores=['seed']), 'RandomUniformLike' : AttrCvt('random_uniform', ignores=['seed']), @@ -182,29 +183,29 @@ def _upsample(name): 'Sub' : _elemwise('sub'), 'Mul' : _elemwise('mul'), 'Div' : _elemwise('div'), - 'Neg' : Renamer('negative'), - 'Abs' : Renamer('abs'), - 'Reciprocal' : Renamer('reciprocal'), - 'Floor' : Renamer('floor'), - 'Ceil' : Renamer('ceil'), - 'Sqrt' : Renamer('sqrt'), + 'Neg' : AttrCvt('negative'), + 'Abs' : AttrCvt('abs'), + 'Reciprocal' : AttrCvt('reciprocal'), + 'Floor' : AttrCvt('floor'), + 'Ceil' : AttrCvt('ceil'), + 'Sqrt' : AttrCvt('sqrt'), 'Gemm' : AttrCvt('linalg_gemm', {'transA':'transpose_a', 'transB':'transpose_b'}, ignores=['broadcast']), - 'Relu' : Renamer('relu'), + 'Relu' : AttrCvt('relu'), 'LeakyRelu' : AttrCvt('LeakyReLU', {'alpha': 'slope'}), # 'Selu' 'Elu' : _activation('elu'), - 'Exp' : Renamer('exp'), - 'Log' : Renamer('log'), - 'Tanh' : Renamer('tanh'), + 'Exp' : AttrCvt('exp'), + 'Log' : AttrCvt('log'), + 'Tanh' : AttrCvt('tanh'), 'Pow' : AttrCvt('pow', {'exponent':'exp'}), - 'Dot' : Renamer('dot'), - 'MatMul' : Renamer('linalg_gemm2'), + 'Dot' : AttrCvt('dot'), + 'MatMul' : AttrCvt('linalg_gemm2'), # 'PRelu' - 'Sigmoid' : Renamer('sigmoid'), - 'Max' : Renamer('maximum'), #elemwise maximum - 'Min' : Renamer('minimum'), #elemwise minimum - 'Sum' : Renamer('add_n'), #elemwise sum + 'Sigmoid' : AttrCvt('sigmoid'), + 'Max' : AttrCvt('maximum'), #elemwise maximum + 'Min' : AttrCvt('minimum'), #elemwise minimum + 'Sum' : AttrCvt('add_n'), #elemwise sum # softmax default axis is different in onnx 'Softmax' : AttrCvt('softmax', extras={'axis': 1}), @@ -218,7 +219,7 @@ def _upsample(name): 'BatchNormalization': _batch_norm(), 'SpatialBN' : _batch_norm(), 'Dropout' : AttrCvt('Dropout', {'ratio': 'p'}, ignores=['is_test']), - 'Flatten' : Renamer('flatten'), + 'Flatten' : AttrCvt('flatten'), 'LRN' : AttrCvt('LRN', {'bias': 'knorm', 'size' : 'nsize'}), # defs/reduction 'ReduceMax' : AttrCvt('max', {'axes': 'axis'}), @@ -227,12 +228,12 @@ def _upsample(name): 'ReduceMean' : AttrCvt('mean', {'axes': 'axis'}), 'ReduceProd' : AttrCvt('prod', {'axes': 'axis'}), # 'ReduceLogSumExp' - 'ArgMax' : Renamer('argmax'), - 'ArgMin' : Renamer('argmin'), + 'ArgMax' : AttrCvt('argmax'), + 'ArgMin' : AttrCvt('argmin'), # defs/tensor 'Cast' : AttrCvt('cast', {'to': 'dtype'}), - 'Reshape' : Renamer('reshape'), + 'Reshape' : AttrCvt('reshape'), 'Concat' : AttrCvt('concat', {'axis': 'dim'}), 'Split' : AttrCvt('split', {'split': 'num_outputs'}), 'Pad' : _pad(), diff --git a/python/mxnet/contrib/serde/_import/import_onnx.py b/python/mxnet/contrib/onnx/_import/import_onnx.py similarity index 83% rename from python/mxnet/contrib/serde/_import/import_onnx.py rename to python/mxnet/contrib/onnx/_import/import_onnx.py index 274dbe0544fb..232ec6673d62 100644 --- a/python/mxnet/contrib/serde/_import/import_onnx.py +++ b/python/mxnet/contrib/onnx/_import/import_onnx.py @@ -1,15 +1,19 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# http://www.apache.org/licenses/LICENSE-2.0 -# or in the "license" file accompanying this file. This file 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. - -# Derived from Apache 2.0 licensed onnx.py file from DMLC NNVM: -# https://github.com/dmlc/nnvm/blob/3da53e46db57c438b05fbebe8aa332ee8c5994d1/python/nnvm/frontend/onnx.py +# 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. # coding: utf-8 # pylint: disable=invalid-name,too-many-locals,no-self-use @@ -17,9 +21,9 @@ from __future__ import absolute_import as _abs from .... import symbol from .... import ndarray as nd -from onnx_mxnet.import_helper import _identity_list, _convert_map, _pad_sequence_fix +from .import_helper import _convert_map, _pad_sequence_fix -def _convert_operator(op_name, attrs, identity_list=None, convert_map=None): +def _convert_operator(op_name, attrs, convert_map=None): """Convert from onnx operator to mxnet operator. The converter must specify conversions explicitly for incompatible name, and apply handlers to operator attributes. @@ -42,11 +46,8 @@ def _convert_operator(op_name, attrs, identity_list=None, convert_map=None): (op_name, attrs) Converted (op_name, attrs) for mxnet. """ - identity_list = identity_list if identity_list else _identity_list convert_map = convert_map if convert_map else _convert_map - if op_name in identity_list: - pass - elif op_name in convert_map: + if op_name in convert_map: op_name, attrs = convert_map[op_name](attrs) else: raise NotImplementedError("Operator {} not implemented.".format(op_name)) @@ -55,7 +56,7 @@ def _convert_operator(op_name, attrs, identity_list=None, convert_map=None): raise RuntimeError("Unable to map op_name {} to sym".format(op_name)) return op, attrs -class GraphProto(object): +class GraphProto(object): # pylint: disable=too-few-public-methods """A helper class for handling mxnet symbol copying from pb2.GraphProto. Definition: https://github.com/onnx/onnx/blob/master/onnx/onnx.proto """ @@ -156,36 +157,6 @@ def from_onnx(self, graph): out = out[0] return out, self._params - def run_node(self, node, device='CPU'): # pylint: disable=unused-argument - """Construct symbol from individual node. - Mainly using this function for unittests""" - op_name = node.op_type - attr = self._parse_attr(node.attribute) - new_op, new_attr = _convert_operator(op_name, attr) - sym_list = [symbol.Variable(node_name) for node_name in node.input] - - # some workarounds for onnx problem - new_attr = self._fix_bias(new_op, new_attr, len(sym_list)) - new_attr = self._fix_channels(new_op, new_attr, list(node.input)) - - # calling again to get new symbols after some workarounds - sym_list = [symbol.Variable(node_name) for node_name in node.input] - - # onnx slice works on multiple axes whereas mxnet's slice_axis is for single axis - if op_name == 'Slice': - op = self._fix_slice(sym_list, new_attr) - elif op_name == 'Squeeze': - op = self._fix_squeeze(sym_list, new_attr) - else: - op = new_op(*sym_list, **new_attr) - - node_output = self._fix_outputs(op_name, node.output) - for k, i in zip(list(node_output), range(len(node_output))): - self._nodes[k] = op[i] - - # now return the outputs - return op - def _fix_pooling(self, op_name, inputs, new_attr): """onnx pooling operator supports asymmetrical padding Adding pad operator before pooling in mxnet to work with onnx""" @@ -295,7 +266,7 @@ def _fix_bias(self, op, attrs, num_inputs): def _fix_bias_shape(self, op_name, inputs, attrs): """A workaround to reshape bias term to (1, num_channel).""" - if (op_name == 'Add' or op_name == 'Mul') and \ + if (op_name == 'Add' or op_name == 'Mul') and (int(len(self._params)) > 0) and \ ('broadcast' in attrs and attrs['broadcast'] == 1): assert len(list(inputs)) == 2 bias_name = self._renames.get(inputs[1], inputs[1]) diff --git a/python/mxnet/contrib/serde/__init__.py b/python/mxnet/contrib/serde/__init__.py deleted file mode 100644 index 30e72ec6d134..000000000000 --- a/python/mxnet/contrib/serde/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import _import -from . import _export \ No newline at end of file diff --git a/python/mxnet/contrib/serde/_export/__init__.py b/python/mxnet/contrib/serde/_export/__init__.py deleted file mode 100644 index 3d09eb53d3c0..000000000000 --- a/python/mxnet/contrib/serde/_export/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -import onnx - -def export_model(sym, params): - pass \ No newline at end of file diff --git a/python/mxnet/contrib/serde/_import/__init__.py b/python/mxnet/contrib/serde/_import/__init__.py deleted file mode 100644 index 3df0384885e1..000000000000 --- a/python/mxnet/contrib/serde/_import/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -# coding: utf-8 -"""import function""" -import onnx -from .import_onnx import GraphProto - -def import_model(model_file): - """Imports the supplied ONNX model file into MXNet symbol and parameters. - - Parameters - ---------- - model_file : ONNX model file name - - Returns - ------- - sym : mx.symbol - Compatible mxnet symbol - - params : dict of str to mx.ndarray - Dict of converted parameters stored in mx.ndarray format - """ - graph = GraphProto() - - # loads model file and returns ONNX protobuf object - model_proto = onnx.load(model_file) - sym, params = graph.from_onnx(model_proto.graph) - return sym, params \ No newline at end of file diff --git a/python/mxnet/contrib/serde/_import/tests/__init__.py b/python/mxnet/contrib/serde/_import/tests/__init__.py deleted file mode 100644 index dfd786a881b3..000000000000 --- a/python/mxnet/contrib/serde/_import/tests/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# http://www.apache.org/licenses/LICENSE-2.0 -# or in the "license" file accompanying this file. This file 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. diff --git a/python/mxnet/contrib/serde/_import/tests/onnx_backend_test.py b/python/mxnet/contrib/serde/_import/tests/onnx_backend_test.py deleted file mode 100644 index 1716ed8f7359..000000000000 --- a/python/mxnet/contrib/serde/_import/tests/onnx_backend_test.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# http://www.apache.org/licenses/LICENSE-2.0 -# or in the "license" file accompanying this file. This file 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. -"""onnx test backend wrapper""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -import unittest - -import onnx.backend.test -from onnx_mxnet import backend as mxnet_backend - -# This is a pytest magic variable to load extra plugins -pytest_plugins = 'onnx.backend.test.report' - -backend_test = onnx.backend.test.BackendTest(mxnet_backend, __name__) - -# will add model tests later -backend_test.exclude('test_bvlc_alexnet') -backend_test.exclude('test_densenet121') -backend_test.exclude('test_inception_v1') -backend_test.exclude('test_inception_v2') -backend_test.exclude('test_resnet50') -backend_test.exclude('test_shufflenet') - -# Not implemented -unimplemented_operators = [ - 'test_and*', - 'test_clip*', - 'test_equal*', - 'test_gather*', - 'test_greater*', - 'test_hardmax*', - 'test_hardsigmoid*', - 'test_less*', - 'test_logsoftmax*', - 'test_mean*', - 'test_not*', - 'test_or*', - 'test_selu*', - 'test_shape*', - 'test_size*', - 'test_softplus*', - 'test_softsign*', - 'test_thresholdedrelu*', - 'test_top*', - 'test_unsqueeze*', - 'test_xor*', - 'test_Embedding*', - 'test_PReLU*', - 'test_Softplus*', - #'test_Upsample*', - 'test_operator*', - 'test_constant_cpu' - ] -for op_test in unimplemented_operators: - backend_test.exclude(op_test) - -# import all test cases at global scope to make them visible to python.unittest -globals().update(backend_test - .enable_report() - .test_cases) - -if __name__ == '__main__': - unittest.main() diff --git a/python/mxnet/contrib/serde/_import/tests/test_models.py b/python/mxnet/contrib/serde/_import/tests/test_models.py deleted file mode 100644 index 354c49a4eae4..000000000000 --- a/python/mxnet/contrib/serde/_import/tests/test_models.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# http://www.apache.org/licenses/LICENSE-2.0 -# or in the "license" file accompanying this file. This file 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. - -# coding: utf-8 -"""Testing model conversions from onnx/models repo""" -from __future__ import absolute_import as _abs -from __future__ import print_function -from collections import namedtuple -import os -import tarfile -from ..... import context -from ..... import module -from ..... import ndarray as nd -from .....test_utils import download -import numpy as np -import numpy.testing as npt -import onnx_mxnet - -URLS = { - 'squeezenet_onnx' : 'https://s3.amazonaws.com/download.onnx/models/squeezenet.tar.gz', - 'shufflenet_onnx' : 'https://s3.amazonaws.com/download.onnx/models/shufflenet.tar.gz', - 'inception_v1_onnx' : 'https://s3.amazonaws.com/download.onnx/models/inception_v1.tar.gz', - 'inception_v2_onnx' : 'https://s3.amazonaws.com/download.onnx/models/inception_v2.tar.gz', - 'bvlc_alexnet_onnx' : 'https://s3.amazonaws.com/download.onnx/models/bvlc_alexnet.tar.gz', - 'densenet121_onnx' : 'https://s3.amazonaws.com/download.onnx/models/densenet121.tar.gz', - 'resnet50_onnx' : 'https://s3.amazonaws.com/download.onnx/models/resnet50.tar.gz', - 'vgg16_onnx' : 'https://s3.amazonaws.com/download.onnx/models/vgg16.tar.gz', - 'vgg19_onnx' : 'https://s3.amazonaws.com/download.onnx/models/vgg19.tar.gz' -} - -def extract_file(model_tar): - """Extract tar file and returns model path and input, output data""" - # extract tar file - tar = tarfile.open(model_tar, "r:*") - tar.extractall() - tar.close() - path = model_tar.rsplit('_', 1)[0] - # return model, inputs, outputs path - cur_dir = os.path.abspath(os.path.dirname(__file__)) - model_path = os.path.join(cur_dir, path, 'model.onnx') - npz_path = os.path.join(cur_dir, path, 'test_data_0.npz') - sample = np.load(npz_path, encoding='bytes') - input_data = list(sample['inputs']) - output_data = list(sample['outputs']) - return model_path, input_data, output_data - -def verify_onnx_forward_impl(model_path, input_data, output_data): - """Verifies result after inference""" - print("Converting onnx format to mxnet's symbol and params...") - sym, params = onnx_mxnet.import_model(model_path) - - # create module - mod = module.Module(symbol=sym, data_names=['input_0'], context=context.cpu(), label_names=None) - mod.bind(for_training=False, data_shapes=[('input_0', input_data.shape)], label_shapes=None) - mod.set_params(arg_params=params, aux_params=params, allow_missing=True, allow_extra=True) - # run inference - Batch = namedtuple('Batch', ['data']) - - mod.forward(Batch([nd.array(input_data)]), is_train=False) - - # Run the model with an onnx backend and verify the results - npt.assert_equal(mod.get_outputs()[0].shape, output_data.shape) - npt.assert_almost_equal(output_data, mod.get_outputs()[0].asnumpy(), decimal=3) - print("Conversion Successful") - -def verify_model(name): - """Testing models from onnx model zoo""" - print("Testing model ", name) - download(URLS.get(name), name) - model_path, inputs, outputs = extract_file(name) - input_data = np.asarray(inputs[0], dtype=np.float32) - output_data = np.asarray(outputs[0], dtype=np.float32) - verify_onnx_forward_impl(model_path, input_data, output_data) - -if __name__ == '__main__': - verify_model('squeezenet_onnx') # working - verify_model('bvlc_alexnet_onnx') # working - verify_model('vgg16_onnx') # working - verify_model('vgg19_onnx') # working - #verify_model('inception_v1_onnx') # working, accuracy is different 1.4 - #verify_model('inception_v2_onnx') # working, accuracy is different 7.4 - #verify_model('shufflenet_onnx') # working, accuracy is different 10.2 - verify_model('densenet121_onnx') # working - #verify_model('resnet50_onnx') # working, accuracy is different 18.1 diff --git a/python/mxnet/contrib/serde/_import/backend.py b/tests/python/onnx_test_utils/backend.py similarity index 53% rename from python/mxnet/contrib/serde/_import/backend.py rename to tests/python/onnx_test_utils/backend.py index f1fe6456900a..33f266598284 100644 --- a/python/mxnet/contrib/serde/_import/backend.py +++ b/tests/python/onnx_test_utils/backend.py @@ -1,23 +1,29 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# http://www.apache.org/licenses/LICENSE-2.0 -# or in the "license" file accompanying this file. This file 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. +# 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. # coding: utf-8 # pylint: disable=too-many-locals,invalid-name """backend wrapper for onnx test infrastructure""" from collections import namedtuple +import mxnet as mx +from onnx import helper, TensorProto from onnx.backend.base import Backend -from .import_onnx import GraphProto +from mxnet.contrib.onnx._import.import_onnx import GraphProto from .backend_rep import MXNetBackendRep -from .... import context -from .... import module -from .... import ndarray as nd # Using these functions for onnx test infrastructure. # Implemented by following onnx docs guide: @@ -27,6 +33,48 @@ class MXNetBackend(Backend): """MXNet backend for ONNX""" + + @staticmethod + def make_graph(node, inputs): + """ Created ONNX GraphProto from node""" + initializer = [] + tensor_input_info = [] + tensor_output_info = [] + + # Adding input tensor info. + for index in range(len(node.input)): + tensor_input_info.append( + helper.make_tensor_value_info(str(node.input[index]), TensorProto.FLOAT, [1])) + + # Creating an initializer for Weight params. + # Assumes that weight params is named as 'W'. + # TODO: Handle multiple weight params. + # TODO: Add for "bias" if needed + if node.input[index] == 'W': + dim = inputs[index].shape + param_tensor = helper.make_tensor( + name=node.input[index], + data_type=TensorProto.FLOAT, + dims=dim, + vals=inputs[index].flatten()) + + initializer.append(param_tensor) + + # Adding output tensor info. + for index in range(len(node.output)): + tensor_output_info.append( + helper.make_tensor_value_info(str(node.output[index]), TensorProto.FLOAT, [1])) + + # creating graph proto object. + graph_proto = helper.make_graph( + [node], + "test", + tensor_input_info, + tensor_output_info, + initializer=initializer) + + return graph_proto + @classmethod def run_node(cls, node, inputs, device='CPU'): """Running individual node inference on mxnet engine and @@ -47,12 +95,12 @@ def run_node(cls, node, inputs, device='CPU'): result obtained after running the operator """ graph = GraphProto() - sym = graph.run_node(node) - data_names = [i for i in node.input] + sym, params = graph.from_onnx(MXNetBackend.make_graph(node, inputs)) + data_names = [i for i in sym.get_internals().list_inputs() if i[:-1] == "input_"] data_shapes = [] - reduce_op_types = set(['ReduceMin', 'ReduceMax', 'ReduceMean', + dim_change_op_types = set(['ReduceMin', 'ReduceMax', 'ReduceMean', 'ReduceProd', 'ReduceSum', 'Slice', 'Pad', - 'Squeeze', 'Upsample', 'Reshape']) + 'Squeeze', 'Upsample', 'Reshape', 'Conv']) # Adding extra dimension of batch_size 1 if the batch_size is different for multiple inputs. for idx, input_name in enumerate(data_names): @@ -67,32 +115,36 @@ def run_node(cls, node, inputs, device='CPU'): # create module, passing cpu context if device == 'CPU': - ctx = context.cpu() + ctx = mx.cpu() else: raise NotImplementedError("Only CPU context is supported for now") # create a module - mod = module.Module(symbol=sym, data_names=data_names, context=ctx, label_names=None) + mod = mx.mod.Module(symbol=sym, data_names=data_names, context=ctx, label_names=None) mod.bind(for_training=False, data_shapes=data_shapes, label_shapes=None) # initializing parameters for calculating result of each individual node - mod.init_params() + if int(len(params)) > 0: + mod.set_params(arg_params=params, aux_params=params) + else: + mod.init_params() batch = namedtuple('Batch', ['data']) data_forward = [] - for val in inputs: + for idx, input_name in enumerate(data_names): # slice and pad operator tests needs 1 less dimension in forward pass # otherwise it will throw an error. # for squeeze operator, need to retain shape of input as provided - if node.op_type in reduce_op_types: - data_forward.append(nd.array(val)) + val = inputs[idx] + if node.op_type in dim_change_op_types: + data_forward.append(mx.nd.array(val)) else: - data_forward.append(nd.array([val])) + data_forward.append(mx.nd.array([val])) mod.forward(batch(data_forward)) result = mod.get_outputs()[0].asnumpy() - if node.op_type in reduce_op_types: + if node.op_type in dim_change_op_types: return [result] return result diff --git a/python/mxnet/contrib/serde/_import/backend_rep.py b/tests/python/onnx_test_utils/backend_rep.py similarity index 67% rename from python/mxnet/contrib/serde/_import/backend_rep.py rename to tests/python/onnx_test_utils/backend_rep.py index 53db24ea07f2..690054238667 100644 --- a/python/mxnet/contrib/serde/_import/backend_rep.py +++ b/tests/python/onnx_test_utils/backend_rep.py @@ -1,12 +1,19 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# http://www.apache.org/licenses/LICENSE-2.0 -# or in the "license" file accompanying this file. This file 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. +# 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. # coding: utf-8 # pylint: disable=too-few-public-methods @@ -14,9 +21,7 @@ from collections import namedtuple import numpy as np from onnx.backend.base import BackendRep -from .... import context -from .... import module -from .... import ndarray as nd +import mxnet as mx # Using these functions for onnx test infrastructure. # Implemented by following onnx docs guide: @@ -52,11 +57,11 @@ def run(self, inputs, **kwargs): # create module, passing cpu context if self.device == 'CPU': - ctx = context.cpu() + ctx = mx.cpu() else: raise NotImplementedError("Only CPU context is supported for now") - mod = module.Module(symbol=self.symbol, data_names=['input_0'], context=ctx, + mod = mx.mod.Module(symbol=self.symbol, data_names=['input_0'], context=ctx, label_names=None) mod.bind(for_training=False, data_shapes=[('input_0', input_data.shape)], label_shapes=None) @@ -65,6 +70,6 @@ def run(self, inputs, **kwargs): # run inference batch = namedtuple('Batch', ['data']) - mod.forward(batch([nd.array(input_data)])) + mod.forward(batch([mx.nd.array(input_data)])) result = mod.get_outputs()[0].asnumpy() return [result] diff --git a/tests/python/unittest/onnx_backend_test.py b/tests/python/unittest/onnx_backend_test.py new file mode 100644 index 000000000000..155b835c28b9 --- /dev/null +++ b/tests/python/unittest/onnx_backend_test.py @@ -0,0 +1,60 @@ +# 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. + +"""onnx test backend wrapper""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import unittest + +import onnx.backend.test +#from onnx_test_utils +import backend as mxnet_backend + +# This is a pytest magic variable to load extra plugins +pytest_plugins = "onnx.backend.test.report", + +backend_test = onnx.backend.test.BackendTest(mxnet_backend, __name__) + +# Not implemented +implemented_operators = [ + 'test_abs*', + 'test_add*', + 'test_neg*', + 'test_relu*', + 'test_reshape_*', + 'test_sqrt*', + 'test_sub*', + 'test_sum*', + 'test_div*', + 'test_tanh*', + 'test_exp*', + 'test_floor*' + ] + +for op_test in implemented_operators: + backend_test.include(op_test) + +# import all test cases at global scope to make them visible to python.unittest +globals().update(backend_test + .enable_report() + .test_cases) + +if __name__ == '__main__': + unittest.main() diff --git a/python/mxnet/contrib/serde/_import/tests/test_super_resolution.py b/tests/python/unittest/test_super_resolution.py similarity index 58% rename from python/mxnet/contrib/serde/_import/tests/test_super_resolution.py rename to tests/python/unittest/test_super_resolution.py index 28cb364a692e..0fdfa63a63d6 100644 --- a/python/mxnet/contrib/serde/_import/tests/test_super_resolution.py +++ b/tests/python/unittest/test_super_resolution.py @@ -1,23 +1,29 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# http://www.apache.org/licenses/LICENSE-2.0 -# or in the "license" file accompanying this file. This file 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. +# 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. + """Testing super_resolution model conversion""" from __future__ import absolute_import as _abs from __future__ import print_function from collections import namedtuple -from ..... import context -from ..... import module -from ..... import ndarray as nd -from .....test_utils import download +import mxnet as mx +from mxnet.test_utils import download +import mxnet.contrib.onnx._import as onnx_mxnet import numpy as np from PIL import Image -import onnx_mxnet model_url = 'https://s3.amazonaws.com/onnx-mxnet/examples/super_resolution.onnx' @@ -37,13 +43,13 @@ x = np.array(img_y)[np.newaxis, np.newaxis, :, :] # create module -mod = module.Module(symbol=sym, data_names=['input_0'], label_names=None) +mod = mx.mod.Module(symbol=sym, data_names=['input_0'], label_names=None) mod.bind(for_training=False, data_shapes=[('input_0', x.shape)]) mod.set_params(arg_params=params, aux_params=None) # run inference Batch = namedtuple('Batch', ['data']) -mod.forward(Batch([nd.array(x)])) +mod.forward(Batch([mx.nd.array(x)])) # Save the result img_out_y = Image.fromarray(np.uint8(mod.get_outputs()[0][0][0].asnumpy().clip(0, 255)), mode='L')