Skip to content

Commit b85136f

Browse files
authored
Add SparseFillEmptyRows-16 reference implementation (#30307)
### Details: - This PR is built on top of #30191 and contains its commits, so it's better to wait for its merge before reviewing - Add reference implementation and tests ### Related PRs: - #28046 - #30191 ### Tickets: - CVS-158910 --------- Signed-off-by: p-wysocki <[email protected]>
1 parent 6ca0586 commit b85136f

File tree

8 files changed

+377
-1
lines changed

8 files changed

+377
-1
lines changed

src/core/include/openvino/opsets/opset16_tbl.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ _OPENVINO_OP_REG(ShapeOf, ov::op::v3)
1717
_OPENVINO_OP_REG(Identity, ov::op::v16)
1818
_OPENVINO_OP_REG(ISTFT, ov::op::v16)
1919
_OPENVINO_OP_REG(SegmentMax, ov::op::v16)
20+
_OPENVINO_OP_REG(SparseFillEmptyRows, ov::op::v16)
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (C) 2018-2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
5+
#pragma once
6+
7+
#include <algorithm>
8+
#include <unordered_set>
9+
#include <vector>
10+
11+
#include "openvino/core/shape.hpp"
12+
13+
namespace ov::reference {
14+
15+
template <typename T, typename T_IDX>
16+
void sparse_fill_empty_rows(const T* values,
17+
const size_t values_size,
18+
const T_IDX* dense_shape,
19+
const T_IDX* indices,
20+
const size_t indices_size,
21+
const T default_value,
22+
T_IDX* output_indices,
23+
T* output_values,
24+
bool* empty_row_indicator) {
25+
const auto num_rows = dense_shape[0];
26+
27+
std::unordered_set<T_IDX> existing_rows;
28+
for (size_t i = 0, idx = 0; i < values_size; i++, idx += 2) {
29+
existing_rows.insert(indices[idx]);
30+
}
31+
32+
std::vector<T_IDX> empty_rows;
33+
empty_rows.reserve(num_rows - existing_rows.size());
34+
for (T_IDX i = 0; i < num_rows; i++) {
35+
const bool is_empty = (existing_rows.find(i) == existing_rows.end());
36+
empty_row_indicator[i] = is_empty;
37+
if (is_empty) {
38+
empty_rows.push_back(i);
39+
}
40+
}
41+
42+
// Vector of pairs containing ((row, column), source_index) for
43+
// both existing values and new empty rows to be added
44+
const size_t total_rows = values_size + empty_rows.size();
45+
std::vector<std::pair<std::pair<T_IDX, T_IDX>, size_t>> row_col_pairs(total_rows);
46+
47+
// Add existing values and then empty rows
48+
for (size_t i = 0, idx = 0; i < values_size; i++, idx += 2) {
49+
row_col_pairs[i] = {{indices[idx], indices[idx + 1]}, i};
50+
}
51+
for (size_t i = 0; i < empty_rows.size(); i++) {
52+
row_col_pairs[values_size + i] = {{empty_rows[i], 0}, values_size + i};
53+
}
54+
55+
std::sort(row_col_pairs.begin(), row_col_pairs.end(), [](const auto& a, const auto& b) {
56+
if (a.first.first != b.first.first) {
57+
return a.first.first < b.first.first;
58+
}
59+
return a.first.second < b.first.second;
60+
});
61+
62+
for (size_t i = 0, out_idx = 0; i < total_rows; i++, out_idx += 2) {
63+
const auto& [row_col, src_idx] = row_col_pairs[i];
64+
const auto& [row, col] = row_col;
65+
66+
output_indices[out_idx] = row;
67+
output_indices[out_idx + 1] = col;
68+
69+
if (src_idx < values_size) {
70+
output_values[i] = values[src_idx];
71+
} else {
72+
output_values[i] = default_value;
73+
}
74+
}
75+
}
76+
77+
} // namespace ov::reference

src/core/tests/opset.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ INSTANTIATE_TEST_SUITE_P(opset,
7777
OpsetTestParams{ov::get_opset13, 186},
7878
OpsetTestParams{ov::get_opset14, 188},
7979
OpsetTestParams{ov::get_opset15, 199},
80-
OpsetTestParams{ov::get_opset16, 6}),
80+
OpsetTestParams{ov::get_opset16, 7}),
8181
OpsetTestNameGenerator{});
8282

