diff --git a/src/core/dev_api/openvino/op/ops_decl.hpp b/src/core/dev_api/openvino/op/ops_decl.hpp index f204104d1710ac..7ac67d44c5a69b 100644 --- a/src/core/dev_api/openvino/op/ops_decl.hpp +++ b/src/core/dev_api/openvino/op/ops_decl.hpp @@ -288,4 +288,5 @@ namespace ov::op::v16 { class ISTFT; class Identity; class SegmentMax; +class SparseFillEmptyRows; } // namespace ov::op::v16 diff --git a/src/core/include/openvino/op/ops.hpp b/src/core/include/openvino/op/ops.hpp index adeb9c25611960..dcb5fc0385ecde 100644 --- a/src/core/include/openvino/op/ops.hpp +++ b/src/core/include/openvino/op/ops.hpp @@ -188,6 +188,7 @@ #include "openvino/op/softsign.hpp" #include "openvino/op/space_to_batch.hpp" #include "openvino/op/space_to_depth.hpp" +#include "openvino/op/sparse_fill_empty_rows.hpp" #include "openvino/op/split.hpp" #include "openvino/op/sqrt.hpp" #include "openvino/op/squared_difference.hpp" diff --git a/src/core/include/openvino/op/sparse_fill_empty_rows.hpp b/src/core/include/openvino/op/sparse_fill_empty_rows.hpp new file mode 100644 index 00000000000000..07ead782522655 --- /dev/null +++ b/src/core/include/openvino/op/sparse_fill_empty_rows.hpp @@ -0,0 +1,34 @@ +// Copyright (C) 2018-2025 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "openvino/op/op.hpp" + +namespace ov::op::v16 { +/// \brief An operation which fills empty rows of a sparse tensor with a default value. +/// \ingroup ov_ops_cpp_api +class OPENVINO_API SparseFillEmptyRows : public ov::op::Op { +public: + OPENVINO_OP("SparseFillEmptyRows", "opset16"); + + SparseFillEmptyRows() = default; + + /// \brief Constructs a SparseFillEmptyRows operation. + /// + /// \param indices 2D tensor indicating the positions of values in the sparse tensor. + /// \param values 1D tensor containing the values to be inserted at the specified indices. + /// \param dense_shape 1D tensor indicating the shape of the 2D dense tensor. + /// \param default_value Scalar value to be inserted into the empty rows. + SparseFillEmptyRows(const Output& indices, + const Output& values, + const Output& dense_shape, + const Output& default_value); + + bool visit_attributes(AttributeVisitor& visitor) override; + void validate_and_infer_types() override; + std::shared_ptr clone_with_new_inputs(const OutputVector& new_args) const override; +}; + +} // namespace ov::op::v16 diff --git a/src/core/shape_inference/include/sparse_fill_empty_rows_shape_inference.hpp b/src/core/shape_inference/include/sparse_fill_empty_rows_shape_inference.hpp new file mode 100644 index 00000000000000..d981ad49a9ae5d --- /dev/null +++ b/src/core/shape_inference/include/sparse_fill_empty_rows_shape_inference.hpp @@ -0,0 +1,125 @@ +// Copyright (C) 2018-2025 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include + +#include "openvino/op/sparse_fill_empty_rows.hpp" +#include "utils.hpp" + +namespace ov { +namespace op { +namespace v16 { +template > +std::vector shape_infer(const SparseFillEmptyRows* op, + const std::vector& input_shapes, + const ITensorAccessor& tensor_accessor = make_tensor_accessor()) { + NODE_VALIDATION_CHECK(op, input_shapes.size() == 4); + + const auto& values_shape = input_shapes[0]; + NODE_SHAPE_INFER_CHECK(op, + input_shapes, + values_shape.rank().compatible(1), + "The values input must be a 1D tensor. Got: ", + values_shape); + + const auto& dense_shape = input_shapes[1]; + NODE_SHAPE_INFER_CHECK(op, + input_shapes, + dense_shape.rank().compatible(1), + "The dense_shape input must be a 1D tensor. Got: ", + dense_shape); + if (dense_shape.rank().is_static()) { + NODE_SHAPE_INFER_CHECK( + op, + input_shapes, + dense_shape[0].compatible(2), + "The dense_shape input must have exactly 2 elements. Only 2D sparse tensors are supported."); + } + + const auto& indices_shape = input_shapes[2]; + NODE_SHAPE_INFER_CHECK(op, + input_shapes, + indices_shape.rank().compatible(2), + "The indices input must be a 2D tensor. Got: ", + indices_shape); + + if (indices_shape.rank().is_static()) { + NODE_SHAPE_INFER_CHECK( + op, + input_shapes, + indices_shape[1].compatible(2), + "The indices_shape's second dimension must have 2 elements. Only 2D sparse tensors are supported."); + if (values_shape.rank().is_static()) { + NODE_SHAPE_INFER_CHECK(op, + input_shapes, + indices_shape[0].compatible(values_shape[0]), + "The first dimension of indices must match the size of values."); + } + } + + const auto& default_value_shape = input_shapes[3]; + NODE_SHAPE_INFER_CHECK(op, + input_shapes, + default_value_shape.rank().compatible(0), + "The default_value input must be a scalar. Got: ", + default_value_shape); + + auto output_shapes = std::vector(3); + auto& output_indices_shape = output_shapes[0]; + auto& output_values_shape = output_shapes[1]; + auto& empty_row_indicator_shape = output_shapes[2]; + output_indices_shape.resize(2); + output_values_shape.resize(1); + empty_row_indicator_shape.resize(1); + output_indices_shape[1] = 2; // Only 2D cases are supported + + if (auto dense_shape_value = get_input_const_data_as_shape(op, 1, tensor_accessor)) { + const auto& number_of_rows = (*dense_shape_value)[0].get_length(); + empty_row_indicator_shape[0] = number_of_rows; + + if (auto indices_value = get_input_const_data_as(op, 2, tensor_accessor)) { + // Rows can be referenced multiple times in sparse representation + std::unordered_set existing_rows; + const auto& indices_data = *indices_value; + size_t indices_count = indices_data.size() / 2; + const auto& number_of_cols = (*dense_shape_value)[1].get_length(); + for (size_t i = 0; i < indices_count; ++i) { + int64_t row = indices_data[i * 2]; + NODE_SHAPE_INFER_CHECK(op, + input_shapes, + row >= 0 && row < static_cast(number_of_rows), + "Sparse tensor index out of bounds: row ", + row, + " is outside the valid range [0, ", + number_of_rows - 1, + "]"); + int64_t col = indices_data[i * 2 + 1]; + NODE_SHAPE_INFER_CHECK(op, + input_shapes, + col >= 0 && col < static_cast(number_of_cols), + "Sparse tensor index out of bounds: column ", + col, + " is outside the valid range [0, ", + number_of_cols - 1, + "]"); + existing_rows.insert(row); + } + int64_t empty_rows_count = number_of_rows - existing_rows.size(); + output_indices_shape[0] = indices_shape[0] + empty_rows_count; + output_values_shape[0] = values_shape[0] + empty_rows_count; + } else { + output_indices_shape[0] = Dimension::dynamic(); + output_values_shape[0] = Dimension::dynamic(); + } + } else { + empty_row_indicator_shape[0] = Dimension::dynamic(); + } + + return output_shapes; +} +} // namespace v16 +} // namespace op +} // namespace ov diff --git a/src/core/src/op/sparse_fill_empty_rows.cpp b/src/core/src/op/sparse_fill_empty_rows.cpp new file mode 100644 index 00000000000000..5a78d446aed7d8 --- /dev/null +++ b/src/core/src/op/sparse_fill_empty_rows.cpp @@ -0,0 +1,59 @@ +// Copyright (C) 2018-2025 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/op/sparse_fill_empty_rows.hpp" + +#include "itt.hpp" +#include "openvino/core/validation_util.hpp" +#include "openvino/op/op.hpp" +#include "sparse_fill_empty_rows_shape_inference.hpp" + +namespace ov { +namespace op { +namespace v16 { + +SparseFillEmptyRows::SparseFillEmptyRows(const Output& values, + const Output& dense_shape, + const Output& indices, + const Output& default_value) + : Op({values, dense_shape, indices, default_value}) { + constructor_validate_and_infer_types(); +} + +bool SparseFillEmptyRows::visit_attributes(ov::AttributeVisitor& visitor) { + OV_OP_SCOPE(v16_SparseFillEmptyRows_visit_attributes); + return true; +} + +void SparseFillEmptyRows::validate_and_infer_types() { + OV_OP_SCOPE(v16_SparseFillEmptyRows_validate_and_infer_types); + + const auto& indices_element_type = get_input_element_type(2); + NODE_VALIDATION_CHECK(this, + indices_element_type == element::i32 || indices_element_type == element::i64, + "The element type of the indices input must be i32 or i64. Got: ", + indices_element_type); + + const auto& dense_shape_element_type = get_input_element_type(1); + NODE_VALIDATION_CHECK(this, + dense_shape_element_type == element::i32 || dense_shape_element_type == element::i64, + "The element type of the dense_shape input must be i32 or i64. Got: ", + dense_shape_element_type); + + const auto output_shapes = shape_infer(this, ov::util::get_node_input_partial_shapes(*this)); + + set_output_type(0, indices_element_type, output_shapes[0]); + set_output_type(1, get_input_element_type(0), output_shapes[1]); + set_output_type(2, element::boolean, output_shapes[2]); +} + +std::shared_ptr SparseFillEmptyRows::clone_with_new_inputs(const ov::OutputVector& new_args) const { + OV_OP_SCOPE(v16_SparseFillEmptyRows_clone_with_new_inputs); + check_new_args_count(this, new_args); + return std::make_shared(new_args.at(0), new_args.at(1), new_args.at(2), new_args.at(3)); +} + +} // namespace v16 +} // namespace op +} // namespace ov diff --git a/src/core/tests/type_prop/sparse_fill_empty_rows.cpp b/src/core/tests/type_prop/sparse_fill_empty_rows.cpp new file mode 100644 index 00000000000000..77ead1640968df --- /dev/null +++ b/src/core/tests/type_prop/sparse_fill_empty_rows.cpp @@ -0,0 +1,326 @@ +// Copyright (C) 2018-2025 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "openvino/op/sparse_fill_empty_rows.hpp" + +#include + +#include "common_test_utils/test_assertions.hpp" +#include "common_test_utils/type_prop.hpp" +#include "openvino/op/constant.hpp" +#include "openvino/op/parameter.hpp" + +namespace ov::test { +using testing::HasSubstr; + +class TypePropSparseFillEmptyRowsTest : public TypePropOpTest {}; + +TEST_F(TypePropSparseFillEmptyRowsTest, default_ctor_valid_inputs) { + const auto values = std::make_shared(element::f32, PartialShape{3}); + const auto dense_shape = std::make_shared(element::i32, PartialShape{2}); + const auto indices = std::make_shared(element::i32, PartialShape{3, 2}); + const auto default_value = std::make_shared(element::f32, PartialShape{}); + + const auto op = make_op(values, dense_shape, indices, default_value); + op->validate_and_infer_types(); + + EXPECT_EQ(op->get_output_element_type(0), element::i32); + EXPECT_EQ(op->get_output_element_type(1), element::f32); + EXPECT_EQ(op->get_output_element_type(2), element::boolean); + + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{Dimension::dynamic(), 2})); + EXPECT_EQ(op->get_output_partial_shape(1), (PartialShape{Dimension::dynamic()})); + EXPECT_EQ(op->get_output_partial_shape(2), (PartialShape{Dimension::dynamic()})); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, with_const_inputs) { + const auto values = std::make_shared(element::f32, PartialShape{2}); + const auto dense_shape = std::make_shared(element::i32, Shape{2}, std::vector{3, 3}); + const auto indices = + std::make_shared(element::i32, Shape{2, 2}, std::vector{0, 0, 2, 2}); + const auto default_value = std::make_shared(element::f32, PartialShape{}); + + const auto op = make_op(values, dense_shape, indices, default_value); + op->validate_and_infer_types(); + + EXPECT_EQ(op->get_output_element_type(0), element::i32); + EXPECT_EQ(op->get_output_element_type(1), element::f32); + EXPECT_EQ(op->get_output_element_type(2), element::boolean); + + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{3, 2})); + EXPECT_EQ(op->get_output_partial_shape(1), (PartialShape{3})); + EXPECT_EQ(op->get_output_partial_shape(2), (PartialShape{3})); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, dynamic_shapes) { + const auto values = std::make_shared(element::f32, PartialShape::dynamic()); + const auto dense_shape = std::make_shared(element::i32, PartialShape::dynamic()); + const auto indices = std::make_shared(element::i32, PartialShape::dynamic()); + const auto default_value = std::make_shared(element::f32, PartialShape::dynamic()); + + const auto op = make_op(values, dense_shape, indices, default_value); + op->validate_and_infer_types(); + + EXPECT_EQ(op->get_output_element_type(0), element::i32); + EXPECT_EQ(op->get_output_element_type(1), element::f32); + EXPECT_EQ(op->get_output_element_type(2), element::boolean); + + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{Dimension::dynamic(), 2})); + EXPECT_EQ(op->get_output_partial_shape(1), (PartialShape{Dimension::dynamic()})); + EXPECT_EQ(op->get_output_partial_shape(2), (PartialShape{Dimension::dynamic()})); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, partially_dynamic_shapes) { + const auto values = std::make_shared(element::f32, PartialShape{Dimension::dynamic()}); + const auto dense_shape = std::make_shared(element::i64, PartialShape{2}); + const auto indices = std::make_shared(element::i64, PartialShape{Dimension::dynamic(), 2}); + const auto default_value = std::make_shared(element::f32, PartialShape{}); + + const auto op = make_op(values, dense_shape, indices, default_value); + op->validate_and_infer_types(); + + EXPECT_EQ(op->get_output_element_type(0), element::i64); + EXPECT_EQ(op->get_output_element_type(1), element::f32); + EXPECT_EQ(op->get_output_element_type(2), element::boolean); + + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{Dimension::dynamic(), 2})); + EXPECT_EQ(op->get_output_partial_shape(1), (PartialShape{Dimension::dynamic()})); + EXPECT_EQ(op->get_output_partial_shape(2), (PartialShape{Dimension::dynamic()})); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, known_dense_shape) { + const auto values = std::make_shared(element::f32, PartialShape{Dimension::dynamic()}); + const auto dense_shape = std::make_shared(element::i32, Shape{2}, std::vector{5, 4}); + const auto indices = std::make_shared(element::i32, PartialShape{Dimension::dynamic(), 2}); + const auto default_value = std::make_shared(element::f32, PartialShape{}); + + const auto op = make_op(values, dense_shape, indices, default_value); + op->validate_and_infer_types(); + + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{Dimension::dynamic(), 2})); + EXPECT_EQ(op->get_output_partial_shape(1), (PartialShape{Dimension::dynamic()})); + EXPECT_EQ(op->get_output_partial_shape(2), (PartialShape{5})); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, known_values) { + const auto values = + std::make_shared(element::f32, Shape{3}, std::vector{1.0f, 2.0f, 3.0f}); + const auto dense_shape = std::make_shared(element::i32, PartialShape{2}); + const auto indices = std::make_shared(element::i32, PartialShape{3, 2}); + const auto default_value = std::make_shared(element::f32, PartialShape{}); + + const auto op = make_op(values, dense_shape, indices, default_value); + op->validate_and_infer_types(); + + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{Dimension::dynamic(), 2})); + EXPECT_EQ(op->get_output_partial_shape(1), (PartialShape{Dimension::dynamic()})); + EXPECT_EQ(op->get_output_partial_shape(2), (PartialShape{Dimension::dynamic()})); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, known_indices) { + const auto values = std::make_shared(element::f32, PartialShape{3}); + const auto dense_shape = std::make_shared(element::i32, PartialShape{2}); + const auto indices = + std::make_shared(element::i32, Shape{3, 2}, std::vector{0, 0, 1, 0, 2, 0}); + const auto default_value = std::make_shared(element::f32, PartialShape{}); + + const auto op = make_op(values, dense_shape, indices, default_value); + op->validate_and_infer_types(); + + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{Dimension::dynamic(), 2})); + EXPECT_EQ(op->get_output_partial_shape(1), (PartialShape{Dimension::dynamic()})); + EXPECT_EQ(op->get_output_partial_shape(2), (PartialShape{Dimension::dynamic()})); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, known_default_value) { + const auto values = std::make_shared(element::f32, PartialShape{3}); + const auto dense_shape = std::make_shared(element::i32, PartialShape{2}); + const auto indices = std::make_shared(element::i32, PartialShape{3, 2}); + const auto default_value = std::make_shared(element::f32, Shape{}, std::vector{0.0f}); + + const auto op = make_op(values, dense_shape, indices, default_value); + op->validate_and_infer_types(); + + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{Dimension::dynamic(), 2})); + EXPECT_EQ(op->get_output_partial_shape(1), (PartialShape{Dimension::dynamic()})); + EXPECT_EQ(op->get_output_partial_shape(2), (PartialShape{Dimension::dynamic()})); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, dense_shape_from_graph_shapeof) { + const auto values = std::make_shared(element::f32, PartialShape{3}); + const auto some_subgraph_result = std::make_shared(element::f32, PartialShape{5, 4}); + const auto shape_of = std::make_shared(some_subgraph_result, element::i32); + const auto indices = + std::make_shared(element::i32, Shape{3, 2}, std::vector{0, 0, 1, 0, 4, 0}); + const auto default_value = std::make_shared(element::f32, PartialShape{}); + + const auto op = make_op(values, shape_of, indices, default_value); + op->validate_and_infer_types(); + + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{5, 2})); + EXPECT_EQ(op->get_output_partial_shape(1), (PartialShape{5})); + EXPECT_EQ(op->get_output_partial_shape(2), (PartialShape{5})); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, symbol_propagation) { + PartialShape values_shape{Dimension::dynamic()}; + PartialShape dense_shape_shape{2}; + PartialShape indices_shape{Dimension::dynamic(), 2}; + auto indices_symbols = set_shape_symbols(indices_shape); + + PartialShape default_value_shape{}; + const auto values = std::make_shared(element::f32, values_shape); + const auto dense_shape = std::make_shared(element::i32, dense_shape_shape); + const auto indices = std::make_shared(element::i32, indices_shape); + const auto default_value = std::make_shared(element::f32, default_value_shape); + + const auto op = make_op(values, dense_shape, indices, default_value); + op->validate_and_infer_types(); + + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{Dimension::dynamic(), 2})); + EXPECT_EQ(op->get_output_partial_shape(1), (PartialShape{Dimension::dynamic()})); + EXPECT_EQ(op->get_output_partial_shape(2), (PartialShape{Dimension::dynamic()})); + + EXPECT_THAT(get_shape_symbols(op->get_output_partial_shape(0)), testing::ElementsAre(nullptr, nullptr)); + EXPECT_THAT(get_shape_symbols(op->get_output_partial_shape(1)), testing::ElementsAre(nullptr)); + EXPECT_THAT(get_shape_symbols(op->get_output_partial_shape(2)), testing::ElementsAre(nullptr)); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, interval_shapes) { + const auto values = std::make_shared(element::f32, PartialShape{{3, 6}}); + const auto dense_shape = std::make_shared(element::i32, Shape{2}, std::vector{8, 4}); + const auto indices = std::make_shared(element::i32, PartialShape{{3, 6}, 2}); + const auto default_value = std::make_shared(element::f32, PartialShape{}); + + const auto op = make_op(values, dense_shape, indices, default_value); + op->validate_and_infer_types(); + EXPECT_EQ(op->get_output_partial_shape(0), (PartialShape{Dimension::dynamic(), 2})); + EXPECT_EQ(op->get_output_partial_shape(1), (PartialShape{Dimension::dynamic()})); + EXPECT_EQ(op->get_output_partial_shape(2), (PartialShape{8})); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, invalid_values_rank) { + const auto values = std::make_shared(element::f32, PartialShape{3, 4}); + const auto dense_shape = std::make_shared(element::i32, PartialShape{2}); + const auto indices = std::make_shared(element::i32, PartialShape{3, 2}); + const auto default_value = std::make_shared(element::f32, PartialShape{}); + + OV_EXPECT_THROW(std::ignore = make_op(values, dense_shape, indices, default_value), + ov::NodeValidationFailure, + HasSubstr("The values input must be a 1D tensor")); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, invalid_dense_shape_rank) { + const auto values = std::make_shared(element::f32, PartialShape{3}); + const auto dense_shape = std::make_shared(element::i32, PartialShape{2, 2}); + const auto indices = std::make_shared(element::i32, PartialShape{3, 2}); + const auto default_value = std::make_shared(element::f32, PartialShape{}); + + OV_EXPECT_THROW(std::ignore = make_op(values, dense_shape, indices, default_value), + ov::NodeValidationFailure, + HasSubstr("The dense_shape input must be a 1D tensor")); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, invalid_indices_rank) { + const auto values = std::make_shared(element::f32, PartialShape{3}); + const auto dense_shape = std::make_shared(element::i32, PartialShape{2}); + const auto indices = std::make_shared(element::i32, PartialShape{3}); + const auto default_value = std::make_shared(element::f32, PartialShape{}); + + OV_EXPECT_THROW(std::ignore = make_op(values, dense_shape, indices, default_value), + ov::NodeValidationFailure, + HasSubstr("The indices input must be a 2D tensor")); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, invalid_default_value_rank) { + const auto values = std::make_shared(element::f32, PartialShape{3}); + const auto dense_shape = std::make_shared(element::i32, PartialShape{2}); + const auto indices = std::make_shared(element::i32, PartialShape{3, 2}); + const auto default_value = std::make_shared(element::f32, PartialShape{1}); + + OV_EXPECT_THROW(std::ignore = make_op(values, dense_shape, indices, default_value), + ov::NodeValidationFailure, + HasSubstr("The default_value input must be a scalar")); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, invalid_indices_dimension) { + const auto values = std::make_shared(element::f32, PartialShape{3}); + const auto dense_shape = std::make_shared(element::i32, PartialShape{2}); + const auto indices = std::make_shared(element::i32, PartialShape{3, 3}); + const auto default_value = std::make_shared(element::f32, PartialShape{}); + + OV_EXPECT_THROW(std::ignore = make_op(values, dense_shape, indices, default_value), + ov::NodeValidationFailure, + HasSubstr("The indices_shape's second dimension must have 2 elements")); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, invalid_indices_element_type) { + const auto values = std::make_shared(element::f32, PartialShape{3}); + const auto dense_shape = std::make_shared(element::i32, PartialShape{2}); + const auto indices = std::make_shared(element::f32, PartialShape{3, 2}); + const auto default_value = std::make_shared(element::f32, PartialShape{}); + + OV_EXPECT_THROW(std::ignore = make_op(values, dense_shape, indices, default_value), + ov::NodeValidationFailure, + HasSubstr("The element type of the indices input must be i32 or i64")); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, invalid_dense_shape_element_type) { + const auto values = std::make_shared(element::f32, PartialShape{3}); + const auto dense_shape = std::make_shared(element::f32, PartialShape{2}); + const auto indices = std::make_shared(element::i32, PartialShape{3, 2}); + const auto default_value = std::make_shared(element::f32, PartialShape{}); + + OV_EXPECT_THROW(std::ignore = make_op(values, dense_shape, indices, default_value), + ov::NodeValidationFailure, + HasSubstr("The element type of the dense_shape input must be i32 or i64")); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, mismatch_values_and_indices_dimensions) { + const auto values = std::make_shared(element::f32, PartialShape{4}); + const auto dense_shape = std::make_shared(element::i32, PartialShape{2}); + const auto indices = std::make_shared(element::i32, PartialShape{3, 2}); + const auto default_value = std::make_shared(element::f32, PartialShape{}); + + OV_EXPECT_THROW(std::ignore = make_op(values, dense_shape, indices, default_value), + ov::NodeValidationFailure, + HasSubstr("The first dimension of indices must match the size of values")); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, incorrect_dense_shape_dimension) { + const auto values = std::make_shared(element::f32, PartialShape{3}); + const auto dense_shape = std::make_shared(element::i32, Shape{3}, std::vector{3, 4, 5}); + const auto indices = std::make_shared(element::i32, PartialShape{3, 2}); + const auto default_value = std::make_shared(element::f32, PartialShape{}); + + OV_EXPECT_THROW(std::ignore = make_op(values, dense_shape, indices, default_value), + ov::NodeValidationFailure, + HasSubstr("The dense_shape input must have exactly 2 elements")); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, out_of_bounds_row_index) { + const auto values = std::make_shared(element::f32, PartialShape{3}); + const auto dense_shape = std::make_shared(element::i32, Shape{2}, std::vector{3, 4}); + const auto indices = + std::make_shared(element::i32, Shape{3, 2}, std::vector{0, 0, 1, 0, 3, 0}); + const auto default_value = std::make_shared(element::f32, PartialShape{}); + + OV_EXPECT_THROW(std::ignore = make_op(values, dense_shape, indices, default_value), + ov::NodeValidationFailure, + HasSubstr("Sparse tensor index out of bounds: row 3 is outside the valid range [0, 2]")); +} + +TEST_F(TypePropSparseFillEmptyRowsTest, out_of_bounds_column_index) { + const auto values = std::make_shared(element::f32, PartialShape{3}); + const auto dense_shape = std::make_shared(element::i32, Shape{2}, std::vector{3, 4}); + const auto indices = + std::make_shared(element::i32, Shape{3, 2}, std::vector{0, 0, 1, 4, 2, 0}); + const auto default_value = std::make_shared(element::f32, PartialShape{}); + + OV_EXPECT_THROW(std::ignore = make_op(values, dense_shape, indices, default_value), + ov::NodeValidationFailure, + HasSubstr("Sparse tensor index out of bounds: column 4 is outside the valid range [0, 3]")); +} + +} // namespace ov::test diff --git a/src/core/tests/visitors/op/sparse_fill_empty_rows.cpp b/src/core/tests/visitors/op/sparse_fill_empty_rows.cpp new file mode 100644 index 00000000000000..fd9bbf618db801 --- /dev/null +++ b/src/core/tests/visitors/op/sparse_fill_empty_rows.cpp @@ -0,0 +1,24 @@ +// Copyright (C) 2018-2025 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +#include "openvino/op/sparse_fill_empty_rows.hpp" + +#include + +#include "visitors/visitors.hpp" + +TEST(attributes, sparse_fill_empty_rows_op) { + ov::test::NodeBuilder::opset().insert(); + const auto values = std::make_shared(ov::element::f32, ov::Shape{3}); + const auto dense_shape = std::make_shared(ov::element::i32, ov::Shape{2}); + const auto indices = std::make_shared(ov::element::i32, ov::Shape{3, 2}); + const auto default_value = std::make_shared(ov::element::f32, ov::Shape{}); + + const auto sparse_fill_empty_rows = + std::make_shared(values, dense_shape, indices, default_value); + + ov::test::NodeBuilder builder(sparse_fill_empty_rows, {values, dense_shape, indices, default_value}); + ASSERT_NO_THROW(std::ignore = ov::as_type_ptr(builder.create())); + const auto expected_attr_count = 0; + EXPECT_EQ(builder.get_value_map_size(), expected_attr_count); +} diff --git a/src/plugins/intel_cpu/src/shape_inference/shape_inference.cpp b/src/plugins/intel_cpu/src/shape_inference/shape_inference.cpp index ecbc796f535614..c9d2421728c398 100644 --- a/src/plugins/intel_cpu/src/shape_inference/shape_inference.cpp +++ b/src/plugins/intel_cpu/src/shape_inference/shape_inference.cpp @@ -119,6 +119,7 @@ #include "slice_shape_inference.hpp" #include "space_to_batch_shape_inference.hpp" #include "space_to_depth_shape_inference.hpp" +#include "sparse_fill_empty_rows_shape_inference.hpp" #include "split_shape_inference.hpp" #include "squeeze_shape_inference.hpp" #include "static_shape.hpp" @@ -448,6 +449,7 @@ const IStaticShapeInferFactory::TRegistry IStaticShapeInferFactory::registry{ // opset16 OV_OP_SHAPE_INFER_MASK_REG(op::v16::ISTFT, ShapeInferTA, util::bit::mask(2, 3, 4)), OV_OP_SHAPE_INFER_MASK_REG(op::v16::SegmentMax, ShapeInferTA, util::bit::mask(1, 2)), + OV_OP_SHAPE_INFER_MASK_REG(op::v16::SparseFillEmptyRows, ShapeInferTA, util::bit::mask(1, 2)), // opset15 OV_OP_SHAPE_INFER_MASK_REG(op::v15::Squeeze, ShapeInferTA, util::bit::mask(1)), OV_OP_SHAPE_INFER_MASK_REG(op::v15::SearchSorted, ShapeInferTA, util::bit::mask()), diff --git a/src/plugins/intel_cpu/tests/unit/shape_inference_test/sparse_fill_empty_rows_shape_inference_test.cpp b/src/plugins/intel_cpu/tests/unit/shape_inference_test/sparse_fill_empty_rows_shape_inference_test.cpp new file mode 100644 index 00000000000000..10aeecdb816935 --- /dev/null +++ b/src/plugins/intel_cpu/tests/unit/shape_inference_test/sparse_fill_empty_rows_shape_inference_test.cpp @@ -0,0 +1,162 @@ +// Copyright (C) 2018-2025 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include "common_test_utils/test_assertions.hpp" +#include "utils.hpp" +#include "openvino/op/sparse_fill_empty_rows.hpp" +#include "openvino/op/constant.hpp" + +using namespace ov::intel_cpu; +using ov::op::v0::Constant, ov::op::v0::Parameter; +using testing::HasSubstr; + +struct SparseFillEmptyRowsTestParams { + ov::Shape values_shape; + ov::Shape dense_shape_shape; + ov::Shape indices_shape; + ov::Shape default_value_shape; + std::vector dense_shape_val; + std::vector indices_val; + ov::Shape expected_output_indices_shape; + ov::Shape expected_output_values_shape; + ov::Shape expected_empty_row_indicator_shape; +}; + +class SparseFillEmptyRowsStaticShapeInferenceTest: public OpStaticShapeInferenceTest {}; + +class SparseFillEmptyRowsStaticTestSuite : public SparseFillEmptyRowsStaticShapeInferenceTest, + public ::testing::WithParamInterface {}; + +TEST_F(SparseFillEmptyRowsStaticShapeInferenceTest, input_from_tensor_accessor) { + const auto values = std::make_shared(ov::element::f32, ov::PartialShape::dynamic()); + const auto dense_shape = std::make_shared(ov::element::i32, ov::PartialShape::dynamic()); + const auto indices = std::make_shared(ov::element::i32, ov::PartialShape::dynamic()); + const auto default_value = std::make_shared(ov::element::f32, ov::PartialShape::dynamic()); + const auto op = make_op(values, dense_shape, indices, default_value); + + float values_val[] = {1.0f, 2.0f, 3.0f, 4.0f}; + int32_t dense_shape_val[] = {5, 6}; + int32_t indices_val[] = {0, 1, 0, 3, 2, 0, 3, 1}; + float default_value_val[] = {0.0f}; + + auto const_inputs = std::unordered_map{ + {0, {ov::element::f32, ov::Shape{4}, values_val}}, + {1, {ov::element::i32, ov::Shape{2}, dense_shape_val}}, + {2, {ov::element::i32, ov::Shape{4, 2}, indices_val}}, + {3, {ov::element::f32, ov::Shape{}, default_value_val}} + }; + + const auto input_shapes = StaticShapeVector{{4}, {2}, {4, 2}, {}}; + auto shape_infer = make_shape_inference(op); + const auto input_shape_refs = make_static_shape_refs(input_shapes); + const auto output_shapes = *shape_infer->infer(input_shape_refs, ov::make_tensor_accessor(const_inputs)); + + EXPECT_EQ(output_shapes.size(), 3); + EXPECT_EQ(output_shapes[0], StaticShape({6, 2})); + EXPECT_EQ(output_shapes[1], StaticShape({6})); + EXPECT_EQ(output_shapes[2], StaticShape({5})); +} + +TEST_F(SparseFillEmptyRowsStaticShapeInferenceTest, static_shapes) { + const auto values = std::make_shared(ov::element::f32, ov::Shape{4}); + const auto dense_shape = std::make_shared(ov::element::i32, ov::Shape{2}); + const auto indices = std::make_shared(ov::element::i32, ov::Shape{4, 2}); + const auto default_value = std::make_shared(ov::element::f32, ov::Shape{}); + const auto op = make_op(values, dense_shape, indices, default_value); + + int32_t dense_shape_val[] = {8, 5}; + int32_t indices_val[] = {0, 1, 0, 3, 2, 0, 3, 1}; + auto const_inputs = std::unordered_map{ + {1, {ov::element::i32, ov::Shape{2}, dense_shape_val}}, + {2, {ov::element::i32, ov::Shape{4, 2}, indices_val}} + }; + + const auto input_shapes = StaticShapeVector{{5}, {2}, {5, 2}, {}}; + auto shape_infer = make_shape_inference(op); + const auto input_shape_refs = make_static_shape_refs(input_shapes); + const auto output_shapes = *shape_infer->infer(input_shape_refs, ov::make_tensor_accessor(const_inputs)); + + EXPECT_EQ(output_shapes.size(), 3); + EXPECT_EQ(output_shapes[0], StaticShape({10, 2})); + EXPECT_EQ(output_shapes[1], StaticShape({10})); + EXPECT_EQ(output_shapes[2], StaticShape({8})); +} + +TEST_P(SparseFillEmptyRowsStaticTestSuite, sparse_fill_empty_rows_static_shape_inference) { + const auto& [values_shape, dense_shape_shape, indices_shape, default_value_shape, + dense_shape_val, indices_val, + expected_output_indices_shape, expected_output_values_shape, expected_empty_row_indicator_shape] = GetParam(); + + const auto values = std::make_shared(ov::element::f32, values_shape); + const auto dense_shape = std::make_shared(ov::element::i32, dense_shape_shape, dense_shape_val); + const auto indices = std::make_shared(ov::element::i32, indices_shape, indices_val); + const auto default_value = std::make_shared(ov::element::f32, default_value_shape); + + const auto op = make_op(values, dense_shape, indices, default_value); + + const auto input_shapes = StaticShapeVector{values_shape, dense_shape_shape, indices_shape, default_value_shape}; + auto shape_infer = make_shape_inference(op); + const auto input_shape_refs = make_static_shape_refs(input_shapes); + const auto output_shapes = *shape_infer->infer(input_shape_refs, ov::make_tensor_accessor()); + + EXPECT_EQ(output_shapes.size(), 3); + EXPECT_EQ(output_shapes[0], StaticShape(expected_output_indices_shape)); + EXPECT_EQ(output_shapes[1], StaticShape(expected_output_values_shape)); + EXPECT_EQ(output_shapes[2], StaticShape(expected_empty_row_indicator_shape)); +} + +INSTANTIATE_TEST_SUITE_P(SparseFillEmptyRowsStaticShapeInferenceTests, + SparseFillEmptyRowsStaticTestSuite, + ::testing::Values( + // No empty rows + SparseFillEmptyRowsTestParams{ + ov::Shape{3}, // values_shape + ov::Shape{2}, // dense_shape_shape + ov::Shape{3, 2}, // indices_shape + ov::Shape{}, // default_value_shape + std::vector{3, 4}, // dense_shape_val + std::vector{0, 0, 1, 0, 2, 0}, // indices_val + ov::Shape{3, 2}, // expected_output_indices_shape + ov::Shape{3}, // expected_output_values_shape + ov::Shape{3} // expected_empty_row_indicator_shape + }, + // One empty row in the middle + SparseFillEmptyRowsTestParams{ + ov::Shape{3}, // values_shape + ov::Shape{2}, // dense_shape_shape + ov::Shape{3, 2}, // indices_shape + ov::Shape{}, // default_value_shape + std::vector{4, 4}, // dense_shape_val + std::vector{0, 0, 1, 0, 3, 0}, // indices_val + ov::Shape{4, 2}, // expected_output_indices_shape + ov::Shape{4}, // expected_output_values_shape + ov::Shape{4} // expected_empty_row_indicator_shape + }, + // Multiple empty rows + SparseFillEmptyRowsTestParams{ + ov::Shape{2}, // values_shape + ov::Shape{2}, // dense_shape_shape + ov::Shape{2, 2}, // indices_shape + ov::Shape{}, // default_value_shape + std::vector{5, 3}, // dense_shape_val + std::vector{0, 0, 4, 0}, // indices_val + ov::Shape{5, 2}, // expected_output_indices_shape + ov::Shape{5}, // expected_output_values_shape + ov::Shape{5} // expected_empty_row_indicator_shape + }, + // All rows empty + SparseFillEmptyRowsTestParams{ + ov::Shape{0}, // values_shape + ov::Shape{2}, // dense_shape_shape + ov::Shape{0, 2}, // indices_shape + ov::Shape{}, // default_value_shape + std::vector{3, 4}, // dense_shape_val + std::vector{}, // indices_val + ov::Shape{3, 2}, // expected_output_indices_shape + ov::Shape{3}, // expected_output_values_shape + ov::Shape{3} // expected_empty_row_indicator_shape + } +));