From 292dbbe051b12700b0432a39746838c1b55d196f Mon Sep 17 00:00:00 2001 From: Marcel Koch Date: Fri, 17 May 2024 14:57:15 +0000 Subject: [PATCH 1/5] [config] adds minimal stopping criteria specification Signed-off-by: Marcel Koch --- core/config/config_helper.cpp | 90 +++++++++++++++++++++++++++++++++-- core/config/config_helper.hpp | 11 ++++- 2 files changed, 96 insertions(+), 5 deletions(-) diff --git a/core/config/config_helper.cpp b/core/config/config_helper.cpp index 30b33063413..b3706863705 100644 --- a/core/config/config_helper.cpp +++ b/core/config/config_helper.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2017 - 2024 The Ginkgo authors +// SPDX-FileCopyrightText: 2017 - 2025 The Ginkgo authors // // SPDX-License-Identifier: BSD-3-Clause @@ -43,7 +43,9 @@ parse_or_get_factory(const pnode& config, if (config.get_tag() == pnode::tag_t::string) { return detail::registry_accessor::get_data( context, config.get_string()); - } else if (config.get_tag() == pnode::tag_t::map) { + } + + if (config.get_tag() == pnode::tag_t::map) { static std::map( @@ -55,9 +57,89 @@ parse_or_get_factory(const pnode& config, {"ImplicitResidualNorm", configure_implicit_residual}}}; return criterion_map.at(config.get("type").get_string())(config, context, td); - } else { - GKO_INVALID_STATE("The type of config is not valid."); } + + GKO_INVALID_STATE( + "Criteria must either be defined as a string or an array."); +} + + +std::vector> +parse_minimal_criteria(const pnode& config, const registry& context, + const type_descriptor& td) +{ + auto map_time = [](const pnode& config, const registry& context, + const type_descriptor& td) { + pnode time_config{{{"time_limit", config.get("time")}}}; + return configure_time(time_config, context, td); + }; + auto map_iteration = [](const pnode& config, const registry& context, + const type_descriptor& td) { + pnode iter_config{{{"max_iters", config.get("iteration")}}}; + return configure_iter(iter_config, context, td); + }; + auto create_residual_mapping = [](const std::string& key, + const std::string& baseline, + auto configure_fn) { + return std::make_pair( + key, [=](const pnode& config, const registry& context, + const type_descriptor& td) { + pnode res_config{{{"baseline", pnode{baseline}}, + {"reduction_factor", config.get(key)}}}; + return configure_fn(res_config, context, td); + }); + }; + std::map< + std::string, + std::function( + const pnode&, const registry&, type_descriptor)>> + criterion_map{ + {{"time", map_time}, + {"iteration", map_iteration}, + create_residual_mapping("relative_residual_norm", "rhs_norm", + configure_residual), + create_residual_mapping("initial_residual_norm", "initial_resnorm", + configure_residual), + create_residual_mapping("absolute_residual_norm", "absolute", + configure_residual), + create_residual_mapping("relative_implicit_residual_norm", + "rhs_norm", configure_implicit_residual), + create_residual_mapping("initial_implicit_residual_norm", + "initial_resnorm", + configure_implicit_residual), + create_residual_mapping("absolute_implicit_residual_norm", + "absolute", configure_implicit_residual)}}; + + std::vector> res; + for (const auto& it : config.get_map()) { + res.emplace_back(criterion_map.at(it.first)(config, context, td)); + } + return res; +} + + +std::vector> +parse_or_get_criteria(const pnode& config, const registry& context, + const type_descriptor& td) +{ + if (config.get_tag() == pnode::tag_t::array || + (config.get_tag() == pnode::tag_t::map && config.get("type"))) { + return parse_or_get_factory_vector( + config, context, td); + } + + if (config.get_tag() == pnode::tag_t::map) { + return parse_minimal_criteria(config, context, td); + } + + if (config.get_tag() == pnode::tag_t::string) { + return {detail::registry_accessor::get_data( + context, config.get_string())}; + } + + GKO_INVALID_STATE( + "Criteria must either be defined as a string, an array," + "or an map."); } } // namespace config diff --git a/core/config/config_helper.hpp b/core/config/config_helper.hpp index b1fa7bd69b5..fa6e5602199 100644 --- a/core/config/config_helper.hpp +++ b/core/config/config_helper.hpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2017 - 2024 The Ginkgo authors +// SPDX-FileCopyrightText: 2017 - 2025 The Ginkgo authors // // SPDX-License-Identifier: BSD-3-Clause @@ -141,6 +141,15 @@ parse_or_get_factory(const pnode& config, const registry& context, const type_descriptor& td); +/** + * parse or get an std::vector of criteria. + * A stored single criterion will be converted to an std::vector. + */ +std::vector> +parse_or_get_criteria(const pnode& config, const registry& context, + const type_descriptor& td); + + /** * give a vector of factory by calling parse_or_get_factory. */ From 13fd50f46a1e8f528c92d13466995e9675a16beb Mon Sep 17 00:00:00 2001 From: Marcel Koch Date: Fri, 17 May 2024 14:57:27 +0000 Subject: [PATCH 2/5] [config] adds test for minimal stopping criteria config Signed-off-by: Marcel Koch --- core/test/config/config.cpp | 57 ++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/core/test/config/config.cpp b/core/test/config/config.cpp index d5fed0f90c3..b3f9ca0f497 100644 --- a/core/test/config/config.cpp +++ b/core/test/config/config.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2017 - 2024 The Ginkgo authors +// SPDX-FileCopyrightText: 2017 - 2025 The Ginkgo authors // // SPDX-License-Identifier: BSD-3-Clause @@ -12,6 +12,7 @@ #include #include #include +#include #include "core/config/config_helper.hpp" #include "core/test/utils.hpp" @@ -122,6 +123,60 @@ TEST_F(Config, GenerateObjectWithCustomBuild) } +TEST_F(Config, GenerateCriteriaFromMinimalConfig) +{ + auto reg = registry(); + reg.emplace("precond", this->mtx); + pnode minimal_stop{{ + {"iteration", pnode{10}}, + {"relative_implicit_residual_norm", pnode{0.01}}, + {"relative_residual_norm", pnode{0.01}}, + {"time", pnode{100}}, + }}; + + pnode p{{{"criteria", minimal_stop}}}; + auto obj = std::dynamic_pointer_cast::Factory>( + parse(p, reg, type_descriptor{"float32", "void"}) + .on(this->exec)); + + ASSERT_NE(obj, nullptr); + auto criteria = obj->get_parameters().criteria; + ASSERT_EQ(criteria.size(), minimal_stop.get_map().size()); + { + SCOPED_TRACE("Iteration Criterion"); + auto it = + std::dynamic_pointer_cast( + criteria[0]); + ASSERT_NE(it, nullptr); + EXPECT_EQ(it->get_parameters().max_iters, 10); + } + { + SCOPED_TRACE("Implicit Residual Criterion"); + auto res = std::dynamic_pointer_cast< + const gko::stop::ImplicitResidualNorm::Factory>(criteria[1]); + ASSERT_NE(res, nullptr); + EXPECT_EQ(res->get_parameters().baseline, gko::stop::mode::rhs_norm); + EXPECT_EQ(res->get_parameters().reduction_factor, 0.01f); + } + { + SCOPED_TRACE("Residual Criterion"); + auto res = std::dynamic_pointer_cast< + const gko::stop::ResidualNorm::Factory>(criteria[2]); + ASSERT_NE(res, nullptr); + EXPECT_EQ(res->get_parameters().baseline, gko::stop::mode::rhs_norm); + EXPECT_EQ(res->get_parameters().reduction_factor, 0.01f); + } + { + SCOPED_TRACE("Time Criterion"); + using namespace std::chrono_literals; + auto time = std::dynamic_pointer_cast( + criteria[3]); + ASSERT_NE(time, nullptr); + EXPECT_EQ(time->get_parameters().time_limit, 100ns); + } +} + + TEST(GetValue, IndexType) { long long int value = 123; From b54469ac10f88c983d3713e30fb5616ee9e58997 Mon Sep 17 00:00:00 2001 From: Marcel Koch Date: Thu, 13 Feb 2025 16:06:33 +0100 Subject: [PATCH 3/5] review updates: - allow changing the value_type - fix docs - call `parse_or_get_criteria` directly from solver config Co-authored-by: Yu-Hsiang M. Tsai --- core/config/config_helper.cpp | 9 +++- core/config/config_helper.hpp | 4 +- core/config/solver_config.hpp | 5 +- core/test/config/config.cpp | 97 +++++++++++++++++++++++++++++++---- 4 files changed, 100 insertions(+), 15 deletions(-) diff --git a/core/config/config_helper.cpp b/core/config/config_helper.cpp index b3706863705..439060f5b41 100644 --- a/core/config/config_helper.cpp +++ b/core/config/config_helper.cpp @@ -12,6 +12,7 @@ #include "core/config/registry_accessor.hpp" #include "core/config/stop_config.hpp" +#include "type_descriptor_helper.hpp" namespace gko { namespace config { @@ -110,9 +111,15 @@ parse_minimal_criteria(const pnode& config, const registry& context, create_residual_mapping("absolute_implicit_residual_norm", "absolute", configure_implicit_residual)}}; + type_descriptor updated_td = update_type(config, td); + std::vector> res; for (const auto& it : config.get_map()) { - res.emplace_back(criterion_map.at(it.first)(config, context, td)); + if (it.first == "value_type") { + continue; + } + res.emplace_back( + criterion_map.at(it.first)(config, context, updated_td)); } return res; } diff --git a/core/config/config_helper.hpp b/core/config/config_helper.hpp index fa6e5602199..c87c3e0cca7 100644 --- a/core/config/config_helper.hpp +++ b/core/config/config_helper.hpp @@ -142,8 +142,8 @@ parse_or_get_factory(const pnode& config, const type_descriptor& td); /** - * parse or get an std::vector of criteria. - * A stored single criterion will be converted to an std::vector. + * parse or get a std::vector of criteria. + * A stored single criterion will be converted to a std::vector. */ std::vector> parse_or_get_criteria(const pnode& config, const registry& context, diff --git a/core/config/solver_config.hpp b/core/config/solver_config.hpp index e5f51ff85f4..e2ed485a70c 100644 --- a/core/config/solver_config.hpp +++ b/core/config/solver_config.hpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2017 - 2024 The Ginkgo authors +// SPDX-FileCopyrightText: 2017 - 2025 The Ginkgo authors // // SPDX-License-Identifier: BSD-3-Clause @@ -27,8 +27,7 @@ inline void common_solver_parse(SolverParam& params, const pnode& config, } if (auto& obj = config.get("criteria")) { params.with_criteria( - gko::config::parse_or_get_factory_vector< - const stop::CriterionFactory>(obj, context, td_for_child)); + gko::config::parse_or_get_criteria(obj, context, td_for_child)); } if (auto& obj = config.get("preconditioner")) { params.with_preconditioner( diff --git a/core/test/config/config.cpp b/core/test/config/config.cpp index b3f9ca0f497..ba3aae2453f 100644 --- a/core/test/config/config.cpp +++ b/core/test/config/config.cpp @@ -125,9 +125,12 @@ TEST_F(Config, GenerateObjectWithCustomBuild) TEST_F(Config, GenerateCriteriaFromMinimalConfig) { - auto reg = registry(); - reg.emplace("precond", this->mtx); + // the map is ordered, since this allows for easier comparison in the test pnode minimal_stop{{ + {"absolute_implicit_residual_norm", pnode{0.01}}, + {"absolute_residual_norm", pnode{0.01}}, + {"initial_implicit_residual_norm", pnode{0.01}}, + {"initial_residual_norm", pnode{0.01}}, {"iteration", pnode{10}}, {"relative_implicit_residual_norm", pnode{0.01}}, {"relative_residual_norm", pnode{0.01}}, @@ -136,32 +139,71 @@ TEST_F(Config, GenerateCriteriaFromMinimalConfig) pnode p{{{"criteria", minimal_stop}}}; auto obj = std::dynamic_pointer_cast::Factory>( - parse(p, reg, type_descriptor{"float32", "void"}) + parse(p, registry(), + type_descriptor{"float32", "void"}) .on(this->exec)); ASSERT_NE(obj, nullptr); auto criteria = obj->get_parameters().criteria; ASSERT_EQ(criteria.size(), minimal_stop.get_map().size()); + try { + throw std::runtime_error("Criteria does not exist"); + } catch (...) { + } + { + SCOPED_TRACE("Absolute Implicit Residual Criterion"); + auto res = std::dynamic_pointer_cast< + const gko::stop::ImplicitResidualNorm::Factory>(criteria[0]); + ASSERT_NE(res, nullptr); + EXPECT_EQ(res->get_parameters().baseline, gko::stop::mode::absolute); + EXPECT_EQ(res->get_parameters().reduction_factor, 0.01f); + } + { + SCOPED_TRACE("Absolute Residual Criterion"); + auto res = std::dynamic_pointer_cast< + const gko::stop::ResidualNorm::Factory>(criteria[1]); + ASSERT_NE(res, nullptr); + EXPECT_EQ(res->get_parameters().baseline, gko::stop::mode::absolute); + EXPECT_EQ(res->get_parameters().reduction_factor, 0.01f); + } + { + SCOPED_TRACE("Initial Implicit Residual Criterion"); + auto res = std::dynamic_pointer_cast< + const gko::stop::ImplicitResidualNorm::Factory>(criteria[2]); + ASSERT_NE(res, nullptr); + EXPECT_EQ(res->get_parameters().baseline, + gko::stop::mode::initial_resnorm); + EXPECT_EQ(res->get_parameters().reduction_factor, 0.01f); + } + { + SCOPED_TRACE("Initial Residual Criterion"); + auto res = std::dynamic_pointer_cast< + const gko::stop::ResidualNorm::Factory>(criteria[3]); + ASSERT_NE(res, nullptr); + EXPECT_EQ(res->get_parameters().baseline, + gko::stop::mode::initial_resnorm); + EXPECT_EQ(res->get_parameters().reduction_factor, 0.01f); + } { SCOPED_TRACE("Iteration Criterion"); auto it = std::dynamic_pointer_cast( - criteria[0]); + criteria[4]); ASSERT_NE(it, nullptr); EXPECT_EQ(it->get_parameters().max_iters, 10); } { - SCOPED_TRACE("Implicit Residual Criterion"); + SCOPED_TRACE("Relative Implicit Residual Criterion"); auto res = std::dynamic_pointer_cast< - const gko::stop::ImplicitResidualNorm::Factory>(criteria[1]); + const gko::stop::ImplicitResidualNorm::Factory>(criteria[5]); ASSERT_NE(res, nullptr); EXPECT_EQ(res->get_parameters().baseline, gko::stop::mode::rhs_norm); EXPECT_EQ(res->get_parameters().reduction_factor, 0.01f); } { - SCOPED_TRACE("Residual Criterion"); + SCOPED_TRACE("Relative Residual Criterion"); auto res = std::dynamic_pointer_cast< - const gko::stop::ResidualNorm::Factory>(criteria[2]); + const gko::stop::ResidualNorm::Factory>(criteria[6]); ASSERT_NE(res, nullptr); EXPECT_EQ(res->get_parameters().baseline, gko::stop::mode::rhs_norm); EXPECT_EQ(res->get_parameters().reduction_factor, 0.01f); @@ -170,7 +212,44 @@ TEST_F(Config, GenerateCriteriaFromMinimalConfig) SCOPED_TRACE("Time Criterion"); using namespace std::chrono_literals; auto time = std::dynamic_pointer_cast( - criteria[3]); + criteria[7]); + ASSERT_NE(time, nullptr); + EXPECT_EQ(time->get_parameters().time_limit, 100ns); + } +} + + +TEST_F(Config, GenerateCriteriaFromMinimalConfigWithValueType) +{ + auto reg = registry(); + reg.emplace("precond", this->mtx); + pnode minimal_stop{{ + {"value_type", pnode{"float64"}}, + {"relative_residual_norm", pnode{0.01}}, + {"time", pnode{100}}, + }}; + + pnode p{{{"criteria", minimal_stop}}}; + auto obj = std::dynamic_pointer_cast::Factory>( + parse(p, reg, type_descriptor{"float32", "void"}) + .on(this->exec)); + + ASSERT_NE(obj, nullptr); + auto criteria = obj->get_parameters().criteria; + ASSERT_EQ(criteria.size(), minimal_stop.get_map().size() - 1); + { + SCOPED_TRACE("Residual Criterion"); + auto res = std::dynamic_pointer_cast< + const gko::stop::ResidualNorm::Factory>(criteria[0]); + ASSERT_NE(res, nullptr); + EXPECT_EQ(res->get_parameters().baseline, gko::stop::mode::rhs_norm); + EXPECT_EQ(res->get_parameters().reduction_factor, 0.01); + } + { + SCOPED_TRACE("Time Criterion"); + using namespace std::chrono_literals; + auto time = std::dynamic_pointer_cast( + criteria[1]); ASSERT_NE(time, nullptr); EXPECT_EQ(time->get_parameters().time_limit, 100ns); } From 9663428c467a0c0b29f744fea70a624dc3415126 Mon Sep 17 00:00:00 2001 From: Marcel Koch Date: Tue, 18 Feb 2025 09:18:28 +0100 Subject: [PATCH 4/5] [config] add doc on minimal criteria --- include/ginkgo/core/config/config.hpp | 49 +++++++++++++++++++++------ 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/include/ginkgo/core/config/config.hpp b/include/ginkgo/core/config/config.hpp index 27c08caa3a3..38979cbc8c8 100644 --- a/include/ginkgo/core/config/config.hpp +++ b/include/ginkgo/core/config/config.hpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2017 - 2024 The Ginkgo authors +// SPDX-FileCopyrightText: 2017 - 2025 The Ginkgo authors // // SPDX-License-Identifier: BSD-3-Clause @@ -87,8 +87,29 @@ class pnode; * interpreted as a 1-element array. This means the following configurations * are equivalent if the key expects an array value: `"": [{object}]` * and `"": {object}`. + * 9. The stopping criteria for a solver can alternatively be defined through a + * simple key-value map, where each key corresponds to a single criterion. + * The available keys are: + * - "iteration": , corresponds to gko::stop::Iteration + * - "relative_residual_norm": , corresponds to + * gko::stop::ResidualNorm build with gko::stop::mode::rhs_norm + * - "initial_residual_norm": , corresponds to + * gko::stop::ResidualNorm build with gko::stop::mode::initial_resnorm + * - "absolute_residual_norm": , corresponds to + * gko::stop::ResidualNorm build with gko::stop::mode::absolute + * - "relative_implicit_residual_norm": , corresponds to + * gko::stop::ImplicitResidualNorm build with gko::stop::mode::rhs_norm + * - "initial_implicit_residual_norm": , corresponds to + * gko::stop::ImplicitResidualNorm build with + * gko::stop::mode::initial_resnorm + * - "absolute_implicit_residual_norm": , corresponds to + * gko::stop::ImplicitResidualNorm build with gko::stop::mode::absolute + * - "time": , corresponds to gko::stop::Time + * The simplified definition also allows for setting the `ValueType` template + * parameter as discussed in 4. and 5. * - * All configurations need to specify the resulting type by the field: + * All configurations (except the simplified stopping criteria) need to specify + * the resulting type by the field: * ``` * "type": "some_supported_ginkgo_type" * ``` @@ -115,6 +136,14 @@ class pnode; * criteria with maximal 10 iterations, and a ResidualNorm criteria with a * reduction factor of 1e-6. * + * The criteria parameter can alternatively be defined as + * ``` + * "criteria": { + * "iteration": 10, + * "relative_residual_norm": 1e-6 + * } + * ``` + * * By default, the factory will use the value type double, and index type * int32 when creating templated types. This can be changed by passing in a * type_descriptor. For example: @@ -150,15 +179,13 @@ class pnode; * base. * @param context The registry which stores the building function map and the * storage for generated objects. - * @param type_descriptor The default value and index type. If any object that - * is created as part of this configuration has a - * templated type, then the value and/or index type from - * the descriptor will be used. Any definition of the - * value and/or index type within the config will take - * precedence over the descriptor. If `void` is used for - * one or both of the types, then the corresponding type - * has to be defined in the config, otherwise the - * parsing will fail. + * @param td The default value and index type. If any object that + * is created as part of this configuration has a templated type, + * then the value and/or index type from the descriptor will be used. + * Any definition of the value and/or index type within the config + * will take precedence over the descriptor. If `void` is used for + * one or both of the types, then the corresponding type has to be + * defined in the config, otherwise the parsing will fail. * * @return a deferred_factory_parameter which creates an LinOpFactory after * `.on(exec)` is called on it. From 7f91879cdc5f31c1c0e954eb984968d24bab80d4 Mon Sep 17 00:00:00 2001 From: Marcel Koch Date: Tue, 18 Feb 2025 09:21:10 +0100 Subject: [PATCH 5/5] [config] more consistent examples --- include/ginkgo/core/config/config.hpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/include/ginkgo/core/config/config.hpp b/include/ginkgo/core/config/config.hpp index 38979cbc8c8..83e9a42ba34 100644 --- a/include/ginkgo/core/config/config.hpp +++ b/include/ginkgo/core/config/config.hpp @@ -108,6 +108,10 @@ class pnode; * The simplified definition also allows for setting the `ValueType` template * parameter as discussed in 4. and 5. * + * @note The formatting for the example snippets is oriented on the JSON format. + * For all supported configuration formats check the headers in + * extension/config + * * All configurations (except the simplified stopping criteria) need to specify * the resulting type by the field: * ``` @@ -155,20 +159,20 @@ class pnode; * will lead to a GMRES solver that uses `float` as its value type. * Additionally, the config can be used to set these types through the fields: * ``` - * value_type: "some_value_type" + * "value_type": "some_value_type" * ``` * These types take precedence over the type descriptor and they are used for * every created object beginning from the config level they are defined on and * every deeper nested level, until a new type is defined. So, considering the * following example * ``` - * type: "solver::Ir", - * value_type: "float32" - * solver: { - * type: "solver::Gmres", - * preconditioner: { - * type: "preconditioner::Jacobi" - * value_type: "float64" + * "type": "solver::Ir", + * "value_type": "float32" + * "solver": { + * "type": "solver::Gmres", + * "preconditioner": { + * "type": "preconditioner::Jacobi" + * "value_type": "float64" * } * } * ```