8383
class MyOpOld : public ov::op::Op {

src/plugins/template/backend/ops/ops_evaluates.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,3 +563,7 @@ extern template bool evaluate_node<ov::op::v16::Identity>(std::shared_ptr<ov::No
563563
extern template bool evaluate_node<ov::op::v16::SegmentMax>(std::shared_ptr<ov::Node> node,
564564
ov::TensorVector& outputs,
565565
const ov::TensorVector& inputs);
566+
567+
extern template bool evaluate_node<ov::op::v16::SparseFillEmptyRows>(std::shared_ptr<ov::Node> node,
568+
ov::TensorVector& outputs,
569+
const ov::TensorVector& inputs);
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright (C) 2018-2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
5+
#include "openvino/reference/sparse_fill_empty_rows.hpp"
6+
7+
#include "element_visitor.hpp"
8+
#include "evaluate_node.hpp"
9+
#include "sparse_fill_empty_rows_shape_inference.hpp"
10+
11+
template <ov::element::Type_t ET_data, ov::element::Type_t ET_idx>
12+
bool evaluate_index_type(const std::shared_ptr<ov::op::v16::SparseFillEmptyRows>& op,
13+
ov::TensorVector& outputs,
14+
const ov::TensorVector& inputs) {
15+
using T_data = typename ov::element_type_traits<ET_data>::value_type;
16+
using T_idx = typename ov::element_type_traits<ET_idx>::value_type;
17+
18+
auto input_shapes = std::vector<ov::PartialShape>{
19+
op->get_input_shape(0), // values
20+
op->get_input_shape(1), // dense_shape
21+
op->get_input_shape(2), // indices
22+
op->get_input_shape(3) // default_value
23+
};
24+
25+
const auto output_shapes = ov::op::v16::shape_infer(op.get(), input_shapes, make_tensor_accessor(inputs));
26+
outputs[0].set_shape(output_shapes[0].to_shape()); // output_indices
27+
outputs[1].set_shape(output_shapes[1].to_shape()); // output_values
28+
outputs[2].set_shape(output_shapes[2].to_shape()); // empty_row_indicator
29+
30+
auto values = inputs[0].data<const T_data>();
31+
const T_idx* dense_shape = inputs[1].data<const T_idx>();
32+
const T_idx* indices = inputs[2].data<const T_idx>();
33+
const T_data default_value = *inputs[3].data<const T_data>();
34+
35+
T_idx* output_indices = outputs[0].data<T_idx>();
36+
T_data* output_values = outputs[1].data<T_data>();
37+
bool* empty_row_indicator = outputs[2].data<bool>();
38+
39+
const size_t values_size = inputs[0].get_shape()[0];
40+
const size_t indices_size = ov::shape_size(inputs[2].get_shape());
41+
42+
ov::reference::sparse_fill_empty_rows(values,
43+
values_size,
44+
dense_shape,
45+
indices,
46+
indices_size,
47+
default_value,
48+
output_indices,
49+
output_values,
50+
empty_row_indicator);
51+
return true;
52+
}
53+
54+
template <ov::element::Type_t ET_data>
55+
bool evaluate_data_type(const std::shared_ptr<ov::op::v16::SparseFillEmptyRows>& op,
56+
ov::TensorVector& outputs,
57+
const ov::TensorVector& inputs) {
58+
const auto& index_type = op->get_input_element_type(1);
59+
using ov::op::v16::SparseFillEmptyRows;
60+
using namespace ov::element;
61+
62+
switch (index_type) {
63+
case i32:
64+
return evaluate_index_type<ET_data, i32>(ov::as_type_ptr<SparseFillEmptyRows>(op), outputs, inputs);
65+
case i64:
66+
return evaluate_index_type<ET_data, i64>(ov::as_type_ptr<SparseFillEmptyRows>(op), outputs, inputs);
67+
default:
68+
OPENVINO_THROW("Unhandled index type ", index_type, " in evaluate_node() for SparseFillEmptyRows");
69+
}
70+
}
71+
72+
template <>
73+
bool evaluate_node<ov::op::v16::SparseFillEmptyRows>(std::shared_ptr<ov::Node> node,
74+
ov::TensorVector& outputs,
75+
const ov::TensorVector& inputs) {
76+
using ov::op::v16::SparseFillEmptyRows;
77+
using namespace ov::element;
78+
79+
switch (const auto& element_type = node->get_output_element_type(1); element_type) {
80+
case i8:
81+
return evaluate_data_type<i8>(ov::as_type_ptr<SparseFillEmptyRows>(node), outputs, inputs);
82+
case i32:
83+
return evaluate_data_type<i32>(ov::as_type_ptr<SparseFillEmptyRows>(node), outputs, inputs);
84+
case i64:
85+
return evaluate_data_type<i64>(ov::as_type_ptr<SparseFillEmptyRows>(node), outputs, inputs);
86+
case u8:
87+
return evaluate_data_type<u8>(ov::as_type_ptr<SparseFillEmptyRows>(node), outputs, inputs);
88+
case u32:
89+
return evaluate_data_type<u32>(ov::as_type_ptr<SparseFillEmptyRows>(node), outputs, inputs);
90+
case u64:
91+
return evaluate_data_type<u64>(ov::as_type_ptr<SparseFillEmptyRows>(node), outputs, inputs);
92+
case f16:
93+
return evaluate_data_type<f16>(ov::as_type_ptr<SparseFillEmptyRows>(node), outputs, inputs);
94+
case f32:
95+
return evaluate_data_type<f32>(ov::as_type_ptr<SparseFillEmptyRows>(node), outputs, inputs);
96+
default:
97+
OPENVINO_THROW("Unhandled data type ", element_type, " in evaluate_node() for SparseFillEmptyRows");
98+
}
99+
}

src/plugins/template/backend/opset_int_tbl.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ _OPENVINO_OP_REG(SearchSorted, ov::op::v15)
178178
_OPENVINO_OP_REG(Identity, ov::op::v16)
179179
_OPENVINO_OP_REG(ISTFT, ov::op::v16)
180180
_OPENVINO_OP_REG(SegmentMax, ov::op::v16)
181+
_OPENVINO_OP_REG(SparseFillEmptyRows, ov::op::v16)
181182

182183
_OPENVINO_OP_REG(AUGRUCell, ov::op::internal)
183184
_OPENVINO_OP_REG(AUGRUSequence, ov::op::internal)
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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 <gtest/gtest.h>
8+
9+
#include "base_reference_test.hpp"
10+
#include "openvino/core/type/element_type_traits.hpp"
11+
#include "openvino/op/constant.hpp"
12+
13+
namespace {
14+
struct SparseFillEmptyRowsParams {
15+
SparseFillEmptyRowsParams(const reference_tests::Tensor& valuesTensor,
16+
const reference_tests::Tensor& denseShapeTensor,
17+
const reference_tests::Tensor& indicesTensor,
18+
const reference_tests::Tensor& defaultValueTensor,
19+
const reference_tests::Tensor& expectedIndicesTensor,
20+
const reference_tests::Tensor& expectedValuesTensor,
21+
const reference_tests::Tensor& expectedEmptyRowIndicatorTensor)
22+
: valuesTensor(valuesTensor),
23+
denseShapeTensor(denseShapeTensor),
24+
indicesTensor(indicesTensor),
25+
defaultValueTensor(defaultValueTensor),
26+
expectedIndicesTensor(expectedIndicesTensor),
27+
expectedValuesTensor(expectedValuesTensor),
28+
expectedEmptyRowIndicatorTensor(expectedEmptyRowIndicatorTensor) {}
29+
30+
reference_tests::Tensor valuesTensor;
31+
reference_tests::Tensor denseShapeTensor;
32+
reference_tests::Tensor indicesTensor;
33+
reference_tests::Tensor defaultValueTensor;
34+
reference_tests::Tensor expectedIndicesTensor;
35+
reference_tests::Tensor expectedValuesTensor;
36+
reference_tests::Tensor expectedEmptyRowIndicatorTensor;
37+
};
38+
39+
class ReferenceSparseFillEmptyRowsV16LayerTest : public testing::TestWithParam<SparseFillEmptyRowsParams>,
40+
public reference_tests::CommonReferenceTest {
41+
protected:
42+
void SetUp() override {
43+
const auto& params = GetParam();
44+
function = CreateFunction(params);
45+
inputData = {params.valuesTensor.data,
46+
params.denseShapeTensor.data,
47+
params.indicesTensor.data,
48+
params.defaultValueTensor.data};
49+
refOutData = {params.expectedIndicesTensor.data,
50+
params.expectedValuesTensor.data,
51+
params.expectedEmptyRowIndicatorTensor.data};
52+
}
53+
54+
public:
55+
static std::string getTestCaseName(const testing::TestParamInfo<SparseFillEmptyRowsParams>& obj) {
56+
auto param = obj.param;
57+
std::ostringstream result;
58+
result << "valuesType=" << param.valuesTensor.type;
59+
result << "_valuesShape=" << param.valuesTensor.shape;
60+
result << "_denseShapeType=" << param.denseShapeTensor.type;
61+
result << "_denseShapeValues=" << testing::PrintToString(param.denseShapeTensor.data);
62+
result << "_indicesType=" << param.indicesTensor.type;
63+
result << "_indicesShape=" << param.indicesTensor.shape;
64+
result << "_defaultValue=" << testing::PrintToString(param.defaultValueTensor.data);
65+
return result.str();
66+
}
67+
68+
private:
69+
static std::shared_ptr<ov::Model> CreateFunction(const SparseFillEmptyRowsParams& params) {
70+
using ov::op::v0::Constant;
71+
using ov::op::v0::Parameter;
72+
73+
const auto values = std::make_shared<Parameter>(params.valuesTensor.type, params.valuesTensor.shape);
74+
const auto dense_shape =
75+
std::make_shared<Parameter>(params.denseShapeTensor.type, params.denseShapeTensor.shape);
76+
const auto indices = std::make_shared<Parameter>(params.indicesTensor.type, params.indicesTensor.shape);
77+
const auto default_value =
78+
std::make_shared<Parameter>(params.defaultValueTensor.type, params.defaultValueTensor.shape);
79+
80+
const auto sparseFillEmptyRows =
81+
std::make_shared<ov::op::v16::SparseFillEmptyRows>(values, dense_shape, indices, default_value);
82+
83+
return std::make_shared<ov::Model>(ov::OutputVector{sparseFillEmptyRows->output(0),
84+
sparseFillEmptyRows->output(1),
85+
sparseFillEmptyRows->output(2)},
86+
ov::ParameterVector{values, dense_shape, indices, default_value});
87+
}
88+
};
89+
90+
TEST_P(ReferenceSparseFillEmptyRowsV16LayerTest, CompareWithRefs) {
91+
Exec();
92+
}
93+
94+
template <ov::element::Type_t T, ov::element::Type_t T_idx>
95+
std::vector<SparseFillEmptyRowsParams> generateSparseFillEmptyRowsParams() {
96+
using T_D = typename ov::element_type_traits<T>::value_type;
97+
using T_I = typename ov::element_type_traits<T_idx>::value_type;
98+
using reference_tests::Tensor;
99+
100+
std::vector<SparseFillEmptyRowsParams> params{
101+
// No empty rows
102+
SparseFillEmptyRowsParams(
103+
Tensor({3}, T, std::vector<T_D>{1, 2, 3}), // values
104+
Tensor({2}, T_idx, std::vector<T_I>{3, 4}), // dense_shape
105+
Tensor({3, 2}, T_idx, std::vector<T_I>{0, 0, 1, 0, 2, 0}), // indices
106+
Tensor({}, T, std::vector<T_D>{-1}), // default_value
107+
Tensor({3, 2}, T_idx, std::vector<T_I>{0, 0, 1, 0, 2, 0}), // expected_indices
108+
Tensor({3}, T, std::vector<T_D>{1, 2, 3}), // expected_values
109+
Tensor({3}, ov::element::boolean, std::vector<uint8_t>{0, 0, 0}) // expected_empty_row_indicator
110+
),
111+
112+
// One empty row in the middle
113+
SparseFillEmptyRowsParams(
114+
Tensor({3}, T, std::vector<T_D>{1, 2, 3}), // values
115+
Tensor({2}, T_idx, std::vector<T_I>{4, 4}), // dense_shape
116+
Tensor({3, 2}, T_idx, std::vector<T_I>{0, 0, 1, 0, 3, 0}), // indices
117+
Tensor({}, T, std::vector<T_D>{-1}), // default_value
118+
Tensor({4, 2}, T_idx, std::vector<T_I>{0, 0, 1, 0, 2, 0, 3, 0}), // expected_indices
119+
Tensor({4}, T, std::vector<T_D>{1, 2, -1, 3}), // expected_values
120+
Tensor({4}, ov::element::boolean, std::vector<uint8_t>{0, 0, 1, 0}) // expected_empty_row_indicator
121+
),
122+
123+
// Multiple empty rows
124+
SparseFillEmptyRowsParams(
125+
Tensor({2}, T, std::vector<T_D>{1, 2}), // values
126+
Tensor({2}, T_idx, std::vector<T_I>{5, 3}), // dense_shape
127+
Tensor({2, 2}, T_idx, std::vector<T_I>{0, 0, 4, 0}), // indices
128+
Tensor({}, T, std::vector<T_D>{-1}), // default_value
129+
Tensor({5, 2}, T_idx, std::vector<T_I>{0, 0, 1, 0, 2, 0, 3, 0, 4, 0}), // expected_indices
130+
Tensor({5}, T, std::vector<T_D>{1, -1, -1, -1, 2}), // expected_values
131+
Tensor({5}, ov::element::boolean, std::vector<uint8_t>{0, 1, 1, 1, 0}) // expected_empty_row_indicator
132+
),
133+
134+
// All rows empty
135+
SparseFillEmptyRowsParams(
136+
Tensor({0}, T, std::vector<T_D>{}), // values
137+
Tensor({2}, T_idx, std::vector<T_I>{3, 4}), // dense_shape
138+
Tensor({0, 2}, T_idx, std::vector<T_I>{}), // indices
139+
Tensor({}, T, std::vector<T_D>{-1}), // default_value
140+
Tensor({3, 2}, T_idx, std::vector<T_I>{0, 0, 1, 0, 2, 0}), // expected_indices
141+
Tensor({3}, T, std::vector<T_D>{-1, -1, -1}), // expected_values
142+
Tensor({3}, ov::element::boolean, std::vector<uint8_t>{1, 1, 1}) // expected_empty_row_indicator
143+
),
144+
145+
// Non-zero column indices for empty rows
146+
SparseFillEmptyRowsParams(
147+
Tensor({4}, T, std::vector<T_D>{1, 2, 3, 4}), // values
148+
Tensor({2}, T_idx, std::vector<T_I>{5, 6}), // dense_shape
149+
Tensor({4, 2}, T_idx, std::vector<T_I>{0, 1, 1, 2, 3, 3, 4, 4}), // indices
150+
Tensor({}, T, std::vector<T_D>{99}), // default_value
151+
Tensor({5, 2}, T_idx, std::vector<T_I>{0, 1, 1, 2, 2, 0, 3, 3, 4, 4}), // expected_indices
152+
Tensor({5}, T, std::vector<T_D>{1, 2, 99, 3, 4}), // expected_values
153+
Tensor({5}, ov::element::boolean, std::vector<uint8_t>{0, 0, 1, 0, 0}) // expected_empty_row_indicator
154+
)};
155+
156+
return params;
157+
}
158+
159+
std::vector<SparseFillEmptyRowsParams> generateSparseFillEmptyRowsV16CombinedParams() {
160+
using ov::element::Type_t;
161+
const std::vector<std::vector<SparseFillEmptyRowsParams>> SparseFillEmptyRowsTypeParams{
162+
generateSparseFillEmptyRowsParams<Type_t::i32, Type_t::i32>(),
163+
generateSparseFillEmptyRowsParams<Type_t::i64, Type_t::i32>(),
164+
generateSparseFillEmptyRowsParams<Type_t::f32, Type_t::i32>(),
165+
generateSparseFillEmptyRowsParams<Type_t::i32, Type_t::i64>(),
166+
generateSparseFillEmptyRowsParams<Type_t::i64, Type_t::i64>(),
167+
generateSparseFillEmptyRowsParams<Type_t::f32, Type_t::i64>()};
168+
169+
std::vector<SparseFillEmptyRowsParams> combinedParams;
170+
for (const auto& params : SparseFillEmptyRowsTypeParams) {
171+
combinedParams.insert(combinedParams.end(), params.begin(), params.end());
172+
}
173+
return combinedParams;
174+
}
175+
176+
INSTANTIATE_TEST_SUITE_P(smoke_SparseFillEmptyRows_With_Hardcoded_Refs,
177+
ReferenceSparseFillEmptyRowsV16LayerTest,
178+
testing::ValuesIn(generateSparseFillEmptyRowsV16CombinedParams()),
179+
ReferenceSparseFillEmptyRowsV16LayerTest::getTestCaseName);
180+
} // namespace

0 commit comments

Comments
 (0)