From f1d7647b8d2da487e5c69a28a33aa3555ea635c0 Mon Sep 17 00:00:00 2001 From: Maxim Vafin Date: Mon, 7 Nov 2022 08:22:22 +0100 Subject: [PATCH] Fix Slice issue in MO IR Reader (#13784) * Fix slice issue in MO IR Reader * Add unit test * Fix slice test --- tools/mo/openvino/tools/mo/ops/slice.py | 78 +++-- .../mo/utils/ir_reader/layer_to_class.py | 2 + tools/mo/unit_tests/mo/ops/slice_test.py | 275 ++++++++++++------ 3 files changed, 240 insertions(+), 115 deletions(-) diff --git a/tools/mo/openvino/tools/mo/ops/slice.py b/tools/mo/openvino/tools/mo/ops/slice.py index 0d0d4201124..b87007e5ea8 100644 --- a/tools/mo/openvino/tools/mo/ops/slice.py +++ b/tools/mo/openvino/tools/mo/ops/slice.py @@ -3,7 +3,8 @@ import numpy as np -from openvino.tools.mo.front.common.partial_infer.utils import get_shape_from_slice, shape_array, dynamic_dimension_value, \ +from openvino.tools.mo.front.common.partial_infer.utils import get_shape_from_slice, shape_array, \ + dynamic_dimension_value, \ dynamic_dimension, is_dynamic_slice from openvino.tools.mo.graph.graph import Node, Graph from openvino.tools.mo.ops.op import Op @@ -56,7 +57,6 @@ class CaffeSlice(Op): }, attrs) - class TFSlice(Op): """ TFSlice differs from Slice in ONNX, Caffe and MXNet. @@ -97,6 +97,37 @@ class MXSlice(Op): }, attrs) +def slice_infer(node: Node, steps_idx: int, axes_idx: int): + input_value = node.in_port(0).data.get_value() + input_shape = node.in_port(0).data.get_shape() + + starts = node.in_port(1).data.get_value() + ends = node.in_port(2).data.get_value() + if node.is_in_port_connected(steps_idx): + steps = node.in_port(steps_idx).data.get_value() + else: + steps = np.ones(len(starts), dtype=np.int64) + + if node.is_in_port_connected(axes_idx): + axes = node.in_port(axes_idx).data.get_value() + else: + axes = [x for x in range(len(starts))] + + if starts is None or ends is None or steps is None or axes is None: + node.out_port(0).data.set_shape(shape_array([dynamic_dimension_value] * len(input_shape))) + return + + slice_idx = [slice(0, in_shape, 1) for in_shape in input_shape] + for i in range(len(axes)): + # Ranged for output value for specified axis + slice_idx[axes[i]] = slice(starts[i], ends[i], steps[i]) + if input_value is None or any(is_dynamic_slice(s) for s in slice_idx): + output_shape = get_shape_from_slice(input_shape, slice_idx) + node.out_port(0).data.set_shape(output_shape) + else: + node.out_port(0).data.set_value(input_value[tuple(slice_idx)]) + + class Slice(Op): """ Semantic of Slice is identical to Slice in ONNX opset >= 10. @@ -117,31 +148,26 @@ class Slice(Op): @staticmethod def infer(node: Node): - input_value = node.in_port(0).data.get_value() - input_shape = node.in_port(0).data.get_shape() + slice_infer(node, 4, 3) - starts = node.in_port(1).data.get_value() - ends = node.in_port(2).data.get_value() - if node.is_in_port_connected(4): - steps = node.in_port(4).data.get_value() - else: - steps = np.ones(len(starts), dtype=np.int64) - if node.is_in_port_connected(3): - axes = node.in_port(3).data.get_value() - else: - axes = [x for x in range(len(starts))] +class OvSlice(Op): + """ + Semantic of OvSlice is identical to Slice in Openvino opset8. + It is introduced for usage in MO IR Reader. + """ + op = 'OvSlice' + enabled = False - if starts is None or ends is None or steps is None or axes is None: - node.out_port(0).data.set_shape(shape_array([dynamic_dimension_value] * len(input_shape))) - return + def __init__(self, graph: Graph, attrs: dict = None): + super().__init__(graph, { + 'type': None, + 'op': self.op, + 'in_ports_count': 5, + 'out_ports_count': 1, + 'infer': self.infer + }, attrs) - slice_idx = [slice(0, in_shape, 1) for in_shape in input_shape] - for i in range(len(axes)): - # Ranged for output value for specified axis - slice_idx[axes[i]] = slice(starts[i], ends[i], steps[i]) - if input_value is None or any(is_dynamic_slice(s) for s in slice_idx): - output_shape = get_shape_from_slice(input_shape, slice_idx) - node.out_port(0).data.set_shape(output_shape) - else: - node.out_port(0).data.set_value(input_value[tuple(slice_idx)]) + @staticmethod + def infer(node: Node): + slice_infer(node, 3, 4) diff --git a/tools/mo/openvino/tools/mo/utils/ir_reader/layer_to_class.py b/tools/mo/openvino/tools/mo/utils/ir_reader/layer_to_class.py index 6e3c3eb0e5c..635023266ed 100644 --- a/tools/mo/openvino/tools/mo/utils/ir_reader/layer_to_class.py +++ b/tools/mo/openvino/tools/mo/utils/ir_reader/layer_to_class.py @@ -27,6 +27,7 @@ from openvino.tools.mo.ops.pooling import Pooling from openvino.tools.mo.ops.psroipooling import DeformablePSROIPoolingOp from openvino.tools.mo.ops.scatter import Scatter from openvino.tools.mo.ops.scatternd import ScatterNDBase +from openvino.tools.mo.ops.slice import OvSlice from openvino.tools.mo.ops.split import Split, VariadicSplit from openvino.tools.mo.utils.class_registration import update_registration from openvino.tools.mo.utils.import_extensions import import_by_path @@ -47,6 +48,7 @@ custom_ops = { 'MaxPool': Pooling, 'Multiply': Mul, 'Power': Pow, + 'Slice': OvSlice, 'Split': Split, 'Subtract': Sub, 'VariadicSplit': VariadicSplit, diff --git a/tools/mo/unit_tests/mo/ops/slice_test.py b/tools/mo/unit_tests/mo/ops/slice_test.py index 4fd3bf8d927..f7b58ef5dd1 100644 --- a/tools/mo/unit_tests/mo/ops/slice_test.py +++ b/tools/mo/unit_tests/mo/ops/slice_test.py @@ -6,106 +6,203 @@ import unittest import numpy as np from generator import generator, generate -from openvino.tools.mo.front.common.partial_infer.utils import int64_array, dynamic_dimension_value, shape_array, strict_compare_tensors +from openvino.tools.mo.front.common.partial_infer.utils import int64_array, dynamic_dimension_value, shape_array, \ + strict_compare_tensors from openvino.tools.mo.graph.graph import Node -from openvino.tools.mo.ops.slice import Slice -from openvino.tools.mo.utils.error import Error +from openvino.tools.mo.ops.slice import Slice, OvSlice from unit_tests.utils.graph import build_graph, valued_const_with_data, valued_data, regular_op_with_empty_data, \ connect, shaped_data, shaped_const_with_data @generator class TestSliceOp(unittest.TestCase): - @generate(*[ - # standard case - ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], [0, 1], [3, 2], [0, 1], [1, 1], - [[5], [3], [6]], [3, 1]), - # negative bounds - ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], [0, 1], [3, -2], [0, 1], [1, 1], - [[5], [3], [6]], [3, 1]), - # unusual order of axes - ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], [0, 1], [3, -2], [1, 0], [1, 1], - [[2, 3, 5]], [1, 3]), - # when only input_shape is defined without values (one from bottom element is shape) - (None, [4, 5, 6], [1, 2], [4, 3], [0, 1], [1, 1], None, [3, 1, 6]), - # boundary case - (None, [4, 5, 6], [0, 2], [np.iinfo(np.int32).max, 3], [0, 1], [1, 1], None, [4, 1, 6]), - # boundary case - (None, [4, 5, 6], [np.iinfo(np.int32).min, 2], [3, 3], [0, 1], [1, 1], None, [3, 1, 6],), - # 1D input - ([1, 3, 224, 224], [4], [1], [2], [0], [1], [3], [1]), - # 1D input with negative starts - (None, [4], [-1], [1], [0], [-1], None, [2]), - # 1D input with negative ends - (None, [4], [1], [-1], [0], [1], None, [2]), - # with rounding (e.g. take from 1st to 3rd with step 4 should give shape 1 not 0) - (None, [4], [1], [3], [0], [4], None, [1]), - # with rounding and negative steps (e.g. take from 1st to 3rd with step 4 should give shape 1 not 0) - (None, [10], [7], [3], [0], [-7], None, [1]), - # reversing the sequence of elements - (None, [10], [-1], [np.iinfo(np.int32).min], [0], [-1], None, [10]), - # dynamic dimensions cases - # starts are non-constant - ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], None, [3, 2], [0, 1], [1, 1], None, - [dynamic_dimension_value, dynamic_dimension_value]), - # ends are non-constant - ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], [0, 1], None, [0, 1], [1, 1], None, - [dynamic_dimension_value, dynamic_dimension_value]), - # axes are non-constant - ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], [0, 1], [3, -2], None, [1, 1], None, - [dynamic_dimension_value, dynamic_dimension_value]), - # steps are non-constant - ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], [0, 1], [3, -2], [0, 1], None, None, - [dynamic_dimension_value, dynamic_dimension_value]), - # negative steps and since after normalization starts < ends output shape has 0-size dimension - (None, [20], [1], [-1], [0], [-2], None, [0]), - # since starts == ends output shape has 0-size dimension - (None, [4], [1], [1], [0], [1], None, [0]), - # since starts > ends output shape has 0-size dimension - (None, [4], [2], [1], [0], [1], None, [0]) - ]) - def test_slice_infer(self, inp_value, inp_shape, starts, ends, axes, steps, expected_value, expected_shape): - if inp_value is None: - input_node = shaped_data('data_1', int64_array(inp_shape)) + @generate(*[ + # standard case + ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], [0, 1], [3, 2], [0, 1], [1, 1], + [[5], [3], [6]], [3, 1]), + # negative bounds + ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], [0, 1], [3, -2], [0, 1], [1, 1], + [[5], [3], [6]], [3, 1]), + # unusual order of axes + ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], [0, 1], [3, -2], [1, 0], [1, 1], + [[2, 3, 5]], [1, 3]), + # when only input_shape is defined without values (one from bottom element is shape) + (None, [4, 5, 6], [1, 2], [4, 3], [0, 1], [1, 1], None, [3, 1, 6]), + # boundary case + (None, [4, 5, 6], [0, 2], [np.iinfo(np.int32).max, 3], [0, 1], [1, 1], None, [4, 1, 6]), + # boundary case + (None, [4, 5, 6], [np.iinfo(np.int32).min, 2], [3, 3], [0, 1], [1, 1], None, [3, 1, 6],), + # 1D input + ([1, 3, 224, 224], [4], [1], [2], [0], [1], [3], [1]), + # 1D input with negative starts + (None, [4], [-1], [1], [0], [-1], None, [2]), + # 1D input with negative ends + (None, [4], [1], [-1], [0], [1], None, [2]), + # with rounding (e.g. take from 1st to 3rd with step 4 should give shape 1 not 0) + (None, [4], [1], [3], [0], [4], None, [1]), + # with rounding and negative steps (e.g. take from 1st to 3rd with step 4 should give shape 1 not 0) + (None, [10], [7], [3], [0], [-7], None, [1]), + # reversing the sequence of elements + (None, [10], [-1], [np.iinfo(np.int32).min], [0], [-1], None, [10]), + # dynamic dimensions cases + # starts are non-constant + ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], None, [3, 2], [0, 1], [1, 1], None, + [dynamic_dimension_value, dynamic_dimension_value]), + # ends are non-constant + ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], [0, 1], None, [0, 1], [1, 1], None, + [dynamic_dimension_value, dynamic_dimension_value]), + # axes are non-constant + ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], [0, 1], [3, -2], None, [1, 1], None, + [dynamic_dimension_value, dynamic_dimension_value]), + # steps are non-constant + ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], [0, 1], [3, -2], [0, 1], None, None, + [dynamic_dimension_value, dynamic_dimension_value]), + # negative steps and since after normalization starts < ends output shape has 0-size dimension + (None, [20], [1], [-1], [0], [-2], None, [0]), + # since starts == ends output shape has 0-size dimension + (None, [4], [1], [1], [0], [1], None, [0]), + # since starts > ends output shape has 0-size dimension + (None, [4], [2], [1], [0], [1], None, [0]) + ]) + def test_slice_infer(self, inp_value, inp_shape, starts, ends, axes, steps, expected_value, expected_shape): + if inp_value is None: + input_node = shaped_data('data_1', int64_array(inp_shape)) + else: + input_node = valued_data('data_1', int64_array(inp_value)) + if inp_value is not None and inp_shape is not None: + assert np.array_equal(np.array(inp_value).shape, inp_shape) + + def convert_args(val, name=''): + if val is not None: + return valued_const_with_data(name, int64_array(val)) else: - input_node = valued_data('data_1', int64_array(inp_value)) - if inp_value is not None and inp_shape is not None: - assert np.array_equal(np.array(inp_value).shape, inp_shape) + return shaped_const_with_data(name, [0]) # fake shape - def convert_args(val, name=''): - if val is not None: - return valued_const_with_data(name, int64_array(val)) - else: - return shaped_const_with_data(name, [0]) #fake shape + starts = convert_args(starts, 'starts') + ends = convert_args(ends, 'ends') + axes = convert_args(axes, 'axes') + steps = convert_args(steps, 'steps') + if expected_shape is not None: + expected_shape = shape_array(expected_shape) - starts = convert_args(starts, 'starts') - ends = convert_args(ends, 'ends') - axes = convert_args(axes, 'axes') - steps = convert_args(steps, 'steps') - if expected_shape is not None: - expected_shape = shape_array(expected_shape) + nodes = { + **input_node, + **regular_op_with_empty_data('slice', {'op': 'Slice'}), + **starts, + **ends, + **axes, + **steps, + } - nodes = { - **input_node, - **regular_op_with_empty_data('slice', {'op': 'Slice'}), - **starts, - **ends, - **axes, - **steps, - } + graph = build_graph(nodes, + [('data_1', 'slice'), + *connect('starts', '1:slice'), + *connect('ends', '2:slice'), + *connect('axes', '3:slice'), + *connect('steps', '4:slice'), + *connect('slice', 'slice_d')]) - graph = build_graph(nodes, - [('data_1', 'slice'), - *connect('starts', '1:slice'), - *connect('ends', '2:slice'), - *connect('axes', '3:slice'), - *connect('steps', '4:slice'), - *connect('slice', 'slice_d')]) + graph.stage = 'middle' + slice_node = Node(graph, 'slice') - graph.stage = 'middle' - slice_node = Node(graph, 'slice') + Slice.infer(slice_node) + if expected_value is not None: + self.assertTrue(strict_compare_tensors(slice_node.out_node().value, expected_value)) + self.assertTrue(strict_compare_tensors(slice_node.out_node().shape, expected_shape)) - Slice.infer(slice_node) - if expected_value is not None: - self.assertTrue(strict_compare_tensors(slice_node.out_node().value, expected_value)) - self.assertTrue(strict_compare_tensors(slice_node.out_node().shape, expected_shape)) + +@generator +class TestOvSliceOp(unittest.TestCase): + @generate(*[ + # standard case + ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], [0, 1], [3, 2], [0, 1], [1, 1], + [[5], [3], [6]], [3, 1]), + # negative bounds + ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], [0, 1], [3, -2], [0, 1], [1, 1], + [[5], [3], [6]], [3, 1]), + # unusual order of axes + ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], [0, 1], [3, -2], [1, 0], [1, 1], + [[2, 3, 5]], [1, 3]), + # when only input_shape is defined without values (one from bottom element is shape) + (None, [4, 5, 6], [1, 2], [4, 3], [0, 1], [1, 1], None, [3, 1, 6]), + # boundary case + (None, [4, 5, 6], [0, 2], [np.iinfo(np.int32).max, 3], [0, 1], [1, 1], None, [4, 1, 6]), + # boundary case + (None, [4, 5, 6], [np.iinfo(np.int32).min, 2], [3, 3], [0, 1], [1, 1], None, [3, 1, 6],), + # 1D input + ([1, 3, 224, 224], [4], [1], [2], [0], [1], [3], [1]), + # 1D input with negative starts + (None, [4], [-1], [1], [0], [-1], None, [2]), + # 1D input with negative ends + (None, [4], [1], [-1], [0], [1], None, [2]), + # with rounding (e.g. take from 1st to 3rd with step 4 should give shape 1 not 0) + (None, [4], [1], [3], [0], [4], None, [1]), + # with rounding and negative steps (e.g. take from 1st to 3rd with step 4 should give shape 1 not 0) + (None, [10], [7], [3], [0], [-7], None, [1]), + # reversing the sequence of elements + (None, [10], [-1], [np.iinfo(np.int32).min], [0], [-1], None, [10]), + # dynamic dimensions cases + # starts are non-constant + ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], None, [3, 2], [0, 1], [1, 1], None, + [dynamic_dimension_value, dynamic_dimension_value]), + # ends are non-constant + ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], [0, 1], None, [0, 1], [1, 1], None, + [dynamic_dimension_value, dynamic_dimension_value]), + # axes are non-constant + ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], [0, 1], [3, -2], None, [1, 1], None, + [dynamic_dimension_value, dynamic_dimension_value]), + # steps are non-constant + ([[4, 5, 6, 7], [2, 3, 5, 6], [5, 6, 8, 9], [5, 6, 8, 9]], [4, 4], [0, 1], [3, -2], [0, 1], None, None, + [dynamic_dimension_value, dynamic_dimension_value]), + # negative steps and since after normalization starts < ends output shape has 0-size dimension + (None, [20], [1], [-1], [0], [-2], None, [0]), + # since starts == ends output shape has 0-size dimension + (None, [4], [1], [1], [0], [1], None, [0]), + # since starts > ends output shape has 0-size dimension + (None, [4], [2], [1], [0], [1], None, [0]) + ]) + def test_ov_slice_infer(self, inp_value, inp_shape, starts, ends, axes, steps, expected_value, expected_shape): + if inp_value is None: + input_node = shaped_data('data_1', int64_array(inp_shape)) + else: + input_node = valued_data('data_1', int64_array(inp_value)) + if inp_value is not None and inp_shape is not None: + assert np.array_equal(np.array(inp_value).shape, inp_shape) + + def convert_args(val, name=''): + if val is not None: + return valued_const_with_data(name, int64_array(val)) + else: + return shaped_const_with_data(name, [0]) # fake shape + + starts = convert_args(starts, 'starts') + ends = convert_args(ends, 'ends') + steps = convert_args(steps, 'steps') + axes = convert_args(axes, 'axes') + if expected_shape is not None: + expected_shape = shape_array(expected_shape) + + nodes = { + **input_node, + **regular_op_with_empty_data('slice', {'op': 'OvSlice'}), + **starts, + **ends, + **steps, + **axes, + } + + graph = build_graph(nodes, + [('data_1', 'slice'), + *connect('starts', '1:slice'), + *connect('ends', '2:slice'), + *connect('steps', '3:slice'), + *connect('axes', '4:slice'), + *connect('slice', 'slice_d')]) + + graph.stage = 'middle' + slice_node = Node(graph, 'slice') + + OvSlice.infer(slice_node) + if expected_value is not None: + self.assertTrue(strict_compare_tensors(slice_node.out_node().value, expected_value)) + self.assertTrue(strict_compare_tensors(slice_node.out_node().shape, expected_shape))