diff --git a/src/core/include/openvino/op/convert_like.hpp b/src/core/include/openvino/op/convert_like.hpp index 9329fafac0e3..18a4a4ac0797 100644 --- a/src/core/include/openvino/op/convert_like.hpp +++ b/src/core/include/openvino/op/convert_like.hpp @@ -28,6 +28,9 @@ class OPENVINO_API ConvertLike : public Op { bool constant_fold(OutputVector& output_values, const OutputVector& input_values) override; bool can_constant_fold(const OutputVector& inputs_values) const override; + bool evaluate_lower(TensorVector& outputs) const override; + bool evaluate_upper(TensorVector& outputs) const override; + bool evaluate_symbol(TensorSymbolVector& output_symbols) const override; }; } // namespace v1 } // namespace op diff --git a/src/core/src/op/convert_like.cpp b/src/core/src/op/convert_like.cpp index 227bd13a35e2..dbc991dc3961 100644 --- a/src/core/src/op/convert_like.cpp +++ b/src/core/src/op/convert_like.cpp @@ -46,6 +46,20 @@ bool ConvertLike::constant_fold(OutputVector& output_values, const OutputVector& return false; } +bool ConvertLike::evaluate_lower(TensorVector& outputs) const { + // ConvertLike(data, like) is Convert(data, like.element_type); reuse Convert's bound evaluation so the + // value bounds propagate through this node (otherwise downstream bounds-based shape inference stalls). + return v0::Convert(input_value(0), get_output_element_type(0)).evaluate_lower(outputs); +} + +bool ConvertLike::evaluate_upper(TensorVector& outputs) const { + return v0::Convert(input_value(0), get_output_element_type(0)).evaluate_upper(outputs); +} + +bool ConvertLike::evaluate_symbol(TensorSymbolVector& output_symbols) const { + return v0::Convert(input_value(0), get_output_element_type(0)).evaluate_symbol(output_symbols); +} + } // namespace v1 } // namespace op } // namespace ov diff --git a/src/core/tests/bound_evaluate.cpp b/src/core/tests/bound_evaluate.cpp index 484179ad7f4e..650d9e9209ff 100644 --- a/src/core/tests/bound_evaluate.cpp +++ b/src/core/tests/bound_evaluate.cpp @@ -9,6 +9,7 @@ #include "common_test_utils/test_assertions.hpp" #include "common_test_utils/type_prop.hpp" #include "openvino/op/concat.hpp" +#include "openvino/op/convert_like.hpp" #include "openvino/op/subtract.hpp" #include "openvino/op/util/framework_node.hpp" @@ -86,3 +87,29 @@ TEST(BoundEvaluatorTest, no_exception_on_single_bound) { OV_ASSERT_NO_THROW(sub->evaluate_upper(output)); EXPECT_EQ(o_[0], 10); } + +// ConvertLike must propagate value bounds (like its v0::Convert sibling), so that a +// ConvertLike(ShapeOf(static), like) node yields a concrete bound during value propagation. Without it, +// bounds-based shape inference (e.g. v8::Slice `stop`) leaves downstream shapes dynamic even when the +// value is statically determinable. +TEST(BoundEvaluatorTest, convert_like_propagates_bounds_from_shape_of) { + const auto data = std::make_shared(element::f32, PartialShape{1, 128, 4, 128}); + const auto shape_of = std::make_shared(data); // i64: [1, 128, 4, 128] + const auto like = Constant::create(element::i32, Shape{4}, {0, 0, 0, 0}); + const auto convert_like = std::make_shared(shape_of, like); + + const auto bounds = ov::util::evaluate_both_bounds(convert_like); + ASSERT_TRUE(static_cast(bounds.first)); + ASSERT_TRUE(static_cast(bounds.second)); + EXPECT_EQ(bounds.first.get_element_type(), element::i32); + EXPECT_EQ(bounds.second.get_element_type(), element::i32); + + const std::vector expected{1, 128, 4, 128}; + ASSERT_EQ(bounds.first.get_shape(), ov::Shape{expected.size()}); + ASSERT_EQ(bounds.second.get_shape(), ov::Shape{expected.size()}); + + const auto* lower = bounds.first.data(); + const auto* upper = bounds.second.data(); + EXPECT_EQ(std::vector(lower, lower + expected.size()), expected); + EXPECT_EQ(std::vector(upper, upper + expected.size()), expected); +} diff --git a/src/core/tests/type_prop/slice.cpp b/src/core/tests/type_prop/slice.cpp index 950cd71e5757..b623b28e04d8 100644 --- a/src/core/tests/type_prop/slice.cpp +++ b/src/core/tests/type_prop/slice.cpp @@ -8,7 +8,14 @@ #include "common_test_utils/test_assertions.hpp" #include "common_test_utils/type_prop.hpp" +#include "openvino/op/add.hpp" #include "openvino/op/broadcast.hpp" +#include "openvino/op/convert.hpp" +#include "openvino/op/convert_like.hpp" +#include "openvino/op/less.hpp" +#include "openvino/op/minimum.hpp" +#include "openvino/op/select.hpp" +#include "openvino/op/shape_of.hpp" #include "openvino/op/subtract.hpp" #include "sequence_generator.hpp" @@ -1180,6 +1187,79 @@ TEST(type_prop, slice_v8_start_stop_is_shape_of_with_bounds) { EXPECT_THAT(get_shape_symbols(slice->get_output_partial_shape(0)), Each(nullptr)); } +// Reproduces the TF/TFLite Slice `stop` cascade with statically determinable inputs: +// stop = Select(Less(size, 0), ConvertLike(ShapeOf(data), size), Add(start, size)) +// `start`/`size`/`step` are static Constants and `data` is a Parameter with a static shape, so the whole +// `stop` subgraph is statically determinable via bounds propagation. Slice shape inference resolves +// `stop` via bounds propagation (evaluate_lower/upper), not ConstantFolding, so the output must come out +// static [1,128,4,128] without any folding pass. This exercises ConvertLike value bound evaluation: if +// ConvertLike does not propagate bounds, the propagation breaks before Select and the Slice output is +// left dynamic. +TEST(type_prop, slice_v8_stop_select_cascade_static_inputs_is_static) { + constexpr auto et = element::i32; + const auto data = std::make_shared(element::f32, PartialShape{1, 128, 8, 256}); + const auto start = op::v0::Constant::create(et, Shape{4}, {0, 0, 0, 0}); + const auto size = op::v0::Constant::create(et, Shape{4}, {1, 128, 4, 128}); + const auto step = op::v0::Constant::create(et, Shape{4}, {1, 1, 1, 1}); + + const auto zero = op::v0::Constant::create(et, Shape{}, {0}); + const auto negative_size_mask = std::make_shared(size, zero); + const auto shape_of = std::make_shared(data); + const auto stop_neg = std::make_shared(shape_of, size); + const auto stop_pos = std::make_shared(start, size); + const auto stop = std::make_shared(negative_size_mask, stop_neg, stop_pos); + + const auto slice = std::make_shared(data, start, stop, step); + + EXPECT_TRUE(slice->get_output_partial_shape(0).is_static()); + EXPECT_EQ(slice->get_output_partial_shape(0), PartialShape({1, 128, 4, 128})); +} + +// Control for the cascade test above: a Slice whose `stop` is Minimum(ShapeOf(data), const) already +// resolves to a static output, because Minimum (BinaryElementwiseArithmetic) supports bound evaluation. +// This pins the dynamism to ops that lack bound evaluation (the Select+ConvertLike cascade), not to +// ShapeOf usage in general. +TEST(type_prop, slice_v8_stop_minimum_shapeof_is_static) { + constexpr auto et = element::i64; + const auto data = std::make_shared(element::f32, PartialShape{1, 128, 8, 256}); + const auto start = op::v0::Constant::create(et, Shape{4}, {0, 0, 0, 0}); + const auto step = op::v0::Constant::create(et, Shape{4}, {1, 1, 1, 1}); + + const auto shape_of = std::make_shared(data); + const auto cap = op::v0::Constant::create(et, Shape{4}, {1, 128, 4, 128}); + const auto stop = std::make_shared(shape_of, cap); + + const auto slice = std::make_shared(data, start, stop, step); + + EXPECT_TRUE(slice->get_output_partial_shape(0).is_static()); + EXPECT_EQ(slice->get_output_partial_shape(0), PartialShape({1, 128, 4, 128})); +} + +// Control pinning the cascade dynamism to ConvertLike specifically: the same cascade as +// slice_v8_stop_select_cascade_static_inputs_is_static, but with the `size < 0` branch built from +// v0::Convert instead of v1::ConvertLike. v0::Convert has always implemented value bound evaluation +// (evaluate_lower/upper/symbol), so this Slice resolves to a static output regardless of ConvertLike's +// own bound-evaluation support. It is the behavioral reference that ConvertLike now mirrors. +TEST(type_prop, slice_v8_stop_select_cascade_convert_is_static) { + constexpr auto et = element::i32; + const auto data = std::make_shared(element::f32, PartialShape{1, 128, 8, 256}); + const auto start = op::v0::Constant::create(et, Shape{4}, {0, 0, 0, 0}); + const auto size = op::v0::Constant::create(et, Shape{4}, {1, 128, 4, 128}); + const auto step = op::v0::Constant::create(et, Shape{4}, {1, 1, 1, 1}); + + const auto zero = op::v0::Constant::create(et, Shape{}, {0}); + const auto negative_size_mask = std::make_shared(size, zero); + const auto shape_of = std::make_shared(data); + const auto stop_neg = std::make_shared(shape_of, et); + const auto stop_pos = std::make_shared(start, size); + const auto stop = std::make_shared(negative_size_mask, stop_neg, stop_pos); + + const auto slice = std::make_shared(data, start, stop, step); + + EXPECT_TRUE(slice->get_output_partial_shape(0).is_static()); + EXPECT_EQ(slice->get_output_partial_shape(0), PartialShape({1, 128, 4, 128})); +} + TEST(type_prop, slice_v8_unknowns_axes) { const auto data = std::make_shared(element::i64, Shape{5, 10, 15}); const auto start = std::make_shared(element::i64, PartialShape{-1});