diff --git a/compiler/minmax-embedder/CMakeLists.txt b/compiler/minmax-embedder/CMakeLists.txt new file mode 100644 index 00000000000..35fdaad0616 --- /dev/null +++ b/compiler/minmax-embedder/CMakeLists.txt @@ -0,0 +1,41 @@ +nnas_find_package(HDF5 COMPONENTS STATIC QUIET) + +if(NOT HDF5_FOUND) + message(STATUS "Build minmax_embedder: FAILED (missing HDF5)") + return() +endif(NOT HDF5_FOUND) + +file(GLOB_RECURSE SOURCES "src/*.cpp") +file(GLOB_RECURSE TESTS "src/*.test.cpp") +list(REMOVE_ITEM SOURCES ${TESTS}) + +# +# Library +# +add_library(minmax_embedder "${SOURCES}") +target_include_directories(minmax_embedder PUBLIC ${HDF5_INCLUDE_DIRS}) +target_include_directories(minmax_embedder PRIVATE include) + +target_link_libraries(minmax_embedder ${HDF5_CXX_LIBRARIES}) +target_link_libraries(minmax_embedder loco) +target_link_libraries(minmax_embedder luci_import) +target_link_libraries(minmax_embedder luci_service) +target_link_libraries(minmax_embedder luci_pass) +target_link_libraries(minmax_embedder luci_export) +target_link_libraries(minmax_embedder luci_env) + +install(TARGETS minmax_embedder DESTINATION lib) +install(DIRECTORY include/ DESTINATION include + FILES_MATCHING PATTERN "*.h") +# +# GTest +# +if(NOT ENABLE_TEST) + return() +endif(NOT ENABLE_TEST) + +nnas_find_package(GTest REQUIRED) + +GTest_AddTest(minmax_embedder_test ${TESTS}) +target_include_directories(minmax_embedder_test PRIVATE include) +target_link_libraries(minmax_embedder_test minmax_embedder) diff --git a/compiler/minmax-embedder/README.md b/compiler/minmax-embedder/README.md new file mode 100644 index 00000000000..ca7efddef80 --- /dev/null +++ b/compiler/minmax-embedder/README.md @@ -0,0 +1,3 @@ +# minmax-embedder + +_minmax-embedder_ embeds minmax to circle. diff --git a/compiler/minmax-embedder/include/minmax-embedder/Embedder.h b/compiler/minmax-embedder/include/minmax-embedder/Embedder.h new file mode 100644 index 00000000000..316148abe14 --- /dev/null +++ b/compiler/minmax-embedder/include/minmax-embedder/Embedder.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 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 __MINMAX_EMBEDDER_EMBEDDER_H__ +#define __MINMAX_EMBEDDER_EMBEDDER_H__ + +#include + +namespace minmax_embedder +{ + +struct EmbedderOptions +{ + float min_percentile = 0.0f; // dummy initial value to make SE tool happy + float max_percentile = 0.0f; // dummy initial value To make SE tool happy +}; + +class Embedder +{ +public: + void embed(const std::string &output_path, const std::string &input_path, + const std::string &minmax_path, const EmbedderOptions &); +}; +} // namespace minmax_embedder + +#endif // __MINMAX_EMBEDDER_EMBEDDER_H__ diff --git a/compiler/minmax-embedder/requires.cmake b/compiler/minmax-embedder/requires.cmake new file mode 100644 index 00000000000..ac1478022ef --- /dev/null +++ b/compiler/minmax-embedder/requires.cmake @@ -0,0 +1,2 @@ +require("loco") +require("luci") diff --git a/compiler/minmax-embedder/src/Embedder.cpp b/compiler/minmax-embedder/src/Embedder.cpp new file mode 100644 index 00000000000..46734ff0536 --- /dev/null +++ b/compiler/minmax-embedder/src/Embedder.cpp @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2023 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 "minmax-embedder/Embedder.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "h5/Reader.h" + +#include +#include // for std::floor +#include +#include + +namespace +{ + +/* NOTE: getNthPercentile is copied from compiler/record-minmax/include/RecordFunction.h */ +/** + * @brief getNthPercentile calculates the n-th percentile of input vector (0.0 <= n <= 100.0) + * linear interpolation is used when the desired percentile lies between two data points + */ +float getNthPercentile(std::vector &vector, float percentile) +{ + if (percentile < 0 || percentile > 100) + throw std::runtime_error("Percentile must be ranged from 0 to 100"); + + if (vector.empty()) + throw std::runtime_error("Percentile must take a non-empty vector as an argument"); + + if (vector.size() == 1) + return vector[0]; + + std::vector copy; + copy.assign(vector.begin(), vector.end()); + std::sort(copy.begin(), copy.end()); + + if (percentile == 0.0) + return copy.front(); + + if (percentile == 100.0) + return copy.back(); + + int index = static_cast(std::floor((copy.size() - 1) * percentile / 100.0)); + + float percent_i = static_cast(index) / static_cast(copy.size() - 1); + float fraction = + (percentile / 100.0 - percent_i) / ((index + 1.0) / (copy.size() - 1.0) - percent_i); + float res = copy[index] + fraction * (copy[index + 1] - copy[index]); + return res; +} + +} // namespace + +namespace minmax_embedder +{ + +void Embedder::embed(const std::string &output_path, const std::string &input_path, + const std::string &minmax_path, const EmbedderOptions &opt) +{ + // Load model from the file + luci::ImporterEx importerex; + auto module = importerex.importVerifyModule(input_path); + if (module.get() == nullptr) + throw std::runtime_error{"Input circle is invalid"}; + + h5::Reader mmr{minmax_path}; + + for (size_t idx = 0; idx < module->size(); ++idx) + { + auto graph = module->graph(idx); + + /* read subgraph inputs */ + const auto input_nodes = loco::input_nodes(graph); + const auto n_inputs = input_nodes.size(); + for (size_t input_idx = 0; input_idx < n_inputs; ++input_idx) + { + const auto *circle_input = loco::must_cast(input_nodes[input_idx]); + if (circle_input->index() != input_idx) + throw std::runtime_error("Input order in minmax recording does not match to circle"); + + auto minmax = mmr.read_input(0, idx, input_idx); + auto min = getNthPercentile(minmax.min_vector, opt.min_percentile); + auto max = getNthPercentile(minmax.max_vector, opt.max_percentile); + auto quantparam = std::make_unique(); + quantparam->min.push_back(min); + quantparam->max.push_back(max); + const auto *circle_node = loco::must_cast(input_nodes[input_idx]); + auto mutable_node = const_cast(circle_node); + mutable_node->quantparam(std::move(quantparam)); + } + + /* read op outputs */ + uint32_t n_nodes = graph->nodes()->size(); + for (uint32_t i = 0; i < n_nodes; ++i) + { + auto node = loco::must_cast(graph->nodes()->at(i)); + if (not luci::has_node_id(node)) // Skip non-op nodes (e.g. input/const/output) + continue; + auto op_idx = luci::get_node_id(node); + auto minmax = mmr.read(0, idx, op_idx); + auto min = getNthPercentile(minmax.min_vector, opt.min_percentile); + auto max = getNthPercentile(minmax.max_vector, opt.max_percentile); + auto quantparam = std::make_unique(); + quantparam->min.push_back(min); + quantparam->max.push_back(max); + auto mutable_node = const_cast(node); + mutable_node->quantparam(std::move(quantparam)); + } + + if (!luci::validate(graph)) + throw std::runtime_error{"Circle after embedding minmax is invalid"}; + } + + // Export to output Circle file + luci::CircleExporter exporter; + + luci::CircleFileExpContract contract(module.get(), output_path); + + if (!exporter.invoke(&contract)) + throw std::runtime_error{"Failed to export circle"}; +} + +} // namespace minmax_embedder diff --git a/compiler/minmax-embedder/src/Embedder.test.cpp b/compiler/minmax-embedder/src/Embedder.test.cpp new file mode 100644 index 00000000000..c747e2ce8f2 --- /dev/null +++ b/compiler/minmax-embedder/src/Embedder.test.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 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 "minmax-embedder/Embedder.h" + +#include + +using namespace minmax_embedder; + +namespace +{ +struct MinMaxEmbedderTest : public ::testing::Test +{ + EmbedderOptions _opt{0, 100}; +}; + +} // namespace + +TEST_F(MinMaxEmbedderTest, invalid_input_NEG) +{ + Embedder embedder; + EXPECT_THROW(embedder.embed("", "not_existing", "", _opt), std::runtime_error); +} diff --git a/compiler/minmax-embedder/src/h5/Reader.cpp b/compiler/minmax-embedder/src/h5/Reader.cpp new file mode 100644 index 00000000000..b0bb8c393bb --- /dev/null +++ b/compiler/minmax-embedder/src/h5/Reader.cpp @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2023 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 "Reader.h" + +#include +#include + +namespace +{ +bool exists(hid_t id, const char *path) { return H5Lexists(id, path, H5P_DEFAULT) > 0; } +} // namespace + +namespace minmax_embedder +{ +namespace h5 +{ +static const char *h5_value_grpname = "value"; + +Reader::Reader(const std::string &filepath) : _file(filepath, H5F_ACC_RDONLY) +{ + _val_grp = _file.openGroup(h5_value_grpname); +} + +// TODO: Handle multiple output +MinMaxVectors Reader::read(int model_idx, int subg_idx, int op_idx) const +{ + MinMaxVectors mmv; + float minmax[2]; + auto num_run = _val_grp.getNumObjs(); + for (uint32_t r = 0; r < num_run; ++r) + { + // check whether minmax exists + char path[128]; // 128 is enough to print "/value/run_%d/model_%d/subg_%d/op_%d" + null + snprintf(path, 128, "/value/run_%d/model_%d/subg_%d/op_%d", r, model_idx, subg_idx, op_idx); + if (!exists(_file.getId(), path)) + continue; + auto run_grp = _val_grp.openGroup(std::string("run_") + std::to_string(r)); + auto model_grp = run_grp.openGroup(std::string("model_") + std::to_string(model_idx)); + auto subg_grp = model_grp.openGroup(std::string("subg_") + std::to_string(subg_idx)); + auto op_dset = subg_grp.openDataSet(std::string("op_") + std::to_string(op_idx)); + H5::DataType dtype = op_dset.getDataType(); + if (not(dtype == H5::PredType::IEEE_F32BE || dtype == H5::PredType::IEEE_F32LE)) + throw std::runtime_error{"dtype of min, max in h5 is not float."}; + op_dset.read(minmax, H5::PredType::NATIVE_FLOAT); + mmv.min_vector.emplace_back(minmax[0]); + mmv.max_vector.emplace_back(minmax[1]); + } + return mmv; +} + +MinMaxVectors Reader::read_input(int model_idx, int subg_idx, int input_idx) const +{ + MinMaxVectors mmv; + float minmax[2]; + auto num_run = _val_grp.getNumObjs(); + for (uint32_t r = 0; r < num_run; ++r) + { + // check whether minmax exists + char path[128]; // 128 is enough to print "/value/run_%d/model_%d/subg_%d/input_%d" + null + snprintf(path, 128, "/value/run_%d/model_%d/subg_%d/input_%d", r, model_idx, subg_idx, + input_idx); + if (!exists(_file.getId(), path)) + continue; + auto run_grp = _val_grp.openGroup(std::string("run_") + std::to_string(r)); + auto model_grp = run_grp.openGroup(std::string("model_") + std::to_string(model_idx)); + auto subg_grp = model_grp.openGroup(std::string("subg_") + std::to_string(subg_idx)); + auto op_dset = subg_grp.openDataSet(std::string("input_") + std::to_string(input_idx)); + + H5::DataType dtype = op_dset.getDataType(); + if (not(dtype == H5::PredType::IEEE_F32BE || dtype == H5::PredType::IEEE_F32LE)) + throw std::runtime_error{"dtype of min, max in h5 is not float."}; + op_dset.read(minmax, H5::PredType::NATIVE_FLOAT); + mmv.min_vector.emplace_back(minmax[0]); + mmv.max_vector.emplace_back(minmax[1]); + } + return mmv; +} + +} // namespace h5 +} // namespace minmax_embedder diff --git a/compiler/minmax-embedder/src/h5/Reader.h b/compiler/minmax-embedder/src/h5/Reader.h new file mode 100644 index 00000000000..4056e56d31f --- /dev/null +++ b/compiler/minmax-embedder/src/h5/Reader.h @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2023 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 __MINMAX_EMBEDDER_H5_READER_H__ +#define __MINMAX_EMBEDDER_H5_READER_H__ + +#include +#include +#include +#include + +namespace minmax_embedder +{ +namespace h5 +{ +// The hierachy of single model minmax h5 file +// +// GROUP / +// GROUP value +// └── GROUP run_{idx} +// └── GROUP model_{idx} +// └── GROUP subg_{idx} +// ├── DATASET op_{idx} +// │ DATATYPE Float32 +// │ DATASPACE (2) +// │ DATA { min, max } +// └── DATASET input_{idx} +// DATATYPE Float32 +// DATASPACE (2) +// DATA { min, max } +// GROUP name (optional, for debug) +// └── GROUP model_{idx} +// └── GROUP subg_{idx} +// ├── ATTRIBUTE op_{idx} +// │ DATATYPE String +// │ DATA { "op/name"} + +// └── ATTRIBUTE input_{idx} +// DATATYPE String +// DATA { "input/name"} +struct MinMaxVectors +{ + std::vector min_vector; + std::vector max_vector; +}; + +class Reader +{ +public: + Reader(const std::string &filepath); + /** + * @brief Returns minmax recording for op {model_idx, subg_idx, op_idx} + * + * @return MinMaxVectors + */ + MinMaxVectors read(int model_idx, int subg_idx, int op_idx) const; + /** + * @brief Returns minmax recording for input {model_idx, subg_idx, input_idx} + * + * @return MinMaxVectors + */ + MinMaxVectors read_input(int model_idx, int subg_idx, int input_idx) const; + +private: + H5::H5File _file; + H5::Group _val_grp; +}; + +} // namespace h5 +} // namespace minmax_embedder + +#endif // __MINMAX_EMBEDDER_H5_READER_H__