From 4f8615e8bd6b5ee93c44c67771b86aa7338f7ca6 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 21 Oct 2026 11:13:25 -0500 Subject: [PATCH 1/3] Split solver settings into plain parameter bases and one shared registry pdlp_solver_settings_t and mip_solver_settings_t mixed user-facing parameters with GPU-backed data (rmm buffers, warm start, callbacks), so anything that needed only the parameters had to link the GPU stack, and the parameter table lived next to the GPU code. - Add pdlp_parameters_t and mip_parameters_t (new headers, no CUDA/rmm/raft includes). The settings classes now derive from them, so every member keeps its name and meaning. - Move the name/default/range table and the set/get/file functions into solver_parameter_registry_t, built into the CPU-only cuopt_client library. solver_settings_t forwards to it. - Add cpu_solver_settings_t, the counterpart of cpu_optimization_problem_t, on the same registry. A test checks it lists the same parameters and defaults as solver_settings_t. Source compatible; the layout of the two settings classes changes. Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: Ramakrishna Prabhu --- .../cpu_solver_settings.hpp | 111 ++++ .../mip/mip_parameters.hpp | 173 +++++ .../mip/solver_settings.hpp | 140 +--- .../pdlp/pdlp_parameters.hpp | 264 ++++++++ .../pdlp/solver_settings.hpp | 220 +----- .../solver_parameter_registry.hpp | 69 ++ .../solver_settings.hpp | 8 +- cpp/src/math_optimization/CMakeLists.txt | 1 + .../solver_parameter_registry.cpp | 625 ++++++++++++++++++ cpp/src/math_optimization/solver_settings.cpp | 340 +--------- cpp/src/math_optimization/solver_settings.cu | 181 +---- cpp/src/mip_heuristics/solver_settings.cpp | 11 - cpp/src/pdlp/solver_settings.cu | 11 - .../unit_tests/solver_settings_test.cu | 59 ++ 14 files changed, 1327 insertions(+), 886 deletions(-) create mode 100644 cpp/include/cuopt/mathematical_optimization/cpu_solver_settings.hpp create mode 100644 cpp/include/cuopt/mathematical_optimization/mip/mip_parameters.hpp create mode 100644 cpp/include/cuopt/mathematical_optimization/pdlp/pdlp_parameters.hpp create mode 100644 cpp/include/cuopt/mathematical_optimization/solver_parameter_registry.hpp create mode 100644 cpp/src/math_optimization/solver_parameter_registry.cpp diff --git a/cpp/include/cuopt/mathematical_optimization/cpu_solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/cpu_solver_settings.hpp new file mode 100644 index 0000000000..227544b2dd --- /dev/null +++ b/cpp/include/cuopt/mathematical_optimization/cpu_solver_settings.hpp @@ -0,0 +1,111 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include +#include +#include + +#include +#include + +namespace cuopt { +namespace mathematical_optimization { + +/** + * @brief Solver settings with no GPU-backed members, the counterpart of cpu_optimization_problem_t. + * + * Same parameters, same names, defaults and ranges as solver_settings_t, because both bind the + * same pdlp_parameters_t / mip_parameters_t through one solver_parameter_registry_t. What it + * leaves out is what needs a GPU: device initial solutions, device warm start and callbacks. + * The PDLP warm start is the CPU one, for remote solves. + */ +template +class cpu_solver_settings_t { + public: + cpu_solver_settings_t() : registry_(pdlp_, mip_) {} + + cpu_solver_settings_t(const cpu_solver_settings_t&) = delete; + cpu_solver_settings_t& operator=(const cpu_solver_settings_t&) = delete; + cpu_solver_settings_t(cpu_solver_settings_t&&) = delete; + cpu_solver_settings_t& operator=(cpu_solver_settings_t&&) = delete; + + void set_parameter_from_string(const std::string& name, const std::string& value) + { + registry_.set_parameter_from_string(name, value); + } + + template + void set_parameter(const std::string& name, T value) + { + registry_.set_parameter(name, value); + } + + template + T get_parameter(const std::string& name) const + { + return registry_.template get_parameter(name); + } + + std::string get_parameter_as_string(const std::string& name) const + { + return registry_.get_parameter_as_string(name); + } + + const std::vector>& get_float_parameters() const + { + return registry_.get_float_parameters(); + } + const std::vector>& get_int_parameters() const + { + return registry_.get_int_parameters(); + } + const std::vector>& get_bool_parameters() const + { + return registry_.get_bool_parameters(); + } + const std::vector>& get_string_parameters() const + { + return registry_.get_string_parameters(); + } + const std::vector get_parameter_names() const + { + return registry_.get_parameter_names(); + } + + void load_parameters_from_file(const std::string& path) + { + registry_.load_parameters_from_file(path); + } + bool dump_parameters_to_file(const std::string& path, bool hyperparameters_only = true) const + { + return registry_.dump_parameters_to_file(path, hyperparameters_only); + } + + pdlp_parameters_t& get_pdlp_parameters() { return pdlp_; } + const pdlp_parameters_t& get_pdlp_parameters() const { return pdlp_; } + mip_parameters_t& get_mip_parameters() { return mip_; } + const mip_parameters_t& get_mip_parameters() const { return mip_; } + + /** LP only. Left unpopulated for a cold start. */ + cpu_pdlp_warm_start_data_t& get_cpu_pdlp_warm_start_data() { return pdlp_warm_start_; } + const cpu_pdlp_warm_start_data_t& get_cpu_pdlp_warm_start_data() const + { + return pdlp_warm_start_; + } + + private: + pdlp_parameters_t pdlp_; + mip_parameters_t mip_; + cpu_pdlp_warm_start_data_t pdlp_warm_start_; + solver_parameter_registry_t registry_; // after pdlp_ and mip_, which it points into +}; + +} // namespace mathematical_optimization +} // namespace cuopt diff --git a/cpp/include/cuopt/mathematical_optimization/mip/mip_parameters.hpp b/cpp/include/cuopt/mathematical_optimization/mip/mip_parameters.hpp new file mode 100644 index 0000000000..715f77216f --- /dev/null +++ b/cpp/include/cuopt/mathematical_optimization/mip/mip_parameters.hpp @@ -0,0 +1,173 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +// Plain, user-facing MIP parameters. This header must not include CUDA, rmm or raft; see +// pdlp_parameters.hpp. + +namespace cuopt { +namespace CUOPT_EXPORT mathematical_optimization { + +struct benchmark_info_t { + double last_improvement_of_best_feasible = 0; + double last_improvement_after_recombination = 0; + double objective_of_initial_population = std::numeric_limits::max(); + // LP relaxation objective at the root node, BEFORE any cuts have been + // added. quiet_NaN() means "B&B did not run cut passes / value was + // never written" — distinguishes it from a legitimate 0.0. + double root_lp_no_cuts = std::numeric_limits::quiet_NaN(); + // LP relaxation objective at the root node, AFTER the full cut loop + // (final pass result). The dual gap "by cuts at the root" is then + // gap_after_cuts = opt - root_lp_with_cuts (in B&B's solver + // objective sense) + // and the classical "gap closed by cuts" metric is + // gap_closed_pct = 100 * (root_lp_with_cuts - root_lp_no_cuts) + // / (opt - root_lp_no_cuts). + // quiet_NaN() means "B&B did not finish the cut loop / value not written". + double root_lp_with_cuts = std::numeric_limits::quiet_NaN(); + + // Wall-clock time spent inside the root-node cut generation loop + // (sum of generate_cuts + score_cuts + check_for_duplicate_cuts + + // get_best_cuts + add_cuts + post-cut LP resolves), in seconds. + // Published by branch_and_bound.cpp::solve() at the same point that + // root_lp_with_cuts is finalised. quiet_NaN() means "cut loop did + // not run / value never written". + double cut_generation_time_sec = std::numeric_limits::quiet_NaN(); +}; + +/** + * @brief The MIP parameters, with no GPU-backed members. + * + * mip_solver_settings_t derives from this and adds initial solutions and callbacks. + */ +template +struct mip_parameters_t { + struct tolerances_t { + f_t presolve_absolute_tolerance = 1.0e-6; + f_t absolute_tolerance = 1.0e-6; + f_t relative_tolerance = 1.0e-12; + f_t integrality_tolerance = 1.0e-5; + f_t absolute_mip_gap = 1.0e-10; + f_t relative_mip_gap = 1.0e-4; + }; + + tolerances_t get_tolerances() const noexcept { return tolerances; } + + tolerances_t tolerances; + + f_t time_limit = std::numeric_limits::infinity(); + f_t work_limit = std::numeric_limits::infinity(); + f_t semi_continuous_big_m = f_t(1e10); + i_t node_limit = std::numeric_limits::max(); + bool heuristics_only = false; + i_t reliability_branching = -1; + i_t num_cpu_threads = -1; // -1 means use default number of threads in branch and bound + i_t symmetry = -1; + i_t max_cut_passes = 10; // number of cut passes to make + i_t mir_cuts = -1; + i_t mixed_integer_gomory_cuts = -1; + i_t knapsack_cuts = -1; + i_t flow_cover_cuts = -1; + i_t clique_cuts = -1; + i_t zero_half_cuts = -1; + i_t implied_bound_cuts = -1; + i_t strong_chvatal_gomory_cuts = -1; + i_t reduced_cost_strengthening = -1; + i_t dual_degenerate_feasibility_pump = -1; // -1 = automatic (off), 0 = off, 1 = on + i_t primal_degenerate_pivots = -1; // -1 = automatic (on), 0 = off, 1 = on + i_t dual_degenerate_pivots = -1; // -1 = automatic (off), 0 = off, 1 = root/dives, 2 = everywhere + i_t objective_step = 1; // 0 = disable objective step tightening, 1 = enable + f_t cut_change_threshold = -1.0; + f_t cut_min_orthogonality = 0.5; + i_t mip_batch_pdlp_strong_branching{ + 0}; // 0 = DS only, 1 = cooperative DS + PDLP, 2 = batch PDLP only + i_t mip_batch_pdlp_reliability_branching{ + 0}; // 0 = DS only, 1 = cooperative DS + PDLP, 2 = batch PDLP only + i_t strong_branching_simplex_iteration_limit = -1; + i_t num_gpus = 1; + method_t method{method_t::Concurrent}; + i_t large_nnz_threshold{50'000'000}; + bool log_to_console = true; + + std::string log_file; + std::string sol_file; + std::string user_problem_file; + std::string presolve_file; + + int mip_scaling = CUOPT_MIP_SCALING_NO_OBJECTIVE; + presolver_t presolver{presolver_t::Default}; + /** + * @brief Enable the cuOpt internal probing-cache step of presolve (MIP only). + * + * Probing is part of cuOpt's internal MIP presolve and runs only when the + * higher-level presolve is enabled (i.e. `presolver != presolver_t::None`). + * When this is `false`, probing is skipped even if presolve is otherwise on. + */ + bool probing{true}; + /** + * @brief Enable the block bounded-variable-elimination step of cuOpt's MIP presolve. + * + * Runs after trivial_presolve and eliminates blocks of functionally-determined binary auxiliary + * variables discovered via the probing-cache implication closure, re-encoding each block's + * projected relation as certified prime-implicate clauses. Requires the probing-cache step; a + * no-op when no certified reduction exists. + */ + bool block_bve{true}; + /** + * @brief Enable the indicator-strengthening step of presolve (MIP only). + * + * Runs before Papilo and only when the higher-level presolve is enabled. It appends implied + * indicator rows and lifts capacity rows by the indicator that bounds all of their members. + */ + bool indicator_strengthening{true}; + /** + * @brief Determinism mode for MIP solver. + * + * Controls the determinism behavior of the MIP solver: + * - CUOPT_MODE_OPPORTUNISTIC (0): Default mode, allows non-deterministic + * parallelism for better performance + * - CUOPT_MODE_DETERMINISTIC (1): Ensures deterministic results across runs + * at potential cost of performance + */ + int determinism_mode = CUOPT_MODE_OPPORTUNISTIC; + /** + * @brief Random seed for the MIP solver. + * + * Controls the initial seed for random number generation in the solver. + * Use -1 to generate a random seed. + */ + i_t seed = -1; + // this is for extracting info from different places of the solver during + // benchmarks + benchmark_info_t* benchmark_info_ptr = nullptr; + + // TODO check with Akif and Alice + pdlp::pdlp_hyper_params_t hyper_params; + + mip_heuristics_hyper_params_t heuristic_params; + mip_diving_hyper_params_t diving_params; + mip_submip_hyper_params_t submip_params; +}; + +} // namespace CUOPT_EXPORT mathematical_optimization +} // namespace cuopt diff --git a/cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp index 8812493b02..753e4a6b3d 100644 --- a/cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -28,33 +29,6 @@ namespace cuopt { namespace CUOPT_EXPORT mathematical_optimization { -struct benchmark_info_t { - double last_improvement_of_best_feasible = 0; - double last_improvement_after_recombination = 0; - double objective_of_initial_population = std::numeric_limits::max(); - // LP relaxation objective at the root node, BEFORE any cuts have been - // added. quiet_NaN() means "B&B did not run cut passes / value was - // never written" — distinguishes it from a legitimate 0.0. - double root_lp_no_cuts = std::numeric_limits::quiet_NaN(); - // LP relaxation objective at the root node, AFTER the full cut loop - // (final pass result). The dual gap "by cuts at the root" is then - // gap_after_cuts = opt - root_lp_with_cuts (in B&B's solver - // objective sense) - // and the classical "gap closed by cuts" metric is - // gap_closed_pct = 100 * (root_lp_with_cuts - root_lp_no_cuts) - // / (opt - root_lp_no_cuts). - // quiet_NaN() means "B&B did not finish the cut loop / value not written". - double root_lp_with_cuts = std::numeric_limits::quiet_NaN(); - - // Wall-clock time spent inside the root-node cut generation loop - // (sum of generate_cuts + score_cuts + check_for_duplicate_cuts + - // get_best_cuts + add_cuts + post-cut LP resolves), in seconds. - // Published by branch_and_bound.cpp::solve() at the same point that - // root_lp_with_cuts is finalised. quiet_NaN() means "cut loop did - // not run / value never written". - double cut_generation_time_sec = std::numeric_limits::quiet_NaN(); -}; - // Forward declare solver_settings_t for friend class template class solver_settings_t; @@ -66,8 +40,10 @@ template struct mip_solver_settings_accessor; template -class mip_solver_settings_t { +class mip_solver_settings_t : public mip_parameters_t { public: + using typename mip_parameters_t::tolerances_t; + mip_solver_settings_t() = default; /** @@ -101,118 +77,10 @@ class mip_solver_settings_t { */ const std::vector get_mip_callbacks() const; - struct tolerances_t { - f_t presolve_absolute_tolerance = 1.0e-6; - f_t absolute_tolerance = 1.0e-6; - f_t relative_tolerance = 1.0e-12; - f_t integrality_tolerance = 1.0e-5; - f_t absolute_mip_gap = 1.0e-10; - f_t relative_mip_gap = 1.0e-4; - }; - - /** - * @brief Get the tolerance settings as a single structure. - */ - tolerances_t get_tolerances() const noexcept; - template friend class problem_checking_t; - tolerances_t tolerances; - - f_t time_limit = std::numeric_limits::infinity(); - f_t work_limit = std::numeric_limits::infinity(); - f_t semi_continuous_big_m = f_t(1e10); - i_t node_limit = std::numeric_limits::max(); - bool heuristics_only = false; - i_t reliability_branching = -1; - i_t num_cpu_threads = -1; // -1 means use default number of threads in branch and bound - i_t symmetry = -1; - i_t max_cut_passes = 10; // number of cut passes to make - i_t mir_cuts = -1; - i_t mixed_integer_gomory_cuts = -1; - i_t knapsack_cuts = -1; - i_t flow_cover_cuts = -1; - i_t clique_cuts = -1; - i_t zero_half_cuts = -1; - i_t implied_bound_cuts = -1; - i_t strong_chvatal_gomory_cuts = -1; - i_t reduced_cost_strengthening = -1; - i_t dual_degenerate_feasibility_pump = -1; // -1 = automatic (off), 0 = off, 1 = on - i_t primal_degenerate_pivots = -1; // -1 = automatic (on), 0 = off, 1 = on - i_t dual_degenerate_pivots = -1; // -1 = automatic (off), 0 = off, 1 = root/dives, 2 = everywhere - i_t objective_step = 1; // 0 = disable objective step tightening, 1 = enable - f_t cut_change_threshold = -1.0; - f_t cut_min_orthogonality = 0.5; - i_t mip_batch_pdlp_strong_branching{ - 0}; // 0 = DS only, 1 = cooperative DS + PDLP, 2 = batch PDLP only - i_t mip_batch_pdlp_reliability_branching{ - 0}; // 0 = DS only, 1 = cooperative DS + PDLP, 2 = batch PDLP only - i_t strong_branching_simplex_iteration_limit = -1; - i_t num_gpus = 1; - method_t method{method_t::Concurrent}; - i_t large_nnz_threshold{50'000'000}; - bool log_to_console = true; - - std::string log_file; - std::string sol_file; - std::string user_problem_file; - std::string presolve_file; - /** Initial primal solutions */ std::vector>> initial_solutions; - int mip_scaling = CUOPT_MIP_SCALING_NO_OBJECTIVE; - presolver_t presolver{presolver_t::Default}; - /** - * @brief Enable the cuOpt internal probing-cache step of presolve (MIP only). - * - * Probing is part of cuOpt's internal MIP presolve and runs only when the - * higher-level presolve is enabled (i.e. `presolver != presolver_t::None`). - * When this is `false`, probing is skipped even if presolve is otherwise on. - */ - bool probing{true}; - /** - * @brief Enable the block bounded-variable-elimination step of cuOpt's MIP presolve. - * - * Runs after trivial_presolve and eliminates blocks of functionally-determined binary auxiliary - * variables discovered via the probing-cache implication closure, re-encoding each block's - * projected relation as certified prime-implicate clauses. Requires the probing-cache step; a - * no-op when no certified reduction exists. - */ - bool block_bve{true}; - /** - * @brief Enable the indicator-strengthening step of presolve (MIP only). - * - * Runs before Papilo and only when the higher-level presolve is enabled. It appends implied - * indicator rows and lifts capacity rows by the indicator that bounds all of their members. - */ - bool indicator_strengthening{true}; - /** - * @brief Determinism mode for MIP solver. - * - * Controls the determinism behavior of the MIP solver: - * - CUOPT_MODE_OPPORTUNISTIC (0): Default mode, allows non-deterministic - * parallelism for better performance - * - CUOPT_MODE_DETERMINISTIC (1): Ensures deterministic results across runs - * at potential cost of performance - */ - int determinism_mode = CUOPT_MODE_OPPORTUNISTIC; - /** - * @brief Random seed for the MIP solver. - * - * Controls the initial seed for random number generation in the solver. - * Use -1 to generate a random seed. - */ - i_t seed = -1; - // this is for extracting info from different places of the solver during - // benchmarks - benchmark_info_t* benchmark_info_ptr = nullptr; - - // TODO check with Akif and Alice - pdlp::pdlp_hyper_params_t hyper_params; - - mip_heuristics_hyper_params_t heuristic_params; - mip_diving_hyper_params_t diving_params; - mip_submip_hyper_params_t submip_params; private: std::vector mip_callbacks_; diff --git a/cpp/include/cuopt/mathematical_optimization/pdlp/pdlp_parameters.hpp b/cpp/include/cuopt/mathematical_optimization/pdlp/pdlp_parameters.hpp new file mode 100644 index 0000000000..560279f57a --- /dev/null +++ b/cpp/include/cuopt/mathematical_optimization/pdlp/pdlp_parameters.hpp @@ -0,0 +1,264 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +// Plain, user-facing PDLP/LP parameters. This header must not include CUDA, rmm or raft: it is +// what pdlp_solver_settings_t derives from, and what a CPU-only client uses without a GPU stack. + +namespace cuopt { +namespace CUOPT_EXPORT mathematical_optimization { + +/** + * @brief Enum representing the different solver modes under which PDLP can + * operate. + * + * Stable3: Best overall mode from experiments; balances speed and convergence + * success. If you want to use the legacy version, use Stable2. + * Methodical1: Usually leads to slower individual steps but fewer are needed to + * converge. It uses from 1.3x up to 1.7x times more memory. + * Fast1: Less convergence success but usually yields the highest speed + * + * @note Default mode is Stable3. + */ +// Forced to use an enum instead of an enum class for compatibility with the +// Cython layer +enum pdlp_solver_mode_t : int { + Stable1 = CUOPT_PDLP_SOLVER_MODE_STABLE1, + Stable2 = CUOPT_PDLP_SOLVER_MODE_STABLE2, + Methodical1 = CUOPT_PDLP_SOLVER_MODE_METHODICAL1, + Fast1 = CUOPT_PDLP_SOLVER_MODE_FAST1, + Stable3 = CUOPT_PDLP_SOLVER_MODE_STABLE3 +}; + +/** + * @brief Enum representing the different methods that can be used to solve the + * linear programming problem. + * + * Concurrent: Use PDLP, Barrier and DualSimplex in parallel. + * PDLP: Use the PDLP method. + * DualSimplex: Use the dual simplex method. + * Barrier: Use the barrier method + * PrimalSimplex: Use the primal simplex method. + * Unset: The value was not set. + * + * @note Default method is Concurrent. + */ +enum method_t : int { + Concurrent = CUOPT_METHOD_CONCURRENT, + PDLP = CUOPT_METHOD_PDLP, + DualSimplex = CUOPT_METHOD_DUAL_SIMPLEX, + Barrier = CUOPT_METHOD_BARRIER, + PrimalSimplex = CUOPT_METHOD_PRIMAL_SIMPLEX, + Unset = CUOPT_METHOD_UNSET +}; + +/// Returns the corresponding string from the enum `method_t`. +inline std::string method_to_string(method_t method) +{ + switch (method) { + case method_t::DualSimplex: return "Dual Simplex"; + case method_t::PDLP: return "PDLP"; + case method_t::Barrier: return "Barrier"; + case method_t::Concurrent: return "Concurrent"; + case method_t::PrimalSimplex: return "Primal Simplex"; + default: return "Unset"; + } +} + +/** + * @brief Enum representing the PDLP precision modes. + * + * DefaultPrecision: Use the type of the problem (FP64 for double problems). + * SinglePrecision: Run PDLP internally in FP32, converting inputs and outputs. + * DoublePrecision: Explicitly run in FP64 (same as default for double problems). + * MixedPrecision: Use mixed precision SpMV (FP32 matrix with FP64 vectors/compute). + */ +enum pdlp_precision_t : int { + DefaultPrecision = CUOPT_PDLP_DEFAULT_PRECISION, + SinglePrecision = CUOPT_PDLP_SINGLE_PRECISION, + DoublePrecision = CUOPT_PDLP_DOUBLE_PRECISION, + MixedPrecision = CUOPT_PDLP_MIXED_PRECISION +}; + +/** + * @brief Which graph partitioner multi-GPU PDLP uses. + * + * Auto: pick automatically (RoundRobin on 1 GPU, KaMinPar otherwise). + * KaMinPar: multi-threaded KaMinPar graph partitioner. + * RoundRobin: round-robin assignment, no graph. + */ +enum multigpu_pdlp_partitioner_t : int { + Auto = CUOPT_MULTIGPU_PDLP_PARTITIONER_AUTO, + KaMinPar = CUOPT_MULTIGPU_PDLP_PARTITIONER_KAMINPAR, + RoundRobin = CUOPT_MULTIGPU_PDLP_PARTITIONER_ROUND_ROBIN, +}; + +/** + * @brief The PDLP/LP parameters, with no GPU-backed members. + * + * pdlp_solver_settings_t derives from this and adds initial solutions, warm start and the + * solver-internal plumbing. Every member here stays reachable by the same name on + * pdlp_solver_settings_t. + */ +template +struct pdlp_parameters_t { + struct tolerances_t { + f_t absolute_dual_tolerance = 1.0e-4; + f_t relative_dual_tolerance = 1.0e-4; + f_t absolute_primal_tolerance = 1.0e-4; + f_t relative_primal_tolerance = 1.0e-4; + f_t absolute_gap_tolerance = 1.0e-4; + f_t relative_gap_tolerance = 1.0e-4; + f_t primal_infeasible_tolerance = 1.0e-10; + f_t dual_infeasible_tolerance = 1.0e-10; + }; + + tolerances_t get_tolerances() const noexcept { return tolerances; } + + /** + * @brief Set both absolute and relative tolerance on the primal feasibility, + dual feasibility and gap. + * Changing this value has a significant impact on accuracy and runtime. + * + * Optimality is computed as follows: + * - dual_feasiblity < absolute_dual_tolerance + relative_dual_tolerance * + norm_objective_coefficient (l2_norm(c)) + * - primal_feasiblity < absolute_primal_tolerance + relative_primal_tolerance + * norm_constraint_bounds (l2_norm(b)) + * - duality_gap < absolute_gap_tolerance + relative_gap_tolerance * + (|primal_objective| + |dual_objective|) + * + * If all three conditions hold, optimality is reached. + * + * @note Default value is 1e-4. + * + * To set each absolute and relative tolerance, use the provided setters. + * + * @param eps_optimal Tolerance to optimality + */ + void set_optimality_tolerance(f_t eps_optimal) + { + tolerances.absolute_dual_tolerance = eps_optimal; + tolerances.relative_dual_tolerance = eps_optimal; + tolerances.absolute_primal_tolerance = eps_optimal; + tolerances.relative_primal_tolerance = eps_optimal; + tolerances.absolute_gap_tolerance = eps_optimal; + tolerances.relative_gap_tolerance = eps_optimal; + } + + tolerances_t tolerances; + bool detect_infeasibility{false}; + bool strict_infeasibility{false}; + i_t iteration_limit{std::numeric_limits::max()}; + f_t time_limit{std::numeric_limits::infinity()}; + pdlp_solver_mode_t pdlp_solver_mode{pdlp_solver_mode_t::Stable3}; + bool log_to_console{true}; + std::string log_file{""}; + std::string sol_file{""}; + std::string user_problem_file{""}; + std::string presolve_file{""}; + bool per_constraint_residual{false}; + bool crossover{false}; + bool cudss_deterministic{false}; + i_t project_initial_primal_override{-1}; + i_t folding{-1}; + i_t augmented{-1}; + i_t dualize{-1}; + i_t ordering{-1}; + i_t initial_perturbation{-1}; + i_t remove_perturbation{-1}; + i_t primal_pricing{1}; + barrier_dual_initial_point_t barrier_dual_initial_point{barrier_dual_initial_point_t::Automatic}; + i_t postsolve_info{-1}; + i_t barrier_presolve_bound_free_variables{-1}; // -1 automatic, 0 disabled, 1 enabled + // Ruiz equilibration for QCQP (barrier) scaling: -1 automatic (row/column + // imbalance heuristic), 0 disabled, 1 enabled. Distinct from PDLP's own Ruiz + // scaling in pdlp_hyper_params_t. + i_t qcqp_ruiz_equilibration{-1}; + // nnz(A)+nnz(Q) at or above which the barrier path runs Ruiz equilibration on GPU instead + // of CPU. Below it the upload costs more than the scaling saves. + i_t gpu_ruiz_nnz_threshold{500000}; + // Margin used to push the barrier method's initial iterate into the interior of the + // nonnegative orthant / SOC (values are shifted to be at least this far from the boundary). + f_t barrier_initial_point_safeguard{10.0}; + bool eliminate_dense_columns{true}; + pdlp_precision_t pdlp_precision{pdlp_precision_t::DefaultPrecision}; + bool barrier_iterative_refinement{true}; + i_t barrier_adaptive_regularization{-1}; // -1 automatic, 0 disabled, 1 enabled + // Initial regularization for the barrier method's augmented KKT system, applied to the first + // factorization only (adaptive regularization, if enabled, still scales it up/down on later + // iterations). -1 automatic (uses the built-in heuristic), else the literal starting value. + f_t barrier_primal_regularization{-1.0}; + f_t barrier_dual_regularization{-1.0}; + i_t barrier_soc_threshold{100}; + f_t barrier_step_scale{0.9}; + bool save_best_primal_so_far{false}; + /** + * @brief Stop the solver as soon as a primal feasible iterate is encountered. + * + * In non-batch mode the solver returns the first primal feasible iterate (without waiting for + * optimality / dual feasibility). In batch mode the whole batch stops the moment any climber + * reaches primal feasibility; every climber returns its current iterate with its current + * termination status. Can be composed with `per_constraint_residual`. + * Mutually exclusive with `all_primal_feasible`. + */ + bool first_primal_feasible{false}; + /** + * @brief Batch-only: stop only once every climber has reached (at least) primal feasibility. + * + * Each climber is individually ejected from the batch the first time it becomes primal + * feasible and its per-climber solution is captured. The solver returns when all climbers + * have been captured. Setting this in non-batch mode is a validation error. Setting it + * together with `first_primal_feasible` is a validation error. + */ + bool all_primal_feasible{false}; + presolver_t presolver{presolver_t::Default}; + bool dual_postsolve{true}; + // Concurrent LP/MIP: 1–2 GPUs. Multi-GPU PDLP (method=PDLP): up to the visible device + // count; -1 selects all visible GPUs, which dispatches to the multi-GPU PDLP engine + // whenever num_gpus == -1 or num_gpus > 1. + int num_gpus{1}; + // Which graph partitioner multi-GPU PDLP uses. See + // multigpu_pdlp_partitioner_t for the meaning of each value. + multigpu_pdlp_partitioner_t multigpu_pdlp_partitioner{multigpu_pdlp_partitioner_t::Auto}; + method_t method{method_t::Concurrent}; + // At or above this reduced NNZ, skip concurrent CPU solvers and enable DEFAULT column reduction + // on compatible PDLP paths. -1 disables both automatic decisions. + // TODO: Remove the concurrent CPU solver bypass once their memory usage and cuDSS long running + // kernels are resolved; retain the size threshold for automatic column reduction. + i_t large_nnz_threshold{50'000'000}; + pdlp::pdlp_hyper_params_t hyper_params; + // Holds per-climber variable-bound overrides in the format: + // (climber id, variable index, new lower bound, new upper bound). + // Per-climber objective coefficients / offsets / constraint bounds must be pre-expanded directly + // on the optimization_problem_t instead. + std::vector> new_bounds; + // By default to save memory and speed we don't store and copy each climber's primal and dual + // solutions We only retrieve termination statistics and the objective values + bool generate_batch_primal_dual_solution{false}; + // Used to force batch PDLP to solve a subbatch of the problems at a time + // The 0 default value will make the solver use its heuristic to determine the subbatch size + i_t fixed_batch_size{0}; + /** When true, the first GPU barrier/QCQP solve retains cache state for later reuse. */ + bool sequence_solve{false}; +}; + +} // namespace CUOPT_EXPORT mathematical_optimization +} // namespace cuopt diff --git a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp index 3554122963..8c773ac2da 100644 --- a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -33,118 +34,12 @@ class barrier_cache_t; template class solver_settings_t; -/** - * @brief Enum representing the different solver modes under which PDLP can - * operate. - * - * Stable3: Best overall mode from experiments; balances speed and convergence - * success. If you want to use the legacy version, use Stable2. - * Methodical1: Usually leads to slower individual steps but fewer are needed to - * converge. It uses from 1.3x up to 1.7x times more memory. - * Fast1: Less convergence success but usually yields the highest speed - * - * @note Default mode is Stable3. - */ -// Forced to use an enum instead of an enum class for compatibility with the -// Cython layer -enum pdlp_solver_mode_t : int { - Stable1 = CUOPT_PDLP_SOLVER_MODE_STABLE1, - Stable2 = CUOPT_PDLP_SOLVER_MODE_STABLE2, - Methodical1 = CUOPT_PDLP_SOLVER_MODE_METHODICAL1, - Fast1 = CUOPT_PDLP_SOLVER_MODE_FAST1, - Stable3 = CUOPT_PDLP_SOLVER_MODE_STABLE3 -}; - -/** - * @brief Enum representing the different methods that can be used to solve the - * linear programming problem. - * - * Concurrent: Use PDLP, Barrier and DualSimplex in parallel. - * PDLP: Use the PDLP method. - * DualSimplex: Use the dual simplex method. - * Barrier: Use the barrier method - * PrimalSimplex: Use the primal simplex method. - * Unset: The value was not set. - * - * @note Default method is Concurrent. - */ -enum method_t : int { - Concurrent = CUOPT_METHOD_CONCURRENT, - PDLP = CUOPT_METHOD_PDLP, - DualSimplex = CUOPT_METHOD_DUAL_SIMPLEX, - Barrier = CUOPT_METHOD_BARRIER, - PrimalSimplex = CUOPT_METHOD_PRIMAL_SIMPLEX, - Unset = CUOPT_METHOD_UNSET -}; - -/// Returns the corresponding string from the enum `method_t`. -inline std::string method_to_string(method_t method) -{ - switch (method) { - case method_t::DualSimplex: return "Dual Simplex"; - case method_t::PDLP: return "PDLP"; - case method_t::Barrier: return "Barrier"; - case method_t::Concurrent: return "Concurrent"; - case method_t::PrimalSimplex: return "Primal Simplex"; - default: return "Unset"; - } -} - -/** - * @brief Enum representing the PDLP precision modes. - * - * DefaultPrecision: Use the type of the problem (FP64 for double problems). - * SinglePrecision: Run PDLP internally in FP32, converting inputs and outputs. - * DoublePrecision: Explicitly run in FP64 (same as default for double problems). - * MixedPrecision: Use mixed precision SpMV (FP32 matrix with FP64 vectors/compute). - */ -enum pdlp_precision_t : int { - DefaultPrecision = CUOPT_PDLP_DEFAULT_PRECISION, - SinglePrecision = CUOPT_PDLP_SINGLE_PRECISION, - DoublePrecision = CUOPT_PDLP_DOUBLE_PRECISION, - MixedPrecision = CUOPT_PDLP_MIXED_PRECISION -}; - -/** - * @brief Which graph partitioner multi-GPU PDLP uses. - * - * Auto: pick automatically (RoundRobin on 1 GPU, KaMinPar otherwise). - * KaMinPar: multi-threaded KaMinPar graph partitioner. - * RoundRobin: round-robin assignment, no graph. - */ -enum multigpu_pdlp_partitioner_t : int { - Auto = CUOPT_MULTIGPU_PDLP_PARTITIONER_AUTO, - KaMinPar = CUOPT_MULTIGPU_PDLP_PARTITIONER_KAMINPAR, - RoundRobin = CUOPT_MULTIGPU_PDLP_PARTITIONER_ROUND_ROBIN, -}; - template -class pdlp_solver_settings_t { +class pdlp_solver_settings_t : public pdlp_parameters_t { public: - pdlp_solver_settings_t() = default; + using typename pdlp_parameters_t::tolerances_t; - /** - * @brief Set both absolute and relative tolerance on the primal feasibility, - dual feasibility and gap. - * Changing this value has a significant impact on accuracy and runtime. - * - * Optimality is computed as follows: - * - dual_feasiblity < absolute_dual_tolerance + relative_dual_tolerance * - norm_objective_coefficient (l2_norm(c)) - * - primal_feasiblity < absolute_primal_tolerance + relative_primal_tolerance - * norm_constraint_bounds (l2_norm(b)) - * - duality_gap < absolute_gap_tolerance + relative_gap_tolerance * - (|primal_objective| + |dual_objective|) - * - * If all three conditions hold, optimality is reached. - * - * @note Default value is 1e-4. - * - * To set each absolute and relative tolerance, use the provided setters. - * - * @param eps_optimal Tolerance to optimality - */ - void set_optimality_tolerance(f_t eps_optimal); + pdlp_solver_settings_t() = default; /** * @brief Set an initial primal solution. @@ -271,122 +166,15 @@ class pdlp_solver_settings_t { bool has_initial_primal_solution() const; bool has_initial_dual_solution() const; - struct tolerances_t { - f_t absolute_dual_tolerance = 1.0e-4; - f_t relative_dual_tolerance = 1.0e-4; - f_t absolute_primal_tolerance = 1.0e-4; - f_t relative_primal_tolerance = 1.0e-4; - f_t absolute_gap_tolerance = 1.0e-4; - f_t relative_gap_tolerance = 1.0e-4; - f_t primal_infeasible_tolerance = 1.0e-10; - f_t dual_infeasible_tolerance = 1.0e-10; - }; - - tolerances_t get_tolerances() const noexcept; template friend class problem_checking_t; - tolerances_t tolerances; - bool detect_infeasibility{false}; - bool strict_infeasibility{false}; - i_t iteration_limit{std::numeric_limits::max()}; - f_t time_limit{std::numeric_limits::infinity()}; - pdlp_solver_mode_t pdlp_solver_mode{pdlp_solver_mode_t::Stable3}; - bool log_to_console{true}; - std::string log_file{""}; - std::string sol_file{""}; - std::string user_problem_file{""}; - std::string presolve_file{""}; - bool per_constraint_residual{false}; - bool crossover{false}; - bool cudss_deterministic{false}; - i_t project_initial_primal_override{-1}; - i_t folding{-1}; - i_t augmented{-1}; - i_t dualize{-1}; - i_t ordering{-1}; - i_t initial_perturbation{-1}; - i_t remove_perturbation{-1}; - i_t primal_pricing{1}; - barrier_dual_initial_point_t barrier_dual_initial_point{barrier_dual_initial_point_t::Automatic}; - i_t postsolve_info{-1}; - i_t barrier_presolve_bound_free_variables{-1}; // -1 automatic, 0 disabled, 1 enabled - // Ruiz equilibration for QCQP (barrier) scaling: -1 automatic (row/column - // imbalance heuristic), 0 disabled, 1 enabled. Distinct from PDLP's own Ruiz - // scaling in pdlp_hyper_params_t. - i_t qcqp_ruiz_equilibration{-1}; - // nnz(A)+nnz(Q) at or above which the barrier path runs Ruiz equilibration on GPU instead - // of CPU. Below it the upload costs more than the scaling saves. - i_t gpu_ruiz_nnz_threshold{500000}; - // Margin used to push the barrier method's initial iterate into the interior of the - // nonnegative orthant / SOC (values are shifted to be at least this far from the boundary). - f_t barrier_initial_point_safeguard{10.0}; - bool eliminate_dense_columns{true}; - pdlp_precision_t pdlp_precision{pdlp_precision_t::DefaultPrecision}; - bool barrier_iterative_refinement{true}; - i_t barrier_adaptive_regularization{-1}; // -1 automatic, 0 disabled, 1 enabled - // Initial regularization for the barrier method's augmented KKT system, applied to the first - // factorization only (adaptive regularization, if enabled, still scales it up/down on later - // iterations). -1 automatic (uses the built-in heuristic), else the literal starting value. - f_t barrier_primal_regularization{-1.0}; - f_t barrier_dual_regularization{-1.0}; - i_t barrier_soc_threshold{100}; - f_t barrier_step_scale{0.9}; - bool save_best_primal_so_far{false}; - /** - * @brief Stop the solver as soon as a primal feasible iterate is encountered. - * - * In non-batch mode the solver returns the first primal feasible iterate (without waiting for - * optimality / dual feasibility). In batch mode the whole batch stops the moment any climber - * reaches primal feasibility; every climber returns its current iterate with its current - * termination status. Can be composed with `per_constraint_residual`. - * Mutually exclusive with `all_primal_feasible`. - */ - bool first_primal_feasible{false}; - /** - * @brief Batch-only: stop only once every climber has reached (at least) primal feasibility. - * - * Each climber is individually ejected from the batch the first time it becomes primal - * feasible and its per-climber solution is captured. The solver returns when all climbers - * have been captured. Setting this in non-batch mode is a validation error. Setting it - * together with `first_primal_feasible` is a validation error. - */ - bool all_primal_feasible{false}; - presolver_t presolver{presolver_t::Default}; - bool dual_postsolve{true}; - // Concurrent LP/MIP: 1–2 GPUs. Multi-GPU PDLP (method=PDLP): up to the visible device - // count; -1 selects all visible GPUs, which dispatches to the multi-GPU PDLP engine - // whenever num_gpus == -1 or num_gpus > 1. - int num_gpus{1}; - // Which graph partitioner multi-GPU PDLP uses. See - // multigpu_pdlp_partitioner_t for the meaning of each value. - multigpu_pdlp_partitioner_t multigpu_pdlp_partitioner{multigpu_pdlp_partitioner_t::Auto}; - method_t method{method_t::Concurrent}; - // At or above this reduced NNZ, skip concurrent CPU solvers and enable DEFAULT column reduction - // on compatible PDLP paths. -1 disables both automatic decisions. - // TODO: Remove the concurrent CPU solver bypass once their memory usage and cuDSS long running - // kernels are resolved; retain the size threshold for automatic column reduction. - i_t large_nnz_threshold{50'000'000}; bool inside_mip{false}; // For concurrent termination std::atomic* concurrent_halt{nullptr}; // Shared strong branching solved flags for cooperative DS + PDLP cuda::std::span> shared_sb_solved; static constexpr f_t minimal_absolute_tolerance = 1.0e-12; - pdlp::pdlp_hyper_params_t hyper_params; - // Holds per-climber variable-bound overrides in the format: - // (climber id, variable index, new lower bound, new upper bound). - // Per-climber objective coefficients / offsets / constraint bounds must be pre-expanded directly - // on the optimization_problem_t instead. - std::vector> new_bounds; - // By default to save memory and speed we don't store and copy each climber's primal and dual - // solutions We only retrieve termination statistics and the objective values - bool generate_batch_primal_dual_solution{false}; - // Used to force batch PDLP to solve a subbatch of the problems at a time - // The 0 default value will make the solver use its heuristic to determine the subbatch size - i_t fixed_batch_size{0}; - /** When true, the first GPU barrier/QCQP solve retains cache state for later reuse. */ - bool sequence_solve{false}; /** Non-owning cache pointer set by ``call_solve`` for barrier cache reuse. */ barrier_cache_t* barrier_cache{nullptr}; diff --git a/cpp/include/cuopt/mathematical_optimization/solver_parameter_registry.hpp b/cpp/include/cuopt/mathematical_optimization/solver_parameter_registry.hpp new file mode 100644 index 0000000000..8f2ad65e3d --- /dev/null +++ b/cpp/include/cuopt/mathematical_optimization/solver_parameter_registry.hpp @@ -0,0 +1,69 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +namespace cuopt { +namespace CUOPT_EXPORT mathematical_optimization { + +/** + * @brief The solver parameters by name: the one table of names, defaults and ranges. + * + * Binds each CUOPT_* name to a member of a pdlp_parameters_t / mip_parameters_t and implements + * set/get by name, parameter files and listing. solver_settings_t and cpu_solver_settings_t both + * hold one, so the two cannot disagree on which parameters exist. Needs no CUDA, rmm or raft. + * + * The registry points into the two parameter objects it was built on, so it is neither copyable + * nor movable and must not outlive them. + */ +template +class solver_parameter_registry_t { + public: + solver_parameter_registry_t(pdlp_parameters_t& pdlp, mip_parameters_t& mip); + + solver_parameter_registry_t(const solver_parameter_registry_t&) = delete; + solver_parameter_registry_t& operator=(const solver_parameter_registry_t&) = delete; + solver_parameter_registry_t(solver_parameter_registry_t&&) = delete; + solver_parameter_registry_t& operator=(solver_parameter_registry_t&&) = delete; + + void set_parameter_from_string(const std::string& name, const std::string& value); + + template + void set_parameter(const std::string& name, T value); + + template + T get_parameter(const std::string& name) const; + + std::string get_parameter_as_string(const std::string& name) const; + + const std::vector>& get_float_parameters() const; + const std::vector>& get_int_parameters() const; + const std::vector>& get_bool_parameters() const; + const std::vector>& get_string_parameters() const; + const std::vector get_parameter_names() const; + + void load_parameters_from_file(const std::string& path); + bool dump_parameters_to_file(const std::string& path, bool hyperparameters_only = true) const; + + private: + std::vector> float_parameters; + std::vector> int_parameters; + std::vector> bool_parameters; + std::vector> string_parameters; +}; + +} // namespace CUOPT_EXPORT mathematical_optimization +} // namespace cuopt diff --git a/cpp/include/cuopt/mathematical_optimization/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/solver_settings.hpp index 0e3a00430e..25536a6412 100644 --- a/cpp/include/cuopt/mathematical_optimization/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/solver_settings.hpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -108,10 +109,9 @@ class solver_settings_t { pdlp_solver_settings_t pdlp_settings; mip_solver_settings_t mip_settings; - std::vector> float_parameters; - std::vector> int_parameters; - std::vector> bool_parameters; - std::vector> string_parameters; + // Names, defaults and ranges live in solver_parameter_registry_t, shared with + // cpu_solver_settings_t. Declared after the two settings objects it points into. + solver_parameter_registry_t registry_; }; } // namespace CUOPT_EXPORT mathematical_optimization diff --git a/cpp/src/math_optimization/CMakeLists.txt b/cpp/src/math_optimization/CMakeLists.txt index 1fe479be02..96e152cf58 100644 --- a/cpp/src/math_optimization/CMakeLists.txt +++ b/cpp/src/math_optimization/CMakeLists.txt @@ -19,6 +19,7 @@ set(MATH_OPT_SRC_FILES ${MATH_OPT_SRC_FILES} PARENT_SCOPE) # in its public API, so this is required for the client library to resolve standalone. set(MATH_OPT_CLIENT_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/solver_settings.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/solver_parameter_registry.cpp ) set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} diff --git a/cpp/src/math_optimization/solver_parameter_registry.cpp b/cpp/src/math_optimization/solver_parameter_registry.cpp new file mode 100644 index 0000000000..91de4edc71 --- /dev/null +++ b/cpp/src/math_optimization/solver_parameter_registry.cpp @@ -0,0 +1,625 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace cuopt::mathematical_optimization { + +namespace { + +bool string_to_int(const std::string& value, int& result) +{ + try { + size_t pos = 0; + result = std::stoi(value, &pos); + return pos == value.size(); + } catch (const std::exception&) { + return false; + } +} + +template +bool string_to_float(const std::string& value, f_t& result) +{ + try { + size_t pos = 0; + if constexpr (std::is_same_v) { result = std::stof(value, &pos); } + if constexpr (std::is_same_v) { result = std::stod(value, &pos); } + if (std::isnan(result)) { return false; } + return pos == value.size(); + } catch (const std::exception&) { + return false; + } +} + +std::string quote_if_needed(const std::string& s) +{ + bool needs_quoting = s.empty() || s.find(' ') != std::string::npos || + s.find('"') != std::string::npos || s.find('\t') != std::string::npos; + if (!needs_quoting) return s; + std::string out = "\""; + for (char c : s) { + if (c == '"') + out += "\\\""; + else + out += c; + } + out += '"'; + return out; +} + +bool string_to_bool(const std::string& value, bool& result) +{ + if (value == "true" || value == "True" || value == "TRUE" || value == "1" || value == "t" || + value == "T") { + result = true; + return true; + } else if (value == "false" || value == "False" || value == "FALSE" || value == "0" || + value == "f" || value == "F") { + result = false; + return true; + } else { + return false; + } +} + +} // namespace + +template +void solver_parameter_registry_t::set_parameter_from_string(const std::string& name, + const std::string& value) +{ + bool found = false; + bool output = false; + for (auto& param : int_parameters) { + if (param.param_name == name) { + i_t value_int; + if (string_to_int(value, value_int)) { + if (value_int < param.min_value || value_int > param.max_value) { + throw std::invalid_argument("Parameter " + name + " value " + value + " out of range"); + } + *param.value_ptr = value_int; + found = true; + if (!output) { + CUOPT_LOG_INFO("Setting parameter %s to %d", name.c_str(), value_int); + output = true; + } + } else { + throw std::invalid_argument("Parameter " + name + " value " + value + " is not an integer"); + } + } + } + for (auto& param : float_parameters) { + if (param.param_name == name) { + f_t value_float; + if (string_to_float(value, value_float)) { + if (value_float < param.min_value || value_float > param.max_value) { + throw std::invalid_argument("Parameter " + name + " value " + value + " out of range"); + } + *param.value_ptr = value_float; + found = true; + if (!output) { + CUOPT_LOG_INFO("Setting parameter %s to %e", name.c_str(), value_float); + output = true; + } + } else { + throw std::invalid_argument("Parameter " + name + " value " + value + " is not a float"); + } + } + } + for (auto& param : bool_parameters) { + if (param.param_name == name) { + bool value_bool; + if (string_to_bool(value, value_bool)) { + *param.value_ptr = value_bool; + found = true; + if (!output) { + CUOPT_LOG_INFO("Setting parameter %s to %s", name.c_str(), value_bool ? "true" : "false"); + output = true; + } + } else { + throw std::invalid_argument("Parameter " + name + " value " + value + + " must be true or false"); + } + } + } + + for (auto& param : string_parameters) { + if (param.param_name == name) { + *param.value_ptr = value; + if (!output) { + CUOPT_LOG_INFO("Setting parameter %s to %s", name.c_str(), value.c_str()); + output = true; + } + found = true; + } + } + if (!found) { throw std::invalid_argument("Parameter " + name + " not found"); } +} + +template +template +void solver_parameter_registry_t::set_parameter(const std::string& name, T value) +{ + bool found = false; + bool output = false; + if constexpr (std::is_same_v) { + for (auto& param : int_parameters) { + if (param.param_name == name) { + if (value < param.min_value || value > param.max_value) { + throw std::invalid_argument("Parameter " + name + " out of range"); + } + *param.value_ptr = value; + if (!output) { + CUOPT_LOG_INFO("Setting parameter %s to %d", name.c_str(), value); + output = true; + } + found = true; + } + } + } + if constexpr (std::is_same_v) { + for (auto& param : float_parameters) { + if (param.param_name == name) { + if (value < param.min_value || value > param.max_value) { + throw std::invalid_argument("Parameter " + name + " out of range"); + } + *param.value_ptr = value; + if (!output) { + CUOPT_LOG_INFO("Setting parameter %s to %e", name.c_str(), value); + output = true; + } + found = true; + } + } + } + if constexpr (std::is_same_v) { + for (auto& param : bool_parameters) { + if (param.param_name == name) { + *param.value_ptr = value; + if (!output) { + CUOPT_LOG_INFO("Setting parameter %s to %s", name.c_str(), value ? "true" : "false"); + output = true; + } + found = true; + } + } + } + if constexpr (std::is_same_v) { + for (auto& param : string_parameters) { + if (param.param_name == name) { + *param.value_ptr = value; + if (!output) { + CUOPT_LOG_INFO("Setting parameter %s to %s", name.c_str(), value.c_str()); + output = true; + } + found = true; + } + } + } + if (!found) { throw std::invalid_argument("Parameter " + name + " not found"); } +} + +template +template +T solver_parameter_registry_t::get_parameter(const std::string& name) const +{ + if constexpr (std::is_same_v) { + for (auto& param : int_parameters) { + if (param.param_name == name) { return *param.value_ptr; } + } + } + if constexpr (std::is_same_v) { + for (auto& param : float_parameters) { + if (param.param_name == name) { return *param.value_ptr; } + } + } + if constexpr (std::is_same_v) { + for (auto& param : bool_parameters) { + if (param.param_name == name) { return *param.value_ptr; } + } + } + if constexpr (std::is_same_v) { + for (auto& param : string_parameters) { + if (param.param_name == name) { return *param.value_ptr; } + } + } + throw std::invalid_argument("Parameter " + name + " not found"); +} + +template +std::string solver_parameter_registry_t::get_parameter_as_string( + const std::string& name) const +{ + for (auto& param : int_parameters) { + if (param.param_name == name) { return std::to_string(*param.value_ptr); } + } + for (auto& param : float_parameters) { + if (param.param_name == name) { return std::to_string(*param.value_ptr); } + } + for (auto& param : bool_parameters) { + if (param.param_name == name) { return *param.value_ptr ? "true" : "false"; } + } + for (auto& param : string_parameters) { + if (param.param_name == name) { return *param.value_ptr; } + } + throw std::invalid_argument("Parameter " + name + " not found"); +} + +template +const std::vector>& +solver_parameter_registry_t::get_float_parameters() const +{ + return float_parameters; +} + +template +const std::vector>& +solver_parameter_registry_t::get_int_parameters() const +{ + return int_parameters; +} + +template +const std::vector>& +solver_parameter_registry_t::get_bool_parameters() const +{ + return bool_parameters; +} + +template +const std::vector>& +solver_parameter_registry_t::get_string_parameters() const +{ + return string_parameters; +} + +template +const std::vector solver_parameter_registry_t::get_parameter_names() const +{ + std::vector parameter_names; + for (auto& param : int_parameters) { + parameter_names.push_back(param.param_name); + } + for (auto& param : float_parameters) { + parameter_names.push_back(param.param_name); + } + for (auto& param : bool_parameters) { + parameter_names.push_back(param.param_name); + } + for (auto& param : string_parameters) { + parameter_names.push_back(param.param_name); + } + return parameter_names; +} + +template +void solver_parameter_registry_t::load_parameters_from_file(const std::string& path) +{ + cuopt_expects(!std::filesystem::is_directory(path) && std::filesystem::exists(path), + error_type_t::ValidationError, + "Parameter config: not a valid file: %s", + path.c_str()); + std::ifstream file(path); + cuopt_expects(file.is_open(), + error_type_t::ValidationError, + "Parameter config: cannot open: %s", + path.c_str()); + std::string line; + while (std::getline(file, line)) { + auto first_non_ws = std::find_if_not(line.begin(), line.end(), ::isspace); + if (first_non_ws == line.end() || *first_non_ws == '#') continue; + line.erase(line.begin(), first_non_ws); + + std::istringstream iss(line); + std::string key; + cuopt_expects(iss >> key >> std::ws && iss.get() == '=', + error_type_t::ValidationError, + "Parameter config: bad line: %s", + line.c_str()); + iss >> std::ws; + cuopt_expects(!iss.eof(), + error_type_t::ValidationError, + "Parameter config: missing value: %s", + line.c_str()); + std::string val; + if (iss.peek() == '"') { + iss.get(); + val.clear(); + char ch; + bool closed = false; + while (iss.get(ch)) { + if (ch == '\\' && iss.peek() == '"') { + iss.get(ch); + val += '"'; + } else if (ch == '"') { + closed = true; + break; + } else { + val += ch; + } + } + cuopt_expects(closed, + error_type_t::ValidationError, + "Parameter config: unterminated quote: %s", + line.c_str()); + } else { + iss >> val; + } + std::string trailing; + cuopt_expects(!bool(iss >> trailing), + error_type_t::ValidationError, + "Parameter config: trailing junk: %s", + line.c_str()); + try { + set_parameter_from_string(key, val); + } catch (const std::invalid_argument& e) { + cuopt_expects(false, error_type_t::ValidationError, "Parameter config: %s", e.what()); + } + } + CUOPT_LOG_INFO("Parameters loaded from: %s", path.c_str()); +} + +template +bool solver_parameter_registry_t::dump_parameters_to_file(const std::string& path, + bool hyperparameters_only) const +{ + std::ofstream file(path); + if (!file.is_open()) { + CUOPT_LOG_ERROR("Cannot open file for writing: %s", path.c_str()); + return false; + } + file << "# cuOpt parameter configuration (auto-generated)\n"; + file << "# Uncomment and change the values you wish to override.\n\n"; + for (const auto& p : int_parameters) { + if (hyperparameters_only && p.param_name.find("hyper_") == std::string::npos) continue; + if (p.description && p.description[0] != '\0') + file << "# " << p.description << " (int, range: [" << p.min_value << ", " << p.max_value + << "])\n"; + file << "# " << p.param_name << " = " << *p.value_ptr << "\n\n"; + } + for (const auto& p : float_parameters) { + if (hyperparameters_only && p.param_name.find("hyper_") == std::string::npos) continue; + if (p.description && p.description[0] != '\0') + file << "# " << p.description << " (double, range: [" << p.min_value << ", " << p.max_value + << "])\n"; + file << "# " << p.param_name << " = " << *p.value_ptr << "\n\n"; + } + for (const auto& p : bool_parameters) { + if (hyperparameters_only && p.param_name.find("hyper_") == std::string::npos) continue; + if (p.description && p.description[0] != '\0') file << "# " << p.description << " (bool)\n"; + file << "# " << p.param_name << " = " << (*p.value_ptr ? "true" : "false") << "\n\n"; + } + for (const auto& p : string_parameters) { + if (hyperparameters_only && p.param_name.find("hyper_") == std::string::npos) continue; + if (p.description && p.description[0] != '\0') file << "# " << p.description << " (string)\n"; + file << "# " << p.param_name << " = " << quote_if_needed(*p.value_ptr) << "\n\n"; + } + return true; +} + +template +solver_parameter_registry_t::solver_parameter_registry_t( + pdlp_parameters_t& pdlp, mip_parameters_t& mip) +{ + // clang-format off + // Float parameters + float_parameters = { + {CUOPT_TIME_LIMIT, &mip.time_limit, f_t(0.0), std::numeric_limits::infinity(), std::numeric_limits::infinity()}, + {CUOPT_TIME_LIMIT, &pdlp.time_limit, f_t(0.0), std::numeric_limits::infinity(), std::numeric_limits::infinity()}, + {CUOPT_WORK_LIMIT, &mip.work_limit, f_t(0.0), std::numeric_limits::infinity(), std::numeric_limits::infinity()}, + {CUOPT_ABSOLUTE_DUAL_TOLERANCE, &pdlp.tolerances.absolute_dual_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-4)}, + {CUOPT_RELATIVE_DUAL_TOLERANCE, &pdlp.tolerances.relative_dual_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-4)}, + {CUOPT_ABSOLUTE_PRIMAL_TOLERANCE, &pdlp.tolerances.absolute_primal_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-4)}, + {CUOPT_RELATIVE_PRIMAL_TOLERANCE, &pdlp.tolerances.relative_primal_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-4)}, + {CUOPT_ABSOLUTE_GAP_TOLERANCE, &pdlp.tolerances.absolute_gap_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-4)}, + {CUOPT_RELATIVE_GAP_TOLERANCE, &pdlp.tolerances.relative_gap_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-4)}, + {CUOPT_MIP_ABSOLUTE_TOLERANCE, &mip.tolerances.absolute_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-6)}, + {CUOPT_MIP_RELATIVE_TOLERANCE, &mip.tolerances.relative_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-12)}, + {CUOPT_MIP_INTEGRALITY_TOLERANCE, &mip.tolerances.integrality_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-5)}, + {CUOPT_MIP_ABSOLUTE_GAP, &mip.tolerances.absolute_mip_gap, f_t(0.0), std::numeric_limits::infinity(), std::max(f_t(1e-10), std::numeric_limits::epsilon())}, + {CUOPT_MIP_RELATIVE_GAP, &mip.tolerances.relative_mip_gap, f_t(0.0), f_t(1e-1), f_t(1e-4)}, + {CUOPT_PRIMAL_INFEASIBLE_TOLERANCE, &pdlp.tolerances.primal_infeasible_tolerance, f_t(0.0), f_t(1e-1), std::max(f_t(1e-10), std::numeric_limits::epsilon())}, + {CUOPT_DUAL_INFEASIBLE_TOLERANCE, &pdlp.tolerances.dual_infeasible_tolerance, f_t(0.0), f_t(1e-1), std::max(f_t(1e-10), std::numeric_limits::epsilon())}, + {CUOPT_MIP_CUT_CHANGE_THRESHOLD, &mip.cut_change_threshold, f_t(-1.0), std::numeric_limits::infinity(), f_t(-1.0)}, + {CUOPT_MIP_CUT_MIN_ORTHOGONALITY, &mip.cut_min_orthogonality, f_t(0.0), f_t(1.0), f_t(0.5)}, + {CUOPT_BARRIER_PRIMAL_REGULARIZATION, &pdlp.barrier_primal_regularization, f_t(-1.0), std::numeric_limits::infinity(), f_t(-1.0), "initial primal regularization for the augmented system; -1 automatic"}, + {CUOPT_BARRIER_DUAL_REGULARIZATION, &pdlp.barrier_dual_regularization, f_t(-1.0), std::numeric_limits::infinity(), f_t(-1.0), "initial dual regularization for the augmented system; -1 automatic"}, + {CUOPT_BARRIER_STEP_SCALE, &pdlp.barrier_step_scale, f_t(0.5), f_t(0.9999), f_t(0.9)}, + {CUOPT_BARRIER_INITIAL_POINT_SAFEGUARD, &pdlp.barrier_initial_point_safeguard, f_t(0.0), std::numeric_limits::infinity(), f_t(10.0), "margin pushing the barrier initial iterate into the interior of the nonnegative orthant / SOC"}, + // MIP heuristic hyper-parameters (hidden from default --help: name contains "hyper_") + {CUOPT_MIP_HYPER_HEURISTIC_ROOT_LP_TIME_RATIO, &mip.heuristic_params.root_lp_time_ratio, f_t(0.0), f_t(1.0), f_t(0.1), "fraction of total time for root LP"}, + {CUOPT_MIP_HYPER_HEURISTIC_ROOT_LP_MAX_TIME, &mip.heuristic_params.root_lp_max_time, f_t(0.0), std::numeric_limits::infinity(), f_t(15.0), "hard cap on root LP seconds"}, + {CUOPT_MIP_HYPER_HEURISTIC_RINS_TIME_LIMIT, &mip.heuristic_params.rins_time_limit, f_t(0.0), std::numeric_limits::infinity(), f_t(3.0), "per-call RINS sub-MIP time"}, + {CUOPT_MIP_HYPER_HEURISTIC_RINS_MAX_TIME_LIMIT, &mip.heuristic_params.rins_max_time_limit, f_t(0.0), std::numeric_limits::infinity(), f_t(20.0), "ceiling for RINS adaptive time budget"}, + {CUOPT_MIP_HYPER_HEURISTIC_RINS_FIX_RATE, &mip.heuristic_params.rins_fix_rate, f_t(0.0), f_t(1.0), f_t(0.5), "RINS variable fix rate"}, + {CUOPT_MIP_HYPER_HEURISTIC_INITIAL_INFEASIBILITY_WEIGHT, &mip.heuristic_params.initial_infeasibility_weight, f_t(1e-9), std::numeric_limits::infinity(), f_t(1000.0), "constraint violation penalty seed"}, + {CUOPT_MIP_HYPER_HEURISTIC_RELAXED_LP_TIME_LIMIT, &mip.heuristic_params.relaxed_lp_time_limit, f_t(1e-9), std::numeric_limits::infinity(), f_t(1.0), "base relaxed LP time cap in heuristics"}, + {CUOPT_MIP_HYPER_HEURISTIC_RELATED_VARS_TIME_LIMIT, &mip.heuristic_params.related_vars_time_limit, f_t(1e-9), std::numeric_limits::infinity(), f_t(2.0), "time for related-variable structure build"}, + {CUOPT_MIP_SEMICONTINUOUS_BIG_M, &mip.semi_continuous_big_m, f_t(1.0), std::numeric_limits::infinity(), f_t(1e10), "big-M value for semi-continuous variables with no finite upper bound"}, + // Diving heuristic hyper-parameters (hidden from default --help: name contains "hyper_") + {CUOPT_MIP_HYPER_DIVING_ITERATION_LIMIT_FACTOR, &mip.diving_params.iteration_limit_factor, f_t(0.0), f_t(1.0), f_t(0.05), "fraction of best-first iterations allowed per dive"}, + // Recursive sub-MIP (RINS) hyper-parameters (hidden from default --help: name contains "hyper_") + {CUOPT_MIP_HYPER_SUBMIP_BASE_TARGET_FIXRATE, &mip.submip_params.base_target_fixrate, f_t(0.0), f_t(1.0), f_t(0.6), "base target fix rate for the RINS neighbourhood"}, + {CUOPT_MIP_HYPER_SUBMIP_MIN_FIXRATE, &mip.submip_params.min_fixrate, f_t(0.0), f_t(1.0), f_t(0.25), "minimum fix rate for accepting the RINS neighbourhood"}, + {CUOPT_MIP_HYPER_SUBMIP_MIN_FIXRATE_CAP, &mip.submip_params.min_fixrate_cap, f_t(0.0), f_t(1.0), f_t(0.1), "hard cap on the minimum fix rate for solving a sub-MIP"}, + {CUOPT_MIP_HYPER_SUBMIP_TARGET_MIP_GAP, &mip.submip_params.target_mip_gap, f_t(0.0), f_t(1.0), f_t(0.01), "MIP gap target for the sub-MIP"}, + {CUOPT_MIP_HYPER_SUBMIP_ITERATION_LIMIT_RATIO, &mip.submip_params.iteration_limit_ratio, f_t(0.0), f_t(1.0), f_t(0.8), "sub-MIP simplex-iteration limit as a factor of parent B&B iterations"}, + {CUOPT_MIP_HYPER_SUBMIP_ROUND_CLOSE_RATIO, &mip.submip_params.round_close_ratio, f_t(0.0), f_t(1.0), f_t(0.8), "share of the still-unfixed integers left for later neighbourhood rounds (0 reaches the target fix rate in a single round)"}, + }; + + // Int parameters + // TODO should we have Stable2 and Methodolical1 here? + int_parameters = { + {CUOPT_ITERATION_LIMIT, &pdlp.iteration_limit, 0, std::numeric_limits::max(), std::numeric_limits::max()}, + {CUOPT_NODE_LIMIT, &mip.node_limit, 0, std::numeric_limits::max(), std::numeric_limits::max()}, + {CUOPT_PDLP_SOLVER_MODE, reinterpret_cast(&pdlp.pdlp_solver_mode), CUOPT_PDLP_SOLVER_MODE_STABLE1, CUOPT_PDLP_SOLVER_MODE_STABLE3, CUOPT_PDLP_SOLVER_MODE_STABLE3}, + {CUOPT_METHOD, reinterpret_cast(&pdlp.method), CUOPT_METHOD_CONCURRENT, CUOPT_METHOD_PRIMAL_SIMPLEX, CUOPT_METHOD_CONCURRENT}, + {CUOPT_METHOD, reinterpret_cast(&mip.method), CUOPT_METHOD_CONCURRENT, CUOPT_METHOD_PRIMAL_SIMPLEX, CUOPT_METHOD_CONCURRENT}, + {CUOPT_LARGE_NNZ_THRESHOLD, &pdlp.large_nnz_threshold, -1, std::numeric_limits::max(), 50'000'000, "at or above this reduced NNZ, skip Barrier and dual simplex in concurrent solves and enable automatic PDLP column reduction when supported. With -1, concurrent solves use their normal solver selection regardless of nonzeros and PDLP column reduction is off unless explicitly enabled"}, + {CUOPT_LARGE_NNZ_THRESHOLD, &mip.large_nnz_threshold, -1, std::numeric_limits::max(), 50'000'000, "at or above this reduced NNZ, skip Barrier and dual simplex in concurrent solves and enable automatic PDLP column reduction when supported. With -1, concurrent solves use their normal solver selection regardless of nonzeros and PDLP column reduction is off unless explicitly enabled"}, + {CUOPT_NUM_CPU_THREADS, &mip.num_cpu_threads, -1, std::numeric_limits::max(), -1}, + {CUOPT_AUGMENTED, &pdlp.augmented, -1, 1, -1}, + {CUOPT_FOLDING, &pdlp.folding, -1, 1, -1}, + {CUOPT_DUALIZE, &pdlp.dualize, -1, 1, -1}, + {CUOPT_ORDERING, &pdlp.ordering, -1, 1, -1}, + {CUOPT_DUAL_SIMPLEX_INITIAL_PERTURBATION, &pdlp.initial_perturbation, -1, 1, -1}, + {CUOPT_DUAL_SIMPLEX_REMOVE_PERTURBATION, &pdlp.remove_perturbation, -1, 1, -1}, + {CUOPT_PRIMAL_SIMPLEX_PRICING, &pdlp.primal_pricing, 0, 1, 1}, + {CUOPT_BARRIER_DUAL_INITIAL_POINT, reinterpret_cast(&pdlp.barrier_dual_initial_point), -1, 2, -1}, + {CUOPT_POSTSOLVE_INFO, &pdlp.postsolve_info, -1, 1, -1}, + {CUOPT_MIP_CUT_PASSES, &mip.max_cut_passes, -1, std::numeric_limits::max(), 10}, + {CUOPT_MIP_MIXED_INTEGER_ROUNDING_CUTS, &mip.mir_cuts, -1, 1, -1}, + {CUOPT_MIP_MIXED_INTEGER_GOMORY_CUTS, &mip.mixed_integer_gomory_cuts, -1, 1, -1}, + {CUOPT_MIP_KNAPSACK_CUTS, &mip.knapsack_cuts, -1, 1, -1}, + {CUOPT_MIP_FLOW_COVER_CUTS, &mip.flow_cover_cuts, -1, 1, -1}, + {CUOPT_MIP_CLIQUE_CUTS, &mip.clique_cuts, -1, 1, -1}, + {CUOPT_MIP_ZERO_HALF_CUTS, &mip.zero_half_cuts, -1, 1, -1}, + {CUOPT_MIP_IMPLIED_BOUND_CUTS, &mip.implied_bound_cuts, -1, 1, -1}, + {CUOPT_MIP_STRONG_CHVATAL_GOMORY_CUTS, &mip.strong_chvatal_gomory_cuts, -1, 1, -1}, + {CUOPT_MIP_REDUCED_COST_STRENGTHENING, &mip.reduced_cost_strengthening, -1, std::numeric_limits::max(), -1}, + {CUOPT_MIP_DUAL_DEGENERATE_FEASIBILITY_PUMP, &mip.dual_degenerate_feasibility_pump, -1, 1, -1}, + {CUOPT_MIP_PRIMAL_DEGENERATE_PIVOTS, &mip.primal_degenerate_pivots, -1, 1, -1}, + {CUOPT_MIP_DUAL_DEGENERATE_PIVOTS, &mip.dual_degenerate_pivots, -1, 2, -1}, + {CUOPT_MIP_RINS, &mip.submip_params.rins, -1, 1, -1}, + {CUOPT_MIP_RENS, &mip.submip_params.rens, -1, 1, -1}, + {CUOPT_MIP_MUTATION, &mip.submip_params.mutation, -1, 1, -1}, + {CUOPT_MIP_OBJECTIVE_STEP, &mip.objective_step, 0, 1, 1}, + {CUOPT_NUM_GPUS, &pdlp.num_gpus, -1, 72, 1}, + {CUOPT_NUM_GPUS, &mip.num_gpus, -1, 72, 1}, + {CUOPT_MIP_BATCH_PDLP_STRONG_BRANCHING, &mip.mip_batch_pdlp_strong_branching, 0, 2, 0}, + {CUOPT_MIP_BATCH_PDLP_RELIABILITY_BRANCHING, &mip.mip_batch_pdlp_reliability_branching, 0, 2, 0}, + {CUOPT_MIP_STRONG_BRANCHING_SIMPLEX_ITERATION_LIMIT, &mip.strong_branching_simplex_iteration_limit, -1,std::numeric_limits::max(), -1}, + {CUOPT_PRESOLVE, reinterpret_cast(&pdlp.presolver), CUOPT_PRESOLVE_DEFAULT, CUOPT_PRESOLVE_PSLP, CUOPT_PRESOLVE_DEFAULT}, + {CUOPT_PRESOLVE, reinterpret_cast(&mip.presolver), CUOPT_PRESOLVE_DEFAULT, CUOPT_PRESOLVE_PSLP, CUOPT_PRESOLVE_DEFAULT}, + {CUOPT_MULTIGPU_PDLP_PARTITIONER, reinterpret_cast(&pdlp.multigpu_pdlp_partitioner), CUOPT_MULTIGPU_PDLP_PARTITIONER_AUTO, CUOPT_MULTIGPU_PDLP_PARTITIONER_ROUND_ROBIN, CUOPT_MULTIGPU_PDLP_PARTITIONER_AUTO}, + {CUOPT_MIP_DETERMINISM_MODE, &mip.determinism_mode, CUOPT_MODE_OPPORTUNISTIC, CUOPT_MODE_DETERMINISTIC, CUOPT_MODE_OPPORTUNISTIC}, + {CUOPT_RANDOM_SEED, &mip.seed, -1, std::numeric_limits::max(), -1}, + {CUOPT_MIP_RELIABILITY_BRANCHING, &mip.reliability_branching, -1, std::numeric_limits::max(), -1}, + {CUOPT_PDLP_PRECISION, reinterpret_cast(&pdlp.pdlp_precision), CUOPT_PDLP_DEFAULT_PRECISION, CUOPT_PDLP_MIXED_PRECISION, CUOPT_PDLP_DEFAULT_PRECISION}, + {CUOPT_PDLP_HYPER_REDUCED_MATRIX, reinterpret_cast(&pdlp.hyper_params.reduced_matrix), CUOPT_PDLP_REDUCED_MATRIX_DEFAULT, CUOPT_PDLP_REDUCED_MATRIX_COLUMN_REDUCTION, CUOPT_PDLP_REDUCED_MATRIX_DEFAULT, "reduced-matrix mode: -1 automatic, 0 disabled, 1 enabled"}, + {CUOPT_MIP_SYMMETRY, &mip.symmetry, -1, 2, -1}, + {CUOPT_MIP_SCALING, &mip.mip_scaling, CUOPT_MIP_SCALING_OFF, CUOPT_MIP_SCALING_NO_OBJECTIVE, CUOPT_MIP_SCALING_NO_OBJECTIVE}, + // MIP heuristic hyper-parameters (hidden from default --help: name contains "hyper_") + {CUOPT_MIP_HYPER_HEURISTIC_POPULATION_SIZE, &mip.heuristic_params.population_size, 1, std::numeric_limits::max(), 32, "max solutions in pool"}, + {CUOPT_MIP_HYPER_HEURISTIC_NUM_CPUFJ_THREADS, &mip.heuristic_params.num_cpufj_threads, 0, std::numeric_limits::max(), 8, "parallel CPU FJ climbers"}, + {CUOPT_MIP_HYPER_HEURISTIC_PRESOLVE_MAX_ROUNDS, &mip.heuristic_params.presolve_max_rounds, -1, std::numeric_limits::max(), -1, "Papilo presolve rounds cap (<0 derives it from the problem, 0 keeps Papilo default)"}, + {CUOPT_MIP_HYPER_HEURISTIC_PAPILO_PROBING_MAX_BADGESIZE, &mip.heuristic_params.papilo_probing_max_badgesize, -1, std::numeric_limits::max(), -1, "ceiling on Papilo probing.minbadgesize (<0 derives it from the problem, 0 leaves it uncapped)"}, + {CUOPT_MIP_HYPER_HEURISTIC_STAGNATION_TRIGGER, &mip.heuristic_params.stagnation_trigger, 1, std::numeric_limits::max(), 3, "FP loops w/o improvement before recombination"}, + {CUOPT_MIP_HYPER_HEURISTIC_MAX_ITERS_WITHOUT_IMPROVEMENT, &mip.heuristic_params.max_iterations_without_improvement, 1, std::numeric_limits::max(), 8, "diversity step depth after stagnation"}, + {CUOPT_MIP_HYPER_HEURISTIC_N_OF_MINIMUMS_FOR_EXIT, &mip.heuristic_params.n_of_minimums_for_exit, 1, std::numeric_limits::max(), 7000, "FJ baseline local-minima exit threshold"}, + {CUOPT_MIP_HYPER_HEURISTIC_ENABLED_RECOMBINERS, &mip.heuristic_params.enabled_recombiners, 0, 15, 15, "bitmask: 1=BP 2=FP 4=LS 8=SubMIP"}, + {CUOPT_MIP_HYPER_HEURISTIC_CYCLE_DETECTION_LENGTH, &mip.heuristic_params.cycle_detection_length, 1, std::numeric_limits::max(), 30, "FP assignment cycle ring buffer length"}, + // Diving heuristic hyper-parameters (hidden from default --help: name contains "hyper_") + {CUOPT_MIP_HYPER_DIVING_LINE_SEARCH, &mip.diving_params.line_search_diving, -1, 1, -1, "line-search diving toggle: -1 automatic, 0 disabled, 1 enabled"}, + {CUOPT_MIP_HYPER_DIVING_PSEUDOCOST, &mip.diving_params.pseudocost_diving, -1, 1, -1, "pseudocost diving toggle: -1 automatic, 0 disabled, 1 enabled"}, + {CUOPT_MIP_HYPER_DIVING_GUIDED, &mip.diving_params.guided_diving, -1, 1, -1, "guided diving toggle: -1 automatic, 0 disabled, 1 enabled"}, + {CUOPT_MIP_HYPER_DIVING_COEFFICIENT, &mip.diving_params.coefficient_diving, -1, 1, -1, "coefficient diving toggle: -1 automatic, 0 disabled, 1 enabled"}, + {CUOPT_MIP_HYPER_DIVING_FARKAS, &mip.diving_params.farkas_diving, -1, 1, -1, "Farkas diving toggle: -1 automatic, 0 disabled, 1 enabled"}, + {CUOPT_MIP_HYPER_DIVING_VECTOR_LENGTH, &mip.diving_params.vector_length_diving, -1, 1, -1, "vector-length diving toggle: -1 automatic, 0 disabled, 1 enabled"}, + {CUOPT_MIP_HYPER_DIVING_NODE_LIMIT, &mip.diving_params.node_limit, 0, std::numeric_limits::max(), 500, "maximum nodes explored per dive"}, + {CUOPT_MIP_HYPER_DIVING_BACKTRACK_LIMIT, &mip.diving_params.backtrack_limit, 0, std::numeric_limits::max(), 5, "maximum backtracking allowed per dive"}, + // Recursive sub-MIP (RINS) hyper-parameters (hidden from default --help: name contains "hyper_") + {CUOPT_MIP_HYPER_SUBMIP_NODE_LIMIT_OFFSET, &mip.submip_params.node_limit_offset, 0, std::numeric_limits::max(), 200, "base node limit for the sub-MIP"}, + {CUOPT_MIP_HYPER_SUBMIP_ITERATION_LIMIT_OFFSET, &mip.submip_params.iteration_limit_offset, 0, std::numeric_limits::max(), 10000, "base sub-MIP simplex-iteration limit for root heuristics"}, + {CUOPT_MIP_HYPER_SUBMIP_MAX_LEVEL, &mip.submip_params.max_level, 0, std::numeric_limits::max(), 10, "maximum sub-MIP recursion level"}, + {CUOPT_BARRIER_PRESOLVE_BOUND_FREE_VARIABLES, &pdlp.barrier_presolve_bound_free_variables, -1, 1, -1, "Bound free variables during barrier presolve: -1 automatic (default behavior), 0 disabled, 1 enabled"}, + {CUOPT_BARRIER_ADAPTIVE_REGULARIZATION, &pdlp.barrier_adaptive_regularization, -1, 1, -1, "Adaptive regularization for barrier method: -1 automatic (default behavior), 0 disabled, 1 enabled"}, + // QCQP (barrier) scaling hyper-parameter + {CUOPT_QCQP_HYPER_RUIZ_EQUILIBRATION, &pdlp.qcqp_ruiz_equilibration, -1, 1, -1, "Ruiz equilibration for QCQP barrier scaling: -1 automatic (row/column imbalance heuristic), 0 disabled, 1 enabled"}, + {CUOPT_BARRIER_HYPER_GPU_RUIZ_NNZ_THRESHOLD, &pdlp.gpu_ruiz_nnz_threshold, 0, std::numeric_limits::max(), 500000, "nnz(A)+nnz(Q) at or above which barrier Ruiz equilibration runs on GPU instead of CPU"}, + // Overrides the pdlp_solver_mode preset when not -1 (hidden: name contains "hyper_") + {CUOPT_PDLP_HYPER_PROJECT_INITIAL_PRIMAL, &pdlp.project_initial_primal_override, -1, 1, -1, "project the initial primal onto its bounds: -1 solver-mode preset, 0 off, 1 on"}, + }; + + // Bool parameters + bool_parameters = { + {CUOPT_INFEASIBILITY_DETECTION, &pdlp.detect_infeasibility, false}, + {CUOPT_STRICT_INFEASIBILITY, &pdlp.strict_infeasibility, false}, + {CUOPT_PER_CONSTRAINT_RESIDUAL, &pdlp.per_constraint_residual, false}, + {CUOPT_SAVE_BEST_PRIMAL_SO_FAR, &pdlp.save_best_primal_so_far, false}, + {CUOPT_FIRST_PRIMAL_FEASIBLE, &pdlp.first_primal_feasible, false}, + {CUOPT_MIP_HEURISTICS_ONLY, &mip.heuristics_only, false}, + {CUOPT_LOG_TO_CONSOLE, &pdlp.log_to_console, true}, + {CUOPT_LOG_TO_CONSOLE, &mip.log_to_console, true}, + {CUOPT_CROSSOVER, &pdlp.crossover, false}, + {CUOPT_ELIMINATE_DENSE_COLUMNS, &pdlp.eliminate_dense_columns, true}, + {CUOPT_CUDSS_DETERMINISTIC, &pdlp.cudss_deterministic, false}, + {CUOPT_DUAL_POSTSOLVE, &pdlp.dual_postsolve, true}, + {CUOPT_SEQUENCE_SOLVE, &pdlp.sequence_solve, false}, + {CUOPT_BARRIER_ITERATIVE_REFINEMENT, &pdlp.barrier_iterative_refinement, true}, + {CUOPT_MIP_PROBING, &mip.probing, true}, + // Diving heuristic hyper-parameters (hidden from default --help: name contains "hyper_") + {CUOPT_MIP_HYPER_DIVING_SHOW_TYPE, &mip.diving_params.show_type, false, "log diving heuristic type when it finds a new incumbent"}, + // Recursive sub-MIP (RINS) hyper-parameters (hidden from default --help: name contains "hyper_") + {CUOPT_MIP_HYPER_SUBMIP_ENABLE_CPUFJ, &mip.submip_params.enable_cpufj, true, "run CPU FJ over the sub-MIP"}, + {CUOPT_MIP_HYPER_BLOCK_BVE, &mip.block_bve, true, "eliminate blocks of binaries in cuOpt's MIP presolve (needs " CUOPT_MIP_PROBING ")"}, + {CUOPT_MIP_HYPER_PRESOLVE_INDICATOR_STRENGTHENING, &mip.indicator_strengthening, true, "append implied indicator rows and lift capacity rows before Papilo presolve"}, + // PDLP scaling hyper-parameter (hidden from default --help: name contains "hyper_") + {CUOPT_PDLP_HYPER_ENABLE_CURTIS_REID_SCALING, &pdlp.hyper_params.do_curtis_reid_scaling, true, "Curtis-Reid prescaling, run before Ruiz/Pock-Chambolle scaling"}, + }; + // String parameters + string_parameters = { + {CUOPT_LOG_FILE, &mip.log_file, ""}, + {CUOPT_LOG_FILE, &pdlp.log_file, ""}, + {CUOPT_SOLUTION_FILE, &mip.sol_file, ""}, + {CUOPT_SOLUTION_FILE, &pdlp.sol_file, ""}, + {CUOPT_USER_PROBLEM_FILE, &mip.user_problem_file, ""}, + {CUOPT_USER_PROBLEM_FILE, &pdlp.user_problem_file, ""}, + {CUOPT_PRESOLVE_FILE, &mip.presolve_file, ""}, + {CUOPT_PRESOLVE_FILE, &pdlp.presolve_file, ""}, + }; + // clang-format on +} + +#if MIP_INSTANTIATE_FLOAT +template class CUOPT_EXPORT solver_parameter_registry_t; +template CUOPT_EXPORT void solver_parameter_registry_t::set_parameter( + const std::string& name, int value); +template CUOPT_EXPORT void solver_parameter_registry_t::set_parameter( + const std::string& name, float value); +template CUOPT_EXPORT void solver_parameter_registry_t::set_parameter( + const std::string& name, bool value); +template CUOPT_EXPORT int solver_parameter_registry_t::get_parameter( + const std::string& name) const; +template CUOPT_EXPORT float solver_parameter_registry_t::get_parameter( + const std::string& name) const; +template CUOPT_EXPORT bool solver_parameter_registry_t::get_parameter( + const std::string& name) const; +template CUOPT_EXPORT std::string solver_parameter_registry_t::get_parameter( + const std::string& name) const; +#endif + +#if MIP_INSTANTIATE_DOUBLE +template class CUOPT_EXPORT solver_parameter_registry_t; +template CUOPT_EXPORT void solver_parameter_registry_t::set_parameter( + const std::string& name, int value); +template CUOPT_EXPORT void solver_parameter_registry_t::set_parameter( + const std::string& name, double value); +template CUOPT_EXPORT void solver_parameter_registry_t::set_parameter( + const std::string& name, bool value); +template CUOPT_EXPORT int solver_parameter_registry_t::get_parameter( + const std::string& name) const; +template CUOPT_EXPORT double solver_parameter_registry_t::get_parameter( + const std::string& name) const; +template CUOPT_EXPORT bool solver_parameter_registry_t::get_parameter( + const std::string& name) const; +template CUOPT_EXPORT std::string solver_parameter_registry_t::get_parameter( + const std::string& name) const; +#endif + +} // namespace cuopt::mathematical_optimization diff --git a/cpp/src/math_optimization/solver_settings.cpp b/cpp/src/math_optimization/solver_settings.cpp index 349dbd8abd..1ff71f57e0 100644 --- a/cpp/src/math_optimization/solver_settings.cpp +++ b/cpp/src/math_optimization/solver_settings.cpp @@ -19,244 +19,31 @@ namespace cuopt::mathematical_optimization { -namespace { - -bool string_to_int(const std::string& value, int& result) -{ - try { - size_t pos = 0; - result = std::stoi(value, &pos); - return pos == value.size(); - } catch (const std::exception&) { - return false; - } -} - -template -bool string_to_float(const std::string& value, f_t& result) -{ - try { - size_t pos = 0; - if constexpr (std::is_same_v) { result = std::stof(value, &pos); } - if constexpr (std::is_same_v) { result = std::stod(value, &pos); } - if (std::isnan(result)) { return false; } - return pos == value.size(); - } catch (const std::exception&) { - return false; - } -} - -std::string quote_if_needed(const std::string& s) -{ - bool needs_quoting = s.empty() || s.find(' ') != std::string::npos || - s.find('"') != std::string::npos || s.find('\t') != std::string::npos; - if (!needs_quoting) return s; - std::string out = "\""; - for (char c : s) { - if (c == '"') - out += "\\\""; - else - out += c; - } - out += '"'; - return out; -} - -bool string_to_bool(const std::string& value, bool& result) -{ - if (value == "true" || value == "True" || value == "TRUE" || value == "1" || value == "t" || - value == "T") { - result = true; - return true; - } else if (value == "false" || value == "False" || value == "FALSE" || value == "0" || - value == "f" || value == "F") { - result = false; - return true; - } else { - return false; - } -} - -} // namespace - template void solver_settings_t::set_parameter_from_string(const std::string& name, const std::string& value) { - bool found = false; - bool output = false; - for (auto& param : int_parameters) { - if (param.param_name == name) { - i_t value_int; - if (string_to_int(value, value_int)) { - if (value_int < param.min_value || value_int > param.max_value) { - throw std::invalid_argument("Parameter " + name + " value " + value + " out of range"); - } - *param.value_ptr = value_int; - found = true; - if (!output) { - CUOPT_LOG_INFO("Setting parameter %s to %d", name.c_str(), value_int); - output = true; - } - } else { - throw std::invalid_argument("Parameter " + name + " value " + value + " is not an integer"); - } - } - } - for (auto& param : float_parameters) { - if (param.param_name == name) { - f_t value_float; - if (string_to_float(value, value_float)) { - if (value_float < param.min_value || value_float > param.max_value) { - throw std::invalid_argument("Parameter " + name + " value " + value + " out of range"); - } - *param.value_ptr = value_float; - found = true; - if (!output) { - CUOPT_LOG_INFO("Setting parameter %s to %e", name.c_str(), value_float); - output = true; - } - } else { - throw std::invalid_argument("Parameter " + name + " value " + value + " is not a float"); - } - } - } - for (auto& param : bool_parameters) { - if (param.param_name == name) { - bool value_bool; - if (string_to_bool(value, value_bool)) { - *param.value_ptr = value_bool; - found = true; - if (!output) { - CUOPT_LOG_INFO("Setting parameter %s to %s", name.c_str(), value_bool ? "true" : "false"); - output = true; - } - } else { - throw std::invalid_argument("Parameter " + name + " value " + value + - " must be true or false"); - } - } - } - - for (auto& param : string_parameters) { - if (param.param_name == name) { - *param.value_ptr = value; - if (!output) { - CUOPT_LOG_INFO("Setting parameter %s to %s", name.c_str(), value.c_str()); - output = true; - } - found = true; - } - } - if (!found) { throw std::invalid_argument("Parameter " + name + " not found"); } + registry_.set_parameter_from_string(name, value); } template template void solver_settings_t::set_parameter(const std::string& name, T value) { - bool found = false; - bool output = false; - if constexpr (std::is_same_v) { - for (auto& param : int_parameters) { - if (param.param_name == name) { - if (value < param.min_value || value > param.max_value) { - throw std::invalid_argument("Parameter " + name + " out of range"); - } - *param.value_ptr = value; - if (!output) { - CUOPT_LOG_INFO("Setting parameter %s to %d", name.c_str(), value); - output = true; - } - found = true; - } - } - } - if constexpr (std::is_same_v) { - for (auto& param : float_parameters) { - if (param.param_name == name) { - if (value < param.min_value || value > param.max_value) { - throw std::invalid_argument("Parameter " + name + " out of range"); - } - *param.value_ptr = value; - if (!output) { - CUOPT_LOG_INFO("Setting parameter %s to %e", name.c_str(), value); - output = true; - } - found = true; - } - } - } - if constexpr (std::is_same_v) { - for (auto& param : bool_parameters) { - if (param.param_name == name) { - *param.value_ptr = value; - if (!output) { - CUOPT_LOG_INFO("Setting parameter %s to %s", name.c_str(), value ? "true" : "false"); - output = true; - } - found = true; - } - } - } - if constexpr (std::is_same_v) { - for (auto& param : string_parameters) { - if (param.param_name == name) { - *param.value_ptr = value; - if (!output) { - CUOPT_LOG_INFO("Setting parameter %s to %s", name.c_str(), value.c_str()); - output = true; - } - found = true; - } - } - } - if (!found) { throw std::invalid_argument("Parameter " + name + " not found"); } + registry_.set_parameter(name, value); } template template T solver_settings_t::get_parameter(const std::string& name) const { - if constexpr (std::is_same_v) { - for (auto& param : int_parameters) { - if (param.param_name == name) { return *param.value_ptr; } - } - } - if constexpr (std::is_same_v) { - for (auto& param : float_parameters) { - if (param.param_name == name) { return *param.value_ptr; } - } - } - if constexpr (std::is_same_v) { - for (auto& param : bool_parameters) { - if (param.param_name == name) { return *param.value_ptr; } - } - } - if constexpr (std::is_same_v) { - for (auto& param : string_parameters) { - if (param.param_name == name) { return *param.value_ptr; } - } - } - throw std::invalid_argument("Parameter " + name + " not found"); + return registry_.template get_parameter(name); } template std::string solver_settings_t::get_parameter_as_string(const std::string& name) const { - for (auto& param : int_parameters) { - if (param.param_name == name) { return std::to_string(*param.value_ptr); } - } - for (auto& param : float_parameters) { - if (param.param_name == name) { return std::to_string(*param.value_ptr); } - } - for (auto& param : bool_parameters) { - if (param.param_name == name) { return *param.value_ptr ? "true" : "false"; } - } - for (auto& param : string_parameters) { - if (param.param_name == name) { return *param.value_ptr; } - } - throw std::invalid_argument("Parameter " + name + " not found"); + return registry_.get_parameter_as_string(name); } template @@ -295,150 +82,45 @@ solver_settings_t::get_pdlp_warm_start_data_view() const noexcept template const std::vector>& solver_settings_t::get_float_parameters() const { - return float_parameters; + return registry_.get_float_parameters(); } template const std::vector>& solver_settings_t::get_int_parameters() const { - return int_parameters; + return registry_.get_int_parameters(); } template const std::vector>& solver_settings_t::get_bool_parameters() const { - return bool_parameters; + return registry_.get_bool_parameters(); } template const std::vector>& solver_settings_t::get_string_parameters() const { - return string_parameters; + return registry_.get_string_parameters(); } template const std::vector solver_settings_t::get_parameter_names() const { - std::vector parameter_names; - for (auto& param : int_parameters) { - parameter_names.push_back(param.param_name); - } - for (auto& param : float_parameters) { - parameter_names.push_back(param.param_name); - } - for (auto& param : bool_parameters) { - parameter_names.push_back(param.param_name); - } - for (auto& param : string_parameters) { - parameter_names.push_back(param.param_name); - } - return parameter_names; + return registry_.get_parameter_names(); } template void solver_settings_t::load_parameters_from_file(const std::string& path) { - cuopt_expects(!std::filesystem::is_directory(path) && std::filesystem::exists(path), - error_type_t::ValidationError, - "Parameter config: not a valid file: %s", - path.c_str()); - std::ifstream file(path); - cuopt_expects(file.is_open(), - error_type_t::ValidationError, - "Parameter config: cannot open: %s", - path.c_str()); - std::string line; - while (std::getline(file, line)) { - auto first_non_ws = std::find_if_not(line.begin(), line.end(), ::isspace); - if (first_non_ws == line.end() || *first_non_ws == '#') continue; - line.erase(line.begin(), first_non_ws); - - std::istringstream iss(line); - std::string key; - cuopt_expects(iss >> key >> std::ws && iss.get() == '=', - error_type_t::ValidationError, - "Parameter config: bad line: %s", - line.c_str()); - iss >> std::ws; - cuopt_expects(!iss.eof(), - error_type_t::ValidationError, - "Parameter config: missing value: %s", - line.c_str()); - std::string val; - if (iss.peek() == '"') { - iss.get(); - val.clear(); - char ch; - bool closed = false; - while (iss.get(ch)) { - if (ch == '\\' && iss.peek() == '"') { - iss.get(ch); - val += '"'; - } else if (ch == '"') { - closed = true; - break; - } else { - val += ch; - } - } - cuopt_expects(closed, - error_type_t::ValidationError, - "Parameter config: unterminated quote: %s", - line.c_str()); - } else { - iss >> val; - } - std::string trailing; - cuopt_expects(!bool(iss >> trailing), - error_type_t::ValidationError, - "Parameter config: trailing junk: %s", - line.c_str()); - try { - set_parameter_from_string(key, val); - } catch (const std::invalid_argument& e) { - cuopt_expects(false, error_type_t::ValidationError, "Parameter config: %s", e.what()); - } - } - CUOPT_LOG_INFO("Parameters loaded from: %s", path.c_str()); + registry_.load_parameters_from_file(path); } template bool solver_settings_t::dump_parameters_to_file(const std::string& path, bool hyperparameters_only) const { - std::ofstream file(path); - if (!file.is_open()) { - CUOPT_LOG_ERROR("Cannot open file for writing: %s", path.c_str()); - return false; - } - file << "# cuOpt parameter configuration (auto-generated)\n"; - file << "# Uncomment and change the values you wish to override.\n\n"; - for (const auto& p : int_parameters) { - if (hyperparameters_only && p.param_name.find("hyper_") == std::string::npos) continue; - if (p.description && p.description[0] != '\0') - file << "# " << p.description << " (int, range: [" << p.min_value << ", " << p.max_value - << "])\n"; - file << "# " << p.param_name << " = " << *p.value_ptr << "\n\n"; - } - for (const auto& p : float_parameters) { - if (hyperparameters_only && p.param_name.find("hyper_") == std::string::npos) continue; - if (p.description && p.description[0] != '\0') - file << "# " << p.description << " (double, range: [" << p.min_value << ", " << p.max_value - << "])\n"; - file << "# " << p.param_name << " = " << *p.value_ptr << "\n\n"; - } - for (const auto& p : bool_parameters) { - if (hyperparameters_only && p.param_name.find("hyper_") == std::string::npos) continue; - if (p.description && p.description[0] != '\0') file << "# " << p.description << " (bool)\n"; - file << "# " << p.param_name << " = " << (*p.value_ptr ? "true" : "false") << "\n\n"; - } - for (const auto& p : string_parameters) { - if (hyperparameters_only && p.param_name.find("hyper_") == std::string::npos) continue; - if (p.description && p.description[0] != '\0') file << "# " << p.description << " (string)\n"; - file << "# " << p.param_name << " = " << quote_if_needed(*p.value_ptr) << "\n\n"; - } - return true; + return registry_.dump_parameters_to_file(path, hyperparameters_only); } // NOTE: deliberately no `template class solver_settings_t<...>` here. diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 141fb86f74..9b0c328dea 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -107,187 +107,10 @@ void solver_settings_t::add_initial_mip_solution(const f_t* solution, mip_settings.add_initial_solution(solution, size, stream); } -// The constructor is here, not in solver_settings.cpp, and its body is a red herring: it -// only builds parameter tables. What forces the placement is the member it default- -// constructs. pdlp_solver_settings_t holds a pdlp_warm_start_data_t by value, and that -// type's default ctor -- defined in pdlp/pdlp_warm_start_data.cu -- constructs nine -// rmm::device_uvectors on cudaStreamDefault. Compiling this constructor into the CUDA-free -// cuopt_client would therefore leave libcuopt_client.so with an undefined reference that -// only surfaces at call time. -// -// Giving pdlp_warm_start_data_t a default ctor that does not touch device memory would let -// this move to the host translation unit. template -solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings() +solver_settings_t::solver_settings_t() + : pdlp_settings(), mip_settings(), registry_(pdlp_settings, mip_settings) { - // clang-format off - // Float parameters - float_parameters = { - {CUOPT_TIME_LIMIT, &mip_settings.time_limit, f_t(0.0), std::numeric_limits::infinity(), std::numeric_limits::infinity()}, - {CUOPT_TIME_LIMIT, &pdlp_settings.time_limit, f_t(0.0), std::numeric_limits::infinity(), std::numeric_limits::infinity()}, - {CUOPT_WORK_LIMIT, &mip_settings.work_limit, f_t(0.0), std::numeric_limits::infinity(), std::numeric_limits::infinity()}, - {CUOPT_ABSOLUTE_DUAL_TOLERANCE, &pdlp_settings.tolerances.absolute_dual_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-4)}, - {CUOPT_RELATIVE_DUAL_TOLERANCE, &pdlp_settings.tolerances.relative_dual_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-4)}, - {CUOPT_ABSOLUTE_PRIMAL_TOLERANCE, &pdlp_settings.tolerances.absolute_primal_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-4)}, - {CUOPT_RELATIVE_PRIMAL_TOLERANCE, &pdlp_settings.tolerances.relative_primal_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-4)}, - {CUOPT_ABSOLUTE_GAP_TOLERANCE, &pdlp_settings.tolerances.absolute_gap_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-4)}, - {CUOPT_RELATIVE_GAP_TOLERANCE, &pdlp_settings.tolerances.relative_gap_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-4)}, - {CUOPT_MIP_ABSOLUTE_TOLERANCE, &mip_settings.tolerances.absolute_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-6)}, - {CUOPT_MIP_RELATIVE_TOLERANCE, &mip_settings.tolerances.relative_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-12)}, - {CUOPT_MIP_INTEGRALITY_TOLERANCE, &mip_settings.tolerances.integrality_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-5)}, - {CUOPT_MIP_ABSOLUTE_GAP, &mip_settings.tolerances.absolute_mip_gap, f_t(0.0), std::numeric_limits::infinity(), std::max(f_t(1e-10), std::numeric_limits::epsilon())}, - {CUOPT_MIP_RELATIVE_GAP, &mip_settings.tolerances.relative_mip_gap, f_t(0.0), f_t(1e-1), f_t(1e-4)}, - {CUOPT_PRIMAL_INFEASIBLE_TOLERANCE, &pdlp_settings.tolerances.primal_infeasible_tolerance, f_t(0.0), f_t(1e-1), std::max(f_t(1e-10), std::numeric_limits::epsilon())}, - {CUOPT_DUAL_INFEASIBLE_TOLERANCE, &pdlp_settings.tolerances.dual_infeasible_tolerance, f_t(0.0), f_t(1e-1), std::max(f_t(1e-10), std::numeric_limits::epsilon())}, - {CUOPT_MIP_CUT_CHANGE_THRESHOLD, &mip_settings.cut_change_threshold, f_t(-1.0), std::numeric_limits::infinity(), f_t(-1.0)}, - {CUOPT_MIP_CUT_MIN_ORTHOGONALITY, &mip_settings.cut_min_orthogonality, f_t(0.0), f_t(1.0), f_t(0.5)}, - {CUOPT_BARRIER_PRIMAL_REGULARIZATION, &pdlp_settings.barrier_primal_regularization, f_t(-1.0), std::numeric_limits::infinity(), f_t(-1.0), "initial primal regularization for the augmented system; -1 automatic"}, - {CUOPT_BARRIER_DUAL_REGULARIZATION, &pdlp_settings.barrier_dual_regularization, f_t(-1.0), std::numeric_limits::infinity(), f_t(-1.0), "initial dual regularization for the augmented system; -1 automatic"}, - {CUOPT_BARRIER_STEP_SCALE, &pdlp_settings.barrier_step_scale, f_t(0.5), f_t(0.9999), f_t(0.9)}, - {CUOPT_BARRIER_INITIAL_POINT_SAFEGUARD, &pdlp_settings.barrier_initial_point_safeguard, f_t(0.0), std::numeric_limits::infinity(), f_t(10.0), "margin pushing the barrier initial iterate into the interior of the nonnegative orthant / SOC"}, - // MIP heuristic hyper-parameters (hidden from default --help: name contains "hyper_") - {CUOPT_MIP_HYPER_HEURISTIC_ROOT_LP_TIME_RATIO, &mip_settings.heuristic_params.root_lp_time_ratio, f_t(0.0), f_t(1.0), f_t(0.1), "fraction of total time for root LP"}, - {CUOPT_MIP_HYPER_HEURISTIC_ROOT_LP_MAX_TIME, &mip_settings.heuristic_params.root_lp_max_time, f_t(0.0), std::numeric_limits::infinity(), f_t(15.0), "hard cap on root LP seconds"}, - {CUOPT_MIP_HYPER_HEURISTIC_RINS_TIME_LIMIT, &mip_settings.heuristic_params.rins_time_limit, f_t(0.0), std::numeric_limits::infinity(), f_t(3.0), "per-call RINS sub-MIP time"}, - {CUOPT_MIP_HYPER_HEURISTIC_RINS_MAX_TIME_LIMIT, &mip_settings.heuristic_params.rins_max_time_limit, f_t(0.0), std::numeric_limits::infinity(), f_t(20.0), "ceiling for RINS adaptive time budget"}, - {CUOPT_MIP_HYPER_HEURISTIC_RINS_FIX_RATE, &mip_settings.heuristic_params.rins_fix_rate, f_t(0.0), f_t(1.0), f_t(0.5), "RINS variable fix rate"}, - {CUOPT_MIP_HYPER_HEURISTIC_INITIAL_INFEASIBILITY_WEIGHT, &mip_settings.heuristic_params.initial_infeasibility_weight, f_t(1e-9), std::numeric_limits::infinity(), f_t(1000.0), "constraint violation penalty seed"}, - {CUOPT_MIP_HYPER_HEURISTIC_RELAXED_LP_TIME_LIMIT, &mip_settings.heuristic_params.relaxed_lp_time_limit, f_t(1e-9), std::numeric_limits::infinity(), f_t(1.0), "base relaxed LP time cap in heuristics"}, - {CUOPT_MIP_HYPER_HEURISTIC_RELATED_VARS_TIME_LIMIT, &mip_settings.heuristic_params.related_vars_time_limit, f_t(1e-9), std::numeric_limits::infinity(), f_t(2.0), "time for related-variable structure build"}, - {CUOPT_MIP_SEMICONTINUOUS_BIG_M, &mip_settings.semi_continuous_big_m, f_t(1.0), std::numeric_limits::infinity(), f_t(1e10), "big-M value for semi-continuous variables with no finite upper bound"}, - // Diving heuristic hyper-parameters (hidden from default --help: name contains "hyper_") - {CUOPT_MIP_HYPER_DIVING_ITERATION_LIMIT_FACTOR, &mip_settings.diving_params.iteration_limit_factor, f_t(0.0), f_t(1.0), f_t(0.05), "fraction of best-first iterations allowed per dive"}, - // Recursive sub-MIP (RINS) hyper-parameters (hidden from default --help: name contains "hyper_") - {CUOPT_MIP_HYPER_SUBMIP_BASE_TARGET_FIXRATE, &mip_settings.submip_params.base_target_fixrate, f_t(0.0), f_t(1.0), f_t(0.6), "base target fix rate for the RINS neighbourhood"}, - {CUOPT_MIP_HYPER_SUBMIP_MIN_FIXRATE, &mip_settings.submip_params.min_fixrate, f_t(0.0), f_t(1.0), f_t(0.25), "minimum fix rate for accepting the RINS neighbourhood"}, - {CUOPT_MIP_HYPER_SUBMIP_MIN_FIXRATE_CAP, &mip_settings.submip_params.min_fixrate_cap, f_t(0.0), f_t(1.0), f_t(0.1), "hard cap on the minimum fix rate for solving a sub-MIP"}, - {CUOPT_MIP_HYPER_SUBMIP_TARGET_MIP_GAP, &mip_settings.submip_params.target_mip_gap, f_t(0.0), f_t(1.0), f_t(0.01), "MIP gap target for the sub-MIP"}, - {CUOPT_MIP_HYPER_SUBMIP_ITERATION_LIMIT_RATIO, &mip_settings.submip_params.iteration_limit_ratio, f_t(0.0), f_t(1.0), f_t(0.8), "sub-MIP simplex-iteration limit as a factor of parent B&B iterations"}, - {CUOPT_MIP_HYPER_SUBMIP_ROUND_CLOSE_RATIO, &mip_settings.submip_params.round_close_ratio, f_t(0.0), f_t(1.0), f_t(0.8), "share of the still-unfixed integers left for later neighbourhood rounds (0 reaches the target fix rate in a single round)"}, - }; - - // Int parameters - // TODO should we have Stable2 and Methodolical1 here? - int_parameters = { - {CUOPT_ITERATION_LIMIT, &pdlp_settings.iteration_limit, 0, std::numeric_limits::max(), std::numeric_limits::max()}, - {CUOPT_NODE_LIMIT, &mip_settings.node_limit, 0, std::numeric_limits::max(), std::numeric_limits::max()}, - {CUOPT_PDLP_SOLVER_MODE, reinterpret_cast(&pdlp_settings.pdlp_solver_mode), CUOPT_PDLP_SOLVER_MODE_STABLE1, CUOPT_PDLP_SOLVER_MODE_STABLE3, CUOPT_PDLP_SOLVER_MODE_STABLE3}, - {CUOPT_METHOD, reinterpret_cast(&pdlp_settings.method), CUOPT_METHOD_CONCURRENT, CUOPT_METHOD_PRIMAL_SIMPLEX, CUOPT_METHOD_CONCURRENT}, - {CUOPT_METHOD, reinterpret_cast(&mip_settings.method), CUOPT_METHOD_CONCURRENT, CUOPT_METHOD_PRIMAL_SIMPLEX, CUOPT_METHOD_CONCURRENT}, - {CUOPT_LARGE_NNZ_THRESHOLD, &pdlp_settings.large_nnz_threshold, -1, std::numeric_limits::max(), 50'000'000, "at or above this reduced NNZ, skip Barrier and dual simplex in concurrent solves and enable automatic PDLP column reduction when supported. With -1, concurrent solves use their normal solver selection regardless of nonzeros and PDLP column reduction is off unless explicitly enabled"}, - {CUOPT_LARGE_NNZ_THRESHOLD, &mip_settings.large_nnz_threshold, -1, std::numeric_limits::max(), 50'000'000, "at or above this reduced NNZ, skip Barrier and dual simplex in concurrent solves and enable automatic PDLP column reduction when supported. With -1, concurrent solves use their normal solver selection regardless of nonzeros and PDLP column reduction is off unless explicitly enabled"}, - {CUOPT_NUM_CPU_THREADS, &mip_settings.num_cpu_threads, -1, std::numeric_limits::max(), -1}, - {CUOPT_AUGMENTED, &pdlp_settings.augmented, -1, 1, -1}, - {CUOPT_FOLDING, &pdlp_settings.folding, -1, 1, -1}, - {CUOPT_DUALIZE, &pdlp_settings.dualize, -1, 1, -1}, - {CUOPT_ORDERING, &pdlp_settings.ordering, -1, 1, -1}, - {CUOPT_DUAL_SIMPLEX_INITIAL_PERTURBATION, &pdlp_settings.initial_perturbation, -1, 1, -1}, - {CUOPT_DUAL_SIMPLEX_REMOVE_PERTURBATION, &pdlp_settings.remove_perturbation, -1, 1, -1}, - {CUOPT_PRIMAL_SIMPLEX_PRICING, &pdlp_settings.primal_pricing, 0, 1, 1}, - {CUOPT_BARRIER_DUAL_INITIAL_POINT, reinterpret_cast(&pdlp_settings.barrier_dual_initial_point), -1, 2, -1}, - {CUOPT_POSTSOLVE_INFO, &pdlp_settings.postsolve_info, -1, 1, -1}, - {CUOPT_MIP_CUT_PASSES, &mip_settings.max_cut_passes, -1, std::numeric_limits::max(), 10}, - {CUOPT_MIP_MIXED_INTEGER_ROUNDING_CUTS, &mip_settings.mir_cuts, -1, 1, -1}, - {CUOPT_MIP_MIXED_INTEGER_GOMORY_CUTS, &mip_settings.mixed_integer_gomory_cuts, -1, 1, -1}, - {CUOPT_MIP_KNAPSACK_CUTS, &mip_settings.knapsack_cuts, -1, 1, -1}, - {CUOPT_MIP_FLOW_COVER_CUTS, &mip_settings.flow_cover_cuts, -1, 1, -1}, - {CUOPT_MIP_CLIQUE_CUTS, &mip_settings.clique_cuts, -1, 1, -1}, - {CUOPT_MIP_ZERO_HALF_CUTS, &mip_settings.zero_half_cuts, -1, 1, -1}, - {CUOPT_MIP_IMPLIED_BOUND_CUTS, &mip_settings.implied_bound_cuts, -1, 1, -1}, - {CUOPT_MIP_STRONG_CHVATAL_GOMORY_CUTS, &mip_settings.strong_chvatal_gomory_cuts, -1, 1, -1}, - {CUOPT_MIP_REDUCED_COST_STRENGTHENING, &mip_settings.reduced_cost_strengthening, -1, std::numeric_limits::max(), -1}, - {CUOPT_MIP_DUAL_DEGENERATE_FEASIBILITY_PUMP, &mip_settings.dual_degenerate_feasibility_pump, -1, 1, -1}, - {CUOPT_MIP_PRIMAL_DEGENERATE_PIVOTS, &mip_settings.primal_degenerate_pivots, -1, 1, -1}, - {CUOPT_MIP_DUAL_DEGENERATE_PIVOTS, &mip_settings.dual_degenerate_pivots, -1, 2, -1}, - {CUOPT_MIP_RINS, &mip_settings.submip_params.rins, -1, 1, -1}, - {CUOPT_MIP_RENS, &mip_settings.submip_params.rens, -1, 1, -1}, - {CUOPT_MIP_MUTATION, &mip_settings.submip_params.mutation, -1, 1, -1}, - {CUOPT_MIP_OBJECTIVE_STEP, &mip_settings.objective_step, 0, 1, 1}, - {CUOPT_NUM_GPUS, &pdlp_settings.num_gpus, -1, 72, 1}, - {CUOPT_NUM_GPUS, &mip_settings.num_gpus, -1, 72, 1}, - {CUOPT_MIP_BATCH_PDLP_STRONG_BRANCHING, &mip_settings.mip_batch_pdlp_strong_branching, 0, 2, 0}, - {CUOPT_MIP_BATCH_PDLP_RELIABILITY_BRANCHING, &mip_settings.mip_batch_pdlp_reliability_branching, 0, 2, 0}, - {CUOPT_MIP_STRONG_BRANCHING_SIMPLEX_ITERATION_LIMIT, &mip_settings.strong_branching_simplex_iteration_limit, -1,std::numeric_limits::max(), -1}, - {CUOPT_PRESOLVE, reinterpret_cast(&pdlp_settings.presolver), CUOPT_PRESOLVE_DEFAULT, CUOPT_PRESOLVE_PSLP, CUOPT_PRESOLVE_DEFAULT}, - {CUOPT_PRESOLVE, reinterpret_cast(&mip_settings.presolver), CUOPT_PRESOLVE_DEFAULT, CUOPT_PRESOLVE_PSLP, CUOPT_PRESOLVE_DEFAULT}, - {CUOPT_MULTIGPU_PDLP_PARTITIONER, reinterpret_cast(&pdlp_settings.multigpu_pdlp_partitioner), CUOPT_MULTIGPU_PDLP_PARTITIONER_AUTO, CUOPT_MULTIGPU_PDLP_PARTITIONER_ROUND_ROBIN, CUOPT_MULTIGPU_PDLP_PARTITIONER_AUTO}, - {CUOPT_MIP_DETERMINISM_MODE, &mip_settings.determinism_mode, CUOPT_MODE_OPPORTUNISTIC, CUOPT_MODE_DETERMINISTIC, CUOPT_MODE_OPPORTUNISTIC}, - {CUOPT_RANDOM_SEED, &mip_settings.seed, -1, std::numeric_limits::max(), -1}, - {CUOPT_MIP_RELIABILITY_BRANCHING, &mip_settings.reliability_branching, -1, std::numeric_limits::max(), -1}, - {CUOPT_PDLP_PRECISION, reinterpret_cast(&pdlp_settings.pdlp_precision), CUOPT_PDLP_DEFAULT_PRECISION, CUOPT_PDLP_MIXED_PRECISION, CUOPT_PDLP_DEFAULT_PRECISION}, - {CUOPT_PDLP_HYPER_REDUCED_MATRIX, reinterpret_cast(&pdlp_settings.hyper_params.reduced_matrix), CUOPT_PDLP_REDUCED_MATRIX_DEFAULT, CUOPT_PDLP_REDUCED_MATRIX_COLUMN_REDUCTION, CUOPT_PDLP_REDUCED_MATRIX_DEFAULT, "reduced-matrix mode: -1 automatic, 0 disabled, 1 enabled"}, - {CUOPT_MIP_SYMMETRY, &mip_settings.symmetry, -1, 2, -1}, - {CUOPT_MIP_SCALING, &mip_settings.mip_scaling, CUOPT_MIP_SCALING_OFF, CUOPT_MIP_SCALING_NO_OBJECTIVE, CUOPT_MIP_SCALING_NO_OBJECTIVE}, - // MIP heuristic hyper-parameters (hidden from default --help: name contains "hyper_") - {CUOPT_MIP_HYPER_HEURISTIC_POPULATION_SIZE, &mip_settings.heuristic_params.population_size, 1, std::numeric_limits::max(), 32, "max solutions in pool"}, - {CUOPT_MIP_HYPER_HEURISTIC_NUM_CPUFJ_THREADS, &mip_settings.heuristic_params.num_cpufj_threads, 0, std::numeric_limits::max(), 8, "parallel CPU FJ climbers"}, - {CUOPT_MIP_HYPER_HEURISTIC_PRESOLVE_MAX_ROUNDS, &mip_settings.heuristic_params.presolve_max_rounds, -1, std::numeric_limits::max(), -1, "Papilo presolve rounds cap (<0 derives it from the problem, 0 keeps Papilo default)"}, - {CUOPT_MIP_HYPER_HEURISTIC_PAPILO_PROBING_MAX_BADGESIZE, &mip_settings.heuristic_params.papilo_probing_max_badgesize, -1, std::numeric_limits::max(), -1, "ceiling on Papilo probing.minbadgesize (<0 derives it from the problem, 0 leaves it uncapped)"}, - {CUOPT_MIP_HYPER_HEURISTIC_STAGNATION_TRIGGER, &mip_settings.heuristic_params.stagnation_trigger, 1, std::numeric_limits::max(), 3, "FP loops w/o improvement before recombination"}, - {CUOPT_MIP_HYPER_HEURISTIC_MAX_ITERS_WITHOUT_IMPROVEMENT, &mip_settings.heuristic_params.max_iterations_without_improvement, 1, std::numeric_limits::max(), 8, "diversity step depth after stagnation"}, - {CUOPT_MIP_HYPER_HEURISTIC_N_OF_MINIMUMS_FOR_EXIT, &mip_settings.heuristic_params.n_of_minimums_for_exit, 1, std::numeric_limits::max(), 7000, "FJ baseline local-minima exit threshold"}, - {CUOPT_MIP_HYPER_HEURISTIC_ENABLED_RECOMBINERS, &mip_settings.heuristic_params.enabled_recombiners, 0, 15, 15, "bitmask: 1=BP 2=FP 4=LS 8=SubMIP"}, - {CUOPT_MIP_HYPER_HEURISTIC_CYCLE_DETECTION_LENGTH, &mip_settings.heuristic_params.cycle_detection_length, 1, std::numeric_limits::max(), 30, "FP assignment cycle ring buffer length"}, - // Diving heuristic hyper-parameters (hidden from default --help: name contains "hyper_") - {CUOPT_MIP_HYPER_DIVING_LINE_SEARCH, &mip_settings.diving_params.line_search_diving, -1, 1, -1, "line-search diving toggle: -1 automatic, 0 disabled, 1 enabled"}, - {CUOPT_MIP_HYPER_DIVING_PSEUDOCOST, &mip_settings.diving_params.pseudocost_diving, -1, 1, -1, "pseudocost diving toggle: -1 automatic, 0 disabled, 1 enabled"}, - {CUOPT_MIP_HYPER_DIVING_GUIDED, &mip_settings.diving_params.guided_diving, -1, 1, -1, "guided diving toggle: -1 automatic, 0 disabled, 1 enabled"}, - {CUOPT_MIP_HYPER_DIVING_COEFFICIENT, &mip_settings.diving_params.coefficient_diving, -1, 1, -1, "coefficient diving toggle: -1 automatic, 0 disabled, 1 enabled"}, - {CUOPT_MIP_HYPER_DIVING_FARKAS, &mip_settings.diving_params.farkas_diving, -1, 1, -1, "Farkas diving toggle: -1 automatic, 0 disabled, 1 enabled"}, - {CUOPT_MIP_HYPER_DIVING_VECTOR_LENGTH, &mip_settings.diving_params.vector_length_diving, -1, 1, -1, "vector-length diving toggle: -1 automatic, 0 disabled, 1 enabled"}, - {CUOPT_MIP_HYPER_DIVING_NODE_LIMIT, &mip_settings.diving_params.node_limit, 0, std::numeric_limits::max(), 500, "maximum nodes explored per dive"}, - {CUOPT_MIP_HYPER_DIVING_BACKTRACK_LIMIT, &mip_settings.diving_params.backtrack_limit, 0, std::numeric_limits::max(), 5, "maximum backtracking allowed per dive"}, - // Recursive sub-MIP (RINS) hyper-parameters (hidden from default --help: name contains "hyper_") - {CUOPT_MIP_HYPER_SUBMIP_NODE_LIMIT_OFFSET, &mip_settings.submip_params.node_limit_offset, 0, std::numeric_limits::max(), 200, "base node limit for the sub-MIP"}, - {CUOPT_MIP_HYPER_SUBMIP_ITERATION_LIMIT_OFFSET, &mip_settings.submip_params.iteration_limit_offset, 0, std::numeric_limits::max(), 10000, "base sub-MIP simplex-iteration limit for root heuristics"}, - {CUOPT_MIP_HYPER_SUBMIP_MAX_LEVEL, &mip_settings.submip_params.max_level, 0, std::numeric_limits::max(), 10, "maximum sub-MIP recursion level"}, - {CUOPT_BARRIER_PRESOLVE_BOUND_FREE_VARIABLES, &pdlp_settings.barrier_presolve_bound_free_variables, -1, 1, -1, "Bound free variables during barrier presolve: -1 automatic (default behavior), 0 disabled, 1 enabled"}, - {CUOPT_BARRIER_ADAPTIVE_REGULARIZATION, &pdlp_settings.barrier_adaptive_regularization, -1, 1, -1, "Adaptive regularization for barrier method: -1 automatic (default behavior), 0 disabled, 1 enabled"}, - // QCQP (barrier) scaling hyper-parameter - {CUOPT_QCQP_HYPER_RUIZ_EQUILIBRATION, &pdlp_settings.qcqp_ruiz_equilibration, -1, 1, -1, "Ruiz equilibration for QCQP barrier scaling: -1 automatic (row/column imbalance heuristic), 0 disabled, 1 enabled"}, - {CUOPT_BARRIER_HYPER_GPU_RUIZ_NNZ_THRESHOLD, &pdlp_settings.gpu_ruiz_nnz_threshold, 0, std::numeric_limits::max(), 500000, "nnz(A)+nnz(Q) at or above which barrier Ruiz equilibration runs on GPU instead of CPU"}, - // Overrides the pdlp_solver_mode preset when not -1 (hidden: name contains "hyper_") - {CUOPT_PDLP_HYPER_PROJECT_INITIAL_PRIMAL, &pdlp_settings.project_initial_primal_override, -1, 1, -1, "project the initial primal onto its bounds: -1 solver-mode preset, 0 off, 1 on"}, - }; - - // Bool parameters - bool_parameters = { - {CUOPT_INFEASIBILITY_DETECTION, &pdlp_settings.detect_infeasibility, false}, - {CUOPT_STRICT_INFEASIBILITY, &pdlp_settings.strict_infeasibility, false}, - {CUOPT_PER_CONSTRAINT_RESIDUAL, &pdlp_settings.per_constraint_residual, false}, - {CUOPT_SAVE_BEST_PRIMAL_SO_FAR, &pdlp_settings.save_best_primal_so_far, false}, - {CUOPT_FIRST_PRIMAL_FEASIBLE, &pdlp_settings.first_primal_feasible, false}, - {CUOPT_MIP_HEURISTICS_ONLY, &mip_settings.heuristics_only, false}, - {CUOPT_LOG_TO_CONSOLE, &pdlp_settings.log_to_console, true}, - {CUOPT_LOG_TO_CONSOLE, &mip_settings.log_to_console, true}, - {CUOPT_CROSSOVER, &pdlp_settings.crossover, false}, - {CUOPT_ELIMINATE_DENSE_COLUMNS, &pdlp_settings.eliminate_dense_columns, true}, - {CUOPT_CUDSS_DETERMINISTIC, &pdlp_settings.cudss_deterministic, false}, - {CUOPT_DUAL_POSTSOLVE, &pdlp_settings.dual_postsolve, true}, - {CUOPT_SEQUENCE_SOLVE, &pdlp_settings.sequence_solve, false}, - {CUOPT_BARRIER_ITERATIVE_REFINEMENT, &pdlp_settings.barrier_iterative_refinement, true}, - {CUOPT_MIP_PROBING, &mip_settings.probing, true}, - // Diving heuristic hyper-parameters (hidden from default --help: name contains "hyper_") - {CUOPT_MIP_HYPER_DIVING_SHOW_TYPE, &mip_settings.diving_params.show_type, false, "log diving heuristic type when it finds a new incumbent"}, - // Recursive sub-MIP (RINS) hyper-parameters (hidden from default --help: name contains "hyper_") - {CUOPT_MIP_HYPER_SUBMIP_ENABLE_CPUFJ, &mip_settings.submip_params.enable_cpufj, true, "run CPU FJ over the sub-MIP"}, - {CUOPT_MIP_HYPER_BLOCK_BVE, &mip_settings.block_bve, true, "eliminate blocks of binaries in cuOpt's MIP presolve (needs " CUOPT_MIP_PROBING ")"}, - {CUOPT_MIP_HYPER_PRESOLVE_INDICATOR_STRENGTHENING, &mip_settings.indicator_strengthening, true, "append implied indicator rows and lift capacity rows before Papilo presolve"}, - // PDLP scaling hyper-parameter (hidden from default --help: name contains "hyper_") - {CUOPT_PDLP_HYPER_ENABLE_CURTIS_REID_SCALING, &pdlp_settings.hyper_params.do_curtis_reid_scaling, true, "Curtis-Reid prescaling, run before Ruiz/Pock-Chambolle scaling"}, - }; - // String parameters - string_parameters = { - {CUOPT_LOG_FILE, &mip_settings.log_file, ""}, - {CUOPT_LOG_FILE, &pdlp_settings.log_file, ""}, - {CUOPT_SOLUTION_FILE, &mip_settings.sol_file, ""}, - {CUOPT_SOLUTION_FILE, &pdlp_settings.sol_file, ""}, - {CUOPT_USER_PROBLEM_FILE, &mip_settings.user_problem_file, ""}, - {CUOPT_USER_PROBLEM_FILE, &pdlp_settings.user_problem_file, ""}, - {CUOPT_PRESOLVE_FILE, &mip_settings.presolve_file, ""}, - {CUOPT_PRESOLVE_FILE, &pdlp_settings.presolve_file, ""}, - }; - // clang-format on } #if MIP_INSTANTIATE_FLOAT diff --git a/cpp/src/mip_heuristics/solver_settings.cpp b/cpp/src/mip_heuristics/solver_settings.cpp index 564248c472..f65ef07903 100644 --- a/cpp/src/mip_heuristics/solver_settings.cpp +++ b/cpp/src/mip_heuristics/solver_settings.cpp @@ -39,20 +39,11 @@ mip_solver_settings_t::get_mip_callbacks() const return mip_callbacks_; } -template -typename mip_solver_settings_t::tolerances_t -mip_solver_settings_t::get_tolerances() const noexcept -{ - return tolerances; -} - #if MIP_INSTANTIATE_FLOAT template CUOPT_EXPORT void mip_solver_settings_t::set_mip_callback( internals::base_solution_callback_t*, void*); template CUOPT_EXPORT const std::vector mip_solver_settings_t::get_mip_callbacks() const; -template CUOPT_EXPORT mip_solver_settings_t::tolerances_t -mip_solver_settings_t::get_tolerances() const noexcept; #endif #if MIP_INSTANTIATE_DOUBLE @@ -60,8 +51,6 @@ template CUOPT_EXPORT void mip_solver_settings_t::set_mip_callback( internals::base_solution_callback_t*, void*); template CUOPT_EXPORT const std::vector mip_solver_settings_t::get_mip_callbacks() const; -template CUOPT_EXPORT mip_solver_settings_t::tolerances_t -mip_solver_settings_t::get_tolerances() const noexcept; #endif } // namespace cuopt::mathematical_optimization diff --git a/cpp/src/pdlp/solver_settings.cu b/cpp/src/pdlp/solver_settings.cu index 76099e6502..484ad5fe25 100644 --- a/cpp/src/pdlp/solver_settings.cu +++ b/cpp/src/pdlp/solver_settings.cu @@ -24,17 +24,6 @@ namespace cuopt::mathematical_optimization { -template -void pdlp_solver_settings_t::set_optimality_tolerance(f_t eps_optimal) -{ - tolerances.absolute_dual_tolerance = eps_optimal; - tolerances.relative_dual_tolerance = eps_optimal; - tolerances.absolute_primal_tolerance = eps_optimal; - tolerances.relative_primal_tolerance = eps_optimal; - tolerances.absolute_gap_tolerance = eps_optimal; - tolerances.relative_gap_tolerance = eps_optimal; -} - template void pdlp_solver_settings_t::set_initial_primal_solution( const f_t* initial_primal_solution, i_t size, cuda::stream_ref stream) diff --git a/cpp/tests/linear_programming/unit_tests/solver_settings_test.cu b/cpp/tests/linear_programming/unit_tests/solver_settings_test.cu index 1827c27d2b..68060383ac 100644 --- a/cpp/tests/linear_programming/unit_tests/solver_settings_test.cu +++ b/cpp/tests/linear_programming/unit_tests/solver_settings_test.cu @@ -5,6 +5,7 @@ */ /* clang-format on */ +#include #include #include @@ -432,4 +433,62 @@ TEST(SolverSettingsWrapperTest, MipCallbackRegistrationAndTolerances) EXPECT_DOUBLE_EQ(tolerances.absolute_tolerance, default_absolute_tol); } +// cpu_solver_settings_t and solver_settings_t bind the same registry, so they must list the same +// parameters with the same defaults and accept the same values. This is what keeps the CPU-only +// settings from drifting from the GPU ones. +TEST(CpuSolverSettingsTest, SameParametersAndDefaultsAsSolverSettings) +{ + solver_settings_t gpu_settings{}; + cpu_solver_settings_t cpu_settings{}; + + auto gpu_names = gpu_settings.get_parameter_names(); + auto cpu_names = cpu_settings.get_parameter_names(); + ASSERT_FALSE(gpu_names.empty()); + EXPECT_EQ(gpu_names, cpu_names); + + for (const auto& name : gpu_names) { + EXPECT_EQ(gpu_settings.get_parameter_as_string(name), + cpu_settings.get_parameter_as_string(name)) + << "default differs for " << name; + } + + // Every name takes its own default value back, on both. + for (const auto& name : gpu_names) { + const auto value = gpu_settings.get_parameter_as_string(name); + EXPECT_NO_THROW(gpu_settings.set_parameter_from_string(name, value)) << name; + EXPECT_NO_THROW(cpu_settings.set_parameter_from_string(name, value)) << name; + } +} + +TEST(CpuSolverSettingsTest, SetByNameReachesTheParameterMembers) +{ + cpu_solver_settings_t settings{}; + settings.set_parameter(CUOPT_TIME_LIMIT, 4.5); + settings.set_parameter(CUOPT_ABSOLUTE_GAP_TOLERANCE, 1e-7); + settings.set_parameter(CUOPT_MIP_ABSOLUTE_TOLERANCE, 2e-7); + + EXPECT_DOUBLE_EQ(settings.get_pdlp_parameters().time_limit, 4.5); + EXPECT_DOUBLE_EQ(settings.get_mip_parameters().time_limit, 4.5); + EXPECT_DOUBLE_EQ(settings.get_pdlp_parameters().tolerances.absolute_gap_tolerance, 1e-7); + EXPECT_DOUBLE_EQ(settings.get_mip_parameters().tolerances.absolute_tolerance, 2e-7); + EXPECT_THROW(settings.set_parameter_from_string("no_such_parameter", "1"), std::invalid_argument); +} + +// The plain bases leave every member reachable under its old name on the settings classes. +TEST(SolverSettingsTest, ParameterMembersStayReachableOnSettingsClasses) +{ + pdlp_solver_settings_t pdlp{}; + pdlp.set_optimality_tolerance(1e-6); + pdlp.time_limit = 3.0; + EXPECT_DOUBLE_EQ(pdlp.tolerances.relative_gap_tolerance, 1e-6); + EXPECT_DOUBLE_EQ(pdlp.get_tolerances().absolute_dual_tolerance, 1e-6); + const pdlp_parameters_t& pdlp_base = pdlp; + EXPECT_DOUBLE_EQ(pdlp_base.time_limit, 3.0); + + mip_solver_settings_t mip{}; + mip.tolerances.integrality_tolerance = 1e-4; + const mip_parameters_t& mip_base = mip; + EXPECT_DOUBLE_EQ(mip_base.get_tolerances().integrality_tolerance, 1e-4); +} + } // namespace cuopt::mathematical_optimization From 7c3ff7bac51ed752c7cfa728fd52e56502da1260 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 21 Oct 2026 13:13:29 -0500 Subject: [PATCH 2/3] Hold the device PDLP warm start behind an opaque pointer so settings build CPU-only pdlp_solver_settings_t held a pdlp_warm_start_data_t by value. Its default constructor is in a CUDA translation unit and its destructor needs rmm, so constructing a solver_settings_t forced every caller to link the GPU library, and its constructor could not live in the CPU-only client. - Add opaque_ptr_t: an owning pointer with value semantics whose clone and destroy functions are recorded where the GPU library creates the object. Copying clones, like a by-value member. - Hold the device warm start through it, created on first use by the GPU library. - Define solver_settings_t's constructor inline, so a CPU-only caller needs no GPU symbol to construct, copy or destroy one. No public API change. Tests cover opaque_ptr_t and that a copied settings object owns its own device warm start. Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: Ramakrishna Prabhu --- .../pdlp/solver_settings.hpp | 10 +- .../solver_settings.hpp | 3 +- .../utilities/opaque_ptr.hpp | 98 +++++++++ cpp/src/math_optimization/solver_settings.cu | 14 +- cpp/src/pdlp/solver_settings.cu | 189 +++++++++--------- .../unit_tests/solver_settings_test.cu | 80 ++++++++ 6 files changed, 287 insertions(+), 107 deletions(-) create mode 100644 cpp/include/cuopt/mathematical_optimization/utilities/opaque_ptr.hpp diff --git a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp index 8c773ac2da..5e80714f45 100644 --- a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -192,13 +193,18 @@ class pdlp_solver_settings_t : public pdlp_parameters_t { /** Initial pdlp iteration */ // TODO batch mode: tmp std::optional initial_pdlp_iteration_; - /** GPU-backed warm start data (device_uvector), used by C++ API and local GPU solves */ - pdlp_warm_start_data_t pdlp_warm_start_data_; + /** GPU-backed warm start data (device_uvector), used by C++ API and local GPU solves. Built on + * first use by the GPU library, so this class needs no GPU symbol to construct, copy or destroy. + */ + mutable opaque_ptr_t> gpu_warm_start_; /** Warm start data as spans over external memory, used by Cython/Python interface */ pdlp_warm_start_data_view_t pdlp_warm_start_data_view_; /** CPU-backed warm start data (std::vector), used for remote execution on CPU-only hosts */ cpu_pdlp_warm_start_data_t cpu_pdlp_warm_start_data_; + /** Creates the device warm start on first use. Defined in the GPU library. */ + pdlp_warm_start_data_t& gpu_warm_start() const; + friend class solver_settings_t; }; diff --git a/cpp/include/cuopt/mathematical_optimization/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/solver_settings.hpp index 25536a6412..86cb6436cd 100644 --- a/cpp/include/cuopt/mathematical_optimization/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/solver_settings.hpp @@ -30,7 +30,8 @@ namespace CUOPT_EXPORT mathematical_optimization { template class solver_settings_t { public: - solver_settings_t(); + // Inline, so a CPU-only caller builds one without linking the GPU library. + solver_settings_t() : registry_(pdlp_settings, mip_settings) {} // Delete copy constructor solver_settings_t(const solver_settings_t& settings) = delete; diff --git a/cpp/include/cuopt/mathematical_optimization/utilities/opaque_ptr.hpp b/cpp/include/cuopt/mathematical_optimization/utilities/opaque_ptr.hpp new file mode 100644 index 0000000000..a45716122f --- /dev/null +++ b/cpp/include/cuopt/mathematical_optimization/utilities/opaque_ptr.hpp @@ -0,0 +1,98 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include + +namespace cuopt { +namespace mathematical_optimization { + +/** + * @brief An owning pointer with value semantics whose copy and destroy are fixed at creation. + * + * Lets a header hold state that only the GPU library can build (for example device buffers) + * without making every user of the header link the GPU library: creating one is the only + * operation that needs T to be complete, and it is done by the library that knows T. Copying, + * moving and destroying go through function pointers recorded at that point, so a translation + * unit that never creates one (a CPU-only client) needs no GPU symbol for any of them. An empty + * one copies and destroys trivially. + * + * Copying clones the pointee, like a by-value member would; it does not share it. + */ +template +class opaque_ptr_t { + public: + opaque_ptr_t() = default; + + opaque_ptr_t(const opaque_ptr_t& other) + : p_(other.p_ ? other.clone_(other.p_) : nullptr), clone_(other.clone_), del_(other.del_) + { + } + + opaque_ptr_t(opaque_ptr_t&& other) noexcept + : p_(std::exchange(other.p_, nullptr)), clone_(other.clone_), del_(other.del_) + { + } + + opaque_ptr_t& operator=(const opaque_ptr_t& other) + { + if (this != &other) { + opaque_ptr_t copy(other); + swap(copy); + } + return *this; + } + + opaque_ptr_t& operator=(opaque_ptr_t&& other) noexcept + { + if (this != &other) { + reset(); + p_ = std::exchange(other.p_, nullptr); + clone_ = other.clone_; + del_ = other.del_; + } + return *this; + } + + ~opaque_ptr_t() { reset(); } + + /** Creates a T. Instantiated only where T is complete, i.e. in the library that owns T. */ + template + static opaque_ptr_t make(Args&&... args) + { + opaque_ptr_t out; + out.p_ = new T(std::forward(args)...); + out.clone_ = [](const T* p) { return new T(*p); }; + out.del_ = [](T* p) { delete p; }; + return out; + } + + T* get() const noexcept { return p_; } + explicit operator bool() const noexcept { return p_ != nullptr; } + + void swap(opaque_ptr_t& other) noexcept + { + std::swap(p_, other.p_); + std::swap(clone_, other.clone_); + std::swap(del_, other.del_); + } + + private: + void reset() noexcept + { + if (p_) { del_(p_); } + p_ = nullptr; + } + + T* p_ = nullptr; + T* (*clone_)(const T*) = nullptr; + void (*del_)(T*) = nullptr; +}; + +} // namespace mathematical_optimization +} // namespace cuopt diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 9b0c328dea..43dda8809e 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -107,21 +107,15 @@ void solver_settings_t::add_initial_mip_solution(const f_t* solution, mip_settings.add_initial_solution(solution, size, stream); } -template -solver_settings_t::solver_settings_t() - : pdlp_settings(), mip_settings(), registry_(pdlp_settings, mip_settings) -{ -} - #if MIP_INSTANTIATE_FLOAT -// Emits the ctor/dtor/copy for the whole class; solver_settings.cpp deliberately does not, -// because those need CUDA (see the note there). +// Emits the dtor for the whole class. The ctor is inline in the header and the by-name +// parameter functions are in solver_settings.cpp, so a CPU-only caller needs none of this. template class CUOPT_EXPORT solver_settings_t; #endif #if MIP_INSTANTIATE_DOUBLE -// Emits the ctor/dtor/copy for the whole class; solver_settings.cpp deliberately does not, -// because those need CUDA (see the note there). +// Emits the dtor for the whole class. The ctor is inline in the header and the by-name +// parameter functions are in solver_settings.cpp, so a CPU-only caller needs none of this. template class CUOPT_EXPORT solver_settings_t; #endif diff --git a/cpp/src/pdlp/solver_settings.cu b/cpp/src/pdlp/solver_settings.cu index 484ad5fe25..aa06a69128 100644 --- a/cpp/src/pdlp/solver_settings.cu +++ b/cpp/src/pdlp/solver_settings.cu @@ -85,83 +85,77 @@ void pdlp_solver_settings_t::set_pdlp_warm_start_data( const rmm::device_uvector& var_mapping, const rmm::device_uvector& constraint_mapping) { - pdlp_warm_start_data_ = std::move(pdlp_warm_start_data_view); + gpu_warm_start() = std::move(pdlp_warm_start_data_view); // A var_mapping was given if (var_mapping.size() != 0) { // If less variables, scatter using the passed argument and reduce the size of all primal // related vectors - if (var_mapping.size() < - pdlp_warm_start_data_.last_restart_duality_gap_primal_solution_.size()) { + if (var_mapping.size() < gpu_warm_start().last_restart_duality_gap_primal_solution_.size()) { thrust::scatter(rmm::exec_policy(var_mapping.stream()), - pdlp_warm_start_data_.current_primal_solution_.begin(), - pdlp_warm_start_data_.current_primal_solution_.end(), + gpu_warm_start().current_primal_solution_.begin(), + gpu_warm_start().current_primal_solution_.end(), var_mapping.begin(), - pdlp_warm_start_data_.current_primal_solution_.begin()); + gpu_warm_start().current_primal_solution_.begin()); thrust::scatter(rmm::exec_policy(var_mapping.stream()), - pdlp_warm_start_data_.initial_primal_average_.begin(), - pdlp_warm_start_data_.initial_primal_average_.end(), + gpu_warm_start().initial_primal_average_.begin(), + gpu_warm_start().initial_primal_average_.end(), var_mapping.begin(), - pdlp_warm_start_data_.initial_primal_average_.begin()); + gpu_warm_start().initial_primal_average_.begin()); thrust::scatter(rmm::exec_policy(var_mapping.stream()), - pdlp_warm_start_data_.current_ATY_.begin(), - pdlp_warm_start_data_.current_ATY_.end(), + gpu_warm_start().current_ATY_.begin(), + gpu_warm_start().current_ATY_.end(), var_mapping.begin(), - pdlp_warm_start_data_.current_ATY_.begin()); + gpu_warm_start().current_ATY_.begin()); thrust::scatter(rmm::exec_policy(var_mapping.stream()), - pdlp_warm_start_data_.sum_primal_solutions_.begin(), - pdlp_warm_start_data_.sum_primal_solutions_.end(), + gpu_warm_start().sum_primal_solutions_.begin(), + gpu_warm_start().sum_primal_solutions_.end(), var_mapping.begin(), - pdlp_warm_start_data_.sum_primal_solutions_.begin()); + gpu_warm_start().sum_primal_solutions_.begin()); thrust::scatter(rmm::exec_policy(var_mapping.stream()), - pdlp_warm_start_data_.last_restart_duality_gap_primal_solution_.begin(), - pdlp_warm_start_data_.last_restart_duality_gap_primal_solution_.end(), + gpu_warm_start().last_restart_duality_gap_primal_solution_.begin(), + gpu_warm_start().last_restart_duality_gap_primal_solution_.end(), var_mapping.begin(), - pdlp_warm_start_data_.last_restart_duality_gap_primal_solution_.begin()); - - pdlp_warm_start_data_.current_primal_solution_.resize(var_mapping.size(), - var_mapping.stream()); - pdlp_warm_start_data_.initial_primal_average_.resize(var_mapping.size(), - var_mapping.stream()); - pdlp_warm_start_data_.current_ATY_.resize(var_mapping.size(), var_mapping.stream()); - pdlp_warm_start_data_.sum_primal_solutions_.resize(var_mapping.size(), var_mapping.stream()); - pdlp_warm_start_data_.last_restart_duality_gap_primal_solution_.resize(var_mapping.size(), - var_mapping.stream()); + gpu_warm_start().last_restart_duality_gap_primal_solution_.begin()); + + gpu_warm_start().current_primal_solution_.resize(var_mapping.size(), var_mapping.stream()); + gpu_warm_start().initial_primal_average_.resize(var_mapping.size(), var_mapping.stream()); + gpu_warm_start().current_ATY_.resize(var_mapping.size(), var_mapping.stream()); + gpu_warm_start().sum_primal_solutions_.resize(var_mapping.size(), var_mapping.stream()); + gpu_warm_start().last_restart_duality_gap_primal_solution_.resize(var_mapping.size(), + var_mapping.stream()); } else if (var_mapping.size() > - pdlp_warm_start_data_.last_restart_duality_gap_primal_solution_.size()) { - const auto previous_size = - pdlp_warm_start_data_.last_restart_duality_gap_primal_solution_.size(); + gpu_warm_start().last_restart_duality_gap_primal_solution_.size()) { + const auto previous_size = gpu_warm_start().last_restart_duality_gap_primal_solution_.size(); // If more variables just pad with 0s - pdlp_warm_start_data_.current_primal_solution_.resize(var_mapping.size(), - var_mapping.stream()); - pdlp_warm_start_data_.initial_primal_average_.resize(var_mapping.size(), - var_mapping.stream()); - pdlp_warm_start_data_.current_ATY_.resize(var_mapping.size(), var_mapping.stream()); - pdlp_warm_start_data_.sum_primal_solutions_.resize(var_mapping.size(), var_mapping.stream()); - pdlp_warm_start_data_.last_restart_duality_gap_primal_solution_.resize(var_mapping.size(), - var_mapping.stream()); + gpu_warm_start().current_primal_solution_.resize(var_mapping.size(), var_mapping.stream()); + gpu_warm_start().initial_primal_average_.resize(var_mapping.size(), var_mapping.stream()); + gpu_warm_start().current_ATY_.resize(var_mapping.size(), var_mapping.stream()); + gpu_warm_start().sum_primal_solutions_.resize(var_mapping.size(), var_mapping.stream()); + gpu_warm_start().last_restart_duality_gap_primal_solution_.resize(var_mapping.size(), + var_mapping.stream()); thrust::fill(rmm::exec_policy(var_mapping.stream()), - pdlp_warm_start_data_.current_primal_solution_.begin() + previous_size, - pdlp_warm_start_data_.current_primal_solution_.end(), + gpu_warm_start().current_primal_solution_.begin() + previous_size, + gpu_warm_start().current_primal_solution_.end(), f_t(0)); thrust::fill(rmm::exec_policy(var_mapping.stream()), - pdlp_warm_start_data_.initial_primal_average_.begin() + previous_size, - pdlp_warm_start_data_.initial_primal_average_.end(), + gpu_warm_start().initial_primal_average_.begin() + previous_size, + gpu_warm_start().initial_primal_average_.end(), f_t(0)); thrust::fill(rmm::exec_policy(var_mapping.stream()), - pdlp_warm_start_data_.current_ATY_.begin() + previous_size, - pdlp_warm_start_data_.current_ATY_.end(), + gpu_warm_start().current_ATY_.begin() + previous_size, + gpu_warm_start().current_ATY_.end(), f_t(0)); thrust::fill(rmm::exec_policy(var_mapping.stream()), - pdlp_warm_start_data_.sum_primal_solutions_.begin() + previous_size, - pdlp_warm_start_data_.sum_primal_solutions_.end(), + gpu_warm_start().sum_primal_solutions_.begin() + previous_size, + gpu_warm_start().sum_primal_solutions_.end(), f_t(0)); thrust::fill( rmm::exec_policy(var_mapping.stream()), - pdlp_warm_start_data_.last_restart_duality_gap_primal_solution_.begin() + previous_size, - pdlp_warm_start_data_.last_restart_duality_gap_primal_solution_.end(), + gpu_warm_start().last_restart_duality_gap_primal_solution_.begin() + previous_size, + gpu_warm_start().last_restart_duality_gap_primal_solution_.end(), f_t(0)); } } @@ -171,68 +165,66 @@ void pdlp_solver_settings_t::set_pdlp_warm_start_data( // If less variables, scatter using the passed argument and reduce the size of all dual related // vectors if (constraint_mapping.size() < - pdlp_warm_start_data_.last_restart_duality_gap_dual_solution_.size()) { + gpu_warm_start().last_restart_duality_gap_dual_solution_.size()) { thrust::scatter(rmm::exec_policy(constraint_mapping.stream()), - pdlp_warm_start_data_.current_dual_solution_.begin(), - pdlp_warm_start_data_.current_dual_solution_.end(), + gpu_warm_start().current_dual_solution_.begin(), + gpu_warm_start().current_dual_solution_.end(), constraint_mapping.begin(), - pdlp_warm_start_data_.current_dual_solution_.begin()); + gpu_warm_start().current_dual_solution_.begin()); thrust::scatter(rmm::exec_policy(constraint_mapping.stream()), - pdlp_warm_start_data_.initial_dual_average_.begin(), - pdlp_warm_start_data_.initial_dual_average_.end(), + gpu_warm_start().initial_dual_average_.begin(), + gpu_warm_start().initial_dual_average_.end(), constraint_mapping.begin(), - pdlp_warm_start_data_.initial_dual_average_.begin()); + gpu_warm_start().initial_dual_average_.begin()); thrust::scatter(rmm::exec_policy(constraint_mapping.stream()), - pdlp_warm_start_data_.sum_dual_solutions_.begin(), - pdlp_warm_start_data_.sum_dual_solutions_.end(), + gpu_warm_start().sum_dual_solutions_.begin(), + gpu_warm_start().sum_dual_solutions_.end(), constraint_mapping.begin(), - pdlp_warm_start_data_.sum_dual_solutions_.begin()); + gpu_warm_start().sum_dual_solutions_.begin()); thrust::scatter(rmm::exec_policy(constraint_mapping.stream()), - pdlp_warm_start_data_.last_restart_duality_gap_dual_solution_.begin(), - pdlp_warm_start_data_.last_restart_duality_gap_dual_solution_.end(), + gpu_warm_start().last_restart_duality_gap_dual_solution_.begin(), + gpu_warm_start().last_restart_duality_gap_dual_solution_.end(), constraint_mapping.begin(), - pdlp_warm_start_data_.last_restart_duality_gap_dual_solution_.begin()); - - pdlp_warm_start_data_.current_dual_solution_.resize(constraint_mapping.size(), - constraint_mapping.stream()); - pdlp_warm_start_data_.initial_dual_average_.resize(constraint_mapping.size(), - constraint_mapping.stream()); - pdlp_warm_start_data_.sum_dual_solutions_.resize(constraint_mapping.size(), - constraint_mapping.stream()); - pdlp_warm_start_data_.last_restart_duality_gap_dual_solution_.resize( - constraint_mapping.size(), constraint_mapping.stream()); + gpu_warm_start().last_restart_duality_gap_dual_solution_.begin()); + + gpu_warm_start().current_dual_solution_.resize(constraint_mapping.size(), + constraint_mapping.stream()); + gpu_warm_start().initial_dual_average_.resize(constraint_mapping.size(), + constraint_mapping.stream()); + gpu_warm_start().sum_dual_solutions_.resize(constraint_mapping.size(), + constraint_mapping.stream()); + gpu_warm_start().last_restart_duality_gap_dual_solution_.resize(constraint_mapping.size(), + constraint_mapping.stream()); } else if (constraint_mapping.size() > - pdlp_warm_start_data_.last_restart_duality_gap_dual_solution_.size()) { - const auto previous_size = - pdlp_warm_start_data_.last_restart_duality_gap_dual_solution_.size(); + gpu_warm_start().last_restart_duality_gap_dual_solution_.size()) { + const auto previous_size = gpu_warm_start().last_restart_duality_gap_dual_solution_.size(); // If more variables just pad with 0s - pdlp_warm_start_data_.current_dual_solution_.resize(constraint_mapping.size(), - constraint_mapping.stream()); - pdlp_warm_start_data_.initial_dual_average_.resize(constraint_mapping.size(), - constraint_mapping.stream()); - pdlp_warm_start_data_.sum_dual_solutions_.resize(constraint_mapping.size(), - constraint_mapping.stream()); - pdlp_warm_start_data_.last_restart_duality_gap_dual_solution_.resize( - constraint_mapping.size(), constraint_mapping.stream()); + gpu_warm_start().current_dual_solution_.resize(constraint_mapping.size(), + constraint_mapping.stream()); + gpu_warm_start().initial_dual_average_.resize(constraint_mapping.size(), + constraint_mapping.stream()); + gpu_warm_start().sum_dual_solutions_.resize(constraint_mapping.size(), + constraint_mapping.stream()); + gpu_warm_start().last_restart_duality_gap_dual_solution_.resize(constraint_mapping.size(), + constraint_mapping.stream()); thrust::fill(rmm::exec_policy(constraint_mapping.stream()), - pdlp_warm_start_data_.current_dual_solution_.begin() + previous_size, - pdlp_warm_start_data_.current_dual_solution_.end(), + gpu_warm_start().current_dual_solution_.begin() + previous_size, + gpu_warm_start().current_dual_solution_.end(), f_t(0)); thrust::fill(rmm::exec_policy(constraint_mapping.stream()), - pdlp_warm_start_data_.initial_dual_average_.begin() + previous_size, - pdlp_warm_start_data_.initial_dual_average_.end(), + gpu_warm_start().initial_dual_average_.begin() + previous_size, + gpu_warm_start().initial_dual_average_.end(), f_t(0)); thrust::fill(rmm::exec_policy(constraint_mapping.stream()), - pdlp_warm_start_data_.sum_dual_solutions_.begin() + previous_size, - pdlp_warm_start_data_.sum_dual_solutions_.end(), + gpu_warm_start().sum_dual_solutions_.begin() + previous_size, + gpu_warm_start().sum_dual_solutions_.end(), + f_t(0)); + thrust::fill(rmm::exec_policy(constraint_mapping.stream()), + gpu_warm_start().last_restart_duality_gap_dual_solution_.begin() + previous_size, + gpu_warm_start().last_restart_duality_gap_dual_solution_.end(), f_t(0)); - thrust::fill( - rmm::exec_policy(constraint_mapping.stream()), - pdlp_warm_start_data_.last_restart_duality_gap_dual_solution_.begin() + previous_size, - pdlp_warm_start_data_.last_restart_duality_gap_dual_solution_.end(), - f_t(0)); } } } @@ -375,13 +367,22 @@ template const pdlp_warm_start_data_t& pdlp_solver_settings_t::get_pdlp_warm_start_data() const noexcept { - return pdlp_warm_start_data_; + return gpu_warm_start(); } template pdlp_warm_start_data_t& pdlp_solver_settings_t::get_pdlp_warm_start_data() { - return pdlp_warm_start_data_; + return gpu_warm_start(); +} + +template +pdlp_warm_start_data_t& pdlp_solver_settings_t::gpu_warm_start() const +{ + if (!gpu_warm_start_) { + gpu_warm_start_ = opaque_ptr_t>::make(); + } + return *gpu_warm_start_.get(); } #if MIP_INSTANTIATE_FLOAT || PDLP_INSTANTIATE_FLOAT diff --git a/cpp/tests/linear_programming/unit_tests/solver_settings_test.cu b/cpp/tests/linear_programming/unit_tests/solver_settings_test.cu index 68060383ac..1e79adb5b7 100644 --- a/cpp/tests/linear_programming/unit_tests/solver_settings_test.cu +++ b/cpp/tests/linear_programming/unit_tests/solver_settings_test.cu @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -19,6 +20,7 @@ #include #include #include +#include #include namespace cuopt::mathematical_optimization { @@ -491,4 +493,82 @@ TEST(SolverSettingsTest, ParameterMembersStayReachableOnSettingsClasses) EXPECT_DOUBLE_EQ(mip_base.get_tolerances().integrality_tolerance, 1e-4); } +// opaque_ptr_t is what lets the settings hold GPU data without a GPU symbol for copy or destroy. +// Copying must clone, as a by-value member would, never share. +namespace { +struct counted_t { + static inline int alive = 0; + int value; + explicit counted_t(int v = 0) : value(v) { ++alive; } + counted_t(const counted_t& o) : value(o.value) { ++alive; } + ~counted_t() { --alive; } +}; +} // namespace + +TEST(OpaquePtrTest, EmptyCopiesAndDestroysTrivially) +{ + opaque_ptr_t empty; + EXPECT_FALSE(empty); + opaque_ptr_t copy(empty); + EXPECT_FALSE(copy); + copy = empty; + EXPECT_FALSE(copy); + EXPECT_EQ(counted_t::alive, 0); +} + +TEST(OpaquePtrTest, CopyClonesMoveTransfersAndDestroyReleases) +{ + { + auto a = opaque_ptr_t::make(7); + ASSERT_TRUE(a); + EXPECT_EQ(counted_t::alive, 1); + + opaque_ptr_t b(a); + ASSERT_TRUE(b); + EXPECT_NE(a.get(), b.get()); + EXPECT_EQ(b.get()->value, 7); + EXPECT_EQ(counted_t::alive, 2); + + b.get()->value = 9; + EXPECT_EQ(a.get()->value, 7) << "a copy must not alias the original"; + + opaque_ptr_t c(std::move(b)); + EXPECT_FALSE(b); + EXPECT_EQ(c.get()->value, 9); + EXPECT_EQ(counted_t::alive, 2); + + a = c; + EXPECT_EQ(a.get()->value, 9); + EXPECT_EQ(counted_t::alive, 2); + } + EXPECT_EQ(counted_t::alive, 0); +} + +// Copying a settings object copies its device warm start; the copy owns its own buffers. +TEST(SolverSettingsTest, CopyOwnsItsDeviceWarmStart) +{ + raft::handle_t handle{}; + pdlp_solver_settings_t original{}; + original.get_pdlp_warm_start_data().last_restart_duality_gap_dual_solution_.resize( + 3, handle.get_stream()); + ASSERT_TRUE(original.get_pdlp_warm_start_data().is_populated()); + + pdlp_solver_settings_t copy(original); + ASSERT_TRUE(copy.get_pdlp_warm_start_data().is_populated()); + EXPECT_NE(copy.get_pdlp_warm_start_data().last_restart_duality_gap_dual_solution_.data(), + original.get_pdlp_warm_start_data().last_restart_duality_gap_dual_solution_.data()); + + copy.get_pdlp_warm_start_data().last_restart_duality_gap_dual_solution_.resize( + 5, handle.get_stream()); + EXPECT_EQ(original.get_pdlp_warm_start_data().last_restart_duality_gap_dual_solution_.size(), 3u); +} + +TEST(SolverSettingsTest, CopyOfSettingsWithoutWarmStartHasNone) +{ + pdlp_solver_settings_t original{}; + pdlp_solver_settings_t copy(original); + EXPECT_FALSE(copy.get_pdlp_warm_start_data().is_populated()); + EXPECT_FALSE(std::as_const(original).get_pdlp_warm_start_data().is_populated()); +} + } // namespace cuopt::mathematical_optimization From 9ff74cb662ed3c81283acfe64406308b6e1453ed Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 21 Oct 2026 13:24:36 -0500 Subject: [PATCH 3/3] Remove cpu_solver_settings_t; solver_settings_t is the only settings type With the device warm start behind an opaque pointer, solver_settings_t builds, copies and destroys without any GPU symbol, so a separate CPU-only settings type is redundant and would be a second type to keep in sync. Replace its tests with equivalent ones on solver_settings_t: every parameter name accepts its own default, and setting by name reaches the members. Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: Ramakrishna Prabhu --- .../cpu_solver_settings.hpp | 111 ------------------ .../solver_parameter_registry.hpp | 4 +- .../solver_settings.hpp | 4 +- .../unit_tests/solver_settings_test.cu | 47 +++----- 4 files changed, 21 insertions(+), 145 deletions(-) delete mode 100644 cpp/include/cuopt/mathematical_optimization/cpu_solver_settings.hpp diff --git a/cpp/include/cuopt/mathematical_optimization/cpu_solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/cpu_solver_settings.hpp deleted file mode 100644 index 227544b2dd..0000000000 --- a/cpp/include/cuopt/mathematical_optimization/cpu_solver_settings.hpp +++ /dev/null @@ -1,111 +0,0 @@ -/* clang-format off */ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -/* clang-format on */ - -#pragma once - -#include -#include -#include -#include - -#include -#include - -namespace cuopt { -namespace mathematical_optimization { - -/** - * @brief Solver settings with no GPU-backed members, the counterpart of cpu_optimization_problem_t. - * - * Same parameters, same names, defaults and ranges as solver_settings_t, because both bind the - * same pdlp_parameters_t / mip_parameters_t through one solver_parameter_registry_t. What it - * leaves out is what needs a GPU: device initial solutions, device warm start and callbacks. - * The PDLP warm start is the CPU one, for remote solves. - */ -template -class cpu_solver_settings_t { - public: - cpu_solver_settings_t() : registry_(pdlp_, mip_) {} - - cpu_solver_settings_t(const cpu_solver_settings_t&) = delete; - cpu_solver_settings_t& operator=(const cpu_solver_settings_t&) = delete; - cpu_solver_settings_t(cpu_solver_settings_t&&) = delete; - cpu_solver_settings_t& operator=(cpu_solver_settings_t&&) = delete; - - void set_parameter_from_string(const std::string& name, const std::string& value) - { - registry_.set_parameter_from_string(name, value); - } - - template - void set_parameter(const std::string& name, T value) - { - registry_.set_parameter(name, value); - } - - template - T get_parameter(const std::string& name) const - { - return registry_.template get_parameter(name); - } - - std::string get_parameter_as_string(const std::string& name) const - { - return registry_.get_parameter_as_string(name); - } - - const std::vector>& get_float_parameters() const - { - return registry_.get_float_parameters(); - } - const std::vector>& get_int_parameters() const - { - return registry_.get_int_parameters(); - } - const std::vector>& get_bool_parameters() const - { - return registry_.get_bool_parameters(); - } - const std::vector>& get_string_parameters() const - { - return registry_.get_string_parameters(); - } - const std::vector get_parameter_names() const - { - return registry_.get_parameter_names(); - } - - void load_parameters_from_file(const std::string& path) - { - registry_.load_parameters_from_file(path); - } - bool dump_parameters_to_file(const std::string& path, bool hyperparameters_only = true) const - { - return registry_.dump_parameters_to_file(path, hyperparameters_only); - } - - pdlp_parameters_t& get_pdlp_parameters() { return pdlp_; } - const pdlp_parameters_t& get_pdlp_parameters() const { return pdlp_; } - mip_parameters_t& get_mip_parameters() { return mip_; } - const mip_parameters_t& get_mip_parameters() const { return mip_; } - - /** LP only. Left unpopulated for a cold start. */ - cpu_pdlp_warm_start_data_t& get_cpu_pdlp_warm_start_data() { return pdlp_warm_start_; } - const cpu_pdlp_warm_start_data_t& get_cpu_pdlp_warm_start_data() const - { - return pdlp_warm_start_; - } - - private: - pdlp_parameters_t pdlp_; - mip_parameters_t mip_; - cpu_pdlp_warm_start_data_t pdlp_warm_start_; - solver_parameter_registry_t registry_; // after pdlp_ and mip_, which it points into -}; - -} // namespace mathematical_optimization -} // namespace cuopt diff --git a/cpp/include/cuopt/mathematical_optimization/solver_parameter_registry.hpp b/cpp/include/cuopt/mathematical_optimization/solver_parameter_registry.hpp index 8f2ad65e3d..20f1d5ed46 100644 --- a/cpp/include/cuopt/mathematical_optimization/solver_parameter_registry.hpp +++ b/cpp/include/cuopt/mathematical_optimization/solver_parameter_registry.hpp @@ -23,8 +23,8 @@ namespace CUOPT_EXPORT mathematical_optimization { * @brief The solver parameters by name: the one table of names, defaults and ranges. * * Binds each CUOPT_* name to a member of a pdlp_parameters_t / mip_parameters_t and implements - * set/get by name, parameter files and listing. solver_settings_t and cpu_solver_settings_t both - * hold one, so the two cannot disagree on which parameters exist. Needs no CUDA, rmm or raft. + * set/get by name, parameter files and listing. solver_settings_t holds one. Needs no CUDA, rmm or + * raft, so it lives in the CPU-only client library. * * The registry points into the two parameter objects it was built on, so it is neither copyable * nor movable and must not outlive them. diff --git a/cpp/include/cuopt/mathematical_optimization/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/solver_settings.hpp index 86cb6436cd..426b4e0d47 100644 --- a/cpp/include/cuopt/mathematical_optimization/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/solver_settings.hpp @@ -110,8 +110,8 @@ class solver_settings_t { pdlp_solver_settings_t pdlp_settings; mip_solver_settings_t mip_settings; - // Names, defaults and ranges live in solver_parameter_registry_t, shared with - // cpu_solver_settings_t. Declared after the two settings objects it points into. + // Names, defaults and ranges live in solver_parameter_registry_t, built into the CPU-only + // client library. Declared after the two settings objects it points into. solver_parameter_registry_t registry_; }; diff --git a/cpp/tests/linear_programming/unit_tests/solver_settings_test.cu b/cpp/tests/linear_programming/unit_tests/solver_settings_test.cu index 1e79adb5b7..e074336916 100644 --- a/cpp/tests/linear_programming/unit_tests/solver_settings_test.cu +++ b/cpp/tests/linear_programming/unit_tests/solver_settings_test.cu @@ -5,7 +5,6 @@ */ /* clang-format on */ -#include #include #include #include @@ -435,44 +434,32 @@ TEST(SolverSettingsWrapperTest, MipCallbackRegistrationAndTolerances) EXPECT_DOUBLE_EQ(tolerances.absolute_tolerance, default_absolute_tol); } -// cpu_solver_settings_t and solver_settings_t bind the same registry, so they must list the same -// parameters with the same defaults and accept the same values. This is what keeps the CPU-only -// settings from drifting from the GPU ones. -TEST(CpuSolverSettingsTest, SameParametersAndDefaultsAsSolverSettings) +// Every registered name must accept the value it reports as its default; a table entry whose +// default is outside its own range, or of the wrong type, would otherwise only fail in use. +TEST(SolverSettingsTest, EveryParameterNameAcceptsItsOwnDefault) { - solver_settings_t gpu_settings{}; - cpu_solver_settings_t cpu_settings{}; - - auto gpu_names = gpu_settings.get_parameter_names(); - auto cpu_names = cpu_settings.get_parameter_names(); - ASSERT_FALSE(gpu_names.empty()); - EXPECT_EQ(gpu_names, cpu_names); - - for (const auto& name : gpu_names) { - EXPECT_EQ(gpu_settings.get_parameter_as_string(name), - cpu_settings.get_parameter_as_string(name)) - << "default differs for " << name; - } - - // Every name takes its own default value back, on both. - for (const auto& name : gpu_names) { - const auto value = gpu_settings.get_parameter_as_string(name); - EXPECT_NO_THROW(gpu_settings.set_parameter_from_string(name, value)) << name; - EXPECT_NO_THROW(cpu_settings.set_parameter_from_string(name, value)) << name; + solver_settings_t settings{}; + + auto names = settings.get_parameter_names(); + ASSERT_FALSE(names.empty()); + for (const auto& name : names) { + const auto value = settings.get_parameter_as_string(name); + EXPECT_NO_THROW(settings.set_parameter_from_string(name, value)) << name; + EXPECT_EQ(settings.get_parameter_as_string(name), value) << name; } } -TEST(CpuSolverSettingsTest, SetByNameReachesTheParameterMembers) +TEST(SolverSettingsTest, SetByNameReachesTheParameterMembers) { - cpu_solver_settings_t settings{}; + solver_settings_t settings{}; settings.set_parameter(CUOPT_TIME_LIMIT, 4.5); settings.set_parameter(CUOPT_ABSOLUTE_GAP_TOLERANCE, 1e-7); settings.set_parameter(CUOPT_MIP_ABSOLUTE_TOLERANCE, 2e-7); - EXPECT_DOUBLE_EQ(settings.get_pdlp_parameters().time_limit, 4.5); - EXPECT_DOUBLE_EQ(settings.get_mip_parameters().time_limit, 4.5); - EXPECT_DOUBLE_EQ(settings.get_pdlp_parameters().tolerances.absolute_gap_tolerance, 1e-7); - EXPECT_DOUBLE_EQ(settings.get_mip_parameters().tolerances.absolute_tolerance, 2e-7); + EXPECT_DOUBLE_EQ(settings.get_pdlp_settings().time_limit, 4.5); + EXPECT_DOUBLE_EQ(settings.get_mip_settings().time_limit, 4.5); + EXPECT_DOUBLE_EQ(settings.get_pdlp_settings().tolerances.absolute_gap_tolerance, 1e-7); + EXPECT_DOUBLE_EQ(settings.get_mip_settings().tolerances.absolute_tolerance, 2e-7); EXPECT_THROW(settings.set_parameter_from_string("no_such_parameter", "1"), std::invalid_argument); }