diff --git a/model-optimizer/extensions/front/caffe/slice_ext.py b/model-optimizer/extensions/front/caffe/slice_ext.py index 43630ef5345c29..fb4f8b0cf55b38 100644 --- a/model-optimizer/extensions/front/caffe/slice_ext.py +++ b/model-optimizer/extensions/front/caffe/slice_ext.py @@ -14,9 +14,9 @@ limitations under the License. """ +from mo.front.common.partial_infer.utils import int64_array from mo.front.extractor import FrontExtractorOp from mo.ops.slice import CaffeSlice -from mo.front.common.partial_infer.utils import int64_array class SliceFrontExtractor(FrontExtractorOp): diff --git a/model-optimizer/extensions/front/onnx/AttributedSliceToSlice_test.py b/model-optimizer/extensions/front/onnx/AttributedSliceToSlice_test.py index 7e772e74653c06..89a675438bdae2 100644 --- a/model-optimizer/extensions/front/onnx/AttributedSliceToSlice_test.py +++ b/model-optimizer/extensions/front/onnx/AttributedSliceToSlice_test.py @@ -21,7 +21,7 @@ from extensions.front.onnx.AttributedSliceToSlice import AttributedSliceToSliceReplacer from mo.utils.ir_engine.compare_graphs import compare_graphs -from mo.utils.unittest.graph import build_graph, regular_op_with_empty_data, result, const, connect_on_front +from mo.utils.unittest.graph import build_graph, regular_op_with_empty_data, result, const, connect @generator @@ -53,9 +53,9 @@ def test_attributed_slice_replacer(self, attributed_slice_attrs): graph_ref = build_graph(nodes_attrs=nodes, edges=[ ('input', 'slice'), - *connect_on_front('start', '1:slice'), - *connect_on_front('end', '2:slice'), - *connect_on_front('axis', '3:slice'), + *connect('start', '1:slice', on_front=True), + *connect('end', '2:slice', on_front=True), + *connect('axis', '3:slice', on_front=True), ('slice', 'output'), ], nodes_with_edges_only=True) diff --git a/model-optimizer/extensions/front/tf/TFSliceToSlice_test.py b/model-optimizer/extensions/front/tf/TFSliceToSlice_test.py index 8a0e04d536ad8b..508b4156541fc3 100644 --- a/model-optimizer/extensions/front/tf/TFSliceToSlice_test.py +++ b/model-optimizer/extensions/front/tf/TFSliceToSlice_test.py @@ -20,7 +20,7 @@ from extensions.front.tf.TFSliceToSlice import TFSliceToSliceReplacer from mo.utils.ir_engine.compare_graphs import compare_graphs -from mo.utils.unittest.graph import build_graph, regular_op_with_empty_data, result, const, connect_on_front +from mo.utils.unittest.graph import build_graph, regular_op_with_empty_data, result, const, connect nodes = { **regular_op_with_empty_data('input', {'type': 'Parameter'}), @@ -45,33 +45,33 @@ class SliceReplacerTest(unittest.TestCase): def test_slice_replacer_begin_with_2_inputs(self): graph = build_graph(nodes_attrs=nodes, edges=[ ('input', 'tfslice'), - *connect_on_front('begin:0', '1:tfslice'), - *connect_on_front('begin:0', '0:john_doe'), - *connect_on_front('size:0', '2:tfslice'), - *connect_on_front('tfslice:0', 'output'), + *connect('begin:0', '1:tfslice', on_front=True), + *connect('begin:0', '0:john_doe', on_front=True), + *connect('size:0', '2:tfslice', on_front=True), + *connect('tfslice:0', 'output', on_front=True), ], nodes_with_edges_only=True) graph.stage = 'front' TFSliceToSliceReplacer().find_and_replace_pattern(graph) graph_ref = build_graph(nodes_attrs=nodes, edges=[ - *connect_on_front('input:0', 'slice'), - *connect_on_front('begin:0', 'slice:1'), - *connect_on_front('begin:0', 'john_doe:1'), + *connect('input:0', 'slice', on_front=True), + *connect('begin:0', 'slice:1', on_front=True), + *connect('begin:0', 'john_doe:1', on_front=True), - *connect_on_front('begin:0', 'end_const:0'), - *connect_on_front('size:0', 'end_const:1'), - *connect_on_front('size:0', 'equal:0'), + *connect('begin:0', 'end_const:0', on_front=True), + *connect('size:0', 'end_const:1', on_front=True), + *connect('size:0', 'equal:0', on_front=True), - *connect_on_front('int32_max:0', 'select:1'), - *connect_on_front('minus_one:0', 'equal:1'), + *connect('int32_max:0', 'select:1', on_front=True), + *connect('minus_one:0', 'equal:1', on_front=True), - *connect_on_front('equal:0', 'select:0'), + *connect('equal:0', 'select:0', on_front=True), - *connect_on_front('end_const:0', 'select:2'), - *connect_on_front('select:0', 'slice:2'), + *connect('end_const:0', 'select:2', on_front=True), + *connect('select:0', 'slice:2', on_front=True), - *connect_on_front('slice:0', 'output'), + *connect('slice:0', 'output', on_front=True), ], nodes_with_edges_only=True) (flag, resp) = compare_graphs(graph, graph_ref, 'output', check_op_attrs=True) @@ -79,27 +79,27 @@ def test_slice_replacer_begin_with_2_inputs(self): def test_slice_replacer(self): graph = build_graph(nodes_attrs=nodes, edges=[ - *connect_on_front('input:0', 'tfslice'), - *connect_on_front('begin:0', '1:tfslice'), - *connect_on_front('size:0', '2:tfslice'), - *connect_on_front('tfslice:0', 'output'), + *connect('input:0', 'tfslice', on_front=True), + *connect('begin:0', '1:tfslice', on_front=True), + *connect('size:0', '2:tfslice', on_front=True), + *connect('tfslice:0', 'output', on_front=True), ], nodes_with_edges_only=True) graph.stage = 'front' TFSliceToSliceReplacer().find_and_replace_pattern(graph) graph_ref = build_graph(nodes_attrs=nodes, edges=[ - *connect_on_front('input:0', 'slice'), - *connect_on_front('begin:0', '1:slice'), - *connect_on_front('begin:0', '0:end_const'), - *connect_on_front('size:0', '1:end_const'), - *connect_on_front('size:0', '0:equal'), - *connect_on_front('int32_max:0', '1:select'), - *connect_on_front('minus_one:0', '1:equal'), - *connect_on_front('equal:0', '0:select'), - *connect_on_front('end_const:0', '2:select'), - *connect_on_front('select:0', '2:slice'), - *connect_on_front('slice:0', 'output'), + *connect('input:0', 'slice', on_front=True), + *connect('begin:0', '1:slice', on_front=True), + *connect('begin:0', '0:end_const', on_front=True), + *connect('size:0', '1:end_const', on_front=True), + *connect('size:0', '0:equal', on_front=True), + *connect('int32_max:0', '1:select', on_front=True), + *connect('minus_one:0', '1:equal', on_front=True), + *connect('equal:0', '0:select', on_front=True), + *connect('end_const:0', '2:select', on_front=True), + *connect('select:0', '2:slice', on_front=True), + *connect('slice:0', 'output', on_front=True), ], nodes_with_edges_only=True) (flag, resp) = compare_graphs(graph, graph_ref, 'output', check_op_attrs=True) diff --git a/model-optimizer/mo/ops/slice.py b/model-optimizer/mo/ops/slice.py index 189a54bdcf95c4..02af9645ea4698 100644 --- a/model-optimizer/mo/ops/slice.py +++ b/model-optimizer/mo/ops/slice.py @@ -19,23 +19,22 @@ from mo.graph.graph import Node, Graph from mo.ops.op import Op from mo.utils.error import Error -from mo.utils.shape import get_shape_after_slice """ -In each framework (or in different opsets of the same framework) Slice operation either has different semantic or -different parameters, or/and they specified differently in one case as attributes in other case as inputs. To -distinguish them and not confuse with OpenVINO Slice these operations were added. OpenVINO Slice operation is same as -ONNX Slice in opset >= 10. To unify all of them before middle phase these replacements take place on the front phase: - AttributedSlice, TFSlice -> Slice - CaffeSlice -> Split - MXSlice -> StridedSlice +Slicing operations have different semantic or different parameters/inputs in different frameworks. To distinguish them +and not confuse with OpenVINO Slice several Model Optimizer internal operations are introduced. OpenVINO Slice operation +behaves as ONNX Slice in opset >= 10. A number of transformations take place on the front phase to convert framework slicing +operations to OpenVINO operations from opset: + - AttributedSlice, TFSlice -> Slice + - CaffeSlice -> Split + - MXSlice -> StridedSlice """ class AttributedSlice(Op): """ - AttributedSlice is used in old versions of ONNX models (opset version < 10). This operation is - used only for extracting. On the front phase is replaced with Slice. + AttributedSlice is used in old versions of ONNX models (opset version < 10). + The operation is replaced with the OpenVINO Slice operation on the front phase. """ op = 'AttributedSlice' enabled = False @@ -57,7 +56,7 @@ class CaffeSlice(Op): """ Slice in Caffe is equivalent to Split operation in OpenVINO. https://caffe.berkeleyvision.org/tutorial/layers/slice.html - After extracting operations on the front phase CaffeSlices are replaced with OpenVINO Splits operation. + The operation is replaced with the OpenVINO Split operation on the front phase. """ op = 'CaffeSlice' enabled = False @@ -80,7 +79,8 @@ class TFSlice(Op): Slice operation in Tensorflow is different from Slice in ONNX, Caffe and MXNet. It has begin and size inputs while OpenVINO Slice has start, end, step and axis parameters specified as inputs. https://www.tensorflow.org/api_docs/python/tf/slice - On the front phase it is replaced with Slice operation. If size[i] == -1 is replaced to int32_max value for the end. + The operation is replaced with the OpenVINO Slice operation on the front phase. + If size[i] == -1 is replaced to int32_max value for the end. """ op = 'TFSlice' enabled = False @@ -99,6 +99,7 @@ class MXSlice(Op): """ Slice operation in MXNet is different from ONNX, Caffe, Tensorflow. It has begin, end & step attributes https://mxnet.apache.org/versions/1.6/api/python/docs/api/symbol/op/index.html#mxnet.symbol.op.slice + The operation is replaced with the OpenVINO StridedSlice operation on the front phase. """ op = 'MXSlice' enabled = False @@ -165,3 +166,48 @@ def infer(node: Node): node.out_port(0).data.set_shape(output_shape) else: node.out_port(0).data.set_value(input_value[tuple(slice_idx)]) + + +def get_shape_after_slice(input_shape: int, slice_idx: int): + """ + Calculate shape of a tensor after slicing without actually creating the resulting tensor. + Is introduced to save memory. + """ + output_shape = np.zeros(len(input_shape), dtype=np.int32) + for i, s in enumerate(slice_idx): + start, end = normalize_slice_indices(input_shape[i], s.start, s.stop) + output_shape[i] = (end - start) / s.step + return output_shape + + +def convert_negative_indices(size: int, val: int): + """ + Converts negative indices of a tensors: e.g. if val == -1 then convert it to size + If val is -1 and we expect it to the start then returned val = size should be clipped by `clip_indices` + Note: returned value is not always positive and it's expected behaviour + """ + if val < 0: + return val + size + 1 + else: + return val + + +def clip_indices(size: int, val: int): + # if slice starts and/or ends exceed indices bounds of a tensor this routine cuts them to size or 0 + # Note: returned value is not always positive and it's expected behaviour + if val >= size: + return size + elif val < 0: + return 0 + else: + return val + + +def normalize_slice_indices(size: int, start: int, end: int): + # convert slice_indices to format in which size of slice can calculated + start = convert_negative_indices(size, start) + end = convert_negative_indices(size, end) + start = clip_indices(size, start) + end = clip_indices(size, end) + return start, end + diff --git a/model-optimizer/mo/ops/slice_test.py b/model-optimizer/mo/ops/slice_test.py index 18a2e4222d6ad1..778c7dee98aba7 100644 --- a/model-optimizer/mo/ops/slice_test.py +++ b/model-optimizer/mo/ops/slice_test.py @@ -99,7 +99,7 @@ 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 + return shaped_const_with_data(name, [0]) #fake shape starts = convert_args(starts, 'starts') ends = convert_args(ends, 'ends') diff --git a/model-optimizer/mo/utils/shape.py b/model-optimizer/mo/utils/shape.py index 15484ddb5cca0b..f04bb8e86383ae 100644 --- a/model-optimizer/mo/utils/shape.py +++ b/model-optimizer/mo/utils/shape.py @@ -13,18 +13,19 @@ See the License for the specific language governing permissions and limitations under the License. """ +import numpy as np + from extensions.ops.elementwise import Add from extensions.ops.gather import Gather from extensions.ops.range import Range from mo.front.common.partial_infer.utils import int64_array from mo.front.tf.graph_utils import create_op_node_with_second_input -from mo.graph.graph import Node, Graph +from mo.graph.graph import Node from mo.graph.port import Port from mo.ops.concat import Concat from mo.ops.const import Const from mo.ops.shape import Shape from mo.ops.squeeze import Squeeze -import numpy as np def get_canonical_axis_index_node(rank: Node, axis: int) -> Node: @@ -221,43 +222,3 @@ def get_shape_and_rank_nodes_by_port(port: Port, return_as_a_scalar: bool = True rank_1_d) return shape, rank - -def convert_negative_indices(size, val): - """ - Converts negative indices of a tensors: e.g. if val == -1 then convert it to size - Note: not necessary returns positive value and it's expected behaviour - """ - if val < 0: - return val + size + 1 - else: - return val - - -def check_ind_boundaries(size, val): - """if slice starts and/or ends exceed indices bounds of a tensor this routine cuts them to size or 0""" - if val >= size: - return size - elif val < 0: - return 0 - else: - return val - - -def check_values(size, start, end): - start = convert_negative_indices(size, start) - end = convert_negative_indices(size, end) - start = check_ind_boundaries(size, start) - end = check_ind_boundaries(size, end) - return start, end - - -def get_shape_after_slice(input_shape, slice_idx): - """ - Calculate shape of a tensor after slicing without actually creating the resulting tensor. - Is introduced to save memory. - """ - output_shape = np.zeros(len(input_shape)) - for i, s in enumerate(slice_idx): - start, end = check_values(input_shape[i], s.start, s.stop) - output_shape[i] = (end - start) / s.step - return output_shape diff --git a/model-optimizer/mo/utils/unittest/graph.py b/model-optimizer/mo/utils/unittest/graph.py index 2b19de411acc6f..55b024556ddbe4 100644 --- a/model-optimizer/mo/utils/unittest/graph.py +++ b/model-optimizer/mo/utils/unittest/graph.py @@ -198,7 +198,6 @@ def build_graph(nodes_attrs: dict, edges: list, update_attributes: dict = None, # Add in_ports attribute in_edges = node.in_edges() for attr in in_edges.values(): - # node.add_input_port(idx=attr['in'], skip_if_exist=True) node.add_input_port(idx=attr['in']) # Add out_ports attribute @@ -331,7 +330,7 @@ def get_name_and_port(tensor_name): return node_name, 0 -def connect(first_tensor_name, second_tensor_name, skip_data=False): +def connect(first_tensor_name, second_tensor_name, skip_data=False, on_front=False): # ports could be skipped -- then zero in/out ports would be used # first_tensor_name = first_op_name:out_port # second_tensor_name = in_port:second_op_name @@ -341,6 +340,8 @@ def connect(first_tensor_name, second_tensor_name, skip_data=False): if skip_data: return [(first_op_name + '_d', second_op_name, {'in': in_port})] + if on_front: + return [(first_op_name, second_op_name, {'out': out_port, 'in': in_port})] return [ (first_op_name, first_op_name + '_d', {'out': out_port}), (first_op_name + '_d', second_op_name, {'in': in_port}), @@ -350,13 +351,3 @@ def connect(first_tensor_name, second_tensor_name, skip_data=False): def connect_data(first_tensor_name, second_tensor_name): return connect(first_tensor_name, second_tensor_name, skip_data=True) - -def connect_on_front(first_tensor_name, second_tensor_name, skip_data=False): - # ports could be skipped -- then zero in/out ports would be used - # first_tensor_name = first_op_name:out_port - # second_tensor_name = in_port:second_op_name - - first_op_name, out_port = get_name_and_port(first_tensor_name) - second_op_name, in_port = get_name_and_port(second_tensor_name) - - return [(first_op_name, second_op_name, {'out': out_port, 'in': in_port})]