Skip to content

Commit 6b2ca78

Browse files
authored
Add SparseFillEmptyRows-16 to Core (#30191)
Related PRs: - #28046 - #30307 ### Tickets: - CVS-158909 --------- Signed-off-by: p-wysocki <[email protected]>
1 parent 67af7c4 commit 6b2ca78

File tree

9 files changed

+713
-0
lines changed

9 files changed

+713
-0
lines changed

src/core/dev_api/openvino/op/ops_decl.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,4 +288,5 @@ namespace ov::op::v16 {
288288
class ISTFT;
289289
class Identity;
290290
class SegmentMax;
291+
class SparseFillEmptyRows;
291292
} // namespace ov::op::v16

src/core/include/openvino/op/ops.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@
188188
#include "openvino/op/softsign.hpp"
189189
#include "openvino/op/space_to_batch.hpp"
190190
#include "openvino/op/space_to_depth.hpp"
191+
#include "openvino/op/sparse_fill_empty_rows.hpp"
191192
#include "openvino/op/split.hpp"
192193
#include "openvino/op/sqrt.hpp"
193194
#include "openvino/op/squared_difference.hpp"
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (C) 2018-2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
5+
#pragma once
6+
7+
#include "openvino/op/op.hpp"
8+
9+
namespace ov::op::v16 {
10+
/// \brief An operation which fills empty rows of a sparse tensor with a default value.
11+
/// \ingroup ov_ops_cpp_api
12+
class OPENVINO_API SparseFillEmptyRows : public ov::op::Op {
13+
public:
14+
OPENVINO_OP("SparseFillEmptyRows", "opset16");
15+
16+
SparseFillEmptyRows() = default;
17+
18+
/// \brief Constructs a SparseFillEmptyRows operation.
19+
///
20+
/// \param indices 2D tensor indicating the positions of values in the sparse tensor.
21+
/// \param values 1D tensor containing the values to be inserted at the specified indices.
22+
/// \param dense_shape 1D tensor indicating the shape of the 2D dense tensor.
23+
/// \param default_value Scalar value to be inserted into the empty rows.
24+
SparseFillEmptyRows(const Output<Node>& indices,
25+
const Output<Node>& values,
26+
const Output<Node>& dense_shape,
27+
const Output<Node>& default_value);
28+
29+
void validate_and_infer_types() override;
30+
std::shared_ptr<Node> clone_with_new_inputs(const OutputVector& new_args) const override;
31+
};
32+
33+
} // namespace ov::op::v16
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright (C) 2018-2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
5+
#pragma once
6+
7+
#include <sstream>
8+
#include <unordered_set>
9+
10+
#include "openvino/op/sparse_fill_empty_rows.hpp"
11+
#include "utils.hpp"
12+
13+
namespace ov::op::v16 {
14+
template <class TShape, class TRShape = result_shape_t<TShape>>
15+
std::vector<TRShape> shape_infer(const SparseFillEmptyRows* op,
16+
const std::vector<TShape>& input_shapes,
17+
const ITensorAccessor& tensor_accessor = make_tensor_accessor()) {
18+
NODE_VALIDATION_CHECK(op, input_shapes.size() == 4);
19+
20+
const auto& values_shape = input_shapes[0];
21+
NODE_SHAPE_INFER_CHECK(op,
22+
input_shapes,
23+
values_shape.rank().compatible(1),
24+
"The values input must be a 1D tensor.",
25+
values_shape);
26+
27+
const auto& dense_shape = input_shapes[1];
28+
const bool is_dense_shape_rank_dynamic = dense_shape.rank().is_dynamic();
29+
const bool is_dense_shape_valid =
30+
is_dense_shape_rank_dynamic || (dense_shape.size() == 1 && dense_shape[0].compatible(2));
31+
NODE_SHAPE_INFER_CHECK(
32+
op,
33+
input_shapes,
34+
is_dense_shape_valid,
35+
"The dense_shape input must be 1D and have exactly 2 elements, meaning only 2D sparse tensors are supported.");
36+
37+
const auto& indices_shape = input_shapes[2];
38+
const bool is_indices_shape_valid = indices_shape.rank().is_dynamic() ||
39+
(indices_shape.size() == 2 && indices_shape[1].compatible(2) &&
40+
(is_dense_shape_rank_dynamic || indices_shape[0].compatible(values_shape[0])));
41+
NODE_SHAPE_INFER_CHECK(op,
42+
input_shapes,
43+
is_indices_shape_valid,
44+
"The indices input must be a 2D tensor with the first dimension matching the size of values "
45+
"and the second dimension having 2 elements.",
46+
indices_shape);
47+
48+
const auto& default_value_shape = input_shapes[3];
49+
NODE_SHAPE_INFER_CHECK(op,
50+
input_shapes,
51+
default_value_shape.rank().compatible(0),
52+
"The default_value input must be a scalar.",
53+
default_value_shape);
54+
55+
auto output_shapes = std::vector<TRShape>(3);
56+
auto& output_indices_shape = output_shapes[0];
57+
auto& output_values_shape = output_shapes[1];
58+
auto& empty_row_indicator_shape = output_shapes[2];
59+
output_indices_shape.resize(2);
60+
output_values_shape.resize(1);
61+
empty_row_indicator_shape.resize(1);
62+
output_indices_shape[1] = 2; // Only 2D cases are supported
63+
64+
if (auto dense_shape_value = get_input_const_data_as_shape<TRShape>(op, 1, tensor_accessor)) {
65+
const auto& number_of_rows = (*dense_shape_value)[0].get_length();
66+
empty_row_indicator_shape[0] = number_of_rows;
67+
68+
if (auto indices_value = get_input_const_data_as<TRShape, int64_t>(op, 2, tensor_accessor)) {
69+
auto is_valid_index = [](int64_t index, int64_t max_value) -> bool {
70+
return index >= 0 && index < max_value;
71+
};
72+
auto create_index_error_message =
73+
[](const std::string& dim_name, int64_t index, int64_t max_value) -> std::string {
74+
std::stringstream ss;
75+
ss << "Sparse tensor index out of bounds: " << dim_name << " " << index
76+
<< " is outside the valid range [0, " << (max_value - 1) << "]";
77+
return ss.str();
78+
};
79+
80+
// Rows can be referenced multiple times in sparse representation
81+
std::unordered_set<int64_t> existing_rows;
82+
const auto& indices_data = *indices_value;
83+
const auto& number_of_cols = (*dense_shape_value)[1].get_length();
84+
for (size_t i = 0, i_next = 1; i_next < indices_data.size(); i += 2, i_next += 2) {
85+
auto row = indices_data[i];
86+
NODE_SHAPE_INFER_CHECK(op,
87+
input_shapes,
88+
is_valid_index(row, number_of_rows),
89+
create_index_error_message("row", row, number_of_rows));
90+
91+
auto col = indices_data[i_next];
92+
NODE_SHAPE_INFER_CHECK(op,
93+
input_shapes,
94+
is_valid_index(col, number_of_cols),
95+
create_index_error_message("column", col, number_of_cols));
96+
97+
existing_rows.insert(row);
98+
}
99+
100+
using TDim = typename TRShape::value_type;
101+
TDim empty_rows_count = number_of_rows - existing_rows.size();
102+
output_indices_shape[0] = indices_shape[0] + empty_rows_count;
103+
output_values_shape[0] = values_shape[0] + empty_rows_count;
104+
} else {
105+
output_indices_shape[0] = Dimension::dynamic();
106+
output_values_shape[0] = Dimension::dynamic();
107+
}
108+
} else {
109+
empty_row_indicator_shape[0] = Dimension::dynamic();
110+
}
111+
112+
return output_shapes;
113+
}
114+
} // namespace ov::op::v16
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (C) 2018-2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
5+
#include "openvino/op/sparse_fill_empty_rows.hpp"
6+
7+
#include "itt.hpp"
8+
#include "openvino/core/validation_util.hpp"
9+
#include "openvino/op/op.hpp"
10+
#include "sparse_fill_empty_rows_shape_inference.hpp"
11+
12+
namespace ov::op::v16 {
13+
14+
SparseFillEmptyRows::SparseFillEmptyRows(const Output<Node>& values,
15+
const Output<Node>& dense_shape,
16+
const Output<Node>& indices,
17+
const Output<Node>& default_value)
18+
: Op({values, dense_shape, indices, default_value}) {
19+
constructor_validate_and_infer_types();
20+
}
21+
22+
void SparseFillEmptyRows::validate_and_infer_types() {
23+
OV_OP_SCOPE(v16_SparseFillEmptyRows_validate_and_infer_types);
24+
25+
const auto& indices_element_type = get_input_element_type(2);
26+
NODE_VALIDATION_CHECK(this,
27+
indices_element_type == element::i32 || indices_element_type == element::i64,
28+
"The element type of the indices input must be i32 or i64. Got: ",
29+
indices_element_type);
30+
31+
const auto& dense_shape_element_type = get_input_element_type(1);
32+
NODE_VALIDATION_CHECK(this,
33+
dense_shape_element_type == element::i32 || dense_shape_element_type == element::i64,
34+
"The element type of the dense_shape input must be i32 or i64. Got: ",
35+
dense_shape_element_type);
36+
37+
const auto output_shapes = shape_infer(this, ov::util::get_node_input_partial_shapes(*this));
38+
39+
set_output_type(0, indices_element_type, output_shapes[0]);
40+
set_output_type(1, get_input_element_type(0), output_shapes[1]);
41+
set_output_type(2, element::boolean, output_shapes[2]);
42+
}
43+
44+
std::shared_ptr<Node> SparseFillEmptyRows::clone_with_new_inputs(const ov::OutputVector& new_args) const {
45+
OV_OP_SCOPE(v16_SparseFillEmptyRows_clone_with_new_inputs);
46+
check_new_args_count(this, new_args);
47+
return std::make_shared<SparseFillEmptyRows>(new_args.at(0), new_args.at(1), new_args.at(2), new_args.at(3));
48+
}
49+
50+
} // namespace ov::op::v16

0 commit comments

Comments
 (0)