Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unit test framework for operators' conversion #29

Merged
merged 6 commits into from
Apr 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis/unittest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ unittest(){
trap 'abort' 0
set -e

unittest .
# unittest .
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unit test in travis-ci is disabled because it requires the installation of caffe2, as the backend of ONNX.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, seems caffe2 backend is necessary, let me try to add the dependencies in the CI.

Copy link
Author

@kuke kuke Apr 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pkuyym the installation of caffe2 always failed in local machine, and seems that it is very sensitive to the dependencies, e.g. version of cmake, protobuf etc. You can find if there is a robust way, so that we can integrate it in CI. Thanks!


trap : 0
4 changes: 2 additions & 2 deletions fluid_onnx/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ def pad_op():
def pool2d_op(operator, scope):
inputs, attrs, outputs = get_op_io_info(operator)
if attrs['global_pooling'] is False:
op_type = {'max': 'MaxPool', 'ave': 'AveragePool'}
op_type = {'max': 'MaxPool', 'avg': 'AveragePool'}
pool2d = make_node(
op_type[attrs['pooling_type']],
inputs=inputs['X'],
Expand All @@ -383,7 +383,7 @@ def pool2d_op(operator, scope):
strides=attrs['strides'],
pads=attrs['paddings'] + attrs['paddings'], )
else:
op_type = {'max': 'GlobalMaxPool', 'ave': 'GlobalAveragePool'}
op_type = {'max': 'GlobalMaxPool', 'avg': 'GlobalAveragePool'}
pool2d = make_node(
op_type[attrs['pooling_type']],
inputs=inputs['X'],
Expand Down
1 change: 1 addition & 0 deletions setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ if [ $? != 0 ]; then
echo "Install python dependencies failed !!!"
exit 1
fi

269 changes: 269 additions & 0 deletions tests/op_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
# Copyright (c) 2018 PaddlePaddle Authors. 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.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import sys
import unittest
import numpy as np

from onnx.helper import make_node, make_graph, make_model
from onnx.checker import check_node
import paddle.fluid.core as core
from paddle.fluid import scope_guard
from paddle.fluid.backward import append_backward
from paddle.fluid.op import Operator
from paddle.fluid.executor import Executor
from paddle.fluid.framework import Program, OpProtoHolder
from caffe2.python.onnx.backend import Caffe2Backend

from fluid_onnx.ops import node_maker
from fluid_onnx.variables import paddle_variable_to_onnx_tensor

"""
NOTE (varunarora): Some of the code snippets below have been inspired from
op_test.py in /python/paddle/fluid/tests/unittests/ in the original
Paddle repository (https://github.com/PaddlePaddle/Paddle/).

When in doubt, keep in sync with it's counterparts.
"""

def append_input_output(block, op_proto, np_list, persistable_list, is_input):
"""Returns a list of Paddle variables associated with a block.

Args:
block:
op_proto: The matching C++ operator type.
np_list: Dict of value names -> values.
persistable_list: List of variables to be persisted.
is_input: Boolean of if this is a set of inputs.

Returns:
A dict of variable names -> Paddle variable instances.
"""
# A list of expected inputs and outputs, as desired by Paddle's
# C++ runtime.
proto_list = op_proto.inputs if is_input else op_proto.outputs

def create_var(block, name, np_list, var_proto):
"""Creates a Paddle var in the given block and C++ proto type"""

# If the expected variable is not found is in the provided list
# of variables, make an assertion. Else, determine the shape and
# and set the LoD level before creating the Paddle variable.
if name not in np_list:
assert var_proto.intermediate, "{} not found".format(name)
shape = None
lod_level = None
else:
np_value = np_list[name]
if isinstance(np_value, tuple):
shape = list(np_value[0].shape)
lod_level = len(np_value[1])
else:
shape = list(np_value.shape)
lod_level = 0

persistable = True if name in persistable_list else False
return block.create_var(
dtype="float32",
shape=shape,
persistable=persistable,
lod_level=lod_level,
name=name)

# Go through all the variables in the expected list for this operator.
var_dict = {}
for var_proto in proto_list:
var_name = str(var_proto.name)

# If these are inputs, and the expected input is not necessary
# are is not provided in the list of inputs, we move on to the next
# expected. input.
# If not, we make sure it the expected input is provided, or that it
# is unnecessary.
if is_input:
if (var_name not in np_list) and var_proto.dispensable:
continue
assert (var_name in np_list) or (var_proto.dispensable), \
"Missing {} as input".format(var_name)

# Set duplicable variables as lists of Paddle variables, and standard
# ones as simple Paddle variables.
if var_proto.duplicable:
assert isinstance(np_list[var_name], list), \
"Duplicable {} should be set as list".format(var_name)
var_list = []
for (name, np_value) in np_list[var_name]:
var_list.append(
create_var(block, name, {name: np_value}, var_proto))
var_dict[var_name] = var_list
else:
var_dict[var_name] = create_var(block, var_name, np_list, var_proto)

return var_dict


class OpTest(unittest.TestCase):
"""Evaluates an op maker's validity.

Using op-specific inputs and attributes, executes:
(1) A Paddle program
(2) A Caffe2 backend consuming Paddle ops converted to ONNX using custom
op makers.

It uses the outputs of both fo these executions to compare the values
of their outputs. Success in these comparisons comes from almost equal
values of this output data across both executions.

Attributes:
inputs (dict): Operator input name -> input value.
outputs (dict): Operator output -> output value placeholders.

Additionally, custom attributes to the op.
"""
def feed_var(self, input_vars, place):
"""Returns a dictionary of variable names -> initialized tensors.

It sets tensors' execution place set (CPU or GPU), and Level of
Details (LoD) using this info from the numpy values.
"""
feed_map = {}
for var_name in input_vars:
if isinstance(input_vars[var_name], list):
for name, np_value in self.inputs[var_name]:
tensor = core.LoDTensor()
if isinstance(np_value, tuple):
tensor.set(np_value[0], place)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to make new function for this code that repeats twice doing the .sets

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is true. I will fix it in next pull request.

tensor.set_lod(np_value[1])
else:
tensor.set(np_value, place)
feed_map[name] = tensor
else:
tensor = core.LoDTensor()
if isinstance(self.inputs[var_name], tuple):
tensor.set(self.inputs[var_name][0], place)
tensor.set_lod(self.inputs[var_name][1])
else:
tensor.set(self.inputs[var_name], place)
feed_map[var_name] = tensor

return feed_map

def eval_fluid_op(self):
"""Run a Paddle program only with the op to test.

Returns the output values after running.
"""
op_proto = OpProtoHolder.instance().get_op_proto(self.op_type)

# Create a new paddle scope and program.
place = core.CPUPlace()
exe = Executor(place)
self.scope = core.Scope()

with scope_guard(self.scope):
program = Program()
self.block = program.global_block()

# A list of inputs and outputs used by the op
# that need to persisted in the global block.
persistable = self.persistable if hasattr(self,
"persistable") else []

# Add input and output variables to the global block.
inputs = append_input_output(self.block, op_proto, self.inputs,
persistable, True)
outputs = append_input_output(self.block, op_proto, self.outputs,
persistable, False)

# Append the op.
self.op = self.block.append_op(
type=self.op_type,
inputs=inputs,
outputs=outputs,
attrs=self.attrs if hasattr(self, "attrs") else dict())

# Infer the var type and share of the op based on the block's
# inputs and outputs.
self.op.desc.infer_var_type(self.block.desc)
self.op.desc.infer_shape(self.block.desc)

# Construct a unique list of outputs to fetch.
self.fetch_list = []
for var_name, var in outputs.iteritems():
if var_name in self.outputs:
if isinstance(var, list):
for v in var:
self.fetch_list.append(v)
else:
self.fetch_list.append(var)

self.feed_map = self.feed_var(inputs, place)

outs = exe.run(program,
feed=self.feed_map,
scope=self.scope,
fetch_list=self.fetch_list,
return_numpy=True)
return outs

def eval_onnx_node(self):
"""Run a Caffe2 program using their ONNX backend.

Prior to running the backend, use the Paddle scope to construct
ONNX ops and prepare the inputs and output values based on ONNX
compatibility.
"""
# Convert inputs and outputs to ONNX tensors.
# Use the Paddle fetch_list to prepare the outputs.
inputs = [
paddle_variable_to_onnx_tensor(v, self.block) for v in self.feed_map
]

fetch_target_names = [
fetch_target.name for fetch_target in self.fetch_list
]
outputs = [
paddle_variable_to_onnx_tensor(v, self.block)
for v in fetch_target_names
]

# Construct the ONNX model using paddle-onnx.
onnx_node = node_maker[self.op_type](operator=self.op, scope=self.scope)
node_list = list(onnx_node) if isinstance(onnx_node,
tuple) else [onnx_node]
for node in node_list:
check_node(node)

onnx_graph = make_graph(node_list, self.op_type, inputs, outputs)
onnx_model = make_model(onnx_graph, producer_name='unittest')

# Run the Caffe2Backend with the ONNX model.
rep = Caffe2Backend.prepare(onnx_model, device='CPU')
in_vals = [self.inputs[input.name] for input in inputs]
outs = rep.run(in_vals)

return outs

def check_output(self, decimal=5):
"""Compares the outputs from the Paddle program and the Caffe2
backend using the ONNX model constructed by paddle-onnx.

Compares accuracy at a precision of 5 decimal places by default.
"""
fluid_result = self.eval_fluid_op()
onnx_result = self.eval_onnx_node()

for ref, hyp in zip(fluid_result, onnx_result):
# Compare the values using numpy's almost_equal comparator.
np.testing.assert_almost_equal(ref, hyp, decimal=decimal)
64 changes: 64 additions & 0 deletions tests/test_conv2d_op.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright (c) 2018 PaddlePaddle Authors. 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.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import unittest
import numpy as np
from op_test import OpTest


class TestConv2dOp(OpTest):
def setUp(self):
self.op_type = "conv2d"
self.use_cudnn = False
self.use_mkldnn = False
self.dtype = np.float32
self.groups = 1
self.dilations = [1, 1]
self.pad = [0, 0]
self.stride = [1, 1]
self.input_size = [2, 3, 5, 5]
f_c = self.input_size[1] / self.groups
self.filter_size = [6, f_c, 3, 3]

conv2d_param = {
'stride': self.stride,
'pad': self.pad,
'dilation': self.dilations
}

input = np.random.random(self.input_size).astype(self.dtype)
filter = np.random.random(self.filter_size).astype(self.dtype)

self.inputs = {'Input': input, 'Filter': filter}

self.persistable = ['Filter']
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This list of persistable vars, usually indicating the parameters


self.attrs = {
'strides': self.stride,
'paddings': self.pad,
'groups': self.groups,
'dilations': self.dilations,
'use_cudnn': self.use_cudnn,
'use_mkldnn': self.use_mkldnn
}

output = np.zeros((1, 1, 1, 1))
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

output is a placeholder, and can be any shape and any value.

self.outputs = {'Output': output}

def test_check_output(self):
self.check_output(decimal=5)


if __name__ == '__main__':
unittest.main()
Loading