Review reverse class for shape inference aspects (#15426)

* Check partial shape and label propagation

* Add template shape_infer implementation
This commit is contained in:
Pawel Raasz
2023-02-08 13:10:51 +01:00
committed by GitHub
parent 1f3e469c5e
commit c3083589bd
6 changed files with 399 additions and 80 deletions

View File

@@ -78,6 +78,23 @@ public:
}
};
/**
* \brief Compare if value is less to expected.
*
* \tparam T Value type to compare.
*/
template <class T>
class Less {
const T m_exp_value{};
public:
constexpr Less(const T& exp_value) : m_exp_value{exp_value} {}
constexpr bool operator()(const T& value) const {
return value < m_exp_value;
}
};
/**
* \brief Compare two values (a < b) in safe way against lossy integer conversion.
*

View File

@@ -0,0 +1,106 @@
// Copyright (C) 2018-2023 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
#pragma once
#include <openvino/op/reverse.hpp>
#include "utils.hpp"
namespace ov {
namespace op {
namespace util {
/** \brief Clip if value type T is less than 0, otherwise cast to AxisSet::value_type. */
struct ClipNegative {
using value_type = typename AxisSet::value_type;
template <class T>
constexpr value_type operator()(const T value) const {
return (std::is_signed<T>::value && value < 0) ? 0 : static_cast<value_type>(value);
}
};
} // namespace util
namespace v1 {
/**
* \brief Reverse shape inference
*
* \tparam TShape Type of shape.
*
* \param op Pointer to Reverse operator.
* \param input_shapes Input shapes of Reverse.
* \param constant_data Map of constant data. Default empty.
*
* \return Vector of output shapes with one shape.
*/
template <class TShape>
std::vector<TShape> shape_infer(const Reverse* op,
const std::vector<TShape>& input_shapes,
const std::map<size_t, std::reference_wrapper<const ov::Tensor>>& constant_data = {}) {
NODE_VALIDATION_CHECK(op, input_shapes.size() == 2);
using DimType = typename TShape::value_type;
const auto& data_shape = input_shapes[0];
const auto& data_rank = data_shape.rank();
const auto& axes_shape = input_shapes[1];
const auto& axes_rank = axes_shape.rank();
NODE_VALIDATION_CHECK(op,
axes_rank.compatible(1),
"The reversed_axes input must be a 1D tensor (got ",
axes_rank,
").");
if (op->get_mode() == Reverse::Mode::MASK) {
NODE_VALIDATION_CHECK(
op,
data_rank.is_dynamic() || axes_rank.is_dynamic() || axes_shape[0].compatible(data_shape.size()),
"The number of elements in the reversed_axes tensor (",
axes_shape[0],
") must match the input data tensor rank (",
data_rank,
") in 'mask' mode.");
} else if (data_rank.is_static()) {
// mode index & data rank is static
using TAxis = typename AxisSet::value_type;
static_assert(std::is_same<TAxis, util::ClipNegative::value_type>(),
"AxisSet::value_type != ClipNegative::value_type");
if (const auto axes =
get_input_const_data_as<TShape, TAxis, AxisSet>(op, 1, constant_data, util::ClipNegative())) {
NODE_VALIDATION_CHECK(op,
all_of(axes->begin(), axes->end(), cmp::Less<TAxis>(data_rank.get_length())),
"Some of the provided axes (",
*axes,
") are out of bounds (input rank: ",
data_rank,
").");
}
}
return {data_shape};
}
/**
* \brief Reverse shape inference
*
* \tparam TShape Type of shape.
*
* \param op Pointer to Reverse operator.
* \param input_shapes Input shapes of Reverse.
* \param output_shapes Output shapes of Reverse
* \param constant_data Map of constant data. Default empty.
*/
template <class TShape>
void shape_infer(const Reverse* op,
const std::vector<TShape>& input_shapes,
std::vector<TShape>& output_shapes,
const std::map<size_t, std::reference_wrapper<const ov::Tensor>>& constant_data = {}) {
output_shapes = shape_infer(op, input_shapes);
}
} // namespace v1
} // namespace op
} // namespace ov

View File

