diff --git a/compiler/circle-resizer/include/ModelEditor.h b/compiler/circle-resizer/include/ModelEditor.h new file mode 100644 index 00000000000..910ea19233a --- /dev/null +++ b/compiler/circle-resizer/include/ModelEditor.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2025 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __CIRCLE_RESIZER_MODEL_EDITOR_H__ +#define __CIRCLE_RESIZER_MODEL_EDITOR_H__ + +#include "Shape.h" +#include "CircleModel.h" + +#include +#include +#include + +namespace circle_resizer +{ + +/** + * The class to modify circle models. + */ +class ModelEditor +{ +public: + /** + * @brief Initialize the editor with CircleModel object. + */ + explicit ModelEditor(std::shared_ptr circle_model); + +public: + /** + * @brief Resize the model. It means changing shape of the inputs + * and propagating changes through the graph. + * + * Exceptions: + * - std::runtime_error if the new_inputs_shapes are invalid. It can happens for scenarios like: + * - new shapes for NOT all inputs are provided + * - an exception was thrown during shape inference pass + */ + ModelEditor &resize_inputs(const std::vector &new_inputs_shapes); + +private: + std::shared_ptr _circle_model; +}; + +} // namespace circle_resizer + +#endif // __CIRCLE_RESIZER_MODEL_EDITOR_H__ diff --git a/compiler/circle-resizer/src/CMakeLists.txt b/compiler/circle-resizer/src/CMakeLists.txt index 85e0380b40e..d099a4991a2 100644 --- a/compiler/circle-resizer/src/CMakeLists.txt +++ b/compiler/circle-resizer/src/CMakeLists.txt @@ -2,6 +2,7 @@ list(APPEND CIRCLE_RESIZER_SOURCES Dim.cpp) list(APPEND CIRCLE_RESIZER_SOURCES Shape.cpp) list(APPEND CIRCLE_RESIZER_SOURCES ShapeParser.cpp) list(APPEND CIRCLE_RESIZER_SOURCES CircleModel.cpp) +list(APPEND CIRCLE_RESIZER_SOURCES ModelEditor.cpp) add_library(circle_resizer_core SHARED "${CIRCLE_RESIZER_SOURCES}") @@ -10,6 +11,8 @@ target_include_directories(circle_resizer_core PUBLIC ../include) target_link_libraries(circle_resizer_core PRIVATE luci_export) target_link_libraries(circle_resizer_core PRIVATE luci_import) target_link_libraries(circle_resizer_core PRIVATE luci_lang) +target_link_libraries(circle_resizer_core PRIVATE luci_pass) +target_link_libraries(circle_resizer_core PRIVATE logo) target_link_libraries(circle_resizer_core PRIVATE mio_circle08) install(TARGETS circle_resizer_core DESTINATION lib) diff --git a/compiler/circle-resizer/src/ModelEditor.cpp b/compiler/circle-resizer/src/ModelEditor.cpp new file mode 100644 index 00000000000..66f45cefce0 --- /dev/null +++ b/compiler/circle-resizer/src/ModelEditor.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2025 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ModelEditor.h" + +#include + +#include +#include +#include +#include +#include +#include + +using namespace circle_resizer; + +namespace +{ + +void change_single_input_shape(luci::CircleInput *circle_input, const Shape &new_shape) +{ + circle_input->rank(new_shape.rank()); + for (uint32_t i = 0; i < new_shape.rank(); ++i) + { + if (new_shape[i].is_dynamic()) + { + circle_input->dim(i) = loco::Dimension(); // empty ctor means dynamic dimension + } + else + { + // a value here can be in range (0, std::numeric_limits::max()) so the cast is safe + circle_input->dim(i) = loco::Dimension(static_cast(new_shape[i].value())); + } + } +} + +void change_inputs_shapes(loco::Graph *graph, const std::vector &new_inputs_shapes) +{ + auto graph_inputs = loco::input_nodes(graph); + if (graph_inputs.size() != new_inputs_shapes.size()) + { + throw std::runtime_error("Expected " + std::to_string(graph_inputs.size()) + + " shapes but provided " + std::to_string(new_inputs_shapes.size())); + } + for (size_t in_idx = 0; in_idx < new_inputs_shapes.size(); ++in_idx) + { + auto circle_input = loco::must_cast(graph_inputs[in_idx]); + change_single_input_shape(circle_input, new_inputs_shapes[in_idx]); + } +} + +} // namespace + +ModelEditor::ModelEditor(std::shared_ptr circle_model) : _circle_model{circle_model} +{ + assert(circle_model != nullptr); // FIX_CALLER_UNLESS +} + +ModelEditor &ModelEditor::resize_inputs(const std::vector &new_inputs_shapes) +{ + auto graph = _circle_model->module()->graph(); + change_inputs_shapes(graph, new_inputs_shapes); + + logo::Phase phase; + phase.emplace_back(std::make_unique()); + phase.emplace_back(std::make_unique()); + phase.emplace_back(std::make_unique()); + + logo::PhaseRunner phase_runner{graph}; + try + { + phase_runner.run(phase); + } + catch (const std::exception &e) + { + throw std::runtime_error("Exception during resizing with message: " + std::string{e.what()}); + } + + return *this; +} diff --git a/compiler/circle-resizer/tests/CMakeLists.txt b/compiler/circle-resizer/tests/CMakeLists.txt index eeebdfe973f..3337718cc34 100644 --- a/compiler/circle-resizer/tests/CMakeLists.txt +++ b/compiler/circle-resizer/tests/CMakeLists.txt @@ -5,6 +5,7 @@ endif(NOT ENABLE_TEST) list(APPEND CIRCLE_RESIZER_TEST_SOURCES Shape.test.cpp) list(APPEND CIRCLE_RESIZER_TEST_SOURCES ShapeParser.test.cpp) list(APPEND CIRCLE_RESIZER_TEST_SOURCES CircleModel.test.cpp) +list(APPEND CIRCLE_RESIZER_TEST_SOURCES ModelEditor.test.cpp) nnas_find_package(GTest REQUIRED) GTest_AddTest(circle_resizer_unit_test ${CIRCLE_RESIZER_TEST_SOURCES}) diff --git a/compiler/circle-resizer/tests/ModelEditor.test.cpp b/compiler/circle-resizer/tests/ModelEditor.test.cpp new file mode 100644 index 00000000000..a1eb6e74057 --- /dev/null +++ b/compiler/circle-resizer/tests/ModelEditor.test.cpp @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2025 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ModelEditor.h" + +#include +#include + +#include +#include +#include + +using namespace circle_resizer; +using ::testing::HasSubstr; + +class ModelEditorTest : public ::testing::Test +{ +protected: + void SetUp() override + { + char *path = std::getenv("ARTIFACTS_PATH"); + if (nullptr == path) + { + throw std::runtime_error("environmental variable ARTIFACTS_PATH required for circle-resizer " + "tests was not provided"); + } + _test_models_dir = path; + } + +protected: + std::string _test_models_dir; +}; + +TEST_F(ModelEditorTest, basic_tests) +{ + // single input, single output + auto circle_model = std::make_shared(_test_models_dir + "/ExpandDims_000.circle"); + ModelEditor editor(circle_model); + auto new_input_shapes = std::vector{Shape{4, 6}}; + editor.resize_inputs(new_input_shapes); + EXPECT_EQ(circle_model->input_shapes(), new_input_shapes); + EXPECT_EQ(circle_model->output_shapes(), (std::vector{Shape{4, 1, 6}})); + + // single input, two outputs + circle_model = std::make_shared(_test_models_dir + "/CSE_Quantize_000.circle"); + editor = ModelEditor(circle_model); + new_input_shapes = std::vector{Shape{1, 6, 6, 4}}; + editor.resize_inputs(new_input_shapes); + EXPECT_EQ(circle_model->input_shapes(), new_input_shapes); + EXPECT_EQ(circle_model->output_shapes(), + (std::vector{Shape{1, 6, 6, 4}, Shape{1, 6, 6, 4}})); + + // two inputs, single output + circle_model = std::make_shared(_test_models_dir + "/Add_000.circle"); + editor = ModelEditor(circle_model); + new_input_shapes = std::vector{Shape{1, 5, 5, 3}, Shape{1, 5, 5, 3}}; + editor.resize_inputs(new_input_shapes); + EXPECT_EQ(circle_model->input_shapes(), new_input_shapes); + EXPECT_EQ(circle_model->output_shapes(), (std::vector{Shape{1, 5, 5, 3}})); + + // two inputs two outputs + circle_model = + std::make_shared(_test_models_dir + "/Part_Add_Sqrt_Rsqrt_000.circle"); + editor = ModelEditor(circle_model); + new_input_shapes = std::vector{Shape{1, 5, 5, 2}, Shape{1, 5, 5, 2}}; + editor.resize_inputs(new_input_shapes); + EXPECT_EQ(circle_model->input_shapes(), new_input_shapes); + EXPECT_EQ(circle_model->output_shapes(), + (std::vector{Shape{1, 5, 5, 2}, Shape{1, 5, 5, 2}})); + + // change even the input rank + circle_model = std::make_shared(_test_models_dir + "/ExpandDims_000.circle"); + editor = ModelEditor(circle_model); + new_input_shapes = std::vector{Shape{1, 2, 3, 4}}; + editor.resize_inputs(new_input_shapes); + EXPECT_EQ(circle_model->input_shapes(), new_input_shapes); + EXPECT_EQ(circle_model->output_shapes(), (std::vector{Shape{1, 1, 2, 3, 4}})); +} + +TEST_F(ModelEditorTest, special_cases) +{ + // resize to dynamic shape + auto circle_model = std::make_shared(_test_models_dir + "/ExpandDims_000.circle"); + ModelEditor editor(circle_model); + auto new_input_shapes = std::vector{Shape{Dim{4}, Dim::dynamic()}}; + editor.resize_inputs(new_input_shapes); + EXPECT_EQ(circle_model->input_shapes(), new_input_shapes); + EXPECT_EQ(circle_model->output_shapes(), + (std::vector{Shape{Dim{4}, Dim{1}, Dim::dynamic()}})); + + // resize to scalars + circle_model = std::make_shared(_test_models_dir + "/Add_000.circle"); + editor = ModelEditor(circle_model); + new_input_shapes = std::vector{Shape::scalar(), Shape::scalar()}; + editor.resize_inputs(new_input_shapes); + EXPECT_EQ(circle_model->input_shapes(), new_input_shapes); + EXPECT_EQ(circle_model->output_shapes(), (std::vector{Shape::scalar()})); +} + +TEST_F(ModelEditorTest, resizing_applied_after_save) +{ + auto circle_model = std::make_shared(_test_models_dir + "/ExpandDims_000.circle"); + ModelEditor editor(circle_model); + std::stringstream out_stream; + const auto new_input_shapes = std::vector{Shape{4, 6}}; + editor.resize_inputs(new_input_shapes); + circle_model->save(out_stream); + const std::string &model_buf_str = out_stream.str(); + std::vector model_buffer(std::begin(model_buf_str), std::end(model_buf_str)); + model_buffer.insert(std::end(model_buffer), std::begin(model_buf_str), std::end(model_buf_str)); + + auto circle_model_from_saved_buffer = std::make_shared(model_buffer); + EXPECT_EQ(circle_model_from_saved_buffer->input_shapes(), new_input_shapes); + EXPECT_EQ(circle_model_from_saved_buffer->output_shapes(), (std::vector{Shape{4, 1, 6}})); +} + +TEST_F(ModelEditorTest, double_resizing) +{ + auto circle_model = std::make_shared(_test_models_dir + "/ExpandDims_000.circle"); + ModelEditor editor(circle_model); + const auto new_input_shapes = std::vector{Shape{4, 6}}; + editor.resize_inputs(std::vector{Shape{6, 8}}).resize_inputs(new_input_shapes); + // check if the last applied shape is set after double resizing call + EXPECT_EQ(circle_model->input_shapes(), new_input_shapes); + EXPECT_EQ(circle_model->output_shapes(), (std::vector{Shape{4, 1, 6}})); +} + +TEST_F(ModelEditorTest, no_inputs_shapes_provided_NEG) +{ + auto circle_model = std::make_shared(_test_models_dir + "/Add_000.circle"); + ModelEditor editor(circle_model); + try + { + editor.resize_inputs({}); + FAIL() << "Unexpected successful resizing with invalid shapes."; + } + catch (const std::runtime_error &err) + { + EXPECT_THAT(err.what(), HasSubstr("Expected 2 shapes but provided 0")); + } + catch (...) + { + FAIL() << "Expected std::runtime_error, other exception thrown"; + } +} + +TEST_F(ModelEditorTest, not_all_inputs_shapes_provided_NEG) +{ + auto circle_model = std::make_shared(_test_models_dir + "/Add_000.circle"); + ModelEditor editor(circle_model); + try + { + editor.resize_inputs(std::vector{Shape{1, 5, 5, 3}}); + FAIL() << "Unexpected successful resizing with invalid shapes."; + } + catch (const std::runtime_error &err) + { + EXPECT_THAT(err.what(), HasSubstr("Expected 2 shapes but provided 1")); + } + catch (...) + { + FAIL() << "Expected std::runtime_error, other exception thrown"; + } +} + +TEST_F(ModelEditorTest, to_much_inputs_shapes_provided_NEG) +{ + auto circle_model = std::make_shared(_test_models_dir + "/Add_000.circle"); + ModelEditor editor(circle_model); + try + { + editor.resize_inputs(std::vector{Shape{1, 2}, Shape{3, 4}, Shape{5, 6}}); + FAIL() << "Unexpected successful resizing with invalid shapes."; + } + catch (const std::runtime_error &err) + { + EXPECT_THAT(err.what(), HasSubstr("Expected 2 shapes but provided 3")); + } + catch (...) + { + FAIL() << "Expected std::runtime_error, other exception thrown"; + } +} + +TEST_F(ModelEditorTest, exception_during_shape_inference_NEG) +{ + auto circle_model = std::make_shared(_test_models_dir + "/Add_000.circle"); + ModelEditor editor(circle_model); + try + { + editor.resize_inputs(std::vector{Shape{1, 2, 3}, Shape{4, 5, 6}}); + FAIL() << "Unexpected successful resizing with invalid shapes."; + } + catch (const std::runtime_error &err) + { + EXPECT_THAT(err.what(), HasSubstr("Exception during resizing with message:")); + } + catch (...) + { + FAIL() << "Expected std::runtime_error, other exception thrown"; + } +}