diff options
Diffstat (limited to 'python/pyarmnn/test')
34 files changed, 2994 insertions, 0 deletions
diff --git a/python/pyarmnn/test/test_caffe_parser.py b/python/pyarmnn/test/test_caffe_parser.py new file mode 100644 index 0000000000..d744b907d4 --- /dev/null +++ b/python/pyarmnn/test/test_caffe_parser.py @@ -0,0 +1,131 @@ +# Copyright © 2020 Arm Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +import os + +import pytest +import pyarmnn as ann +import numpy as np + + +@pytest.fixture() +def parser(shared_data_folder): + """ + Parse and setup the test network to be used for the tests below + """ + + # Create caffe parser + parser = ann.ICaffeParser() + + # Specify path to model + path_to_model = os.path.join(shared_data_folder, 'mock_model.caffemodel') + + # Specify the tensor shape relative to the input [1, 1, 28, 28] + tensor_shape = {'Placeholder': ann.TensorShape((1, 1, 28, 28))} + + # Specify the requested_outputs + requested_outputs = ["output"] + + # Parse caffe binary & create network + parser.CreateNetworkFromBinaryFile(path_to_model, tensor_shape, requested_outputs) + + yield parser + + +def test_caffe_parser_swig_destroy(): + assert ann.ICaffeParser.__swig_destroy__, "There is a swig python destructor defined" + assert ann.ICaffeParser.__swig_destroy__.__name__ == "delete_ICaffeParser" + + +def test_check_caffe_parser_swig_ownership(parser): + # Check to see that SWIG has ownership for parser. This instructs SWIG to take + # ownership of the return value. This allows the value to be automatically + # garbage-collected when it is no longer in use + assert parser.thisown + + +def test_get_network_input_binding_info(parser): + input_binding_info = parser.GetNetworkInputBindingInfo("Placeholder") + + tensor = input_binding_info[1] + assert tensor.GetDataType() == 1 + assert tensor.GetNumDimensions() == 4 + assert tensor.GetNumElements() == 784 + + +def test_get_network_output_binding_info(parser): + output_binding_info1 = parser.GetNetworkOutputBindingInfo("output") + + # Check the tensor info retrieved from GetNetworkOutputBindingInfo + tensor1 = output_binding_info1[1] + + assert tensor1.GetDataType() == 1 + assert tensor1.GetNumDimensions() == 2 + assert tensor1.GetNumElements() == 10 + + +def test_filenotfound_exception(shared_data_folder): + parser = ann.ICaffeParser() + + # path to model + path_to_model = os.path.join(shared_data_folder, 'some_unknown_network.caffemodel') + + # generic tensor shape [1, 1, 1, 1] + tensor_shape = {'data': ann.TensorShape((1, 1, 1, 1))} + + # requested_outputs + requested_outputs = [""] + + with pytest.raises(RuntimeError) as err: + parser.CreateNetworkFromBinaryFile(path_to_model, tensor_shape, requested_outputs) + + # Only check for part of the exception since the exception returns + # absolute path which will change on different machines. + assert 'Failed to open graph file' in str(err.value) + + +def test_caffe_parser_end_to_end(shared_data_folder): + parser = ann.ICaffeParser = ann.ICaffeParser() + + # Load the network specifying the inputs and outputs + input_name = "Placeholder" + tensor_shape = {input_name: ann.TensorShape((1, 1, 28, 28))} + requested_outputs = ["output"] + + network = parser.CreateNetworkFromBinaryFile(os.path.join(shared_data_folder, 'mock_model.caffemodel'), + tensor_shape, requested_outputs) + + # Specify preferred backend + preferred_backends = [ann.BackendId('CpuAcc'), ann.BackendId('CpuRef')] + + input_binding_info = parser.GetNetworkInputBindingInfo(input_name) + + options = ann.CreationOptions() + runtime = ann.IRuntime(options) + + opt_network, messages = ann.Optimize(network, preferred_backends, runtime.GetDeviceSpec(), ann.OptimizerOptions()) + + assert 0 == len(messages) + + net_id, messages = runtime.LoadNetwork(opt_network) + + assert "" == messages + + # Load test image data stored in input_caffe.npy + input_tensor_data = np.load(os.path.join(shared_data_folder, 'caffe_parser/input_caffe.npy')).astype(np.float32) + input_tensors = ann.make_input_tensors([input_binding_info], [input_tensor_data]) + + # Load output binding info and + outputs_binding_info = [] + for output_name in requested_outputs: + outputs_binding_info.append(parser.GetNetworkOutputBindingInfo(output_name)) + output_tensors = ann.make_output_tensors(outputs_binding_info) + + runtime.EnqueueWorkload(net_id, input_tensors, output_tensors) + + output_vectors = ann.workload_tensors_to_ndarray(output_tensors) + + # Load golden output file for result comparison. + expected_output = np.load(os.path.join(shared_data_folder, 'caffe_parser/golden_output_caffe.npy')) + + # Check that output matches golden output to 4 decimal places (there are slight rounding differences after this) + np.testing.assert_almost_equal(output_vectors[0], expected_output, 4) diff --git a/python/pyarmnn/test/test_const_tensor.py b/python/pyarmnn/test/test_const_tensor.py new file mode 100644 index 0000000000..fa6327f19c --- /dev/null +++ b/python/pyarmnn/test/test_const_tensor.py @@ -0,0 +1,251 @@ +# Copyright © 2020 Arm Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +import pytest +import numpy as np + +import pyarmnn as ann + + +def _get_tensor_info(dt): + tensor_info = ann.TensorInfo(ann.TensorShape((2, 3)), dt) + + return tensor_info + + +@pytest.mark.parametrize("dt, data", + [ + (ann.DataType_Float32, np.random.randint(1, size=(2, 4)).astype(np.float32)), + (ann.DataType_Float16, np.random.randint(1, size=(2, 4)).astype(np.float16)), + (ann.DataType_QAsymmU8, np.random.randint(1, size=(2, 4)).astype(np.uint8)), + (ann.DataType_QAsymmS8, np.random.randint(1, size=(2, 4)).astype(np.int8)), + (ann.DataType_QSymmS8, np.random.randint(1, size=(2, 4)).astype(np.int8)), + (ann.DataType_Signed32, np.random.randint(1, size=(2, 4)).astype(np.int32)), + (ann.DataType_QSymmS16, np.random.randint(1, size=(2, 4)).astype(np.int16)) + ], ids=['float32', 'float16', 'unsigned int8', 'signed int8', 'signed int8', 'int32', 'int16']) +def test_const_tensor_too_many_elements(dt, data): + tensor_info = _get_tensor_info(dt) + num_bytes = tensor_info.GetNumBytes() + + with pytest.raises(ValueError) as err: + ann.ConstTensor(tensor_info, data) + + assert 'ConstTensor requires {} bytes, {} provided.'.format(num_bytes, data.nbytes) in str(err.value) + + +@pytest.mark.parametrize("dt, data", + [ + (ann.DataType_Float32, np.random.randint(1, size=(2, 2)).astype(np.float32)), + (ann.DataType_Float16, np.random.randint(1, size=(2, 2)).astype(np.float16)), + (ann.DataType_QAsymmU8, np.random.randint(1, size=(2, 2)).astype(np.uint8)), + (ann.DataType_QAsymmS8, np.random.randint(1, size=(2, 2)).astype(np.int8)), + (ann.DataType_QSymmS8, np.random.randint(1, size=(2, 2)).astype(np.int8)), + (ann.DataType_Signed32, np.random.randint(1, size=(2, 2)).astype(np.int32)), + (ann.DataType_QSymmS16, np.random.randint(1, size=(2, 2)).astype(np.int16)) + ], ids=['float32', 'float16', 'unsigned int8', 'signed int8', 'signed int8', 'int32', 'int16']) +def test_const_tensor_too_little_elements(dt, data): + tensor_info = _get_tensor_info(dt) + num_bytes = tensor_info.GetNumBytes() + + with pytest.raises(ValueError) as err: + ann.ConstTensor(tensor_info, data) + + assert 'ConstTensor requires {} bytes, {} provided.'.format(num_bytes, data.nbytes) in str(err.value) + + +@pytest.mark.parametrize("dt, data", + [ + (ann.DataType_Float32, np.random.randint(1, size=(2, 2, 3, 3)).astype(np.float32)), + (ann.DataType_Float16, np.random.randint(1, size=(2, 2, 3, 3)).astype(np.float16)), + (ann.DataType_QAsymmU8, np.random.randint(1, size=(2, 2, 3, 3)).astype(np.uint8)), + (ann.DataType_QAsymmS8, np.random.randint(1, size=(2, 2, 3, 3)).astype(np.int8)), + (ann.DataType_QSymmS8, np.random.randint(1, size=(2, 2, 3, 3)).astype(np.int8)), + (ann.DataType_Signed32, np.random.randint(1, size=(2, 2, 3, 3)).astype(np.int32)), + (ann.DataType_QSymmS16, np.random.randint(1, size=(2, 2, 3, 3)).astype(np.int16)) + ], ids=['float32', 'float16', 'unsigned int8', 'signed int8', 'signed int8', 'int32', 'int16']) +def test_const_tensor_multi_dimensional_input(dt, data): + tensor = ann.ConstTensor(ann.TensorInfo(ann.TensorShape((2, 2, 3, 3)), dt), data) + + assert data.size == tensor.GetNumElements() + assert data.nbytes == tensor.GetNumBytes() + assert dt == tensor.GetDataType() + assert tensor.get_memory_area().data + + +def test_create_const_tensor_from_tensor(): + tensor_info = ann.TensorInfo(ann.TensorShape((2, 3)), ann.DataType_Float32) + tensor = ann.Tensor(tensor_info) + copied_tensor = ann.ConstTensor(tensor) + + assert copied_tensor != tensor, "Different objects" + assert copied_tensor.GetInfo() != tensor.GetInfo(), "Different objects" + assert copied_tensor.get_memory_area().ctypes.data == tensor.get_memory_area().ctypes.data, "Same memory area" + assert copied_tensor.GetNumElements() == tensor.GetNumElements() + assert copied_tensor.GetNumBytes() == tensor.GetNumBytes() + assert copied_tensor.GetDataType() == tensor.GetDataType() + + +def test_const_tensor_from_tensor_has_memory_area_access_after_deletion_of_original_tensor(): + tensor_info = ann.TensorInfo(ann.TensorShape((2, 3)), ann.DataType_Float32) + tensor = ann.Tensor(tensor_info) + + tensor.get_memory_area()[0] = 100 + + copied_mem = tensor.get_memory_area().copy() + + assert 100 == copied_mem[0], "Memory was copied correctly" + + copied_tensor = ann.ConstTensor(tensor) + + tensor.get_memory_area()[0] = 200 + + assert 200 == tensor.get_memory_area()[0], "Tensor and copied Tensor point to the same memory" + assert 200 == copied_tensor.get_memory_area()[0], "Tensor and copied Tensor point to the same memory" + + assert 100 == copied_mem[0], "Copied test memory not affected" + + copied_mem[0] = 200 # modify test memory to equal copied Tensor + + del tensor + np.testing.assert_array_equal(copied_tensor.get_memory_area(), copied_mem), "After initial tensor was deleted, " \ + "copied Tensor still has " \ + "its memory as expected" + + +def test_create_const_tensor_incorrect_args(): + with pytest.raises(ValueError) as err: + ann.ConstTensor('something', 'something') + + expected_error_message = "Incorrect number of arguments or type of arguments provided to create Const Tensor." + assert expected_error_message in str(err.value) + + +@pytest.mark.parametrize("dt, data", + [ + # -1 not in data type enum + (-1, np.random.randint(1, size=(2, 3)).astype(np.float32)), + ], ids=['unknown']) +def test_const_tensor_unsupported_datatype(dt, data): + tensor_info = _get_tensor_info(dt) + + with pytest.raises(ValueError) as err: + ann.ConstTensor(tensor_info, data) + + assert 'The data type provided for this Tensor is not supported: -1' in str(err.value) + + +@pytest.mark.parametrize("dt, data", + [ + (ann.DataType_Float32, [[1, 1, 1], [1, 1, 1]]), + (ann.DataType_Float16, [[1, 1, 1], [1, 1, 1]]), + (ann.DataType_QAsymmU8, [[1, 1, 1], [1, 1, 1]]), + (ann.DataType_QAsymmS8, [[1, 1, 1], [1, 1, 1]]), + (ann.DataType_QSymmS8, [[1, 1, 1], [1, 1, 1]]) + ], ids=['float32', 'float16', 'unsigned int8', 'signed int8', 'signed int8']) +def test_const_tensor_incorrect_input_datatype(dt, data): + tensor_info = _get_tensor_info(dt) + + with pytest.raises(TypeError) as err: + ann.ConstTensor(tensor_info, data) + + assert 'Data must be provided as a numpy array.' in str(err.value) + + +@pytest.mark.parametrize("dt, data", + [ + (ann.DataType_Float32, np.random.randint(1, size=(2, 3)).astype(np.float32)), + (ann.DataType_Float16, np.random.randint(1, size=(2, 3)).astype(np.float16)), + (ann.DataType_QAsymmU8, np.random.randint(1, size=(2, 3)).astype(np.uint8)), + (ann.DataType_QAsymmS8, np.random.randint(1, size=(2, 3)).astype(np.int8)), + (ann.DataType_QSymmS8, np.random.randint(1, size=(2, 3)).astype(np.int8)), + (ann.DataType_Signed32, np.random.randint(1, size=(2, 3)).astype(np.int32)), + (ann.DataType_QSymmS16, np.random.randint(1, size=(2, 3)).astype(np.int16)) + ], ids=['float32', 'float16', 'unsigned int8', 'signed int8', 'signed int8', 'int32', 'int16']) +class TestNumpyDataTypes: + + def test_copy_const_tensor(self, dt, data): + tensor_info = _get_tensor_info(dt) + tensor = ann.ConstTensor(tensor_info, data) + copied_tensor = ann.ConstTensor(tensor) + + assert copied_tensor != tensor, "Different objects" + assert copied_tensor.GetInfo() != tensor.GetInfo(), "Different objects" + assert copied_tensor.get_memory_area().ctypes.data == tensor.get_memory_area().ctypes.data, "Same memory area" + assert copied_tensor.GetNumElements() == tensor.GetNumElements() + assert copied_tensor.GetNumBytes() == tensor.GetNumBytes() + assert copied_tensor.GetDataType() == tensor.GetDataType() + + def test_const_tensor__str__(self, dt, data): + tensor_info = _get_tensor_info(dt) + d_type = tensor_info.GetDataType() + num_dimensions = tensor_info.GetNumDimensions() + num_bytes = tensor_info.GetNumBytes() + num_elements = tensor_info.GetNumElements() + tensor = ann.ConstTensor(tensor_info, data) + + assert str(tensor) == "ConstTensor{{DataType: {}, NumBytes: {}, NumDimensions: " \ + "{}, NumElements: {}}}".format(d_type, num_bytes, num_dimensions, num_elements) + + def test_const_tensor_with_info(self, dt, data): + tensor_info = _get_tensor_info(dt) + elements = tensor_info.GetNumElements() + num_bytes = tensor_info.GetNumBytes() + d_type = dt + + tensor = ann.ConstTensor(tensor_info, data) + + assert tensor_info != tensor.GetInfo(), "Different objects" + assert elements == tensor.GetNumElements() + assert num_bytes == tensor.GetNumBytes() + assert d_type == tensor.GetDataType() + + def test_immutable_memory(self, dt, data): + tensor_info = _get_tensor_info(dt) + + tensor = ann.ConstTensor(tensor_info, data) + + with pytest.raises(ValueError) as err: + tensor.get_memory_area()[0] = 0 + + assert 'is read-only' in str(err.value) + + def test_numpy_dtype_matches_ann_dtype(self, dt, data): + np_data_type_mapping = {ann.DataType_QAsymmU8: np.uint8, + ann.DataType_QAsymmS8: np.int8, + ann.DataType_QSymmS8: np.int8, + ann.DataType_Float32: np.float32, + ann.DataType_QSymmS16: np.int16, + ann.DataType_Signed32: np.int32, + ann.DataType_Float16: np.float16} + + tensor_info = _get_tensor_info(dt) + tensor = ann.ConstTensor(tensor_info, data) + assert np_data_type_mapping[tensor.GetDataType()] == data.dtype + + +# This test checks that mismatched numpy and PyArmNN datatypes with same number of bits raises correct error. +@pytest.mark.parametrize("dt, data", + [ + (ann.DataType_Float32, np.random.randint(1, size=(2, 3)).astype(np.int32)), + (ann.DataType_Float16, np.random.randint(1, size=(2, 3)).astype(np.int16)), + (ann.DataType_QAsymmU8, np.random.randint(1, size=(2, 3)).astype(np.int8)), + (ann.DataType_QAsymmS8, np.random.randint(1, size=(2, 3)).astype(np.uint8)), + (ann.DataType_QSymmS8, np.random.randint(1, size=(2, 3)).astype(np.uint8)), + (ann.DataType_Signed32, np.random.randint(1, size=(2, 3)).astype(np.float32)), + (ann.DataType_QSymmS16, np.random.randint(1, size=(2, 3)).astype(np.float16)) + ], ids=['float32', 'float16', 'unsigned int8', 'signed int8', 'signed int8', 'int32', 'int16']) +def test_numpy_dtype_mismatch_ann_dtype(dt, data): + np_data_type_mapping = {ann.DataType_QAsymmU8: np.uint8, + ann.DataType_QAsymmS8: np.int8, + ann.DataType_QSymmS8: np.int8, + ann.DataType_Float32: np.float32, + ann.DataType_QSymmS16: np.int16, + ann.DataType_Signed32: np.int32, + ann.DataType_Float16: np.float16} + + tensor_info = _get_tensor_info(dt) + with pytest.raises(TypeError) as err: + ann.ConstTensor(tensor_info, data) + + assert str(err.value) == "Expected data to have type {} for type {} but instead got numpy.{}".format( + np_data_type_mapping[dt], dt, data.dtype) + diff --git a/python/pyarmnn/test/test_descriptors.py b/python/pyarmnn/test/test_descriptors.py new file mode 100644 index 0000000000..1a41105aa9 --- /dev/null +++ b/python/pyarmnn/test/test_descriptors.py @@ -0,0 +1,535 @@ +# Copyright © 2020 Arm Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +import inspect + +import pytest + +import pyarmnn as ann +import numpy as np +import pyarmnn._generated.pyarmnn as generated + + +def test_activation_descriptor_default_values(): + desc = ann.ActivationDescriptor() + assert desc.m_Function == ann.ActivationFunction_Sigmoid + assert desc.m_A == 0 + assert desc.m_B == 0 + + +def test_argminmax_descriptor_default_values(): + desc = ann.ArgMinMaxDescriptor() + assert desc.m_Function == ann.ArgMinMaxFunction_Min + assert desc.m_Axis == -1 + + +def test_batchnormalization_descriptor_default_values(): + desc = ann.BatchNormalizationDescriptor() + assert desc.m_DataLayout == ann.DataLayout_NCHW + np.allclose(0.0001, desc.m_Eps) + + +def test_batchtospacend_descriptor_default_values(): + desc = ann.BatchToSpaceNdDescriptor() + assert desc.m_DataLayout == ann.DataLayout_NCHW + assert [1, 1] == desc.m_BlockShape + assert [(0, 0), (0, 0)] == desc.m_Crops + + +def test_batchtospacend_descriptor_assignment(): + desc = ann.BatchToSpaceNdDescriptor() + desc.m_BlockShape = (1, 2, 3) + + ololo = [(1, 2), (3, 4)] + size_1 = len(ololo) + desc.m_Crops = ololo + + assert size_1 == len(ololo) + desc.m_DataLayout = ann.DataLayout_NHWC + assert ann.DataLayout_NHWC == desc.m_DataLayout + assert [1, 2, 3] == desc.m_BlockShape + assert [(1, 2), (3, 4)] == desc.m_Crops + + +@pytest.mark.parametrize("input_shape, value, vtype", [([-1], -1, 'int'), (("one", "two"), "'one'", 'str'), + ([1.33, 4.55], 1.33, 'float'), + ([{1: "one"}], "{1: 'one'}", 'dict')], ids=lambda x: str(x)) +def test_batchtospacend_descriptor_rubbish_assignment_shape(input_shape, value, vtype): + desc = ann.BatchToSpaceNdDescriptor() + with pytest.raises(TypeError) as err: + desc.m_BlockShape = input_shape + + assert "Failed to convert python input value {} of type '{}' to C type 'j'".format(value, vtype) in str(err.value) + + +@pytest.mark.parametrize("input_crops, value, vtype", [([(1, 2), (3, 4, 5)], '(3, 4, 5)', 'tuple'), + ([(1, 'one')], "(1, 'one')", 'tuple'), + ([-1], -1, 'int'), + ([(1, (1, 2))], '(1, (1, 2))', 'tuple'), + ([[1, [1, 2]]], '[1, [1, 2]]', 'list') + ], ids=lambda x: str(x)) +def test_batchtospacend_descriptor_rubbish_assignment_crops(input_crops, value, vtype): + desc = ann.BatchToSpaceNdDescriptor() + with pytest.raises(TypeError) as err: + desc.m_Crops = input_crops + + assert "Failed to convert python input value {} of type '{}' to C type".format(value, vtype) in str(err.value) + + +def test_batchtospacend_descriptor_empty_assignment(): + desc = ann.BatchToSpaceNdDescriptor() + desc.m_BlockShape = [] + assert [] == desc.m_BlockShape + + +def test_batchtospacend_descriptor_ctor(): + desc = ann.BatchToSpaceNdDescriptor([1, 2, 3], [(4, 5), (6, 7)]) + assert desc.m_DataLayout == ann.DataLayout_NCHW + assert [1, 2, 3] == desc.m_BlockShape + assert [(4, 5), (6, 7)] == desc.m_Crops + + +def test_convolution2d_descriptor_default_values(): + desc = ann.Convolution2dDescriptor() + assert desc.m_PadLeft == 0 + assert desc.m_PadTop == 0 + assert desc.m_PadRight == 0 + assert desc.m_PadBottom == 0 + assert desc.m_StrideX == 0 + assert desc.m_StrideY == 0 + assert desc.m_DilationX == 1 + assert desc.m_DilationY == 1 + assert desc.m_BiasEnabled == False + assert desc.m_DataLayout == ann.DataLayout_NCHW + + +def test_depthtospace_descriptor_default_values(): + desc = ann.DepthToSpaceDescriptor() + assert desc.m_BlockSize == 1 + assert desc.m_DataLayout == ann.DataLayout_NHWC + + +def test_depthwise_convolution2d_descriptor_default_values(): + desc = ann.DepthwiseConvolution2dDescriptor() + assert desc.m_PadLeft == 0 + assert desc.m_PadTop == 0 + assert desc.m_PadRight == 0 + assert desc.m_PadBottom == 0 + assert desc.m_StrideX == 0 + assert desc.m_StrideY == 0 + assert desc.m_DilationX == 1 + assert desc.m_DilationY == 1 + assert desc.m_BiasEnabled == False + assert desc.m_DataLayout == ann.DataLayout_NCHW + + +def test_detectionpostprocess_descriptor_default_values(): + desc = ann.DetectionPostProcessDescriptor() + assert desc.m_MaxDetections == 0 + assert desc.m_MaxClassesPerDetection == 1 + assert desc.m_DetectionsPerClass == 1 + assert desc.m_NmsScoreThreshold == 0 + assert desc.m_NmsIouThreshold == 0 + assert desc.m_NumClasses == 0 + assert desc.m_UseRegularNms == False + assert desc.m_ScaleH == 0 + assert desc.m_ScaleW == 0 + assert desc.m_ScaleX == 0 + assert desc.m_ScaleY == 0 + + +def test_fakequantization_descriptor_default_values(): + desc = ann.FakeQuantizationDescriptor() + np.allclose(6, desc.m_Max) + np.allclose(-6, desc.m_Min) + + +def test_fully_connected_descriptor_default_values(): + desc = ann.FullyConnectedDescriptor() + assert desc.m_BiasEnabled == False + assert desc.m_TransposeWeightMatrix == False + + +def test_instancenormalization_descriptor_default_values(): + desc = ann.InstanceNormalizationDescriptor() + assert desc.m_Gamma == 1 + assert desc.m_Beta == 0 + assert desc.m_DataLayout == ann.DataLayout_NCHW + np.allclose(1e-12, desc.m_Eps) + + +def test_lstm_descriptor_default_values(): + desc = ann.LstmDescriptor() + assert desc.m_ActivationFunc == 1 + assert desc.m_ClippingThresCell == 0 + assert desc.m_ClippingThresProj == 0 + assert desc.m_CifgEnabled == True + assert desc.m_PeepholeEnabled == False + assert desc.m_ProjectionEnabled == False + assert desc.m_LayerNormEnabled == False + + +def test_l2normalization_descriptor_default_values(): + desc = ann.L2NormalizationDescriptor() + assert desc.m_DataLayout == ann.DataLayout_NCHW + np.allclose(1e-12, desc.m_Eps) + + +def test_mean_descriptor_default_values(): + desc = ann.MeanDescriptor() + assert desc.m_KeepDims == False + + +def test_normalization_descriptor_default_values(): + desc = ann.NormalizationDescriptor() + assert desc.m_NormChannelType == ann.NormalizationAlgorithmChannel_Across + assert desc.m_NormMethodType == ann.NormalizationAlgorithmMethod_LocalBrightness + assert desc.m_NormSize == 0 + assert desc.m_Alpha == 0 + assert desc.m_Beta == 0 + assert desc.m_K == 0 + assert desc.m_DataLayout == ann.DataLayout_NCHW + + +def test_origin_descriptor_default_values(): + desc = ann.ConcatDescriptor() + assert 0 == desc.GetNumViews() + assert 0 == desc.GetNumDimensions() + assert 1 == desc.GetConcatAxis() + + +def test_origin_descriptor_incorrect_views(): + desc = ann.ConcatDescriptor(2, 2) + with pytest.raises(RuntimeError) as err: + desc.SetViewOriginCoord(1000, 100, 1000) + assert "Failed to set view origin coordinates." in str(err.value) + + +def test_origin_descriptor_ctor(): + desc = ann.ConcatDescriptor(2, 2) + value = 5 + for i in range(desc.GetNumViews()): + for j in range(desc.GetNumDimensions()): + desc.SetViewOriginCoord(i, j, value+i) + desc.SetConcatAxis(1) + + assert 2 == desc.GetNumViews() + assert 2 == desc.GetNumDimensions() + assert [5, 5] == desc.GetViewOrigin(0) + assert [6, 6] == desc.GetViewOrigin(1) + assert 1 == desc.GetConcatAxis() + + +def test_pad_descriptor_default_values(): + desc = ann.PadDescriptor() + assert desc.m_PadValue == 0 + + +def test_permute_descriptor_default_values(): + pv = ann.PermutationVector((0, 2, 3, 1)) + desc = ann.PermuteDescriptor(pv) + assert desc.m_DimMappings.GetSize() == 4 + assert desc.m_DimMappings[0] == 0 + assert desc.m_DimMappings[1] == 2 + assert desc.m_DimMappings[2] == 3 + assert desc.m_DimMappings[3] == 1 + + +def test_pooling_descriptor_default_values(): + desc = ann.Pooling2dDescriptor() + assert desc.m_PoolType == ann.PoolingAlgorithm_Max + assert desc.m_PadLeft == 0 + assert desc.m_PadTop == 0 + assert desc.m_PadRight == 0 + assert desc.m_PadBottom == 0 + assert desc.m_PoolHeight == 0 + assert desc.m_PoolWidth == 0 + assert desc.m_StrideX == 0 + assert desc.m_StrideY == 0 + assert desc.m_OutputShapeRounding == ann.OutputShapeRounding_Floor + assert desc.m_PaddingMethod == ann.PaddingMethod_Exclude + assert desc.m_DataLayout == ann.DataLayout_NCHW + + +def test_reshape_descriptor_default_values(): + desc = ann.ReshapeDescriptor() + # check the empty Targetshape + assert desc.m_TargetShape.GetNumDimensions() == 0 + + +def test_slice_descriptor_default_values(): + desc = ann.SliceDescriptor() + assert desc.m_TargetWidth == 0 + assert desc.m_TargetHeight == 0 + assert desc.m_Method == ann.ResizeMethod_NearestNeighbor + assert desc.m_DataLayout == ann.DataLayout_NCHW + + +def test_resize_descriptor_default_values(): + desc = ann.ResizeDescriptor() + assert desc.m_TargetWidth == 0 + assert desc.m_TargetHeight == 0 + assert desc.m_Method == ann.ResizeMethod_NearestNeighbor + assert desc.m_DataLayout == ann.DataLayout_NCHW + assert desc.m_BilinearAlignCorners == False + + +def test_spacetobatchnd_descriptor_default_values(): + desc = ann.SpaceToBatchNdDescriptor() + assert desc.m_DataLayout == ann.DataLayout_NCHW + + +def test_spacetodepth_descriptor_default_values(): + desc = ann.SpaceToDepthDescriptor() + assert desc.m_BlockSize == 1 + assert desc.m_DataLayout == ann.DataLayout_NHWC + + +def test_stack_descriptor_default_values(): + desc = ann.StackDescriptor() + assert desc.m_Axis == 0 + assert desc.m_NumInputs == 0 + # check the empty Inputshape + assert desc.m_InputShape.GetNumDimensions() == 0 + + +def test_slice_descriptor_default_values(): + desc = ann.SliceDescriptor() + desc.m_Begin = [1, 2, 3, 4, 5] + desc.m_Size = (1, 2, 3, 4) + + assert [1, 2, 3, 4, 5] == desc.m_Begin + assert [1, 2, 3, 4] == desc.m_Size + + +def test_slice_descriptor_ctor(): + desc = ann.SliceDescriptor([1, 2, 3, 4, 5], (1, 2, 3, 4)) + + assert [1, 2, 3, 4, 5] == desc.m_Begin + assert [1, 2, 3, 4] == desc.m_Size + + +def test_strided_slice_descriptor_default_values(): + desc = ann.StridedSliceDescriptor() + desc.m_Begin = [1, 2, 3, 4, 5] + desc.m_End = [6, 7, 8, 9, 10] + desc.m_Stride = (10, 10) + desc.m_BeginMask = 1 + desc.m_EndMask = 2 + desc.m_ShrinkAxisMask = 3 + desc.m_EllipsisMask = 4 + desc.m_NewAxisMask = 5 + + assert [1, 2, 3, 4, 5] == desc.m_Begin + assert [6, 7, 8, 9, 10] == desc.m_End + assert [10, 10] == desc.m_Stride + assert 1 == desc.m_BeginMask + assert 2 == desc.m_EndMask + assert 3 == desc.m_ShrinkAxisMask + assert 4 == desc.m_EllipsisMask + assert 5 == desc.m_NewAxisMask + + +def test_strided_slice_descriptor_ctor(): + desc = ann.StridedSliceDescriptor([1, 2, 3, 4, 5], [6, 7, 8, 9, 10], (10, 10)) + desc.m_Begin = [1, 2, 3, 4, 5] + desc.m_End = [6, 7, 8, 9, 10] + desc.m_Stride = (10, 10) + + assert [1, 2, 3, 4, 5] == desc.m_Begin + assert [6, 7, 8, 9, 10] == desc.m_End + assert [10, 10] == desc.m_Stride + + +def test_softmax_descriptor_default_values(): + desc = ann.SoftmaxDescriptor() + assert desc.m_Axis == -1 + np.allclose(1.0, desc.m_Beta) + + +def test_space_to_batch_nd_descriptor_default_values(): + desc = ann.SpaceToBatchNdDescriptor() + assert [1, 1] == desc.m_BlockShape + assert [(0, 0), (0, 0)] == desc.m_PadList + assert ann.DataLayout_NCHW == desc.m_DataLayout + + +def test_space_to_batch_nd_descriptor_assigned_values(): + desc = ann.SpaceToBatchNdDescriptor() + desc.m_BlockShape = (90, 100) + desc.m_PadList = [(1, 2), (3, 4)] + assert [90, 100] == desc.m_BlockShape + assert [(1, 2), (3, 4)] == desc.m_PadList + assert ann.DataLayout_NCHW == desc.m_DataLayout + + +def test_space_to_batch_nd_descriptor_ctor(): + desc = ann.SpaceToBatchNdDescriptor((1, 2, 3), [(1, 2), (3, 4)]) + assert [1, 2, 3] == desc.m_BlockShape + assert [(1, 2), (3, 4)] == desc.m_PadList + assert ann.DataLayout_NCHW == desc.m_DataLayout + + +def test_transpose_convolution2d_descriptor_default_values(): + desc = ann.DepthwiseConvolution2dDescriptor() + assert desc.m_PadLeft == 0 + assert desc.m_PadTop == 0 + assert desc.m_PadRight == 0 + assert desc.m_PadBottom == 0 + assert desc.m_StrideX == 0 + assert desc.m_StrideY == 0 + assert desc.m_BiasEnabled == False + assert desc.m_DataLayout == ann.DataLayout_NCHW + + +def test_view_descriptor_default_values(): + desc = ann.SplitterDescriptor() + assert 0 == desc.GetNumViews() + assert 0 == desc.GetNumDimensions() + + +def test_elementwise_unary_descriptor_default_values(): + desc = ann.ElementwiseUnaryDescriptor() + assert desc.m_Operation == ann.UnaryOperation_Abs + + +def test_view_descriptor_incorrect_input(): + desc = ann.SplitterDescriptor(2, 3) + with pytest.raises(RuntimeError) as err: + desc.SetViewOriginCoord(1000, 100, 1000) + assert "Failed to set view origin coordinates." in str(err.value) + + with pytest.raises(RuntimeError) as err: + desc.SetViewSize(1000, 100, 1000) + assert "Failed to set view size." in str(err.value) + + +def test_view_descriptor_ctor(): + desc = ann.SplitterDescriptor(2, 3) + value_size = 1 + value_orig_coord = 5 + for i in range(desc.GetNumViews()): + for j in range(desc.GetNumDimensions()): + desc.SetViewOriginCoord(i, j, value_orig_coord+i) + desc.SetViewSize(i, j, value_size+i) + + assert 2 == desc.GetNumViews() + assert 3 == desc.GetNumDimensions() + assert [5, 5] == desc.GetViewOrigin(0) + assert [6, 6] == desc.GetViewOrigin(1) + assert [1, 1] == desc.GetViewSizes(0) + assert [2, 2] == desc.GetViewSizes(1) + + +def test_createdescriptorforconcatenation_ctor(): + input_shape_vector = [ann.TensorShape((2, 1)), ann.TensorShape((3, 1)), ann.TensorShape((4, 1))] + desc = ann.CreateDescriptorForConcatenation(input_shape_vector, 0) + assert 3 == desc.GetNumViews() + assert 0 == desc.GetConcatAxis() + assert 2 == desc.GetNumDimensions() + c = desc.GetViewOrigin(1) + d = desc.GetViewOrigin(0) + + +def test_createdescriptorforconcatenation_wrong_shape_for_axis(): + input_shape_vector = [ann.TensorShape((1, 2)), ann.TensorShape((3, 4)), ann.TensorShape((5, 6))] + with pytest.raises(RuntimeError) as err: + desc = ann.CreateDescriptorForConcatenation(input_shape_vector, 0) + + assert "All inputs to concatenation must be the same size along all dimensions except the concatenation dimension" in str( + err.value) + + +@pytest.mark.parametrize("input_shape_vector", [([-1, "one"]), + ([1.33, 4.55]), + ([{1: "one"}])], ids=lambda x: str(x)) +def test_createdescriptorforconcatenation_rubbish_assignment_shape_vector(input_shape_vector): + with pytest.raises(TypeError) as err: + desc = ann.CreateDescriptorForConcatenation(input_shape_vector, 0) + + assert "in method 'CreateDescriptorForConcatenation', argument 1 of type 'std::vector< armnn::TensorShape,std::allocator< armnn::TensorShape > >'" in str( + err.value) + + +generated_classes = inspect.getmembers(generated, inspect.isclass) +generated_classes_names = list(map(lambda x: x[0], generated_classes)) +@pytest.mark.parametrize("desc_name", ['ActivationDescriptor', + 'ArgMinMaxDescriptor', + 'PermuteDescriptor', + 'SoftmaxDescriptor', + 'ConcatDescriptor', + 'SplitterDescriptor', + 'Pooling2dDescriptor', + 'FullyConnectedDescriptor', + 'Convolution2dDescriptor', + 'DepthwiseConvolution2dDescriptor', + 'DetectionPostProcessDescriptor', + 'NormalizationDescriptor', + 'L2NormalizationDescriptor', + 'BatchNormalizationDescriptor', + 'InstanceNormalizationDescriptor', + 'BatchToSpaceNdDescriptor', + 'FakeQuantizationDescriptor', + 'ResizeDescriptor', + 'ReshapeDescriptor', + 'SpaceToBatchNdDescriptor', + 'SpaceToDepthDescriptor', + 'LstmDescriptor', + 'MeanDescriptor', + 'PadDescriptor', + 'SliceDescriptor', + 'StackDescriptor', + 'StridedSliceDescriptor', + 'TransposeConvolution2dDescriptor', + 'ElementwiseUnaryDescriptor']) +class TestDescriptorMassChecks: + + def test_desc_implemented(self, desc_name): + assert desc_name in generated_classes_names + + def test_desc_equal(self, desc_name): + desc_class = next(filter(lambda x: x[0] == desc_name, generated_classes))[1] + + assert desc_class() == desc_class() + + +generated_classes = inspect.getmembers(generated, inspect.isclass) +generated_classes_names = list(map(lambda x: x[0], generated_classes)) +@pytest.mark.parametrize("desc_name", ['ActivationDescriptor', + 'ArgMinMaxDescriptor', + 'PermuteDescriptor', + 'SoftmaxDescriptor', + 'ConcatDescriptor', + 'SplitterDescriptor', + 'Pooling2dDescriptor', + 'FullyConnectedDescriptor', + 'Convolution2dDescriptor', + 'DepthwiseConvolution2dDescriptor', + 'DetectionPostProcessDescriptor', + 'NormalizationDescriptor', + 'L2NormalizationDescriptor', + 'BatchNormalizationDescriptor', + 'InstanceNormalizationDescriptor', + 'BatchToSpaceNdDescriptor', + 'FakeQuantizationDescriptor', + 'ResizeDescriptor', + 'ReshapeDescriptor', + 'SpaceToBatchNdDescriptor', + 'SpaceToDepthDescriptor', + 'LstmDescriptor', + 'MeanDescriptor', + 'PadDescriptor', + 'SliceDescriptor', + 'StackDescriptor', + 'StridedSliceDescriptor', + 'TransposeConvolution2dDescriptor', + 'ElementwiseUnaryDescriptor']) +class TestDescriptorMassChecks: + + def test_desc_implemented(self, desc_name): + assert desc_name in generated_classes_names + + def test_desc_equal(self, desc_name): + desc_class = next(filter(lambda x: x[0] == desc_name, generated_classes))[1] + + assert desc_class() == desc_class() + diff --git a/python/pyarmnn/test/test_generated.py b/python/pyarmnn/test/test_generated.py new file mode 100644 index 0000000000..24765c73ab --- /dev/null +++ b/python/pyarmnn/test/test_generated.py @@ -0,0 +1,52 @@ +# Copyright © 2020 Arm Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +import inspect +from typing import Tuple + +import pytest + +import pyarmnn._generated.pyarmnn as generated_armnn +import pyarmnn._generated.pyarmnn_caffeparser as generated_caffe +import pyarmnn._generated.pyarmnn_onnxparser as generated_onnx +import pyarmnn._generated.pyarmnn_tfliteparser as generated_tflite +import pyarmnn._generated.pyarmnn_tfparser as generated_tf + +swig_independent_classes = ('IBackend', + 'IDeviceSpec', + 'IConnectableLayer', + 'IInputSlot', + 'IOutputSlot', + 'IProfiler') + + +def get_classes(swig_independent_classes: Tuple): + # We need to ignore some swig generated_armnn classes. This is because some are abstract classes + # They cannot be created with the swig generated_armnn wrapper, therefore they don't need a destructor. + # Swig also generates its own meta class - this needs to be ignored. + ignored_class_names = (*swig_independent_classes, '_SwigNonDynamicMeta') + return list(filter(lambda x: x[0] not in ignored_class_names, + inspect.getmembers(generated_armnn, inspect.isclass) + + inspect.getmembers(generated_caffe, inspect.isclass) + + inspect.getmembers(generated_tflite, inspect.isclass) + + inspect.getmembers(generated_onnx, inspect.isclass) + + inspect.getmembers(generated_tf, inspect.isclass))) + + +@pytest.mark.parametrize("class_instance", get_classes(swig_independent_classes), ids=lambda x: 'class={}'.format(x[0])) +class TestPyOwnedClasses: + + def test_destructors_exist_per_class(self, class_instance): + assert getattr(class_instance[1], '__swig_destroy__', None) + + def test_owned(self, class_instance): + assert getattr(class_instance[1], 'thisown', None) + + +@pytest.mark.parametrize("class_instance", swig_independent_classes) +class TestPyIndependentClasses: + + def test_destructors_does_not_exist_per_class(self, class_instance): + assert not getattr(class_instance[1], '__swig_destroy__', None) + + def test_not_owned(self, class_instance): + assert not getattr(class_instance[1], 'thisown', None) diff --git a/python/pyarmnn/test/test_iconnectable.py b/python/pyarmnn/test/test_iconnectable.py new file mode 100644 index 0000000000..0d15be5e73 --- /dev/null +++ b/python/pyarmnn/test/test_iconnectable.py @@ -0,0 +1,142 @@ +# Copyright © 2020 Arm Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +import pytest + +import pyarmnn as ann + + +@pytest.fixture(scope="function") +def network(): + return ann.INetwork() + + +class TestIInputIOutputIConnectable: + + def test_input_slot(self, network): + # Create input, addition & output layer + input1 = network.AddInputLayer(0, "input1") + input2 = network.AddInputLayer(1, "input2") + add = network.AddAdditionLayer("addition") + output = network.AddOutputLayer(0, "output") + + # Connect the input/output slots for each layer + input1.GetOutputSlot(0).Connect(add.GetInputSlot(0)) + input2.GetOutputSlot(0).Connect(add.GetInputSlot(1)) + add.GetOutputSlot(0).Connect(output.GetInputSlot(0)) + + # Check IInputSlot GetConnection() + input_slot = add.GetInputSlot(0) + input_slot_connection = input_slot.GetConnection() + + assert isinstance(input_slot_connection, ann.IOutputSlot) + + del input_slot_connection + + assert input_slot.GetConnection() + assert isinstance(input_slot.GetConnection(), ann.IOutputSlot) + + del input_slot + + assert add.GetInputSlot(0) + + def test_output_slot(self, network): + + # Create input, addition & output layer + input1 = network.AddInputLayer(0, "input1") + input2 = network.AddInputLayer(1, "input2") + add = network.AddAdditionLayer("addition") + output = network.AddOutputLayer(0, "output") + + # Connect the input/output slots for each layer + input1.GetOutputSlot(0).Connect(add.GetInputSlot(0)) + input2.GetOutputSlot(0).Connect(add.GetInputSlot(1)) + add.GetOutputSlot(0).Connect(output.GetInputSlot(0)) + + # Check IInputSlot GetConnection() + add_get_input_connection = add.GetInputSlot(0).GetConnection() + output_get_input_connection = output.GetInputSlot(0).GetConnection() + + # Check IOutputSlot GetConnection() + add_get_output_connect = add.GetOutputSlot(0).GetConnection(0) + assert isinstance(add_get_output_connect.GetConnection(), ann.IOutputSlot) + + # Test IOutputSlot GetNumConnections() & CalculateIndexOnOwner() + assert add_get_input_connection.GetNumConnections() == 1 + assert len(add_get_input_connection) == 1 + assert add_get_input_connection[0] + assert add_get_input_connection.CalculateIndexOnOwner() == 0 + + # Check GetOwningLayerGuid(). Check that it is different for add and output layer + assert add_get_input_connection.GetOwningLayerGuid() != output_get_input_connection.GetOwningLayerGuid() + + # Set TensorInfo + test_tensor_info = ann.TensorInfo(ann.TensorShape((2, 3)), ann.DataType_Float32) + + # Check IsTensorInfoSet() + assert not add_get_input_connection.IsTensorInfoSet() + add_get_input_connection.SetTensorInfo(test_tensor_info) + assert add_get_input_connection.IsTensorInfoSet() + + # Check GetTensorInfo() + output_tensor_info = add_get_input_connection.GetTensorInfo() + assert 2 == output_tensor_info.GetNumDimensions() + assert 6 == output_tensor_info.GetNumElements() + + # Check Disconnect() + assert output_get_input_connection.GetNumConnections() == 1 # 1 connection to Outputslot0 from input1 + add.GetOutputSlot(0).Disconnect(output.GetInputSlot(0)) # disconnect add.OutputSlot0 from Output.InputSlot0 + assert output_get_input_connection.GetNumConnections() == 0 + + def test_output_slot__out_of_range(self, network): + # Create input layer to check output slot get item handling + input1 = network.AddInputLayer(0, "input1") + + outputSlot = input1.GetOutputSlot(0) + with pytest.raises(ValueError) as err: + outputSlot[1] + + assert "Invalid index 1 provided" in str(err.value) + + def test_iconnectable_guid(self, network): + + # Check IConnectable GetGuid() + # Note Guid can change based on which tests are run so + # checking here that each layer does not have the same guid + add_id = network.AddAdditionLayer().GetGuid() + output_id = network.AddOutputLayer(0).GetGuid() + assert add_id != output_id + + def test_iconnectable_layer_functions(self, network): + + # Create input, addition & output layer + input1 = network.AddInputLayer(0, "input1") + input2 = network.AddInputLayer(1, "input2") + add = network.AddAdditionLayer("addition") + output = network.AddOutputLayer(0, "output") + + # Check GetNumInputSlots(), GetName() & GetNumOutputSlots() + assert input1.GetNumInputSlots() == 0 + assert input1.GetName() == "input1" + assert input1.GetNumOutputSlots() == 1 + + assert input2.GetNumInputSlots() == 0 + assert input2.GetName() == "input2" + assert input2.GetNumOutputSlots() == 1 + + assert add.GetNumInputSlots() == 2 + assert add.GetName() == "addition" + assert add.GetNumOutputSlots() == 1 + + assert output.GetNumInputSlots() == 1 + assert output.GetName() == "output" + assert output.GetNumOutputSlots() == 0 + + # Check GetOutputSlot() + input1_get_output = input1.GetOutputSlot(0) + assert input1_get_output.GetNumConnections() == 0 + assert len(input1_get_output) == 0 + + # Check GetInputSlot() + add_get_input = add.GetInputSlot(0) + add_get_input.GetConnection() + assert isinstance(add_get_input, ann.IInputSlot) diff --git a/python/pyarmnn/test/test_network.py b/python/pyarmnn/test/test_network.py new file mode 100644 index 0000000000..fc2591c1d5 --- /dev/null +++ b/python/pyarmnn/test/test_network.py @@ -0,0 +1,288 @@ +# Copyright © 2020 Arm Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +import os +import stat + +import pytest +import pyarmnn as ann + + +@pytest.fixture(scope="function") +def get_runtime(shared_data_folder, network_file): + parser= ann.ITfLiteParser() + preferred_backends = [ann.BackendId('CpuAcc'), ann.BackendId('CpuRef')] + network = parser.CreateNetworkFromBinaryFile(os.path.join(shared_data_folder, network_file)) + options = ann.CreationOptions() + runtime = ann.IRuntime(options) + + yield preferred_backends, network, runtime + + +@pytest.mark.parametrize("network_file", + [ + 'mock_model.tflite', + ], + ids=['mock_model']) +def test_optimize_executes_successfully(network_file, get_runtime): + preferred_backends = [ann.BackendId('CpuRef')] + network = get_runtime[1] + runtime = get_runtime[2] + + opt_network, messages = ann.Optimize(network, preferred_backends, runtime.GetDeviceSpec(), ann.OptimizerOptions()) + + assert len(messages) == 0, 'With only CpuRef, there should be no warnings irrelevant of architecture.' + assert opt_network + + +@pytest.mark.parametrize("network_file", + [ + 'mock_model.tflite', + ], + ids=['mock_model']) +def test_optimize_owned_by_python(network_file, get_runtime): + preferred_backends = get_runtime[0] + network = get_runtime[1] + runtime = get_runtime[2] + + opt_network, _ = ann.Optimize(network, preferred_backends, runtime.GetDeviceSpec(), ann.OptimizerOptions()) + assert opt_network.thisown + + +@pytest.mark.aarch64 +@pytest.mark.parametrize("network_file", + [ + 'mock_model.tflite' + ], + ids=['mock_model']) +def test_optimize_executes_successfully_for_neon_backend_only(network_file, get_runtime): + preferred_backends = [ann.BackendId('CpuAcc')] + network = get_runtime[1] + runtime = get_runtime[2] + + opt_network, messages = ann.Optimize(network, preferred_backends, runtime.GetDeviceSpec(), ann.OptimizerOptions()) + assert 0 == len(messages) + assert opt_network + + +@pytest.mark.parametrize("network_file", + [ + 'mock_model.tflite' + ], + ids=['mock_model']) +def test_optimize_fails_for_invalid_backends(network_file, get_runtime): + invalid_backends = [ann.BackendId('Unknown')] + network = get_runtime[1] + runtime = get_runtime[2] + + with pytest.raises(RuntimeError) as err: + ann.Optimize(network, invalid_backends, runtime.GetDeviceSpec(), ann.OptimizerOptions()) + + expected_error_message = "None of the preferred backends [Unknown ] are supported." + assert expected_error_message in str(err.value) + + +@pytest.mark.parametrize("network_file", + [ + 'mock_model.tflite' + ], + ids=['mock_model']) +def test_optimize_fails_for_no_backends_specified(network_file, get_runtime): + empty_backends = [] + network = get_runtime[1] + runtime = get_runtime[2] + + with pytest.raises(RuntimeError) as err: + ann.Optimize(network, empty_backends, runtime.GetDeviceSpec(), ann.OptimizerOptions()) + + expected_error_message = "Invoked Optimize with no backends specified" + assert expected_error_message in str(err.value) + + +@pytest.mark.parametrize("network_file", + [ + 'mock_model.tflite' + ], + ids=['mock_model']) +def test_serialize_to_dot(network_file, get_runtime, tmpdir): + preferred_backends = get_runtime[0] + network = get_runtime[1] + runtime = get_runtime[2] + opt_network, _ = ann.Optimize(network, preferred_backends, + runtime.GetDeviceSpec(), ann.OptimizerOptions()) + dot_file_path = os.path.join(tmpdir, 'mock_model.dot') + """Check that serialized file does not exist at the start, gets created after SerializeToDot and is not empty""" + assert not os.path.exists(dot_file_path) + opt_network.SerializeToDot(dot_file_path) + + assert os.path.exists(dot_file_path) + + with open(dot_file_path) as res_file: + expected_data = res_file.read() + assert len(expected_data) > 1 + assert '[label=< [1,28,28,1] >]' in expected_data + + +@pytest.mark.x86_64 +@pytest.mark.parametrize("network_file", + [ + 'mock_model.tflite' + ], + ids=['mock_model']) +def test_serialize_to_dot_mode_readonly(network_file, get_runtime, tmpdir): + preferred_backends = get_runtime[0] + network = get_runtime[1] + runtime = get_runtime[2] + opt_network, _ = ann.Optimize(network, preferred_backends, + runtime.GetDeviceSpec(), ann.OptimizerOptions()) + """Create file, write to it and change mode to read-only""" + dot_file_path = os.path.join(tmpdir, 'mock_model.dot') + f = open(dot_file_path, "w+") + f.write("test") + f.close() + os.chmod(dot_file_path, stat.S_IREAD) + assert os.path.exists(dot_file_path) + + with pytest.raises(RuntimeError) as err: + opt_network.SerializeToDot(dot_file_path) + + expected_error_message = "Failed to open dot file" + assert expected_error_message in str(err.value) + + +@pytest.mark.parametrize("method", [ + 'AddActivationLayer', + 'AddAdditionLayer', + 'AddArgMinMaxLayer', + 'AddBatchNormalizationLayer', + 'AddBatchToSpaceNdLayer', + 'AddComparisonLayer', + 'AddConcatLayer', + 'AddConstantLayer', + 'AddConvolution2dLayer', + 'AddDepthToSpaceLayer', + 'AddDepthwiseConvolution2dLayer', + 'AddDequantizeLayer', + 'AddDetectionPostProcessLayer', + 'AddDivisionLayer', + 'AddElementwiseUnaryLayer', + 'AddFloorLayer', + 'AddFullyConnectedLayer', + 'AddGatherLayer', + 'AddInputLayer', + 'AddInstanceNormalizationLayer', + 'AddLogSoftmaxLayer', + 'AddL2NormalizationLayer', + 'AddLstmLayer', + 'AddMaximumLayer', + 'AddMeanLayer', + 'AddMergeLayer', + 'AddMinimumLayer', + 'AddMultiplicationLayer', + 'AddNormalizationLayer', + 'AddOutputLayer', + 'AddPadLayer', + 'AddPermuteLayer', + 'AddPooling2dLayer', + 'AddPreluLayer', + 'AddQuantizeLayer', + 'AddQuantizedLstmLayer', + 'AddReshapeLayer', + 'AddResizeLayer', + 'AddSliceLayer', + 'AddSoftmaxLayer', + 'AddSpaceToBatchNdLayer', + 'AddSpaceToDepthLayer', + 'AddSplitterLayer', + 'AddStackLayer', + 'AddStandInLayer', + 'AddStridedSliceLayer', + 'AddSubtractionLayer', + 'AddSwitchLayer', + 'AddTransposeConvolution2dLayer' +]) +def test_network_method_exists(method): + assert getattr(ann.INetwork, method, None) + + +def test_fullyconnected_layer_optional_none(): + net = ann.INetwork() + layer = net.AddFullyConnectedLayer(fullyConnectedDescriptor=ann.FullyConnectedDescriptor(), + weights=ann.ConstTensor()) + + assert layer + + +def test_fullyconnected_layer_optional_provided(): + net = ann.INetwork() + layer = net.AddFullyConnectedLayer(fullyConnectedDescriptor=ann.FullyConnectedDescriptor(), + weights=ann.ConstTensor(), + biases=ann.ConstTensor()) + + assert layer + + +def test_fullyconnected_layer_all_args(): + net = ann.INetwork() + layer = net.AddFullyConnectedLayer(fullyConnectedDescriptor=ann.FullyConnectedDescriptor(), + weights=ann.ConstTensor(), + biases=ann.ConstTensor(), + name='NAME1') + + assert layer + assert 'NAME1' == layer.GetName() + + +def test_DepthwiseConvolution2d_layer_optional_none(): + net = ann.INetwork() + layer = net.AddDepthwiseConvolution2dLayer(convolution2dDescriptor=ann.DepthwiseConvolution2dDescriptor(), + weights=ann.ConstTensor()) + + assert layer + + +def test_DepthwiseConvolution2d_layer_optional_provided(): + net = ann.INetwork() + layer = net.AddDepthwiseConvolution2dLayer(convolution2dDescriptor=ann.DepthwiseConvolution2dDescriptor(), + weights=ann.ConstTensor(), + biases=ann.ConstTensor()) + + assert layer + + +def test_DepthwiseConvolution2d_layer_all_args(): + net = ann.INetwork() + layer = net.AddDepthwiseConvolution2dLayer(convolution2dDescriptor=ann.DepthwiseConvolution2dDescriptor(), + weights=ann.ConstTensor(), + biases=ann.ConstTensor(), + name='NAME1') + + assert layer + assert 'NAME1' == layer.GetName() + + +def test_Convolution2d_layer_optional_none(): + net = ann.INetwork() + layer = net.AddConvolution2dLayer(convolution2dDescriptor=ann.Convolution2dDescriptor(), + weights=ann.ConstTensor()) + + assert layer + + +def test_Convolution2d_layer_optional_provided(): + net = ann.INetwork() + layer = net.AddConvolution2dLayer(convolution2dDescriptor=ann.Convolution2dDescriptor(), + weights=ann.ConstTensor(), + biases=ann.ConstTensor()) + + assert layer + + +def test_Convolution2d_layer_all_args(): + net = ann.INetwork() + layer = net.AddConvolution2dLayer(convolution2dDescriptor=ann.Convolution2dDescriptor(), + weights=ann.ConstTensor(), + biases=ann.ConstTensor(), + name='NAME1') + + assert layer + assert 'NAME1' == layer.GetName() diff --git a/python/pyarmnn/test/test_onnx_parser.py b/python/pyarmnn/test/test_onnx_parser.py new file mode 100644 index 0000000000..99353a0959 --- /dev/null +++ b/python/pyarmnn/test/test_onnx_parser.py @@ -0,0 +1,111 @@ +# Copyright © 2020 Arm Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +import os + +import pytest +import pyarmnn as ann +import numpy as np +from typing import List + + +@pytest.fixture() +def parser(shared_data_folder): + """ + Parse and setup the test network to be used for the tests below + """ + + # create onnx parser + parser = ann.IOnnxParser() + + # path to model + path_to_model = os.path.join(shared_data_folder, 'mock_model.onnx') + + # parse onnx binary & create network + parser.CreateNetworkFromBinaryFile(path_to_model) + + yield parser + + +def test_onnx_parser_swig_destroy(): + assert ann.IOnnxParser.__swig_destroy__, "There is a swig python destructor defined" + assert ann.IOnnxParser.__swig_destroy__.__name__ == "delete_IOnnxParser" + + +def test_check_onnx_parser_swig_ownership(parser): + # Check to see that SWIG has ownership for parser. This instructs SWIG to take + # ownership of the return value. This allows the value to be automatically + # garbage-collected when it is no longer in use + assert parser.thisown + + +def test_onnx_parser_get_network_input_binding_info(parser): + input_binding_info = parser.GetNetworkInputBindingInfo("input") + + tensor = input_binding_info[1] + assert tensor.GetDataType() == 1 + assert tensor.GetNumDimensions() == 4 + assert tensor.GetNumElements() == 784 + assert tensor.GetQuantizationOffset() == 0 + assert tensor.GetQuantizationScale() == 0 + + +def test_onnx_parser_get_network_output_binding_info(parser): + output_binding_info = parser.GetNetworkOutputBindingInfo("output") + + tensor = output_binding_info[1] + assert tensor.GetDataType() == 1 + assert tensor.GetNumDimensions() == 4 + assert tensor.GetNumElements() == 10 + assert tensor.GetQuantizationOffset() == 0 + assert tensor.GetQuantizationScale() == 0 + + +def test_onnx_filenotfound_exception(shared_data_folder): + parser = ann.IOnnxParser() + + # path to model + path_to_model = os.path.join(shared_data_folder, 'some_unknown_model.onnx') + + # parse onnx binary & create network + + with pytest.raises(RuntimeError) as err: + parser.CreateNetworkFromBinaryFile(path_to_model) + + # Only check for part of the exception since the exception returns + # absolute path which will change on different machines. + assert 'Invalid (null) filename' in str(err.value) + + +def test_onnx_parser_end_to_end(shared_data_folder): + parser = ann.IOnnxParser = ann.IOnnxParser() + + network = parser.CreateNetworkFromBinaryFile(os.path.join(shared_data_folder, 'mock_model.onnx')) + + # load test image data stored in input_onnx.npy + input_binding_info = parser.GetNetworkInputBindingInfo("input") + input_tensor_data = np.load(os.path.join(shared_data_folder, 'onnx_parser/input_onnx.npy')).astype(np.float32) + + options = ann.CreationOptions() + runtime = ann.IRuntime(options) + + preferred_backends = [ann.BackendId('CpuAcc'), ann.BackendId('CpuRef')] + opt_network, messages = ann.Optimize(network, preferred_backends, runtime.GetDeviceSpec(), ann.OptimizerOptions()) + + assert 0 == len(messages) + + net_id, messages = runtime.LoadNetwork(opt_network) + + assert "" == messages + + input_tensors = ann.make_input_tensors([input_binding_info], [input_tensor_data]) + output_tensors = ann.make_output_tensors([parser.GetNetworkOutputBindingInfo("output")]) + + runtime.EnqueueWorkload(net_id, input_tensors, output_tensors) + + output = ann.workload_tensors_to_ndarray(output_tensors) + + # Load golden output file for result comparison. + golden_output = np.load(os.path.join(shared_data_folder, 'onnx_parser/golden_output_onnx.npy')) + + # Check that output matches golden output to 4 decimal places (there are slight rounding differences after this) + np.testing.assert_almost_equal(output[0], golden_output, decimal=4) diff --git a/python/pyarmnn/test/test_profiling_utilities.py b/python/pyarmnn/test/test_profiling_utilities.py new file mode 100644 index 0000000000..b7b91b5613 --- /dev/null +++ b/python/pyarmnn/test/test_profiling_utilities.py @@ -0,0 +1,68 @@ +# Copyright © 2020 Arm Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +import os + +import pytest + +import pyarmnn as ann + + +class MockIProfiler: + def __init__(self, json_string): + self._profile_json = json_string + + def as_json(self): + return self._profile_json + + +@pytest.fixture() +def mock_profiler(shared_data_folder): + path_to_file = os.path.join(shared_data_folder, 'mock_profile_out.json') + with open(path_to_file, 'r') as file: + profiler_output = file.read() + return MockIProfiler(profiler_output) + + +def test_inference_exec(mock_profiler): + profiling_data_obj = ann.get_profiling_data(mock_profiler) + + assert (len(profiling_data_obj.inference_data) > 0) + assert (len(profiling_data_obj.per_workload_execution_data) > 0) + + # Check each total execution time + assert (profiling_data_obj.inference_data["execution_time"] == [1.1, 2.2, 3.3, 4.4, 5.5, 6.6]) + assert (profiling_data_obj.inference_data["time_unit"] == "us") + + +@pytest.mark.parametrize("exec_times, unit, backend, workload", [([2, 2, + 2, 2, + 2, 2], + 'us', + 'CpuRef', + 'RefSomeMock1dWorkload_Execute_#5'), + ([2, 2, + 2, 2, + 2, 2], + 'us', + 'CpuAcc', + 'NeonSomeMock2Workload_Execute_#6'), + ([2, 2, + 2, 2, + 2, 2], + 'us', + 'GpuAcc', + 'ClSomeMock3dWorkload_Execute_#7'), + ([2, 2, + 2, 2, + 2, 2], + 'us', + 'EthosNAcc', + 'EthosNSomeMock4dWorkload_Execute_#8') + ]) +def test_profiler_workloads(mock_profiler, exec_times, unit, backend, workload): + profiling_data_obj = ann.get_profiling_data(mock_profiler) + + work_load_exec = profiling_data_obj.per_workload_execution_data[workload] + assert work_load_exec["execution_time"] == exec_times + assert work_load_exec["time_unit"] == unit + assert work_load_exec["backend"] == backend diff --git a/python/pyarmnn/test/test_quantize_and_dequantize.py b/python/pyarmnn/test/test_quantize_and_dequantize.py new file mode 100644 index 0000000000..08fea39eda --- /dev/null +++ b/python/pyarmnn/test/test_quantize_and_dequantize.py @@ -0,0 +1,91 @@ +# Copyright © 2020 Arm Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +import pytest +import numpy as np + +import pyarmnn as ann + +# import generated so we can test for Dequantize_* and Quantize_* +# functions not available in the public API. +import pyarmnn._generated.pyarmnn as gen_ann + + +@pytest.mark.parametrize('method', ['Quantize_int8_t', + 'Quantize_uint8_t', + 'Quantize_int16_t', + 'Quantize_int32_t', + 'Dequantize_int8_t', + 'Dequantize_uint8_t', + 'Dequantize_int16_t', + 'Dequantize_int32_t']) +def test_quantize_exists(method): + assert method in dir(gen_ann) and callable(getattr(gen_ann, method)) + + +@pytest.mark.parametrize('dt, min, max', [('uint8', 0, 255), + ('int8', -128, 127), + ('int16', -32768, 32767), + ('int32', -2147483648, 2147483647)]) +def test_quantize_uint8_output(dt, min, max): + result = ann.quantize(3.3274056911468506, 0.02620004490017891, 128, dt) + assert type(result) is int and min <= result <= max + + +@pytest.mark.parametrize('dt', ['uint8', + 'int8', + 'int16', + 'int32']) +def test_dequantize_uint8_output(dt): + result = ann.dequantize(3, 0.02620004490017891, 128, dt) + assert type(result) is float + + +def test_quantize_unsupported_dtype(): + with pytest.raises(ValueError) as err: + ann.quantize(3.3274056911468506, 0.02620004490017891, 128, 'uint16') + + assert 'Unexpected target datatype uint16 given.' in str(err.value) + + +def test_dequantize_unsupported_dtype(): + with pytest.raises(ValueError) as err: + ann.dequantize(3, 0.02620004490017891, 128, 'uint16') + + assert 'Unexpected value datatype uint16 given.' in str(err.value) + + +def test_dequantize_value_range(): + with pytest.raises(ValueError) as err: + ann.dequantize(-1, 0.02620004490017891, 128, 'uint8') + + assert 'Value is not within range of the given datatype uint8' in str(err.value) + + +@pytest.mark.parametrize('dt, data', [('uint8', np.uint8(255)), + ('int8', np.int8(127)), + ('int16', np.int16(32767)), + ('int32', np.int32(2147483647)), + + ('uint8', np.int8(127)), + ('uint8', np.int16(255)), + ('uint8', np.int32(255)), + + ('int8', np.uint8(127)), + ('int8', np.int16(127)), + ('int8', np.int32(127)), + + ('int16', np.int8(127)), + ('int16', np.uint8(255)), + ('int16', np.int32(32767)), + + ('int32', np.uint8(255)), + ('int16', np.int8(127)), + ('int32', np.int16(32767)) + + ]) +def test_dequantize_numpy_dt(dt, data): + result = ann.dequantize(data, 1, 0, dt) + + assert type(result) is float + + assert np.float32(data) == result diff --git a/python/pyarmnn/test/test_runtime.py b/python/pyarmnn/test/test_runtime.py new file mode 100644 index 0000000000..2943be8f16 --- /dev/null +++ b/python/pyarmnn/test/test_runtime.py @@ -0,0 +1,257 @@ +# Copyright © 2020 Arm Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +import os + +import pytest +import numpy as np + +import pyarmnn as ann + + +@pytest.fixture(scope="function") +def random_runtime(shared_data_folder): + parser = ann.ITfLiteParser() + network = parser.CreateNetworkFromBinaryFile(os.path.join(shared_data_folder, 'mock_model.tflite')) + preferred_backends = [ann.BackendId('CpuRef')] + options = ann.CreationOptions() + runtime = ann.IRuntime(options) + + graphs_count = parser.GetSubgraphCount() + + graph_id = graphs_count - 1 + input_names = parser.GetSubgraphInputTensorNames(graph_id) + + input_binding_info = parser.GetNetworkInputBindingInfo(graph_id, input_names[0]) + input_tensor_id = input_binding_info[0] + + input_tensor_info = input_binding_info[1] + + output_names = parser.GetSubgraphOutputTensorNames(graph_id) + + input_data = np.random.randint(255, size=input_tensor_info.GetNumElements(), dtype=np.uint8) + + const_tensor_pair = (input_tensor_id, ann.ConstTensor(input_tensor_info, input_data)) + + input_tensors = [const_tensor_pair] + + output_tensors = [] + + for index, output_name in enumerate(output_names): + out_bind_info = parser.GetNetworkOutputBindingInfo(graph_id, output_name) + + out_tensor_info = out_bind_info[1] + out_tensor_id = out_bind_info[0] + + output_tensors.append((out_tensor_id, + ann.Tensor(out_tensor_info))) + + yield preferred_backends, network, runtime, input_tensors, output_tensors + + +@pytest.fixture(scope='function') +def mock_model_runtime(shared_data_folder): + parser = ann.ITfLiteParser() + network = parser.CreateNetworkFromBinaryFile(os.path.join(shared_data_folder, 'mock_model.tflite')) + graph_id = 0 + + input_binding_info = parser.GetNetworkInputBindingInfo(graph_id, "input_1") + + input_tensor_data = np.load(os.path.join(shared_data_folder, 'tflite_parser/input_lite.npy')) + + preferred_backends = [ann.BackendId('CpuRef')] + + options = ann.CreationOptions() + runtime = ann.IRuntime(options) + + opt_network, messages = ann.Optimize(network, preferred_backends, runtime.GetDeviceSpec(), ann.OptimizerOptions()) + + print(messages) + + net_id, messages = runtime.LoadNetwork(opt_network) + + print(messages) + + input_tensors = ann.make_input_tensors([input_binding_info], [input_tensor_data]) + + output_names = parser.GetSubgraphOutputTensorNames(graph_id) + outputs_binding_info = [] + + for output_name in output_names: + outputs_binding_info.append(parser.GetNetworkOutputBindingInfo(graph_id, output_name)) + + output_tensors = ann.make_output_tensors(outputs_binding_info) + + yield runtime, net_id, input_tensors, output_tensors + + +def test_python_disowns_network(random_runtime): + preferred_backends = random_runtime[0] + network = random_runtime[1] + runtime = random_runtime[2] + opt_network, _ = ann.Optimize(network, preferred_backends, + runtime.GetDeviceSpec(), ann.OptimizerOptions()) + + runtime.LoadNetwork(opt_network) + + assert not opt_network.thisown + + +def test_load_network(random_runtime): + preferred_backends = random_runtime[0] + network = random_runtime[1] + runtime = random_runtime[2] + + opt_network, _ = ann.Optimize(network, preferred_backends, + runtime.GetDeviceSpec(), ann.OptimizerOptions()) + + net_id, messages = runtime.LoadNetwork(opt_network) + assert "" == messages + assert net_id == 0 + + +def test_load_network_properties_provided(random_runtime): + preferred_backends = random_runtime[0] + network = random_runtime[1] + runtime = random_runtime[2] + + opt_network, _ = ann.Optimize(network, preferred_backends, + runtime.GetDeviceSpec(), ann.OptimizerOptions()) + + properties = ann.INetworkProperties(True, True) + net_id, messages = runtime.LoadNetwork(opt_network, properties) + assert "" == messages + assert net_id == 0 + + +def test_unload_network_fails_for_invalid_net_id(random_runtime): + preferred_backends = random_runtime[0] + network = random_runtime[1] + runtime = random_runtime[2] + + ann.Optimize(network, preferred_backends, runtime.GetDeviceSpec(), ann.OptimizerOptions()) + + with pytest.raises(RuntimeError) as err: + runtime.UnloadNetwork(9) + + expected_error_message = "Failed to unload network." + assert expected_error_message in str(err.value) + + +def test_enqueue_workload(random_runtime): + preferred_backends = random_runtime[0] + network = random_runtime[1] + runtime = random_runtime[2] + input_tensors = random_runtime[3] + output_tensors = random_runtime[4] + + opt_network, _ = ann.Optimize(network, preferred_backends, + runtime.GetDeviceSpec(), ann.OptimizerOptions()) + + net_id, _ = runtime.LoadNetwork(opt_network) + runtime.EnqueueWorkload(net_id, input_tensors, output_tensors) + + +def test_enqueue_workload_fails_with_empty_input_tensors(random_runtime): + preferred_backends = random_runtime[0] + network = random_runtime[1] + runtime = random_runtime[2] + input_tensors = [] + output_tensors = random_runtime[4] + + opt_network, _ = ann.Optimize(network, preferred_backends, + runtime.GetDeviceSpec(), ann.OptimizerOptions()) + + net_id, _ = runtime.LoadNetwork(opt_network) + with pytest.raises(RuntimeError) as err: + runtime.EnqueueWorkload(net_id, input_tensors, output_tensors) + + expected_error_message = "Number of inputs provided does not match network." + assert expected_error_message in str(err.value) + + +@pytest.mark.x86_64 +@pytest.mark.parametrize('count', [5]) +def test_multiple_inference_runs_yield_same_result(count, mock_model_runtime): + """ + Test that results remain consistent among multiple runs of the same inference. + """ + runtime = mock_model_runtime[0] + net_id = mock_model_runtime[1] + input_tensors = mock_model_runtime[2] + output_tensors = mock_model_runtime[3] + + expected_results = np.array([[4, 85, 108, 29, 8, 16, 0, 2, 5, 0]]) + + for _ in range(count): + runtime.EnqueueWorkload(net_id, input_tensors, output_tensors) + + output_vectors = ann.workload_tensors_to_ndarray(output_tensors) + + for i in range(len(expected_results)): + assert output_vectors[i].all() == expected_results[i].all() + + +@pytest.mark.aarch64 +def test_aarch64_inference_results(mock_model_runtime): + + runtime = mock_model_runtime[0] + net_id = mock_model_runtime[1] + input_tensors = mock_model_runtime[2] + output_tensors = mock_model_runtime[3] + + runtime.EnqueueWorkload(net_id, input_tensors, output_tensors) + + output_vectors = ann.workload_tensors_to_ndarray(output_tensors) + + expected_outputs = expected_results = np.array([[4, 85, 108, 29, 8, 16, 0, 2, 5, 0]]) + + for i in range(len(expected_outputs)): + assert output_vectors[i].all() == expected_results[i].all() + + +def test_enqueue_workload_with_profiler(random_runtime): + """ + Tests ArmNN's profiling extension + """ + preferred_backends = random_runtime[0] + network = random_runtime[1] + runtime = random_runtime[2] + input_tensors = random_runtime[3] + output_tensors = random_runtime[4] + + opt_network, _ = ann.Optimize(network, preferred_backends, + runtime.GetDeviceSpec(), ann.OptimizerOptions()) + net_id, _ = runtime.LoadNetwork(opt_network) + + profiler = runtime.GetProfiler(net_id) + # By default profiling should be turned off: + assert profiler.IsProfilingEnabled() is False + + # Enable profiling: + profiler.EnableProfiling(True) + assert profiler.IsProfilingEnabled() is True + + # Run the inference: + runtime.EnqueueWorkload(net_id, input_tensors, output_tensors) + + # Get profile output as a string: + str_profile = profiler.as_json() + + # Verify that certain markers are present: + assert len(str_profile) != 0 + assert str_profile.find('\"ArmNN\": {') > 0 + + # Get events analysis output as a string: + str_events_analysis = profiler.event_log() + + assert "Event Sequence - Name | Duration (ms) | Start (ms) | Stop (ms) | Device" in str_events_analysis + + assert profiler.thisown == 0 + + +def test_check_runtime_swig_ownership(random_runtime): + # Check to see that SWIG has ownership for runtime. This instructs SWIG to take + # ownership of the return value. This allows the value to be automatically + # garbage-collected when it is no longer in use + runtime = random_runtime[2] + assert runtime.thisown diff --git a/python/pyarmnn/test/test_setup.py b/python/pyarmnn/test/test_setup.py new file mode 100644 index 0000000000..8396ca0587 --- /dev/null +++ b/python/pyarmnn/test/test_setup.py @@ -0,0 +1,100 @@ +# Copyright © 2020 Arm Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +import os +import sys +import shutil + +import pytest + +sys.path.append(os.path.abspath('..')) +from setup import find_armnn, find_includes, linux_gcc_lib_search, check_armnn_version + + +@pytest.fixture(autouse=True) +def _setup_armnn(tmpdir): + includes = str(os.path.join(tmpdir, 'include')) + libs = str(os.path.join(tmpdir, 'lib')) + os.environ["TEST_ARMNN_INCLUDE"] = includes + os.environ["TEST_ARMNN_LIB"] = libs + os.environ["EMPTY_ARMNN_INCLUDE"] = '' + + os.mkdir(includes) + os.mkdir(libs) + + with open(os.path.join(libs, "libarmnn.so"), "w"): + pass + + with open(os.path.join(libs, "libarmnnSomeThing1.so"), "w"): + pass + with open(os.path.join(libs, "libarmnnSomeThing1.so.1"), "w"): + pass + with open(os.path.join(libs, "libarmnnSomeThing1.so.1.2"), "w"): + pass + + with open(os.path.join(libs, "libarmnnSomeThing2.so"), "w"): + pass + + with open(os.path.join(libs, "libSomeThing3.so"), "w"): + pass + + yield + + del os.environ["TEST_ARMNN_INCLUDE"] + del os.environ["TEST_ARMNN_LIB"] + del os.environ["EMPTY_ARMNN_INCLUDE"] + shutil.rmtree(includes) + shutil.rmtree(libs) + + +def test_find_armnn(tmpdir): + lib_names, lib_paths = find_armnn(lib_name='libarmnn*.so', + armnn_libs_env="TEST_ARMNN_LIB", + default_lib_search=("/lib",)) + armnn_includes = find_includes(armnn_include_env="TEST_ARMNN_INCLUDE") + + assert [':libarmnn.so', ':libarmnnSomeThing1.so', ':libarmnnSomeThing2.so'] == sorted(lib_names) + assert [os.path.join(tmpdir, 'lib')] == lib_paths + assert [os.path.join(tmpdir, 'include')] == armnn_includes + + +def test_find_armnn_default_path(tmpdir): + lib_names, lib_paths = find_armnn(lib_name='libarmnn*.so', + armnn_libs_env="RUBBISH_LIB", + default_lib_search=(os.environ["TEST_ARMNN_LIB"],)) + armnn_includes = find_includes('TEST_ARMNN_INCLUDE') + assert [':libarmnn.so', ':libarmnnSomeThing1.so', ':libarmnnSomeThing2.so'] == sorted(lib_names) + assert [os.path.join(tmpdir, 'lib')] == lib_paths + assert [os.path.join(tmpdir, 'include')] == armnn_includes + + +def test_not_find_armnn(tmpdir): + with pytest.raises(RuntimeError) as err: + find_armnn(lib_name='libarmnn*.so', armnn_libs_env="RUBBISH_LIB", + default_lib_search=("/lib",)) + + assert 'ArmNN library libarmnn*.so was not found in (\'/lib\',)' in str(err.value) + + +@pytest.mark.parametrize("env", ["RUBBISH_INCLUDE", "EMPTY_ARMNN_INCLUDE"]) +def test_rubbish_armnn_include(tmpdir, env): + includes = find_includes(armnn_include_env=env) + assert includes == ['/usr/local/include', '/usr/include'] + + +def test_gcc_serch_path(): + assert linux_gcc_lib_search() + + +def test_armnn_version(): + check_armnn_version('20190800', '20190800') + + +def test_incorrect_armnn_version(): + with pytest.raises(AssertionError) as err: + check_armnn_version('20190800', '20190500') + + assert 'Expected ArmNN version is 201905 but installed ArmNN version is 201908' in str(err.value) + + +def test_armnn_version_patch_does_not_matter(): + check_armnn_version('20190800', '20190801') diff --git a/python/pyarmnn/test/test_supported_backends.py b/python/pyarmnn/test/test_supported_backends.py new file mode 100644 index 0000000000..e1ca5ee6df --- /dev/null +++ b/python/pyarmnn/test/test_supported_backends.py @@ -0,0 +1,50 @@ +# Copyright © 2020 Arm Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +import os +import platform +import pytest +import pyarmnn as ann + + +@pytest.fixture() +def get_supported_backends_setup(shared_data_folder): + options = ann.CreationOptions() + runtime = ann.IRuntime(options) + + get_device_spec = runtime.GetDeviceSpec() + supported_backends = get_device_spec.GetSupportedBackends() + + yield supported_backends + + +def test_ownership(): + options = ann.CreationOptions() + runtime = ann.IRuntime(options) + + device_spec = runtime.GetDeviceSpec() + + assert not device_spec.thisown + + +def test_to_string(): + options = ann.CreationOptions() + runtime = ann.IRuntime(options) + + device_spec = runtime.GetDeviceSpec() + expected_str = "IDeviceSpec {{ supportedBackends: [" \ + "{}" \ + "]}}".format(', '.join(map(lambda b: str(b), device_spec.GetSupportedBackends()))) + + assert expected_str == str(device_spec) + + +def test_get_supported_backends_cpu_ref(get_supported_backends_setup): + assert "CpuRef" in map(lambda b: str(b), get_supported_backends_setup) + + +@pytest.mark.aarch64 +class TestNoneCpuRefBackends: + + @pytest.mark.parametrize("backend", ["CpuAcc"]) + def test_get_supported_backends_cpu_acc(self, get_supported_backends_setup, backend): + assert backend in map(lambda b: str(b), get_supported_backends_setup) diff --git a/python/pyarmnn/test/test_tensor.py b/python/pyarmnn/test/test_tensor.py new file mode 100644 index 0000000000..8b57169596 --- /dev/null +++ b/python/pyarmnn/test/test_tensor.py @@ -0,0 +1,144 @@ +# Copyright © 2020 Arm Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +from copy import copy + +import pytest +import numpy as np +import pyarmnn as ann + + +def __get_tensor_info(dt): + tensor_info = ann.TensorInfo(ann.TensorShape((2, 3)), dt) + + return tensor_info + + +@pytest.mark.parametrize("dt", [ann.DataType_Float32, ann.DataType_Float16, + ann.DataType_QAsymmU8, ann.DataType_QSymmS8, + ann.DataType_QAsymmS8]) +def test_create_tensor_with_info(dt): + tensor_info = __get_tensor_info(dt) + elements = tensor_info.GetNumElements() + num_bytes = tensor_info.GetNumBytes() + d_type = dt + + tensor = ann.Tensor(tensor_info) + + assert tensor_info != tensor.GetInfo(), "Different objects" + assert elements == tensor.GetNumElements() + assert num_bytes == tensor.GetNumBytes() + assert d_type == tensor.GetDataType() + + +def test_create_tensor_undefined_datatype(): + tensor_info = ann.TensorInfo() + tensor_info.SetDataType(99) + + with pytest.raises(ValueError) as err: + ann.Tensor(tensor_info) + + assert 'The data type provided for this Tensor is not supported.' in str(err.value) + + +@pytest.mark.parametrize("dt", [ann.DataType_Float32]) +def test_tensor_memory_output(dt): + tensor_info = __get_tensor_info(dt) + tensor = ann.Tensor(tensor_info) + + # empty memory area because inference has not yet been run. + assert tensor.get_memory_area().tolist() # has random stuff + assert 4 == tensor.get_memory_area().itemsize, "it is float32" + + +@pytest.mark.parametrize("dt", [ann.DataType_Float32, ann.DataType_Float16, + ann.DataType_QAsymmU8, ann.DataType_QSymmS8, + ann.DataType_QAsymmS8]) +def test_tensor__str__(dt): + tensor_info = __get_tensor_info(dt) + elements = tensor_info.GetNumElements() + num_bytes = tensor_info.GetNumBytes() + d_type = dt + dimensions = tensor_info.GetNumDimensions() + + tensor = ann.Tensor(tensor_info) + + assert str(tensor) == "Tensor{{DataType: {}, NumBytes: {}, NumDimensions: " \ + "{}, NumElements: {}}}".format(d_type, num_bytes, dimensions, elements) + + +def test_create_empty_tensor(): + tensor = ann.Tensor() + + assert 0 == tensor.GetNumElements() + assert 0 == tensor.GetNumBytes() + assert tensor.get_memory_area() is None + + +@pytest.mark.parametrize("dt", [ann.DataType_Float32, ann.DataType_Float16, + ann.DataType_QAsymmU8, ann.DataType_QSymmS8, + ann.DataType_QAsymmS8]) +def test_create_tensor_from_tensor(dt): + tensor_info = __get_tensor_info(dt) + tensor = ann.Tensor(tensor_info) + copied_tensor = ann.Tensor(tensor) + + assert copied_tensor != tensor, "Different objects" + assert copied_tensor.GetInfo() != tensor.GetInfo(), "Different objects" + assert copied_tensor.get_memory_area().ctypes.data == tensor.get_memory_area().ctypes.data, "Same memory area" + assert copied_tensor.GetNumElements() == tensor.GetNumElements() + assert copied_tensor.GetNumBytes() == tensor.GetNumBytes() + assert copied_tensor.GetDataType() == tensor.GetDataType() + + +@pytest.mark.parametrize("dt", [ann.DataType_Float32, ann.DataType_Float16, + ann.DataType_QAsymmU8, ann.DataType_QSymmS8, + ann.DataType_QAsymmS8]) +def test_copy_tensor(dt): + tensor = ann.Tensor(__get_tensor_info(dt)) + copied_tensor = copy(tensor) + + assert copied_tensor != tensor, "Different objects" + assert copied_tensor.GetInfo() != tensor.GetInfo(), "Different objects" + assert copied_tensor.get_memory_area().ctypes.data == tensor.get_memory_area().ctypes.data, "Same memory area" + assert copied_tensor.GetNumElements() == tensor.GetNumElements() + assert copied_tensor.GetNumBytes() == tensor.GetNumBytes() + assert copied_tensor.GetDataType() == tensor.GetDataType() + + +@pytest.mark.parametrize("dt", [ann.DataType_Float32, ann.DataType_Float16, + ann.DataType_QAsymmU8, ann.DataType_QSymmS8, + ann.DataType_QAsymmS8]) +def test_copied_tensor_has_memory_area_access_after_deletion_of_original_tensor(dt): + + tensor = ann.Tensor(__get_tensor_info(dt)) + + tensor.get_memory_area()[0] = 100 + + initial_mem_copy = np.array(tensor.get_memory_area()) + + assert 100 == initial_mem_copy[0] + + copied_tensor = ann.Tensor(tensor) + + del tensor + np.testing.assert_array_equal(copied_tensor.get_memory_area(), initial_mem_copy) + assert 100 == copied_tensor.get_memory_area()[0] + + +def test_create_const_tensor_incorrect_args(): + with pytest.raises(ValueError) as err: + ann.Tensor('something', 'something') + + expected_error_message = "Incorrect number of arguments or type of arguments provided to create Tensor." + assert expected_error_message in str(err.value) + + +@pytest.mark.parametrize("dt", [ann.DataType_Float16]) +def test_tensor_memory_output_fp16(dt): + # Check Tensor with float16 + tensor_info = __get_tensor_info(dt) + tensor = ann.Tensor(tensor_info) + + assert tensor.GetNumElements() == 6 + assert tensor.GetNumBytes() == 12 + assert tensor.GetDataType() == ann.DataType_Float16 diff --git a/python/pyarmnn/test/test_tensor_conversion.py b/python/pyarmnn/test/test_tensor_conversion.py new file mode 100644 index 0000000000..a48b00f431 --- /dev/null +++ b/python/pyarmnn/test/test_tensor_conversion.py @@ -0,0 +1,99 @@ +# Copyright © 2020 Arm Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +import os + +import pytest +import pyarmnn as ann +import numpy as np + + +@pytest.fixture(scope="function") +def get_tensor_info_input(shared_data_folder): + """ + Sample input tensor information. + """ + parser = ann.ITfLiteParser() + parser.CreateNetworkFromBinaryFile(os.path.join(shared_data_folder, 'mock_model.tflite')) + graph_id = 0 + + input_binding_info = [parser.GetNetworkInputBindingInfo(graph_id, 'input_1')] + + yield input_binding_info + + +@pytest.fixture(scope="function") +def get_tensor_info_output(shared_data_folder): + """ + Sample output tensor information. + """ + parser = ann.ITfLiteParser() + parser.CreateNetworkFromBinaryFile(os.path.join(shared_data_folder, 'mock_model.tflite')) + graph_id = 0 + + output_names = parser.GetSubgraphOutputTensorNames(graph_id) + outputs_binding_info = [] + + for output_name in output_names: + outputs_binding_info.append(parser.GetNetworkOutputBindingInfo(graph_id, output_name)) + + yield outputs_binding_info + + +def test_make_input_tensors(get_tensor_info_input): + input_tensor_info = get_tensor_info_input + input_data = [] + + for tensor_id, tensor_info in input_tensor_info: + input_data.append(np.random.randint(0, 255, size=(1, tensor_info.GetNumElements())).astype(np.uint8)) + + input_tensors = ann.make_input_tensors(input_tensor_info, input_data) + assert len(input_tensors) == 1 + + for tensor, tensor_info in zip(input_tensors, input_tensor_info): + # Because we created ConstTensor function, we cannot check type directly. + assert type(tensor[1]).__name__ == 'ConstTensor' + assert str(tensor[1].GetInfo()) == str(tensor_info[1]) + + +def test_make_output_tensors(get_tensor_info_output): + output_binding_info = get_tensor_info_output + + output_tensors = ann.make_output_tensors(output_binding_info) + assert len(output_tensors) == 1 + + for tensor, tensor_info in zip(output_tensors, output_binding_info): + assert type(tensor[1]) == ann.Tensor + assert str(tensor[1].GetInfo()) == str(tensor_info[1]) + + +def test_workload_tensors_to_ndarray(get_tensor_info_output): + # Check shape and size of output from workload_tensors_to_ndarray matches expected. + output_binding_info = get_tensor_info_output + output_tensors = ann.make_output_tensors(output_binding_info) + + data = ann.workload_tensors_to_ndarray(output_tensors) + + for i in range(0, len(output_tensors)): + assert data[i].shape == tuple(output_tensors[i][1].GetShape()) + assert data[i].size == output_tensors[i][1].GetNumElements() + + +def test_make_input_tensors_fp16(get_tensor_info_input): + # Check ConstTensor with float16 + input_tensor_info = get_tensor_info_input + input_data = [] + + for tensor_id, tensor_info in input_tensor_info: + input_data.append(np.random.randint(0, 255, size=(1, tensor_info.GetNumElements())).astype(np.float16)) + tensor_info.SetDataType(ann.DataType_Float16) # set datatype to float16 + + input_tensors = ann.make_input_tensors(input_tensor_info, input_data) + assert len(input_tensors) == 1 + + for tensor, tensor_info in zip(input_tensors, input_tensor_info): + # Because we created ConstTensor function, we cannot check type directly. + assert type(tensor[1]).__name__ == 'ConstTensor' + assert str(tensor[1].GetInfo()) == str(tensor_info[1]) + assert tensor[1].GetDataType() == ann.DataType_Float16 + assert tensor[1].GetNumElements() == 28*28*1 + assert tensor[1].GetNumBytes() == (28*28*1)*2 # check each element is two byte diff --git a/python/pyarmnn/test/test_tensor_info.py b/python/pyarmnn/test/test_tensor_info.py new file mode 100644 index 0000000000..dc73533869 --- /dev/null +++ b/python/pyarmnn/test/test_tensor_info.py @@ -0,0 +1,27 @@ +# Copyright © 2020 Arm Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +import pyarmnn as ann + + +def test_tensor_info_ctor_shape(): + tensor_shape = ann.TensorShape((1, 1, 2)) + + tensor_info = ann.TensorInfo(tensor_shape, ann.DataType_QAsymmU8, 0.5, 1) + + assert 2 == tensor_info.GetNumElements() + assert 3 == tensor_info.GetNumDimensions() + assert ann.DataType_QAsymmU8 == tensor_info.GetDataType() + assert 0.5 == tensor_info.GetQuantizationScale() + assert 1 == tensor_info.GetQuantizationOffset() + + shape = tensor_info.GetShape() + + assert 2 == shape.GetNumElements() + assert 3 == shape.GetNumDimensions() + + +def test_tensor_info__str__(): + tensor_info = ann.TensorInfo(ann.TensorShape((2, 3)), ann.DataType_QAsymmU8, 0.5, 1) + + assert tensor_info.__str__() == "TensorInfo{DataType: 2, IsQuantized: 1, QuantizationScale: 0.500000, " \ + "QuantizationOffset: 1, NumDimensions: 2, NumElements: 6}" diff --git a/python/pyarmnn/test/test_tensor_shape.py b/python/pyarmnn/test/test_tensor_shape.py new file mode 100644 index 0000000000..c6f731fb43 --- /dev/null +++ b/python/pyarmnn/test/test_tensor_shape.py @@ -0,0 +1,78 @@ +# Copyright © 2020 Arm Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +import pytest +import pyarmnn as ann + + +def test_tensor_shape_tuple(): + tensor_shape = ann.TensorShape((1, 2, 3)) + + assert 3 == tensor_shape.GetNumDimensions() + assert 6 == tensor_shape.GetNumElements() + + +def test_tensor_shape_one(): + tensor_shape = ann.TensorShape((10,)) + assert 1 == tensor_shape.GetNumDimensions() + assert 10 == tensor_shape.GetNumElements() + + +def test_tensor_shape_empty(): + with pytest.raises(RuntimeError) as err: + ann.TensorShape(()) + + assert "Tensor numDimensions must be greater than 0" in str(err.value) + + +def test_tensor_shape_tuple_mess(): + tensor_shape = ann.TensorShape((1, "2", 3.0)) + + assert 3 == tensor_shape.GetNumDimensions() + assert 6 == tensor_shape.GetNumElements() + + +def test_tensor_shape_list(): + + with pytest.raises(TypeError) as err: + ann.TensorShape([1, 2, 3]) + + assert "Argument is not a tuple" in str(err.value) + + +def test_tensor_shape_tuple_mess_fail(): + + with pytest.raises(TypeError) as err: + ann.TensorShape((1, "two", 3.0)) + + assert "All elements must be numbers" in str(err.value) + + +def test_tensor_shape_varags(): + with pytest.raises(TypeError) as err: + ann.TensorShape(1, 2, 3) + + assert "__init__() takes 2 positional arguments but 4 were given" in str(err.value) + + +def test_tensor_shape__get_item_out_of_bounds(): + tensor_shape = ann.TensorShape((1, 2, 3)) + with pytest.raises(ValueError) as err: + for i in range(4): + tensor_shape[i] + + assert "Invalid dimension index: 3 (number of dimensions is 3)" in str(err.value) + + +def test_tensor_shape__set_item_out_of_bounds(): + tensor_shape = ann.TensorShape((1, 2, 3)) + with pytest.raises(ValueError) as err: + for i in range(4): + tensor_shape[i] = 1 + + assert "Invalid dimension index: 3 (number of dimensions is 3)" in str(err.value) + + +def test_tensor_shape___str__(): + tensor_shape = ann.TensorShape((1, 2, 3)) + + assert str(tensor_shape) == "TensorShape{Shape(1, 2, 3), NumDimensions: 3, NumElements: 6}" diff --git a/python/pyarmnn/test/test_tf_parser.py b/python/pyarmnn/test/test_tf_parser.py new file mode 100644 index 0000000000..796dd71e7b --- /dev/null +++ b/python/pyarmnn/test/test_tf_parser.py @@ -0,0 +1,133 @@ +# Copyright © 2020 Arm Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +import os + +import pytest +import pyarmnn as ann +import numpy as np + + +@pytest.fixture() +def parser(shared_data_folder): + """ + Parse and setup the test network to be used for the tests below + """ + + # create tf parser + parser = ann.ITfParser() + + # path to model + path_to_model = os.path.join(shared_data_folder, 'mock_model.pb') + + # tensor shape [1, 28, 28, 1] + tensorshape = {'input': ann.TensorShape((1, 28, 28, 1))} + + # requested_outputs + requested_outputs = ["output"] + + # parse tf binary & create network + parser.CreateNetworkFromBinaryFile(path_to_model, tensorshape, requested_outputs) + + yield parser + + +def test_tf_parser_swig_destroy(): + assert ann.ITfParser.__swig_destroy__, "There is a swig python destructor defined" + assert ann.ITfParser.__swig_destroy__.__name__ == "delete_ITfParser" + + +def test_check_tf_parser_swig_ownership(parser): + # Check to see that SWIG has ownership for parser. This instructs SWIG to take + # ownership of the return value. This allows the value to be automatically + # garbage-collected when it is no longer in use + assert parser.thisown + + +def test_tf_parser_get_network_input_binding_info(parser): + input_binding_info = parser.GetNetworkInputBindingInfo("input") + + tensor = input_binding_info[1] + assert tensor.GetDataType() == 1 + assert tensor.GetNumDimensions() == 4 + assert tensor.GetNumElements() == 28*28*1 + assert tensor.GetQuantizationOffset() == 0 + assert tensor.GetQuantizationScale() == 0 + + +def test_tf_parser_get_network_output_binding_info(parser): + output_binding_info = parser.GetNetworkOutputBindingInfo("output") + + tensor = output_binding_info[1] + assert tensor.GetDataType() == 1 + assert tensor.GetNumDimensions() == 2 + assert tensor.GetNumElements() == 10 + assert tensor.GetQuantizationOffset() == 0 + assert tensor.GetQuantizationScale() == 0 + + +def test_tf_filenotfound_exception(shared_data_folder): + parser = ann.ITfParser() + + # path to model + path_to_model = os.path.join(shared_data_folder, 'some_unknown_model.pb') + + # tensor shape [1, 1, 1, 1] + tensorshape = {'input': ann.TensorShape((1, 1, 1, 1))} + + # requested_outputs + requested_outputs = [""] + + # parse tf binary & create network + + with pytest.raises(RuntimeError) as err: + parser.CreateNetworkFromBinaryFile(path_to_model, tensorshape, requested_outputs) + + # Only check for part of the exception since the exception returns + # absolute path which will change on different machines. + assert 'failed to open' in str(err.value) + + +def test_tf_parser_end_to_end(shared_data_folder): + parser = ann.ITfParser = ann.ITfParser() + + tensorshape = {'input': ann.TensorShape((1, 28, 28, 1))} + requested_outputs = ["output"] + + network = parser.CreateNetworkFromBinaryFile(os.path.join(shared_data_folder, 'mock_model.pb'), + tensorshape, requested_outputs) + + input_binding_info = parser.GetNetworkInputBindingInfo("input") + + # load test image data stored in input_tf.npy + input_tensor_data = np.load(os.path.join(shared_data_folder, 'tf_parser/input_tf.npy')).astype(np.float32) + + preferred_backends = [ann.BackendId('CpuAcc'), ann.BackendId('CpuRef')] + + options = ann.CreationOptions() + runtime = ann.IRuntime(options) + + opt_network, messages = ann.Optimize(network, preferred_backends, runtime.GetDeviceSpec(), ann.OptimizerOptions()) + + assert 0 == len(messages) + + net_id, messages = runtime.LoadNetwork(opt_network) + + assert "" == messages + + input_tensors = ann.make_input_tensors([input_binding_info], [input_tensor_data]) + + outputs_binding_info = [] + + for output_name in requested_outputs: + outputs_binding_info.append(parser.GetNetworkOutputBindingInfo(output_name)) + + output_tensors = ann.make_output_tensors(outputs_binding_info) + + runtime.EnqueueWorkload(net_id, input_tensors, output_tensors) + output_vectors = ann.workload_tensors_to_ndarray(output_tensors) + + # Load golden output file for result comparison. + golden_output = np.load(os.path.join(shared_data_folder, 'tf_parser/golden_output_tf.npy')) + + # Check that output matches golden output to 4 decimal places (there are slight rounding differences after this) + np.testing.assert_almost_equal(output_vectors[0], golden_output, decimal=4) diff --git a/python/pyarmnn/test/test_tflite_parser.py b/python/pyarmnn/test/test_tflite_parser.py new file mode 100644 index 0000000000..344ec7ca13 --- /dev/null +++ b/python/pyarmnn/test/test_tflite_parser.py @@ -0,0 +1,147 @@ +# Copyright © 2020 Arm Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +import os + +import pytest +import pyarmnn as ann +import numpy as np + + +@pytest.fixture() +def parser(shared_data_folder): + """ + Parse and setup the test network to be used for the tests below + """ + parser = ann.ITfLiteParser() + parser.CreateNetworkFromBinaryFile(os.path.join(shared_data_folder, 'mock_model.tflite')) + + yield parser + + +def test_tflite_parser_swig_destroy(): + assert ann.ITfLiteParser.__swig_destroy__, "There is a swig python destructor defined" + assert ann.ITfLiteParser.__swig_destroy__.__name__ == "delete_ITfLiteParser" + + +def test_check_tflite_parser_swig_ownership(parser): + # Check to see that SWIG has ownership for parser. This instructs SWIG to take + # ownership of the return value. This allows the value to be automatically + # garbage-collected when it is no longer in use + assert parser.thisown + + +def test_tflite_get_sub_graph_count(parser): + graphs_count = parser.GetSubgraphCount() + assert graphs_count == 1 + + +def test_tflite_get_network_input_binding_info(parser): + graphs_count = parser.GetSubgraphCount() + graph_id = graphs_count - 1 + + input_names = parser.GetSubgraphInputTensorNames(graph_id) + + input_binding_info = parser.GetNetworkInputBindingInfo(graph_id, input_names[0]) + + tensor = input_binding_info[1] + assert tensor.GetDataType() == 2 + assert tensor.GetNumDimensions() == 4 + assert tensor.GetNumElements() == 784 + assert tensor.GetQuantizationOffset() == 128 + assert tensor.GetQuantizationScale() == 0.007843137718737125 + + +def test_tflite_get_network_output_binding_info(parser): + graphs_count = parser.GetSubgraphCount() + graph_id = graphs_count - 1 + + output_names = parser.GetSubgraphOutputTensorNames(graph_id) + + output_binding_info1 = parser.GetNetworkOutputBindingInfo(graph_id, output_names[0]) + + # Check the tensor info retrieved from GetNetworkOutputBindingInfo + tensor1 = output_binding_info1[1] + + assert tensor1.GetDataType() == 2 + assert tensor1.GetNumDimensions() == 2 + assert tensor1.GetNumElements() == 10 + assert tensor1.GetQuantizationOffset() == 0 + assert tensor1.GetQuantizationScale() == 0.00390625 + + +def test_tflite_get_subgraph_input_tensor_names(parser): + graphs_count = parser.GetSubgraphCount() + graph_id = graphs_count - 1 + + input_names = parser.GetSubgraphInputTensorNames(graph_id) + + assert input_names == ('input_1',) + + +def test_tflite_get_subgraph_output_tensor_names(parser): + graphs_count = parser.GetSubgraphCount() + graph_id = graphs_count - 1 + + output_names = parser.GetSubgraphOutputTensorNames(graph_id) + + assert output_names[0] == 'dense/Softmax' + + +def test_tflite_filenotfound_exception(shared_data_folder): + parser = ann.ITfLiteParser() + + with pytest.raises(RuntimeError) as err: + parser.CreateNetworkFromBinaryFile(os.path.join(shared_data_folder, 'some_unknown_network.tflite')) + + # Only check for part of the exception since the exception returns + # absolute path which will change on different machines. + assert 'Cannot find the file' in str(err.value) + + +def test_tflite_parser_end_to_end(shared_data_folder): + parser = ann.ITfLiteParser() + + network = parser.CreateNetworkFromBinaryFile(os.path.join(shared_data_folder, "mock_model.tflite")) + + graphs_count = parser.GetSubgraphCount() + graph_id = graphs_count - 1 + + input_names = parser.GetSubgraphInputTensorNames(graph_id) + input_binding_info = parser.GetNetworkInputBindingInfo(graph_id, input_names[0]) + + output_names = parser.GetSubgraphOutputTensorNames(graph_id) + + preferred_backends = [ann.BackendId('CpuAcc'), ann.BackendId('CpuRef')] + + options = ann.CreationOptions() + runtime = ann.IRuntime(options) + + opt_network, messages = ann.Optimize(network, preferred_backends, runtime.GetDeviceSpec(), ann.OptimizerOptions()) + assert 0 == len(messages) + + net_id, messages = runtime.LoadNetwork(opt_network) + assert "" == messages + + # Load test image data stored in input_lite.npy + input_tensor_data = np.load(os.path.join(shared_data_folder, 'tflite_parser/input_lite.npy')) + input_tensors = ann.make_input_tensors([input_binding_info], [input_tensor_data]) + + output_tensors = [] + for index, output_name in enumerate(output_names): + out_bind_info = parser.GetNetworkOutputBindingInfo(graph_id, output_name) + out_tensor_info = out_bind_info[1] + out_tensor_id = out_bind_info[0] + output_tensors.append((out_tensor_id, + ann.Tensor(out_tensor_info))) + + runtime.EnqueueWorkload(net_id, input_tensors, output_tensors) + + output_vectors = [] + for index, out_tensor in enumerate(output_tensors): + output_vectors.append(out_tensor[1].get_memory_area()) + + # Load golden output file for result comparison. + expected_outputs = np.load(os.path.join(shared_data_folder, 'tflite_parser/golden_output_lite.npy')) + + # Check that output matches golden output + assert (expected_outputs == output_vectors[0]).all() diff --git a/python/pyarmnn/test/test_types.py b/python/pyarmnn/test/test_types.py new file mode 100644 index 0000000000..dfe1429c02 --- /dev/null +++ b/python/pyarmnn/test/test_types.py @@ -0,0 +1,29 @@ +# Copyright © 2020 Arm Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +import pytest +import pyarmnn as ann + + +def test_activation_function(): + assert 0 == ann.ActivationFunction_Sigmoid + assert 1 == ann.ActivationFunction_TanH + assert 2 == ann.ActivationFunction_Linear + assert 3 == ann.ActivationFunction_ReLu + assert 4 == ann.ActivationFunction_BoundedReLu + assert 5 == ann.ActivationFunction_SoftReLu + assert 6 == ann.ActivationFunction_LeakyReLu + assert 7 == ann.ActivationFunction_Abs + assert 8 == ann.ActivationFunction_Sqrt + assert 9 == ann.ActivationFunction_Square + + +def test_permutation_vector(): + pv = ann.PermutationVector((0, 2, 3, 1)) + assert pv[0] == 0 + assert pv[2] == 3 + + pv2 = ann.PermutationVector((0, 2, 3, 1)) + assert pv == pv2 + + pv4 = ann.PermutationVector((0, 3, 1, 2)) + assert pv.IsInverse(pv4) diff --git a/python/pyarmnn/test/test_version.py b/python/pyarmnn/test/test_version.py new file mode 100644 index 0000000000..2ea0fd85b4 --- /dev/null +++ b/python/pyarmnn/test/test_version.py @@ -0,0 +1,35 @@ +# Copyright © 2020 Arm Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +import os +import importlib + + +def test_rel_version(): + import pyarmnn._version as v + importlib.reload(v) + assert "dev" not in v.__version__ + del v + + +def test_dev_version(): + import pyarmnn._version as v + os.environ["PYARMNN_DEV_VER"] = "1" + + importlib.reload(v) + + assert "20.2.0.dev1" == v.__version__ + + del os.environ["PYARMNN_DEV_VER"] + del v + + +def test_arm_version_not_affected(): + import pyarmnn._version as v + os.environ["PYARMNN_DEV_VER"] = "1" + + importlib.reload(v) + + assert "20200200" == v.__arm_ml_version__ + + del os.environ["PYARMNN_DEV_VER"] + del v diff --git a/python/pyarmnn/test/testdata/shared/caffe_parser/golden_output_caffe.npy b/python/pyarmnn/test/testdata/shared/caffe_parser/golden_output_caffe.npy Binary files differnew file mode 100644 index 0000000000..007141cb9f --- /dev/null +++ b/python/pyarmnn/test/testdata/shared/caffe_parser/golden_output_caffe.npy diff --git a/python/pyarmnn/test/testdata/shared/caffe_parser/input_caffe.npy b/python/pyarmnn/test/testdata/shared/caffe_parser/input_caffe.npy Binary files differnew file mode 100644 index 0000000000..15df758b58 --- /dev/null +++ b/python/pyarmnn/test/testdata/shared/caffe_parser/input_caffe.npy diff --git a/python/pyarmnn/test/testdata/shared/license.txt b/python/pyarmnn/test/testdata/shared/license.txt new file mode 100644 index 0000000000..1e95a68e0d --- /dev/null +++ b/python/pyarmnn/test/testdata/shared/license.txt @@ -0,0 +1,10 @@ +This folder contains models and data needed for the testing of PyArmNN. + +All models and files found in this folder were created by ARM for the purpose +of testing PyArmNN. + +All the contents of this folder are distributed with the following license. + +Copyright © 2020 Arm Ltd. All rights reserved. +SPDX-License-Identifier: MIT + diff --git a/python/pyarmnn/test/testdata/shared/mock_model.caffemodel b/python/pyarmnn/test/testdata/shared/mock_model.caffemodel Binary files differnew file mode 100644 index 0000000000..df4079b729 --- /dev/null +++ b/python/pyarmnn/test/testdata/shared/mock_model.caffemodel diff --git a/python/pyarmnn/test/testdata/shared/mock_model.onnx b/python/pyarmnn/test/testdata/shared/mock_model.onnx Binary files differnew file mode 100644 index 0000000000..c1b506cc16 --- /dev/null +++ b/python/pyarmnn/test/testdata/shared/mock_model.onnx diff --git a/python/pyarmnn/test/testdata/shared/mock_model.pb b/python/pyarmnn/test/testdata/shared/mock_model.pb Binary files differnew file mode 100644 index 0000000000..cff9dc7add --- /dev/null +++ b/python/pyarmnn/test/testdata/shared/mock_model.pb diff --git a/python/pyarmnn/test/testdata/shared/mock_model.tflite b/python/pyarmnn/test/testdata/shared/mock_model.tflite Binary files differnew file mode 100644 index 0000000000..0b8944d3ed --- /dev/null +++ b/python/pyarmnn/test/testdata/shared/mock_model.tflite diff --git a/python/pyarmnn/test/testdata/shared/mock_profile_out.json b/python/pyarmnn/test/testdata/shared/mock_profile_out.json new file mode 100644 index 0000000000..8e1056160b --- /dev/null +++ b/python/pyarmnn/test/testdata/shared/mock_profile_out.json @@ -0,0 +1,216 @@ +{ + "ArmNN": { + "inference_measurements_#1": { + "type": "Event", + "Wall clock time_#1": { + "type": "Measurement", + "raw": [ + 1.1, + 2.2, + 3.3, + 4.4, + 5.5, + 6.6 + ], + "unit": "us" + }, + + "Execute_#2": { + "type": "Event", + "Wall clock time_#2": { + "type": "Measurement", + "raw": [ + 1.1, + 2.2, + 3.3, + 4.4, + 5.5, + 6.6 + ], + "unit": "us" + }, + "Wall clock time (Start)_#2": { + "type": "Measurement", + "raw": [ + 1, + 1, + 1, + 1, + 1, + 1 + ], + "unit": "us" + }, + "Wall clock time (Stop)_#2": { + "type": "Measurement", + "raw": [ + 2, + 2, + 2, + 2, + 2, + 2 + ], + "unit": "us" + }, + + "RefSomeMock1dWorkload_Execute_#5": { + "type": "Event", + "Wall clock time_#5": { + "type": "Measurement", + "raw": [ + 2, + 2, + 2, + 2, + 2, + 2 + ], + "unit": "us" + }, + "Wall clock time (Start)_#5": { + "type": "Measurement", + "raw": [ + 2, + 2, + 2, + 2, + 2, + 2 + ], + "unit": "us" + }, + "Wall clock time (Stop)_#5": { + "type": "Measurement", + "raw": [ + 4, + 4, + 4, + 4, + 4, + 4 + ], + "unit": "us" + } + }, + "NeonSomeMock2Workload_Execute_#6": { + "type": "Event", + "Wall clock time_#6": { + "type": "Measurement", + "raw": [ + 2, + 2, + 2, + 2, + 2, + 2 + ], + "unit": "us" + }, + "Wall clock time (Start)_#6": { + "type": "Measurement", + "raw": [ + 2, + 2, + 2, + 2, + 2, + 2 + ], + "unit": "us" + }, + "Wall clock time (Stop)_#6": { + "type": "Measurement", + "raw": [ + 4, + 4, + 4, + 4, + 4, + 4 + ], + "unit": "us" + } + }, + "ClSomeMock3dWorkload_Execute_#7": { + "type": "Event", + "Wall clock time_#7": { + "type": "Measurement", + "raw": [ + 2, + 2, + 2, + 2, + 2, + 2 + ], + "unit": "us" + }, + "Wall clock time (Start)_#7": { + "type": "Measurement", + "raw": [ + 2, + 2, + 2, + 2, + 2, + 2 + ], + "unit": "us" + }, + "Wall clock time (Stop)_#7": { + "type": "Measurement", + "raw": [ + 4, + 4, + 4, + 4, + 4, + 4 + ], + "unit": "us" + } + }, + "EthosNSomeMock4dWorkload_Execute_#8": { + "type": "Event", + "Wall clock time_#8": { + "type": "Measurement", + "raw": [ + 2, + 2, + 2, + 2, + 2, + 2 + ], + "unit": "us" + }, + "Wall clock time (Start)_#8": { + "type": "Measurement", + "raw": [ + 2, + 2, + 2, + 2, + 2, + 2 + ], + "unit": "us" + }, + "Wall clock time (Stop)_#8": { + "type": "Measurement", + "raw": [ + 4, + 4, + 4, + 4, + 4, + 4 + ], + "unit": "us" + } + } + } + } + } +} diff --git a/python/pyarmnn/test/testdata/shared/onnx_parser/golden_output_onnx.npy b/python/pyarmnn/test/testdata/shared/onnx_parser/golden_output_onnx.npy Binary files differnew file mode 100644 index 0000000000..f83d6ea7cb --- /dev/null +++ b/python/pyarmnn/test/testdata/shared/onnx_parser/golden_output_onnx.npy diff --git a/python/pyarmnn/test/testdata/shared/onnx_parser/input_onnx.npy b/python/pyarmnn/test/testdata/shared/onnx_parser/input_onnx.npy Binary files differnew file mode 100644 index 0000000000..15df758b58 --- /dev/null +++ b/python/pyarmnn/test/testdata/shared/onnx_parser/input_onnx.npy diff --git a/python/pyarmnn/test/testdata/shared/tf_parser/golden_output_tf.npy b/python/pyarmnn/test/testdata/shared/tf_parser/golden_output_tf.npy Binary files differnew file mode 100644 index 0000000000..007141cb9f --- /dev/null +++ b/python/pyarmnn/test/testdata/shared/tf_parser/golden_output_tf.npy diff --git a/python/pyarmnn/test/testdata/shared/tf_parser/input_tf.npy b/python/pyarmnn/test/testdata/shared/tf_parser/input_tf.npy Binary files differnew file mode 100644 index 0000000000..a21802e4b8 --- /dev/null +++ b/python/pyarmnn/test/testdata/shared/tf_parser/input_tf.npy diff --git a/python/pyarmnn/test/testdata/shared/tflite_parser/golden_output_lite.npy b/python/pyarmnn/test/testdata/shared/tflite_parser/golden_output_lite.npy Binary files differnew file mode 100644 index 0000000000..099f7fed22 --- /dev/null +++ b/python/pyarmnn/test/testdata/shared/tflite_parser/golden_output_lite.npy diff --git a/python/pyarmnn/test/testdata/shared/tflite_parser/input_lite.npy b/python/pyarmnn/test/testdata/shared/tflite_parser/input_lite.npy Binary files differnew file mode 100644 index 0000000000..53174683ff --- /dev/null +++ b/python/pyarmnn/test/testdata/shared/tflite_parser/input_lite.npy |