@@ -6,7 +6,6 @@
#include <algorithm>
#include <iterator>
#include <ngraph/validation_util.hpp>
#include <sstream>
#include "itt.hpp"
@@ -15,6 +14,7 @@
#include "ngraph/op/constant.hpp"
#include "ngraph/op/util/op_types.hpp"
#include "ngraph/runtime/reference/reverse.hpp"
#include "reverse_shape_inference.hpp"
using namespace std;
using namespace ngraph;
@@ -43,66 +43,15 @@ void op::v1::Reverse::validate_and_infer_types() {
NODE_VALIDATION_CHECK(this,
get_input_element_type(1) == element::boolean,
"In 'mask' mode the second input must contain boolean values.");
}
const auto input_shape = get_input_partial_shape(0);
const auto input_rank = input_shape.rank();
const auto rev_axes_shape = get_input_partial_shape(1);
const auto rev_axes_rank = rev_axes_shape.rank();
if (rev_axes_rank.is_static()) {
} else {
// Index mode
NODE_VALIDATION_CHECK(this,
rev_axes_rank.get_length() == 1,
"The reversed_axes input must be a 1D tensor (got ",
rev_axes_rank.get_length(),
").");
if (m_mode == Mode::MASK) {
if (input_rank.is_static() && rev_axes_shape[0].is_static()) {
const auto rev_axes_mask_elems_count = rev_axes_shape[0].get_length();
NODE_VALIDATION_CHECK(this,
rev_axes_mask_elems_count == input_rank.get_length(),
"The number of elements in the reversed_axes tensor (",
rev_axes_mask_elems_count,
") must match the input data tensor rank (",
input_rank.get_length(),
") in 'mask' mode.");
}
}
get_input_element_type(1).is_integral_number(),
"In 'index' mode the second input must contain integer values.");
}
if (input_rank.is_static()) {
const size_t rank = input_rank.get_length();
if (const auto& rev_axes_constant = get_constant_from_source(input_value(1))) {
if (m_mode == Mode::INDEX) {
const AxisSet rev_axes = rev_axes_constant->get_axis_set_val();
NODE_VALIDATION_CHECK(this,
rev_axes.size() <= rank,
"Too many axes(",
rev_axes,
") have been provided for given input shape(",
input_shape,
").");
bool all_axes_in_range = all_of(rev_axes.begin(), rev_axes.end(), [&rank](const size_t axis) {
return axis < rank;
});
NODE_VALIDATION_CHECK(this,
all_axes_in_range,
"Some of the provided axes (",
rev_axes,
") are out of bounds (input rank: ",
input_rank.get_length(),
").");
}
}
}
set_output_type(0, get_input_element_type(0), input_shape);
const auto output_shape = shape_infer(this, get_node_input_partial_shapes(*this)).front();
set_output_type(0, get_input_element_type(0), output_shape);
}
shared_ptr<Node> op::v1::Reverse::clone_with_new_inputs(const OutputVector& new_args) const {

View File

@@ -2,14 +2,19 @@
// SPDX-License-Identifier: Apache-2.0
//
#include "common_test_utils/test_assertions.hpp"
#include "gtest/gtest.h"
#include "ngraph/ngraph.hpp"
#include "openvino/opsets/opset10.hpp"
#include "util/type_prop.hpp"
NGRAPH_SUPPRESS_DEPRECATED_START
using namespace std;
using namespace ngraph;
using namespace testing;
class TypePropReverseV1Test : public TypePropOpTest<op::v1::Reverse> {};
TEST(type_prop, reverse_1d_deduce) {
// Deduce type
@@ -155,39 +160,175 @@ TEST(type_prop, reverse_partial_rank_dynamic) {
op::v1::Reverse::Mode::INDEX);
EXPECT_EQ(rev->get_element_type(), element::f32);
EXPECT_TRUE(rev->get_output_partial_shape(0).rank().is_dynamic());
EXPECT_EQ(rev->get_output_partial_shape(0), PartialShape::dynamic());
}
using namespace ov::opset10;
//
// If the input rank is static but the shape is dynamic, we should pass if the axis indices are
// in bounds.
//
TEST(type_prop, reverse_partial_rank_static_dynamic_axes_ok) {
PartialShape param_shape{Dimension::dynamic(), Dimension::dynamic(), 2, 3};
auto param = make_shared<op::Parameter>(element::f32, param_shape);
auto rev = make_shared<op::v1::Reverse>(param,
op::Constant::create(element::i64, {2}, {0, 2}),
op::v1::Reverse::Mode::INDEX);
TEST_F(TypePropReverseV1Test, partial_rank_static_dynamic_axes_ok) {
PartialShape param_shape{Dimension::dynamic(), {10, 300}, 2, 3};
set_shape_labels(param_shape, 10);
auto param = make_shared<Parameter>(element::f32, param_shape);
auto rev = make_op(param, Constant::create(element::i64, {2}, {0, 2}), op::v1::Reverse::Mode::INDEX);
EXPECT_EQ(rev->get_element_type(), element::f32);
EXPECT_TRUE(rev->get_output_partial_shape(0).same_scheme(param_shape));
EXPECT_EQ(rev->get_output_partial_shape(0), param_shape);
EXPECT_THAT(get_shape_labels(rev->get_output_partial_shape(0)), ElementsAre(10, 11, 12, 13));
}
TEST(type_prop, reverse_partial_rank_static_dynamic_axes_oob) {
TEST_F(TypePropReverseV1Test, axes_index_is_not_1d_tensor) {
PartialShape param_shape{Dimension::dynamic(), Dimension::dynamic(), 2, 3};
auto param = make_shared<op::Parameter>(element::f32, param_shape);
try {
auto rev = make_shared<op::v1::Reverse>(param,
op::Constant::create(element::i64, {3}, {0, 4, 2}),
op::v1::Reverse::Mode::INDEX);
auto axes = make_shared<Parameter>(element::i64, PartialShape{2, 3});
// Should have thrown, so fail if it didn't
FAIL() << "Axis out of bounds not detected";
} catch (const NodeValidationFailure& error) {
EXPECT_HAS_SUBSTRING(
error.what(),
std::string("Some of the provided axes (AxisSet{0, 2, 4}) are out of bounds (input rank: 4)."));
} catch (...) {
FAIL() << "Deduced type check failed for unexpected reason";
}
OV_EXPECT_THROW(auto op = make_op(param, axes, op::v1::Reverse::Mode::INDEX),
NodeValidationFailure,
HasSubstr("The reversed_axes input must be a 1D tensor"));
}
TEST_F(TypePropReverseV1Test, axes_mask_is_not_1d_tensor) {
PartialShape param_shape{Dimension::dynamic(), Dimension::dynamic(), 2, 3};
auto param = make_shared<op::Parameter>(element::f32, param_shape);
auto axes = make_shared<Parameter>(element::boolean, PartialShape{2, 3});
OV_EXPECT_THROW(auto op = make_op(param, axes, op::v1::Reverse::Mode::MASK),
NodeValidationFailure,
HasSubstr("The reversed_axes input must be a 1D tensor"));
}
TEST_F(TypePropReverseV1Test, axes_mask_length_lt_input_rank) {
PartialShape param_shape{Dimension::dynamic(), Dimension::dynamic(), 2, 3};
auto param = make_shared<op::Parameter>(element::f32, param_shape);
auto axes = make_shared<Parameter>(element::boolean, PartialShape{2});
OV_EXPECT_THROW(
auto op = make_op(param, axes, op::v1::Reverse::Mode::MASK),
NodeValidationFailure,
HasSubstr("The number of elements in the reversed_axes tensor (2) must match the input data tensor rank"));
}
TEST_F(TypePropReverseV1Test, axes_mask_length_gt_input_rank) {
PartialShape param_shape{Dimension::dynamic(), Dimension::dynamic(), 2, 3};
auto param = make_shared<op::Parameter>(element::f32, param_shape);
auto axes = make_shared<Parameter>(element::boolean, PartialShape{5});
OV_EXPECT_THROW(
auto op = make_op(param, axes, op::v1::Reverse::Mode::MASK),
NodeValidationFailure,
HasSubstr("The number of elements in the reversed_axes tensor (5) must match the input data tensor rank"));
}
TEST_F(TypePropReverseV1Test, axes_index_is_scalar) {
PartialShape param_shape{2, {2, 10}, 8};
auto param = make_shared<Parameter>(element::f32, param_shape);
auto axes = make_shared<Parameter>(element::i64, PartialShape{});
OV_EXPECT_THROW(auto op = make_op(param, axes, op::v1::Reverse::Mode::INDEX),
NodeValidationFailure,
HasSubstr("The reversed_axes input must be a 1D tensor"));
}
TEST_F(TypePropReverseV1Test, axes_mask_is_scalar) {
PartialShape param_shape{2, {2, 10}, 8};
auto param = make_shared<Parameter>(element::f32, param_shape);
auto axes = make_shared<Parameter>(element::boolean, PartialShape{});
OV_EXPECT_THROW(auto op = make_op(param, axes, op::v1::Reverse::Mode::MASK),
NodeValidationFailure,
HasSubstr("The reversed_axes input must be a 1D tensor"));
}
TEST_F(TypePropReverseV1Test, axes_mask_not_boolean_type) {
PartialShape param_shape{2, {2, 10}, 8};
auto param = make_shared<Parameter>(element::f32, param_shape);
auto axes = make_shared<Parameter>(element::i32, PartialShape{4});
OV_EXPECT_THROW(auto op = make_op(param, axes, op::v1::Reverse::Mode::MASK),
NodeValidationFailure,
HasSubstr("In 'mask' mode the second input must contain boolean values"));
}
TEST_F(TypePropReverseV1Test, axes_index_not_integer_type) {
PartialShape param_shape{2, {2, 10}, 8};
auto param = make_shared<Parameter>(element::f32, param_shape);
auto axes = make_shared<Parameter>(element::f32, PartialShape{4});
OV_EXPECT_THROW(auto op = make_op(param, axes, op::v1::Reverse::Mode::INDEX),
NodeValidationFailure,
HasSubstr("In 'index' mode the second input must contain integer values"));
}
TEST_F(TypePropReverseV1Test, param_static_rank_partial_shape_axes_out_of_input_rank) {
PartialShape param_shape{Dimension::dynamic(), Dimension::dynamic(), 2, 3};
auto param = make_shared<op::Parameter>(element::f32, param_shape);
OV_EXPECT_THROW(
auto op = make_op(param, Constant::create(element::i64, {3}, {0, 4, 2}), op::v1::Reverse::Mode::INDEX),
NodeValidationFailure,
HasSubstr("Some of the provided axes (AxisSet{0, 2, 4}) are out of bounds (input rank: 4)."));
}
TEST_F(TypePropReverseV1Test, param_static_rank_partial_shape_axes_negatives) {
PartialShape param_shape{-1, {2, -1}, {-1, 3}, 5};
set_shape_labels(param_shape, 10);
auto param = make_shared<op::Parameter>(element::f32, param_shape);
auto op = make_op(param, Constant::create(element::i64, {3}, {0, -1, 2}), op::v1::Reverse::Mode::INDEX);
EXPECT_EQ(op->get_element_type(), element::f32);
EXPECT_EQ(op->get_output_partial_shape(0), param_shape);
EXPECT_THAT(get_shape_labels(op->get_output_partial_shape(0)), ElementsAre(10, 11, 12, 13));
}
TEST_F(TypePropReverseV1Test, more_axes_index_than_input_rank) {
PartialShape param_shape{-1, {2, -1}, {-1, 3}, 5};
auto param = make_shared<op::Parameter>(element::f32, param_shape);
auto op = make_op(param, Constant::create(element::i64, {7}, {0, -1, 1, 2, 3, 3, 2}), op::v1::Reverse::Mode::INDEX);
EXPECT_EQ(op->get_element_type(), element::f32);
EXPECT_EQ(op->get_output_partial_shape(0), param_shape);
}
TEST_F(TypePropReverseV1Test, axes_index_is_dynamic) {
PartialShape param_shape{2, {2, 10}, 8};
auto param = make_shared<Parameter>(element::f32, param_shape);
auto axes = make_shared<Parameter>(element::i64, PartialShape::dynamic());
auto op = make_op(param, axes, op::v1::Reverse::Mode::INDEX);
EXPECT_EQ(op->get_element_type(), element::f32);
EXPECT_EQ(op->get_output_partial_shape(0), param_shape);
}
TEST_F(TypePropReverseV1Test, axes_index_interval_1d_tensor) {
PartialShape param_shape{2, {2, 10}, 8};
auto param = make_shared<Parameter>(element::f32, param_shape);
auto axes = make_shared<Parameter>(element::i64, PartialShape{{2, 4}});
auto op = make_op(param, axes, op::v1::Reverse::Mode::INDEX);
EXPECT_EQ(op->get_element_type(), element::f32);
EXPECT_EQ(op->get_output_partial_shape(0), param_shape);
}
TEST_F(TypePropReverseV1Test, default_ctor) {
PartialShape param_shape{2, {2, 10}, 8};
auto param = make_shared<Parameter>(element::f32, param_shape);
auto axes = Constant::create(element::i64, Shape{3}, {2, 0, 1});
auto op = make_op();
op->set_arguments(OutputVector{param, axes});
op->set_mode(op::v1::Reverse::Mode::INDEX);
op->validate_and_infer_types();
EXPECT_EQ(op->get_mode(), op::v1::Reverse::Mode::INDEX);
EXPECT_EQ(op->get_input_size(), 2);
EXPECT_EQ(op->get_output_size(), 1);
EXPECT_EQ(op->get_element_type(), element::f32);
EXPECT_EQ(op->get_output_partial_shape(0), param_shape);
}