From 51b7e8177a10a59d63af12df4f9ea5845d3352c6 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Mon, 23 Mar 2026 10:33:02 -0700 Subject: [PATCH 01/14] initial commit, loading and storing --- .../linear_programming/cuopt/run_mip.cpp | 42 +++-- cpp/cuopt_cli.cpp | 31 +++- .../mip/heuristic_hyper_params.hpp | 42 +++++ .../mip/solver_settings.hpp | 3 + .../diversity/diversity_manager.cu | 31 +++- .../diversity/recombiners/recombiner.cuh | 6 +- .../heuristic_hyper_params_loader.hpp | 142 ++++++++++++++++ .../feasibility_pump/feasibility_pump.cu | 5 +- .../feasibility_pump/feasibility_pump.cuh | 5 +- .../local_search/local_search.cu | 48 +++--- .../local_search/local_search.cuh | 4 +- cpp/src/mip_heuristics/problem/problem.cu | 3 +- cpp/src/mip_heuristics/problem/problem.cuh | 1 + cpp/src/mip_heuristics/solve.cu | 5 +- cpp/src/mip_heuristics/solver.cu | 3 +- cpp/tests/mip/CMakeLists.txt | 3 + cpp/tests/mip/heuristic_hyper_params_test.cu | 160 ++++++++++++++++++ 17 files changed, 483 insertions(+), 51 deletions(-) create mode 100644 cpp/include/cuopt/linear_programming/mip/heuristic_hyper_params.hpp create mode 100644 cpp/src/mip_heuristics/heuristic_hyper_params_loader.hpp create mode 100644 cpp/tests/mip/heuristic_hyper_params_test.cu diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index e01e533a65..6575880e5d 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -40,6 +40,8 @@ #include "initial_problem_check.hpp" +#include + void merge_result_files(const std::string& out_dir, const std::string& final_result_file, int n_gpus, @@ -150,7 +152,8 @@ int run_single_file(std::string file_path, int reliability_branching, double time_limit, double work_limit, - bool deterministic) + bool deterministic, + const std::string& heuristic_config_file = "") { const raft::handle_t handle_{}; cuopt::linear_programming::mip_solver_settings_t settings; @@ -215,6 +218,10 @@ int run_single_file(std::string file_path, settings.reliability_branching = reliability_branching; settings.clique_cuts = -1; settings.seed = 42; + if (!heuristic_config_file.empty()) { + cuopt::linear_programming::fill_mip_heuristic_hyper_params(heuristic_config_file, + settings.heuristic_params); + } cuopt::linear_programming::benchmark_info_t benchmark_info; settings.benchmark_info_ptr = &benchmark_info; auto start_run_solver = std::chrono::high_resolution_clock::now(); @@ -270,7 +277,8 @@ void run_single_file_mp(std::string file_path, int reliability_branching, double time_limit, double work_limit, - bool deterministic) + bool deterministic, + const std::string& heuristic_config_file = "") { std::cout << "running file " << file_path << " on gpu : " << device << std::endl; auto memory_resource = make_async(); @@ -288,7 +296,8 @@ void run_single_file_mp(std::string file_path, reliability_branching, time_limit, work_limit, - deterministic); + deterministic, + heuristic_config_file); // this is a bad design to communicate the result but better than adding complexity of IPC or // pipes exit(sol_found); @@ -386,6 +395,10 @@ int main(int argc, char* argv[]) .default_value(false) .implicit_value(true); + program.add_argument("--mip-heuristic-config") + .help("path to MIP heuristic hyper-parameters config file (key = value format)") + .default_value(std::string("")); + // Parse arguments try { program.parse_args(argc, argv); @@ -409,14 +422,15 @@ int main(int argc, char* argv[]) std::string result_file; int batch_num = -1; - bool heuristics_only = program.get("--heuristics-only")[0] == 't'; - int num_cpu_threads = program.get("--num-cpu-threads"); - bool write_log_file = program.get("--write-log-file")[0] == 't'; - bool log_to_console = program.get("--log-to-console")[0] == 't'; - double memory_limit = program.get("--memory-limit"); - bool track_allocations = program.get("--track-allocations")[0] == 't'; - int reliability_branching = program.get("--reliability-branching"); - bool deterministic = program.get("--determinism"); + bool heuristics_only = program.get("--heuristics-only")[0] == 't'; + int num_cpu_threads = program.get("--num-cpu-threads"); + bool write_log_file = program.get("--write-log-file")[0] == 't'; + bool log_to_console = program.get("--log-to-console")[0] == 't'; + double memory_limit = program.get("--memory-limit"); + bool track_allocations = program.get("--track-allocations")[0] == 't'; + int reliability_branching = program.get("--reliability-branching"); + bool deterministic = program.get("--determinism"); + std::string heuristic_config_file = program.get("--mip-heuristic-config"); if (num_cpu_threads < 0) { num_cpu_threads = omp_get_max_threads() / n_gpus; @@ -516,7 +530,8 @@ int main(int argc, char* argv[]) reliability_branching, time_limit, work_limit, - deterministic); + deterministic, + heuristic_config_file); } else if (sys_pid < 0) { std::cerr << "Fork failed!" << std::endl; exit(1); @@ -559,7 +574,8 @@ int main(int argc, char* argv[]) reliability_branching, time_limit, work_limit, - deterministic); + deterministic, + heuristic_config_file); } return 0; diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 899a3118b3..59931dd3b4 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -27,6 +27,7 @@ #include #include +#include #include @@ -90,7 +91,8 @@ inline cuopt::init_logger_t dummy_logger( int run_single_file(const std::string& file_path, const std::string& initial_solution_file, bool solve_relaxation, - const std::map& settings_strings) + const std::map& settings_strings, + const std::string& heuristic_config_file = "") { cuopt::linear_programming::solver_settings_t settings; @@ -177,6 +179,10 @@ int run_single_file(const std::string& file_path, try { if (is_mip) { auto& mip_settings = settings.get_mip_settings(); + if (!heuristic_config_file.empty()) { + cuopt::linear_programming::fill_mip_heuristic_hyper_params(heuristic_config_file, + mip_settings.heuristic_params); + } auto solution = cuopt::linear_programming::solve_mip(problem_interface.get(), mip_settings); } else { auto& lp_settings = settings.get_pdlp_settings(); @@ -258,6 +264,16 @@ int set_cuda_module_loading(int argc, char* argv[]) */ int main(int argc, char* argv[]) { + // Handle --dump-mip-heuristic-config before argparse so no other args are required + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + if (arg == "--dump-mip-heuristic-config" && i + 1 < argc) { + cuopt::linear_programming::mip_heuristic_hyper_params_t defaults; + cuopt::linear_programming::dump_mip_heuristic_hyper_params(argv[i + 1], defaults); + return 0; + } + } + if (set_cuda_module_loading(argc, argv) != 0) { return 1; } // Get the version string from the version_config.hpp file @@ -286,6 +302,14 @@ int main(int argc, char* argv[]) .default_value(true) .implicit_value(true); + program.add_argument("--mip-heuristic-config") + .help("path to MIP heuristic hyper-parameters config file (key = value format)") + .default_value(std::string("")); + + program.add_argument("--dump-mip-heuristic-config") + .help("write default MIP heuristic hyper-parameters to the given file and exit") + .default_value(std::string("")); + std::map arg_name_to_param_name; // Register --pdlp-precision with string-to-int mapping so that it flows @@ -392,5 +416,8 @@ int main(int argc, char* argv[]) RAFT_CUDA_TRY(cudaSetDevice(0)); } - return run_single_file(file_name, initial_solution_file, solve_relaxation, settings_strings); + const auto heuristic_config = program.get("--mip-heuristic-config"); + + return run_single_file( + file_name, initial_solution_file, solve_relaxation, settings_strings, heuristic_config); } diff --git a/cpp/include/cuopt/linear_programming/mip/heuristic_hyper_params.hpp b/cpp/include/cuopt/linear_programming/mip/heuristic_hyper_params.hpp new file mode 100644 index 0000000000..dac23604e9 --- /dev/null +++ b/cpp/include/cuopt/linear_programming/mip/heuristic_hyper_params.hpp @@ -0,0 +1,42 @@ +/* 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 + +namespace cuopt::linear_programming { + +/** + * @brief Tuning knobs for MIP GPU heuristics. + * + * All fields carry their actual defaults. A config file only needs to list + * the knobs being changed; omitted keys keep the values shown here. + * Loadable via fill_mip_heuristic_hyper_params(); dumpable via + * dump_mip_heuristic_hyper_params(). + */ +// TODO: unify field declarations, loader tables, and dump logic via X macros +// so that adding a parameter is a single-line change. +struct mip_heuristic_hyper_params_t { + int population_size = 32; // max solutions in pool + int num_cpufj_threads = 8; // parallel CPU FJ climbers + double presolve_time_ratio = 0.1; // fraction of total time for presolve + double presolve_max_time = 60.0; // hard cap on presolve seconds + double root_lp_time_ratio = 0.1; // fraction of total time for root LP + double root_lp_max_time = 15.0; // hard cap on root LP seconds + double rins_time_limit = 3.0; // per-call RINS sub-MIP time + double rins_max_time_limit = 20.0; // ceiling for RINS adaptive time budget + double rins_fix_rate = 0.5; // RINS variable fix rate + int stagnation_trigger = 3; // FP loops w/o improvement before recombination + int max_iterations_without_improvement = 8; // diversity step depth after stagnation + double initial_infeasibility_weight = 1000.0; // constraint violation penalty seed + int n_of_minimums_for_exit = 7000; // FJ baseline local-minima exit threshold + int enabled_recombiners = 15; // bitmask: 1=BP 2=FP 4=LS 8=SubMIP + int cycle_detection_length = 30; // FP assignment cycle ring buffer + double relaxed_lp_time_limit = 1.0; // base relaxed LP time cap in heuristics + double related_vars_time_limit = 30.0; // time for related-variable structure build +}; + +} // namespace cuopt::linear_programming diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index 2c92d26231..29993ef6f0 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -134,6 +135,8 @@ class mip_solver_settings_t { // TODO check with Akif and Alice pdlp_hyper_params::pdlp_hyper_params_t hyper_params; + mip_heuristic_hyper_params_t heuristic_params; + private: std::vector mip_callbacks_; diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 0ded8337d8..5034f990a3 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -37,12 +37,33 @@ size_t sub_mip_recombiner_config_t::max_n_of_vars_from_other = template std::vector recombiner_t::enabled_recombiners; +namespace { +diversity_config_t make_diversity_config(const mip_heuristic_hyper_params_t& hp) +{ + diversity_config_t c; + c.max_solutions = hp.population_size; + c.time_ratio_on_init_lp = hp.root_lp_time_ratio; + c.max_time_on_lp = hp.root_lp_max_time; + c.initial_infeasibility_weight = hp.initial_infeasibility_weight; + return c; +} + +rins_settings_t make_rins_settings(const mip_heuristic_hyper_params_t& hp) +{ + rins_settings_t s; + s.default_fixrate = hp.rins_fix_rate; + s.default_time_limit = hp.rins_time_limit; + s.max_time_limit = hp.rins_max_time_limit; + return s; +} +} // namespace + template diversity_manager_t::diversity_manager_t(mip_solver_context_t& context_) : context(context_), branch_and_bound_ptr(nullptr), problem_ptr(context.problem_ptr), - diversity_config(), + diversity_config(make_diversity_config(context_.settings.heuristic_params)), population("population", context, *this, @@ -54,7 +75,7 @@ diversity_manager_t::diversity_manager_t(mip_solver_context_tn_constraints, context.problem_ptr->handle_ptr->get_stream()), ls(context, lp_optimal_solution), - rins(context, *this), + rins(context, *this, make_rins_settings(context_.settings.heuristic_params)), timer(diversity_config.default_time_limit), bound_prop_recombiner(context, context.problem_ptr->n_variables, @@ -206,7 +227,8 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ compute_probing_cache(ls.constraint_prop.bounds_update, *problem_ptr, probing_timer); if (problem_is_infeasible) { return false; } } - const bool remap_cache_ids = true; + const bool remap_cache_ids = true; + problem_ptr->related_vars_time_limit = context.settings.heuristic_params.related_vars_time_limit; if (!global_timer.check_time_limit()) { trivial_presolve(*problem_ptr, remap_cache_ids); } if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } // if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && @@ -394,7 +416,8 @@ solution_t diversity_manager_t::run_solver() problem_ptr->check_problem_representation(true); // have the structure ready for reusing later problem_ptr->compute_integer_fixed_problem(); - recombiner_t::init_enabled_recombiners(*problem_ptr); + recombiner_t::init_enabled_recombiners( + *problem_ptr, context.settings.heuristic_params.enabled_recombiners); mab_recombiner.resize_mab_arm_stats(recombiner_t::enabled_recombiners.size()); // test problem is not ii cuopt_func_call( diff --git a/cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh b/cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh index 89a5e86c17..4782e9612b 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh @@ -195,10 +195,14 @@ class recombiner_t { "vars_to_fix should be sorted!"); } - static void init_enabled_recombiners(const problem_t& problem) + static void init_enabled_recombiners(const problem_t& problem, + int user_enabled_mask = -1) { std::unordered_set enabled_recombiners; for (auto recombiner : recombiner_types) { + if (user_enabled_mask >= 0 && !(user_enabled_mask & (1 << (uint32_t)recombiner))) { + continue; + } enabled_recombiners.insert(recombiner); } if (problem.expensive_to_fix_vars) { diff --git a/cpp/src/mip_heuristics/heuristic_hyper_params_loader.hpp b/cpp/src/mip_heuristics/heuristic_hyper_params_loader.hpp new file mode 100644 index 0000000000..93fee219ca --- /dev/null +++ b/cpp/src/mip_heuristics/heuristic_hyper_params_loader.hpp @@ -0,0 +1,142 @@ +/* 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 +#include + +namespace cuopt::linear_programming { + +namespace { + +template +void parse_hyper_param_value(std::istringstream& iss, T& value) +{ + iss >> value; +} + +struct double_entry_t { + const char* name; + double mip_heuristic_hyper_params_t::* field; +}; + +struct int_entry_t { + const char* name; + int mip_heuristic_hyper_params_t::* field; +}; + +// clang-format off +constexpr double_entry_t double_entries[] = { + {"presolve_time_ratio", &mip_heuristic_hyper_params_t::presolve_time_ratio}, + {"presolve_max_time", &mip_heuristic_hyper_params_t::presolve_max_time}, + {"root_lp_time_ratio", &mip_heuristic_hyper_params_t::root_lp_time_ratio}, + {"root_lp_max_time", &mip_heuristic_hyper_params_t::root_lp_max_time}, + {"rins_time_limit", &mip_heuristic_hyper_params_t::rins_time_limit}, + {"rins_max_time_limit", &mip_heuristic_hyper_params_t::rins_max_time_limit}, + {"rins_fix_rate", &mip_heuristic_hyper_params_t::rins_fix_rate}, + {"initial_infeasibility_weight", &mip_heuristic_hyper_params_t::initial_infeasibility_weight}, + {"relaxed_lp_time_limit", &mip_heuristic_hyper_params_t::relaxed_lp_time_limit}, + {"related_vars_time_limit", &mip_heuristic_hyper_params_t::related_vars_time_limit}, +}; + +constexpr int_entry_t int_entries[] = { + {"population_size", &mip_heuristic_hyper_params_t::population_size}, + {"num_cpufj_threads", &mip_heuristic_hyper_params_t::num_cpufj_threads}, + {"stagnation_trigger", &mip_heuristic_hyper_params_t::stagnation_trigger}, + {"max_iterations_without_improvement", &mip_heuristic_hyper_params_t::max_iterations_without_improvement}, + {"n_of_minimums_for_exit", &mip_heuristic_hyper_params_t::n_of_minimums_for_exit}, + {"enabled_recombiners", &mip_heuristic_hyper_params_t::enabled_recombiners}, + {"cycle_detection_length", &mip_heuristic_hyper_params_t::cycle_detection_length}, +}; +// clang-format on + +} // namespace + +/** + * @brief Load MIP heuristic hyper-parameters from a key=value text file. + * + * Format: one assignment per line, e.g. population_size = 64 + * Unknown keys cause a hard error. Partial files are fine — unmentioned + * fields keep their struct defaults. + */ +inline void fill_mip_heuristic_hyper_params(const std::string& path, + mip_heuristic_hyper_params_t& params) +{ + if (!std::filesystem::exists(path)) { + std::cerr << "MIP heuristic config file path is not valid: " << path << std::endl; + exit(-1); + } + std::ifstream file(path); + std::string line; + + while (std::getline(file, line)) { + if (line.empty() || line[0] == '#') continue; + + std::istringstream iss(line); + std::string var; + + if (!(iss >> var >> std::ws && iss.get() == '=')) { + std::cerr << "MIP heuristic config: bad line: " << line << std::endl; + exit(-1); + } + + bool found = false; + for (const auto& e : double_entries) { + if (var == e.name) { + parse_hyper_param_value(iss, params.*e.field); + found = true; + break; + } + } + if (!found) { + for (const auto& e : int_entries) { + if (var == e.name) { + parse_hyper_param_value(iss, params.*e.field); + found = true; + break; + } + } + } + if (!found) { + std::cerr << "MIP heuristic config: unknown parameter: " << var << std::endl; + exit(-1); + } + } +} + +/** + * @brief Dump current hyper-parameters to a key=value text file. + * + * The output is a valid config file that can be loaded back with + * fill_mip_heuristic_hyper_params(). + */ +inline void dump_mip_heuristic_hyper_params(const std::string& path, + const mip_heuristic_hyper_params_t& params) +{ + std::ofstream file(path); + if (!file.is_open()) { + std::cerr << "Cannot open file for writing: " << path << std::endl; + return; + } + file << "# MIP heuristic hyper-parameters (auto-generated)\n\n"; + for (const auto& e : int_entries) { + file << e.name << " = " << params.*e.field << "\n"; + } + for (const auto& e : double_entries) { + file << e.name << " = " << params.*e.field << "\n"; + } +} + +} // namespace cuopt::linear_programming diff --git a/cpp/src/mip_heuristics/local_search/feasibility_pump/feasibility_pump.cu b/cpp/src/mip_heuristics/local_search/feasibility_pump/feasibility_pump.cu index f28faec249..0a17e3ebfd 100644 --- a/cpp/src/mip_heuristics/local_search/feasibility_pump/feasibility_pump.cu +++ b/cpp/src/mip_heuristics/local_search/feasibility_pump/feasibility_pump.cu @@ -43,7 +43,7 @@ feasibility_pump_t::feasibility_pump_t( fj(fj_), // fj_tree(fj_tree_), line_segment_search(line_segment_search_), - cycle_queue(*context.problem_ptr), + cycle_queue(*context.problem_ptr, context.settings.heuristic_params.cycle_detection_length), constraint_prop(constraint_prop_), last_rounding(context.problem_ptr->n_variables, context.problem_ptr->handle_ptr->get_stream()), last_projection(context.problem_ptr->n_variables, @@ -208,7 +208,8 @@ bool feasibility_pump_t::linear_project_onto_polytope(solution_t struct cycle_queue_t { - cycle_queue_t(problem_t& problem) : curr_recent_sol(cycle_detection_length - 1) + cycle_queue_t(problem_t& problem, i_t cycle_len = 30) + : cycle_detection_length(cycle_len), curr_recent_sol(cycle_detection_length - 1) { for (i_t i = 0; i < cycle_detection_length; ++i) { recent_solutions.emplace_back( @@ -86,7 +87,7 @@ struct cycle_queue_t { } std::vector> recent_solutions; - const i_t cycle_detection_length = 30; + const i_t cycle_detection_length; i_t curr_recent_sol; i_t n_iterations_without_cycle = 0; }; diff --git a/cpp/src/mip_heuristics/local_search/local_search.cu b/cpp/src/mip_heuristics/local_search/local_search.cu index 118b7181ab..da29511d70 100644 --- a/cpp/src/mip_heuristics/local_search/local_search.cu +++ b/cpp/src/mip_heuristics/local_search/local_search.cu @@ -46,13 +46,16 @@ local_search_t::local_search_t(mip_solver_context_t& context rng(cuopt::seed_generator::get_seed()), problem_with_objective_cut(*context.problem_ptr, context.problem_ptr->handle_ptr) { - for (auto& cpu_fj : ls_cpu_fj) { - cpu_fj.fj_ptr = &fj; - } - for (auto& cpu_fj : scratch_cpu_fj) { - cpu_fj.fj_ptr = &fj; + const int n_cpufj = context.settings.heuristic_params.num_cpufj_threads; + for (int i = 0; i < n_cpufj; ++i) { + ls_cpu_fj.push_back(std::make_unique>()); + ls_cpu_fj.back()->fj_ptr = &fj; } + scratch_cpu_fj.push_back(std::make_unique>()); + scratch_cpu_fj.back()->fj_ptr = &fj; scratch_cpu_fj_on_lp_opt.fj_ptr = &fj; + + fj.settings.n_of_minimums_for_exit = context.settings.heuristic_params.n_of_minimums_for_exit; } static double local_search_best_obj = std::numeric_limits::max(); @@ -72,7 +75,8 @@ void local_search_t::start_cpufj_scratch_threads(population_t 0) solution.assign_random_within_bounds(0.4); cpu_fj.fj_cpu = cpu_fj.fj_ptr->create_cpu_climber(solution, default_weights, @@ -100,8 +104,8 @@ void local_search_t::start_cpufj_scratch_threads(population_tstart_cpu_solver(); } } @@ -141,8 +145,8 @@ void local_search_t::start_cpufj_lptopt_scratch_threads( template void local_search_t::stop_cpufj_scratch_threads() { - for (auto& cpu_fj : scratch_cpu_fj) { - cpu_fj.request_termination(); + for (auto& cpu_fj_ptr : scratch_cpu_fj) { + cpu_fj_ptr->request_termination(); } scratch_cpu_fj_on_lp_opt.request_termination(); } @@ -229,7 +233,8 @@ bool local_search_t::do_fj_solve(solution_t& solution, } auto h_weights = cuopt::host_copy(in_fj.cstr_weights, solution.handle_ptr->get_stream()); auto h_objective_weight = in_fj.objective_weight.value(solution.handle_ptr->get_stream()); - for (auto& cpu_fj : ls_cpu_fj) { + for (auto& cpu_fj_ptr : ls_cpu_fj) { + auto& cpu_fj = *cpu_fj_ptr; cpu_fj.fj_cpu = cpu_fj.fj_ptr->create_cpu_climber(solution, h_weights, h_weights, @@ -242,8 +247,8 @@ bool local_search_t::do_fj_solve(solution_t& solution, auto solution_copy = solution; // Start CPU solver in background thread - for (auto& cpu_fj : ls_cpu_fj) { - cpu_fj.start_cpu_solver(); + for (auto& cpu_fj_ptr : ls_cpu_fj) { + cpu_fj_ptr->start_cpu_solver(); } // Run GPU solver and measure execution time @@ -252,8 +257,8 @@ bool local_search_t::do_fj_solve(solution_t& solution, in_fj.solve(solution); // Stop CPU solver - for (auto& cpu_fj : ls_cpu_fj) { - cpu_fj.stop_cpu_solver(); + for (auto& cpu_fj_ptr : ls_cpu_fj) { + cpu_fj_ptr->stop_cpu_solver(); } auto gpu_fj_end = std::chrono::high_resolution_clock::now(); @@ -263,13 +268,13 @@ bool local_search_t::do_fj_solve(solution_t& solution, f_t best_cpu_obj = std::numeric_limits::max(); // // Wait for CPU solver to finish - for (auto& cpu_fj : ls_cpu_fj) { - bool cpu_sol_found = cpu_fj.wait_for_cpu_solver(); + for (auto& cpu_fj_ptr : ls_cpu_fj) { + bool cpu_sol_found = cpu_fj_ptr->wait_for_cpu_solver(); if (cpu_sol_found) { - f_t cpu_obj = cpu_fj.fj_cpu->h_best_objective; + f_t cpu_obj = cpu_fj_ptr->fj_cpu->h_best_objective; if (cpu_obj < best_cpu_obj) { best_cpu_obj = cpu_obj; - solution_cpu.copy_new_assignment(cpu_fj.fj_cpu->h_best_assignment); + solution_cpu.copy_new_assignment(cpu_fj_ptr->fj_cpu->h_best_assignment); solution_cpu.compute_feasibility(); } } @@ -686,8 +691,9 @@ void local_search_t::reset_alpha_and_run_recombiners( f_t& best_objective) { raft::common::nvtx::range fun_scope("reset_alpha_and_run_recombiners"); - constexpr i_t iterations_for_stagnation = 3; - constexpr i_t max_iterations_without_improvement = 8; + const auto& hp = context.settings.heuristic_params; + const i_t iterations_for_stagnation = hp.stagnation_trigger; + const i_t max_iterations_without_improvement = hp.max_iterations_without_improvement; population_ptr->add_external_solutions_to_population(); if (population_ptr->current_size() > 1 && i - last_improved_iteration > iterations_for_stagnation) { diff --git a/cpp/src/mip_heuristics/local_search/local_search.cuh b/cpp/src/mip_heuristics/local_search/local_search.cuh index a36688d71d..94493ebcb3 100644 --- a/cpp/src/mip_heuristics/local_search/local_search.cuh +++ b/cpp/src/mip_heuristics/local_search/local_search.cuh @@ -126,8 +126,8 @@ class local_search_t { feasibility_pump_t fp; std::mt19937 rng; - std::array, 8> ls_cpu_fj; - std::array, 1> scratch_cpu_fj; + std::vector>> ls_cpu_fj; + std::vector>> scratch_cpu_fj; cpu_fj_thread_t scratch_cpu_fj_on_lp_opt; cpu_fj_thread_t deterministic_cpu_fj; problem_t problem_with_objective_cut; diff --git a/cpp/src/mip_heuristics/problem/problem.cu b/cpp/src/mip_heuristics/problem/problem.cu index 90d80f5948..cf749fe850 100644 --- a/cpp/src/mip_heuristics/problem/problem.cu +++ b/cpp/src/mip_heuristics/problem/problem.cu @@ -802,8 +802,7 @@ void problem_t::recompute_auxilliary_data(bool check_representation) compute_binary_var_table(); compute_vars_with_objective_coeffs(); // TODO: speedup compute related variables - const double time_limit = 30.; - compute_related_variables(time_limit); + compute_related_variables(related_vars_time_limit); if (check_representation) cuopt_func_call(check_problem_representation(true)); } diff --git a/cpp/src/mip_heuristics/problem/problem.cuh b/cpp/src/mip_heuristics/problem/problem.cuh index 9771bab568..7e2fdebe1b 100644 --- a/cpp/src/mip_heuristics/problem/problem.cuh +++ b/cpp/src/mip_heuristics/problem/problem.cuh @@ -321,6 +321,7 @@ class problem_t { bool cutting_plane_added{false}; std::pair, std::vector> vars_with_objective_coeffs; bool expensive_to_fix_vars{false}; + double related_vars_time_limit{30.}; std::vector Q_offsets; std::vector Q_indices; std::vector Q_values; diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index f5a2172f2e..3bc06ee430 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -157,6 +157,7 @@ mip_solution_t run_mip(detail::problem_t& problem, // only call preprocess on scaled problem, so we can compute feasibility on the original problem scaled_problem.preprocess_problem(); // cuopt_func_call((check_scaled_problem(scaled_problem, saved_problem))); + scaled_problem.related_vars_time_limit = settings.heuristic_params.related_vars_time_limit; detail::trivial_presolve(scaled_problem); detail::mip_solver_t solver(scaled_problem, settings, scaling, timer); @@ -270,7 +271,9 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, detail::sort_csr(op_problem); // allocate not more than 10% of the time limit to presolve. // Note that this is not the presolve time, but the time limit for presolve. - double presolve_time_limit = std::min(0.1 * time_limit, 60.0); + const auto& hp = settings.heuristic_params; + double presolve_time_limit = + std::min(hp.presolve_time_ratio * time_limit, hp.presolve_max_time); if (settings.determinism_mode == CUOPT_MODE_DETERMINISTIC) { presolve_time_limit = std::numeric_limits::infinity(); } diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index f25c093afb..2e1dc3f73b 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -112,7 +112,8 @@ solution_t mip_solver_t::run_solver() f_t time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC ? std::numeric_limits::infinity() : timer_.remaining_time(); - double presolve_time_limit = std::min(0.1 * time_limit, 60.0); + const auto& hp = context.settings.heuristic_params; + double presolve_time_limit = std::min(hp.presolve_time_ratio * time_limit, hp.presolve_max_time); presolve_time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC ? std::numeric_limits::infinity() : presolve_time_limit; diff --git a/cpp/tests/mip/CMakeLists.txt b/cpp/tests/mip/CMakeLists.txt index 2f2139890f..c631522e97 100644 --- a/cpp/tests/mip/CMakeLists.txt +++ b/cpp/tests/mip/CMakeLists.txt @@ -49,3 +49,6 @@ ConfigureTest(MIP_TERMINATION_STATUS_TEST ConfigureTest(DETERMINISM_TEST ${CMAKE_CURRENT_SOURCE_DIR}/determinism_test.cu ) +ConfigureTest(HEURISTIC_HYPER_PARAMS_TEST + ${CMAKE_CURRENT_SOURCE_DIR}/heuristic_hyper_params_test.cu +) diff --git a/cpp/tests/mip/heuristic_hyper_params_test.cu b/cpp/tests/mip/heuristic_hyper_params_test.cu new file mode 100644 index 0000000000..de6362afe7 --- /dev/null +++ b/cpp/tests/mip/heuristic_hyper_params_test.cu @@ -0,0 +1,160 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include +#include + +#include + +#include +#include +#include + +namespace cuopt::linear_programming::test { + +class HeuristicHyperParamsTest : public ::testing::Test { + protected: + std::string tmp_path; + + void SetUp() override + { + tmp_path = std::filesystem::temp_directory_path() / "cuopt_heuristic_params_test.config"; + } + + void TearDown() override { std::remove(tmp_path.c_str()); } +}; + +TEST_F(HeuristicHyperParamsTest, DefaultRoundTrip) +{ + mip_heuristic_hyper_params_t original; + dump_mip_heuristic_hyper_params(tmp_path, original); + + mip_heuristic_hyper_params_t loaded; + // Perturb all fields to ensure the loader actually writes them back + loaded.population_size = -999; + loaded.num_cpufj_threads = -999; + loaded.presolve_time_ratio = -999; + loaded.presolve_max_time = -999; + loaded.root_lp_time_ratio = -999; + loaded.root_lp_max_time = -999; + loaded.rins_time_limit = -999; + loaded.rins_max_time_limit = -999; + loaded.rins_fix_rate = -999; + loaded.stagnation_trigger = -999; + loaded.max_iterations_without_improvement = -999; + loaded.initial_infeasibility_weight = -999; + loaded.n_of_minimums_for_exit = -999; + loaded.enabled_recombiners = -999; + loaded.cycle_detection_length = -999; + loaded.relaxed_lp_time_limit = -999; + loaded.related_vars_time_limit = -999; + + fill_mip_heuristic_hyper_params(tmp_path, loaded); + + EXPECT_EQ(loaded.population_size, original.population_size); + EXPECT_EQ(loaded.num_cpufj_threads, original.num_cpufj_threads); + EXPECT_DOUBLE_EQ(loaded.presolve_time_ratio, original.presolve_time_ratio); + EXPECT_DOUBLE_EQ(loaded.presolve_max_time, original.presolve_max_time); + EXPECT_DOUBLE_EQ(loaded.root_lp_time_ratio, original.root_lp_time_ratio); + EXPECT_DOUBLE_EQ(loaded.root_lp_max_time, original.root_lp_max_time); + EXPECT_DOUBLE_EQ(loaded.rins_time_limit, original.rins_time_limit); + EXPECT_DOUBLE_EQ(loaded.rins_max_time_limit, original.rins_max_time_limit); + EXPECT_DOUBLE_EQ(loaded.rins_fix_rate, original.rins_fix_rate); + EXPECT_EQ(loaded.stagnation_trigger, original.stagnation_trigger); + EXPECT_EQ(loaded.max_iterations_without_improvement, original.max_iterations_without_improvement); + EXPECT_DOUBLE_EQ(loaded.initial_infeasibility_weight, original.initial_infeasibility_weight); + EXPECT_EQ(loaded.n_of_minimums_for_exit, original.n_of_minimums_for_exit); + EXPECT_EQ(loaded.enabled_recombiners, original.enabled_recombiners); + EXPECT_EQ(loaded.cycle_detection_length, original.cycle_detection_length); + EXPECT_DOUBLE_EQ(loaded.relaxed_lp_time_limit, original.relaxed_lp_time_limit); + EXPECT_DOUBLE_EQ(loaded.related_vars_time_limit, original.related_vars_time_limit); +} + +TEST_F(HeuristicHyperParamsTest, CustomValuesRoundTrip) +{ + mip_heuristic_hyper_params_t original; + original.population_size = 64; + original.num_cpufj_threads = 4; + original.presolve_time_ratio = 0.2; + original.presolve_max_time = 120.0; + original.root_lp_time_ratio = 0.05; + original.root_lp_max_time = 30.0; + original.rins_time_limit = 5.0; + original.rins_max_time_limit = 40.0; + original.rins_fix_rate = 0.7; + original.stagnation_trigger = 5; + original.max_iterations_without_improvement = 12; + original.initial_infeasibility_weight = 500.0; + original.n_of_minimums_for_exit = 10000; + original.enabled_recombiners = 5; + original.cycle_detection_length = 50; + original.relaxed_lp_time_limit = 2.0; + original.related_vars_time_limit = 60.0; + + dump_mip_heuristic_hyper_params(tmp_path, original); + + mip_heuristic_hyper_params_t loaded; + fill_mip_heuristic_hyper_params(tmp_path, loaded); + + EXPECT_EQ(loaded.population_size, 64); + EXPECT_EQ(loaded.num_cpufj_threads, 4); + EXPECT_DOUBLE_EQ(loaded.presolve_time_ratio, 0.2); + EXPECT_DOUBLE_EQ(loaded.presolve_max_time, 120.0); + EXPECT_DOUBLE_EQ(loaded.root_lp_time_ratio, 0.05); + EXPECT_DOUBLE_EQ(loaded.root_lp_max_time, 30.0); + EXPECT_DOUBLE_EQ(loaded.rins_time_limit, 5.0); + EXPECT_DOUBLE_EQ(loaded.rins_max_time_limit, 40.0); + EXPECT_DOUBLE_EQ(loaded.rins_fix_rate, 0.7); + EXPECT_EQ(loaded.stagnation_trigger, 5); + EXPECT_EQ(loaded.max_iterations_without_improvement, 12); + EXPECT_DOUBLE_EQ(loaded.initial_infeasibility_weight, 500.0); + EXPECT_EQ(loaded.n_of_minimums_for_exit, 10000); + EXPECT_EQ(loaded.enabled_recombiners, 5); + EXPECT_EQ(loaded.cycle_detection_length, 50); + EXPECT_DOUBLE_EQ(loaded.relaxed_lp_time_limit, 2.0); + EXPECT_DOUBLE_EQ(loaded.related_vars_time_limit, 60.0); +} + +TEST_F(HeuristicHyperParamsTest, PartialConfigKeepsDefaults) +{ + // Write a config with only two keys + { + std::ofstream f(tmp_path); + f << "population_size = 128\n"; + f << "rins_fix_rate = 0.3\n"; + } + + mip_heuristic_hyper_params_t loaded; + fill_mip_heuristic_hyper_params(tmp_path, loaded); + + EXPECT_EQ(loaded.population_size, 128); + EXPECT_DOUBLE_EQ(loaded.rins_fix_rate, 0.3); + // Everything else should be unchanged from struct defaults + mip_heuristic_hyper_params_t defaults; + EXPECT_EQ(loaded.num_cpufj_threads, defaults.num_cpufj_threads); + EXPECT_DOUBLE_EQ(loaded.presolve_time_ratio, defaults.presolve_time_ratio); + EXPECT_EQ(loaded.n_of_minimums_for_exit, defaults.n_of_minimums_for_exit); + EXPECT_EQ(loaded.enabled_recombiners, defaults.enabled_recombiners); +} + +TEST_F(HeuristicHyperParamsTest, CommentsAndBlankLinesIgnored) +{ + { + std::ofstream f(tmp_path); + f << "# This is a comment\n"; + f << "\n"; + f << "# Another comment\n"; + f << "population_size = 42\n"; + f << "\n"; + } + + mip_heuristic_hyper_params_t loaded; + fill_mip_heuristic_hyper_params(tmp_path, loaded); + EXPECT_EQ(loaded.population_size, 42); +} + +} // namespace cuopt::linear_programming::test From 12122a33678a7329f0f0d1927cd18a144f080c1f Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 24 Mar 2026 07:47:26 -0700 Subject: [PATCH 02/14] output default values, range, and description of heuristic params to config file --- .../linear_programming/cuopt/run_mip.cpp | 6 +- cpp/cuopt_cli.cpp | 12 +- ...params.hpp => heuristics_hyper_params.hpp} | 6 +- .../mip/solver_settings.hpp | 4 +- .../diversity/diversity_manager.cu | 4 +- .../heuristic_hyper_params_loader.hpp | 142 ------------ .../heuristics_hyper_params_loader.hpp | 187 +++++++++++++++ cpp/src/mip_heuristics/solver.cu | 4 +- cpp/tests/mip/CMakeLists.txt | 4 +- cpp/tests/mip/heuristic_hyper_params_test.cu | 160 ------------- cpp/tests/mip/heuristics_hyper_params_test.cu | 212 ++++++++++++++++++ 11 files changed, 420 insertions(+), 321 deletions(-) rename cpp/include/cuopt/linear_programming/mip/{heuristic_hyper_params.hpp => heuristics_hyper_params.hpp} (93%) delete mode 100644 cpp/src/mip_heuristics/heuristic_hyper_params_loader.hpp create mode 100644 cpp/src/mip_heuristics/heuristics_hyper_params_loader.hpp delete mode 100644 cpp/tests/mip/heuristic_hyper_params_test.cu create mode 100644 cpp/tests/mip/heuristics_hyper_params_test.cu diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index 6575880e5d..dcd10024aa 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -40,7 +40,7 @@ #include "initial_problem_check.hpp" -#include +#include void merge_result_files(const std::string& out_dir, const std::string& final_result_file, @@ -219,8 +219,8 @@ int run_single_file(std::string file_path, settings.clique_cuts = -1; settings.seed = 42; if (!heuristic_config_file.empty()) { - cuopt::linear_programming::fill_mip_heuristic_hyper_params(heuristic_config_file, - settings.heuristic_params); + cuopt::linear_programming::fill_mip_heuristics_hyper_params(heuristic_config_file, + settings.heuristic_params); } cuopt::linear_programming::benchmark_info_t benchmark_info; settings.benchmark_info_ptr = &benchmark_info; diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 59931dd3b4..cb91693206 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -27,7 +27,7 @@ #include #include -#include +#include #include @@ -180,8 +180,8 @@ int run_single_file(const std::string& file_path, if (is_mip) { auto& mip_settings = settings.get_mip_settings(); if (!heuristic_config_file.empty()) { - cuopt::linear_programming::fill_mip_heuristic_hyper_params(heuristic_config_file, - mip_settings.heuristic_params); + cuopt::linear_programming::fill_mip_heuristics_hyper_params(heuristic_config_file, + mip_settings.heuristic_params); } auto solution = cuopt::linear_programming::solve_mip(problem_interface.get(), mip_settings); } else { @@ -268,9 +268,9 @@ int main(int argc, char* argv[]) for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; if (arg == "--dump-mip-heuristic-config" && i + 1 < argc) { - cuopt::linear_programming::mip_heuristic_hyper_params_t defaults; - cuopt::linear_programming::dump_mip_heuristic_hyper_params(argv[i + 1], defaults); - return 0; + cuopt::linear_programming::mip_heuristics_hyper_params_t defaults; + bool ok = cuopt::linear_programming::dump_mip_heuristics_hyper_params(argv[i + 1], defaults); + return ok ? 0 : 1; } } diff --git a/cpp/include/cuopt/linear_programming/mip/heuristic_hyper_params.hpp b/cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp similarity index 93% rename from cpp/include/cuopt/linear_programming/mip/heuristic_hyper_params.hpp rename to cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp index dac23604e9..a0521d6f67 100644 --- a/cpp/include/cuopt/linear_programming/mip/heuristic_hyper_params.hpp +++ b/cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp @@ -14,12 +14,12 @@ namespace cuopt::linear_programming { * * All fields carry their actual defaults. A config file only needs to list * the knobs being changed; omitted keys keep the values shown here. - * Loadable via fill_mip_heuristic_hyper_params(); dumpable via - * dump_mip_heuristic_hyper_params(). + * Loadable via fill_mip_heuristics_hyper_params(); dumpable via + * dump_mip_heuristics_hyper_params(). */ // TODO: unify field declarations, loader tables, and dump logic via X macros // so that adding a parameter is a single-line change. -struct mip_heuristic_hyper_params_t { +struct mip_heuristics_hyper_params_t { int population_size = 32; // max solutions in pool int num_cpufj_threads = 8; // parallel CPU FJ climbers double presolve_time_ratio = 0.1; // fraction of total time for presolve diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index 29993ef6f0..2dc67a52fc 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -10,7 +10,7 @@ #include #include -#include +#include #include #include @@ -135,7 +135,7 @@ class mip_solver_settings_t { // TODO check with Akif and Alice pdlp_hyper_params::pdlp_hyper_params_t hyper_params; - mip_heuristic_hyper_params_t heuristic_params; + mip_heuristics_hyper_params_t heuristic_params; private: std::vector mip_callbacks_; diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 5034f990a3..f15dc70cd1 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -38,7 +38,7 @@ template std::vector recombiner_t::enabled_recombiners; namespace { -diversity_config_t make_diversity_config(const mip_heuristic_hyper_params_t& hp) +diversity_config_t make_diversity_config(const mip_heuristics_hyper_params_t& hp) { diversity_config_t c; c.max_solutions = hp.population_size; @@ -48,7 +48,7 @@ diversity_config_t make_diversity_config(const mip_heuristic_hyper_params_t& hp) return c; } -rins_settings_t make_rins_settings(const mip_heuristic_hyper_params_t& hp) +rins_settings_t make_rins_settings(const mip_heuristics_hyper_params_t& hp) { rins_settings_t s; s.default_fixrate = hp.rins_fix_rate; diff --git a/cpp/src/mip_heuristics/heuristic_hyper_params_loader.hpp b/cpp/src/mip_heuristics/heuristic_hyper_params_loader.hpp deleted file mode 100644 index 93fee219ca..0000000000 --- a/cpp/src/mip_heuristics/heuristic_hyper_params_loader.hpp +++ /dev/null @@ -1,142 +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 -#include -#include - -namespace cuopt::linear_programming { - -namespace { - -template -void parse_hyper_param_value(std::istringstream& iss, T& value) -{ - iss >> value; -} - -struct double_entry_t { - const char* name; - double mip_heuristic_hyper_params_t::* field; -}; - -struct int_entry_t { - const char* name; - int mip_heuristic_hyper_params_t::* field; -}; - -// clang-format off -constexpr double_entry_t double_entries[] = { - {"presolve_time_ratio", &mip_heuristic_hyper_params_t::presolve_time_ratio}, - {"presolve_max_time", &mip_heuristic_hyper_params_t::presolve_max_time}, - {"root_lp_time_ratio", &mip_heuristic_hyper_params_t::root_lp_time_ratio}, - {"root_lp_max_time", &mip_heuristic_hyper_params_t::root_lp_max_time}, - {"rins_time_limit", &mip_heuristic_hyper_params_t::rins_time_limit}, - {"rins_max_time_limit", &mip_heuristic_hyper_params_t::rins_max_time_limit}, - {"rins_fix_rate", &mip_heuristic_hyper_params_t::rins_fix_rate}, - {"initial_infeasibility_weight", &mip_heuristic_hyper_params_t::initial_infeasibility_weight}, - {"relaxed_lp_time_limit", &mip_heuristic_hyper_params_t::relaxed_lp_time_limit}, - {"related_vars_time_limit", &mip_heuristic_hyper_params_t::related_vars_time_limit}, -}; - -constexpr int_entry_t int_entries[] = { - {"population_size", &mip_heuristic_hyper_params_t::population_size}, - {"num_cpufj_threads", &mip_heuristic_hyper_params_t::num_cpufj_threads}, - {"stagnation_trigger", &mip_heuristic_hyper_params_t::stagnation_trigger}, - {"max_iterations_without_improvement", &mip_heuristic_hyper_params_t::max_iterations_without_improvement}, - {"n_of_minimums_for_exit", &mip_heuristic_hyper_params_t::n_of_minimums_for_exit}, - {"enabled_recombiners", &mip_heuristic_hyper_params_t::enabled_recombiners}, - {"cycle_detection_length", &mip_heuristic_hyper_params_t::cycle_detection_length}, -}; -// clang-format on - -} // namespace - -/** - * @brief Load MIP heuristic hyper-parameters from a key=value text file. - * - * Format: one assignment per line, e.g. population_size = 64 - * Unknown keys cause a hard error. Partial files are fine — unmentioned - * fields keep their struct defaults. - */ -inline void fill_mip_heuristic_hyper_params(const std::string& path, - mip_heuristic_hyper_params_t& params) -{ - if (!std::filesystem::exists(path)) { - std::cerr << "MIP heuristic config file path is not valid: " << path << std::endl; - exit(-1); - } - std::ifstream file(path); - std::string line; - - while (std::getline(file, line)) { - if (line.empty() || line[0] == '#') continue; - - std::istringstream iss(line); - std::string var; - - if (!(iss >> var >> std::ws && iss.get() == '=')) { - std::cerr << "MIP heuristic config: bad line: " << line << std::endl; - exit(-1); - } - - bool found = false; - for (const auto& e : double_entries) { - if (var == e.name) { - parse_hyper_param_value(iss, params.*e.field); - found = true; - break; - } - } - if (!found) { - for (const auto& e : int_entries) { - if (var == e.name) { - parse_hyper_param_value(iss, params.*e.field); - found = true; - break; - } - } - } - if (!found) { - std::cerr << "MIP heuristic config: unknown parameter: " << var << std::endl; - exit(-1); - } - } -} - -/** - * @brief Dump current hyper-parameters to a key=value text file. - * - * The output is a valid config file that can be loaded back with - * fill_mip_heuristic_hyper_params(). - */ -inline void dump_mip_heuristic_hyper_params(const std::string& path, - const mip_heuristic_hyper_params_t& params) -{ - std::ofstream file(path); - if (!file.is_open()) { - std::cerr << "Cannot open file for writing: " << path << std::endl; - return; - } - file << "# MIP heuristic hyper-parameters (auto-generated)\n\n"; - for (const auto& e : int_entries) { - file << e.name << " = " << params.*e.field << "\n"; - } - for (const auto& e : double_entries) { - file << e.name << " = " << params.*e.field << "\n"; - } -} - -} // namespace cuopt::linear_programming diff --git a/cpp/src/mip_heuristics/heuristics_hyper_params_loader.hpp b/cpp/src/mip_heuristics/heuristics_hyper_params_loader.hpp new file mode 100644 index 0000000000..a4ad97bcbb --- /dev/null +++ b/cpp/src/mip_heuristics/heuristics_hyper_params_loader.hpp @@ -0,0 +1,187 @@ +/* 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 +#include +#include + +namespace cuopt::linear_programming { + +namespace { + +using hp_t = mip_heuristics_hyper_params_t; +using double_member_ptr = double hp_t::*; +using int_member_ptr = int hp_t::*; + +struct double_param_t { + const char* name; + double_member_ptr field; + double min_val; + double max_val; + const char* description; +}; + +struct int_param_t { + const char* name; + int_member_ptr field; + int min_val; + int max_val; + const char* description; +}; + +constexpr double inf = std::numeric_limits::infinity(); + +// clang-format off +constexpr double_param_t double_params[] = { + {"presolve_time_ratio", &hp_t::presolve_time_ratio, 0.0, 1.0, "fraction of total time for presolve"}, + {"presolve_max_time", &hp_t::presolve_max_time, 0.0, inf, "hard cap on presolve seconds"}, + {"root_lp_time_ratio", &hp_t::root_lp_time_ratio, 0.0, 1.0, "fraction of total time for root LP"}, + {"root_lp_max_time", &hp_t::root_lp_max_time, 0.0, inf, "hard cap on root LP seconds"}, + {"rins_time_limit", &hp_t::rins_time_limit, 0.0, inf, "per-call RINS sub-MIP time"}, + {"rins_max_time_limit", &hp_t::rins_max_time_limit, 0.0, inf, "ceiling for RINS adaptive time budget"}, + {"rins_fix_rate", &hp_t::rins_fix_rate, 0.0, 1.0, "RINS variable fix rate"}, + {"initial_infeasibility_weight", &hp_t::initial_infeasibility_weight, 1e-9, inf, "constraint violation penalty seed"}, + {"relaxed_lp_time_limit", &hp_t::relaxed_lp_time_limit, 1e-9, inf, "base relaxed LP time cap in heuristics"}, + {"related_vars_time_limit", &hp_t::related_vars_time_limit, 1e-9, inf, "time for related-variable structure build"}, +}; + +constexpr int_param_t int_params[] = { + {"population_size", &hp_t::population_size, 1, std::numeric_limits::max(), "max solutions in pool"}, + {"num_cpufj_threads", &hp_t::num_cpufj_threads, 0, std::numeric_limits::max(), "parallel CPU FJ climbers"}, + {"stagnation_trigger", &hp_t::stagnation_trigger, 1, std::numeric_limits::max(), "FP loops w/o improvement before recombination"}, + {"max_iterations_without_improvement", &hp_t::max_iterations_without_improvement, 1, std::numeric_limits::max(), "diversity step depth after stagnation"}, + {"n_of_minimums_for_exit", &hp_t::n_of_minimums_for_exit, 1, std::numeric_limits::max(), "FJ baseline local-minima exit threshold"}, + {"enabled_recombiners", &hp_t::enabled_recombiners, 0, 15, "bitmask: 1=BP 2=FP 4=LS 8=SubMIP"}, + {"cycle_detection_length", &hp_t::cycle_detection_length, 1, std::numeric_limits::max(), "FP assignment cycle ring buffer length"}, +}; +// clang-format on + +template +bool try_parse_param(const ParamDesc* params, + size_t n_params, + const std::string& var, + std::istringstream& iss, + hp_t& hp, + const std::string& line) +{ + for (size_t i = 0; i < n_params; ++i) { + const auto& p = params[i]; + if (var != p.name) continue; + cuopt_expects(bool(iss >> hp.*p.field), + error_type_t::ValidationError, + "MIP heuristic config: bad value for %s: %s", + p.name, + line.c_str()); + std::string trailing; + cuopt_expects(!bool(iss >> trailing), + error_type_t::ValidationError, + "MIP heuristic config: trailing junk for %s: %s", + p.name, + line.c_str()); + cuopt_expects(hp.*p.field >= p.min_val && hp.*p.field <= p.max_val, + error_type_t::ValidationError, + "MIP heuristic config: %s = %s out of range [%s, %s]", + p.name, + std::to_string(hp.*p.field).c_str(), + std::to_string(p.min_val).c_str(), + std::to_string(p.max_val).c_str()); + CUOPT_LOG_INFO("MIP heuristic config: %s = %s", p.name, std::to_string(hp.*p.field).c_str()); + return true; + } + return false; +} + +} // namespace + +/** + * @brief Load MIP heuristic hyper-parameters from a key=value text file. + * + * Format: one assignment per line, e.g. population_size = 64 + * Lines starting with # (optionally indented) are comments. + * Unknown keys, malformed values, and out-of-range values all throw. + * Partial files are fine — omitted keys keep their struct defaults. + */ +inline void fill_mip_heuristics_hyper_params(const std::string& path, + mip_heuristics_hyper_params_t& params) +{ + cuopt_expects(!std::filesystem::is_directory(path) && std::filesystem::exists(path), + error_type_t::ValidationError, + "MIP heuristic config: not a valid file: %s", + path.c_str()); + std::ifstream file(path); + cuopt_expects(file.is_open(), + error_type_t::ValidationError, + "MIP heuristic config: cannot open: %s", + path.c_str()); + std::string line; + + while (std::getline(file, line)) { + // Trim leading whitespace, then skip blank lines and comments + 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 var; + + cuopt_expects(iss >> var >> std::ws && iss.get() == '=', + error_type_t::ValidationError, + "MIP heuristic config: bad line: %s", + line.c_str()); + + bool found = try_parse_param(double_params, std::size(double_params), var, iss, params, line) || + try_parse_param(int_params, std::size(int_params), var, iss, params, line); + + cuopt_expects(found, + error_type_t::ValidationError, + "MIP heuristic config: unknown parameter: %s", + var.c_str()); + } + + CUOPT_LOG_INFO("MIP heuristic config loaded from: %s", path.c_str()); +} + +/** + * @brief Dump current hyper-parameters to a key=value text file. + * + * Each entry is commented out and annotated with its type, allowed range, + * and description. The output is a valid config file that can be loaded + * back with fill_mip_heuristics_hyper_params() after uncommenting the + * desired overrides. + */ +inline bool dump_mip_heuristics_hyper_params(const std::string& path, + const mip_heuristics_hyper_params_t& params) +{ + std::ofstream file(path); + if (!file.is_open()) { + CUOPT_LOG_ERROR("Cannot open file for writing: %s", path.c_str()); + return false; + } + file << "# MIP heuristic hyper-parameters (auto-generated)\n"; + file << "# Uncomment and change only the values you want to override.\n\n"; + for (const auto& p : int_params) { + file << "# " << p.description << " (int, range: [" << p.min_val << ", " << p.max_val << "])\n"; + file << "# " << p.name << " = " << params.*p.field << "\n\n"; + } + for (const auto& p : double_params) { + file << "# " << p.description << " (double, range: [" << p.min_val << ", " << p.max_val + << "])\n"; + file << "# " << p.name << " = " << params.*p.field << "\n\n"; + } + return true; +} + +} // namespace cuopt::linear_programming diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 2e1dc3f73b..e947c69ba6 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -117,7 +117,9 @@ solution_t mip_solver_t::run_solver() presolve_time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC ? std::numeric_limits::infinity() : presolve_time_limit; - bool presolve_success = run_presolve ? dm.run_presolve(presolve_time_limit, timer_) : true; + if (std::isfinite(presolve_time_limit)) + CUOPT_LOG_DEBUG("Presolve time limit: %g", presolve_time_limit); + bool presolve_success = run_presolve ? dm.run_presolve(presolve_time_limit, timer_) : true; if (!presolve_success) { CUOPT_LOG_INFO("Problem proven infeasible in presolve"); solution_t sol(*context.problem_ptr); diff --git a/cpp/tests/mip/CMakeLists.txt b/cpp/tests/mip/CMakeLists.txt index c631522e97..f2cf53ff6c 100644 --- a/cpp/tests/mip/CMakeLists.txt +++ b/cpp/tests/mip/CMakeLists.txt @@ -49,6 +49,6 @@ ConfigureTest(MIP_TERMINATION_STATUS_TEST ConfigureTest(DETERMINISM_TEST ${CMAKE_CURRENT_SOURCE_DIR}/determinism_test.cu ) -ConfigureTest(HEURISTIC_HYPER_PARAMS_TEST - ${CMAKE_CURRENT_SOURCE_DIR}/heuristic_hyper_params_test.cu +ConfigureTest(HEURISTICS_HYPER_PARAMS_TEST + ${CMAKE_CURRENT_SOURCE_DIR}/heuristics_hyper_params_test.cu ) diff --git a/cpp/tests/mip/heuristic_hyper_params_test.cu b/cpp/tests/mip/heuristic_hyper_params_test.cu deleted file mode 100644 index de6362afe7..0000000000 --- a/cpp/tests/mip/heuristic_hyper_params_test.cu +++ /dev/null @@ -1,160 +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 */ - -#include -#include - -#include - -#include -#include -#include - -namespace cuopt::linear_programming::test { - -class HeuristicHyperParamsTest : public ::testing::Test { - protected: - std::string tmp_path; - - void SetUp() override - { - tmp_path = std::filesystem::temp_directory_path() / "cuopt_heuristic_params_test.config"; - } - - void TearDown() override { std::remove(tmp_path.c_str()); } -}; - -TEST_F(HeuristicHyperParamsTest, DefaultRoundTrip) -{ - mip_heuristic_hyper_params_t original; - dump_mip_heuristic_hyper_params(tmp_path, original); - - mip_heuristic_hyper_params_t loaded; - // Perturb all fields to ensure the loader actually writes them back - loaded.population_size = -999; - loaded.num_cpufj_threads = -999; - loaded.presolve_time_ratio = -999; - loaded.presolve_max_time = -999; - loaded.root_lp_time_ratio = -999; - loaded.root_lp_max_time = -999; - loaded.rins_time_limit = -999; - loaded.rins_max_time_limit = -999; - loaded.rins_fix_rate = -999; - loaded.stagnation_trigger = -999; - loaded.max_iterations_without_improvement = -999; - loaded.initial_infeasibility_weight = -999; - loaded.n_of_minimums_for_exit = -999; - loaded.enabled_recombiners = -999; - loaded.cycle_detection_length = -999; - loaded.relaxed_lp_time_limit = -999; - loaded.related_vars_time_limit = -999; - - fill_mip_heuristic_hyper_params(tmp_path, loaded); - - EXPECT_EQ(loaded.population_size, original.population_size); - EXPECT_EQ(loaded.num_cpufj_threads, original.num_cpufj_threads); - EXPECT_DOUBLE_EQ(loaded.presolve_time_ratio, original.presolve_time_ratio); - EXPECT_DOUBLE_EQ(loaded.presolve_max_time, original.presolve_max_time); - EXPECT_DOUBLE_EQ(loaded.root_lp_time_ratio, original.root_lp_time_ratio); - EXPECT_DOUBLE_EQ(loaded.root_lp_max_time, original.root_lp_max_time); - EXPECT_DOUBLE_EQ(loaded.rins_time_limit, original.rins_time_limit); - EXPECT_DOUBLE_EQ(loaded.rins_max_time_limit, original.rins_max_time_limit); - EXPECT_DOUBLE_EQ(loaded.rins_fix_rate, original.rins_fix_rate); - EXPECT_EQ(loaded.stagnation_trigger, original.stagnation_trigger); - EXPECT_EQ(loaded.max_iterations_without_improvement, original.max_iterations_without_improvement); - EXPECT_DOUBLE_EQ(loaded.initial_infeasibility_weight, original.initial_infeasibility_weight); - EXPECT_EQ(loaded.n_of_minimums_for_exit, original.n_of_minimums_for_exit); - EXPECT_EQ(loaded.enabled_recombiners, original.enabled_recombiners); - EXPECT_EQ(loaded.cycle_detection_length, original.cycle_detection_length); - EXPECT_DOUBLE_EQ(loaded.relaxed_lp_time_limit, original.relaxed_lp_time_limit); - EXPECT_DOUBLE_EQ(loaded.related_vars_time_limit, original.related_vars_time_limit); -} - -TEST_F(HeuristicHyperParamsTest, CustomValuesRoundTrip) -{ - mip_heuristic_hyper_params_t original; - original.population_size = 64; - original.num_cpufj_threads = 4; - original.presolve_time_ratio = 0.2; - original.presolve_max_time = 120.0; - original.root_lp_time_ratio = 0.05; - original.root_lp_max_time = 30.0; - original.rins_time_limit = 5.0; - original.rins_max_time_limit = 40.0; - original.rins_fix_rate = 0.7; - original.stagnation_trigger = 5; - original.max_iterations_without_improvement = 12; - original.initial_infeasibility_weight = 500.0; - original.n_of_minimums_for_exit = 10000; - original.enabled_recombiners = 5; - original.cycle_detection_length = 50; - original.relaxed_lp_time_limit = 2.0; - original.related_vars_time_limit = 60.0; - - dump_mip_heuristic_hyper_params(tmp_path, original); - - mip_heuristic_hyper_params_t loaded; - fill_mip_heuristic_hyper_params(tmp_path, loaded); - - EXPECT_EQ(loaded.population_size, 64); - EXPECT_EQ(loaded.num_cpufj_threads, 4); - EXPECT_DOUBLE_EQ(loaded.presolve_time_ratio, 0.2); - EXPECT_DOUBLE_EQ(loaded.presolve_max_time, 120.0); - EXPECT_DOUBLE_EQ(loaded.root_lp_time_ratio, 0.05); - EXPECT_DOUBLE_EQ(loaded.root_lp_max_time, 30.0); - EXPECT_DOUBLE_EQ(loaded.rins_time_limit, 5.0); - EXPECT_DOUBLE_EQ(loaded.rins_max_time_limit, 40.0); - EXPECT_DOUBLE_EQ(loaded.rins_fix_rate, 0.7); - EXPECT_EQ(loaded.stagnation_trigger, 5); - EXPECT_EQ(loaded.max_iterations_without_improvement, 12); - EXPECT_DOUBLE_EQ(loaded.initial_infeasibility_weight, 500.0); - EXPECT_EQ(loaded.n_of_minimums_for_exit, 10000); - EXPECT_EQ(loaded.enabled_recombiners, 5); - EXPECT_EQ(loaded.cycle_detection_length, 50); - EXPECT_DOUBLE_EQ(loaded.relaxed_lp_time_limit, 2.0); - EXPECT_DOUBLE_EQ(loaded.related_vars_time_limit, 60.0); -} - -TEST_F(HeuristicHyperParamsTest, PartialConfigKeepsDefaults) -{ - // Write a config with only two keys - { - std::ofstream f(tmp_path); - f << "population_size = 128\n"; - f << "rins_fix_rate = 0.3\n"; - } - - mip_heuristic_hyper_params_t loaded; - fill_mip_heuristic_hyper_params(tmp_path, loaded); - - EXPECT_EQ(loaded.population_size, 128); - EXPECT_DOUBLE_EQ(loaded.rins_fix_rate, 0.3); - // Everything else should be unchanged from struct defaults - mip_heuristic_hyper_params_t defaults; - EXPECT_EQ(loaded.num_cpufj_threads, defaults.num_cpufj_threads); - EXPECT_DOUBLE_EQ(loaded.presolve_time_ratio, defaults.presolve_time_ratio); - EXPECT_EQ(loaded.n_of_minimums_for_exit, defaults.n_of_minimums_for_exit); - EXPECT_EQ(loaded.enabled_recombiners, defaults.enabled_recombiners); -} - -TEST_F(HeuristicHyperParamsTest, CommentsAndBlankLinesIgnored) -{ - { - std::ofstream f(tmp_path); - f << "# This is a comment\n"; - f << "\n"; - f << "# Another comment\n"; - f << "population_size = 42\n"; - f << "\n"; - } - - mip_heuristic_hyper_params_t loaded; - fill_mip_heuristic_hyper_params(tmp_path, loaded); - EXPECT_EQ(loaded.population_size, 42); -} - -} // namespace cuopt::linear_programming::test diff --git a/cpp/tests/mip/heuristics_hyper_params_test.cu b/cpp/tests/mip/heuristics_hyper_params_test.cu new file mode 100644 index 0000000000..3fa8db43c4 --- /dev/null +++ b/cpp/tests/mip/heuristics_hyper_params_test.cu @@ -0,0 +1,212 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include +#include + +#include + +#include +#include +#include + +namespace cuopt::linear_programming::test { + +class HeuristicsHyperParamsTest : public ::testing::Test { + protected: + std::string tmp_path; + + void SetUp() override + { + tmp_path = std::filesystem::temp_directory_path() / "cuopt_heuristic_params_test.config"; + } + + void TearDown() override { std::remove(tmp_path.c_str()); } +}; + +TEST_F(HeuristicsHyperParamsTest, DumpedFileIsAllCommentedOut) +{ + mip_heuristics_hyper_params_t original; + dump_mip_heuristics_hyper_params(tmp_path, original); + + // Loading the commented-out dump should leave struct defaults unchanged + mip_heuristics_hyper_params_t loaded; + loaded.population_size = 9999; + fill_mip_heuristics_hyper_params(tmp_path, loaded); + EXPECT_EQ(loaded.population_size, 9999); +} + +TEST_F(HeuristicsHyperParamsTest, DumpedFileIsParseable) +{ + mip_heuristics_hyper_params_t original; + dump_mip_heuristics_hyper_params(tmp_path, original); + + // The dumped file should parse without errors (all lines are comments) + mip_heuristics_hyper_params_t loaded; + EXPECT_NO_THROW(fill_mip_heuristics_hyper_params(tmp_path, loaded)); +} + +TEST_F(HeuristicsHyperParamsTest, CustomValuesRoundTrip) +{ + { + std::ofstream f(tmp_path); + f << "population_size = 64\n"; + f << "num_cpufj_threads = 4\n"; + f << "presolve_time_ratio = 0.2\n"; + f << "presolve_max_time = 120\n"; + f << "root_lp_time_ratio = 0.05\n"; + f << "root_lp_max_time = 30\n"; + f << "rins_time_limit = 5\n"; + f << "rins_max_time_limit = 40\n"; + f << "rins_fix_rate = 0.7\n"; + f << "stagnation_trigger = 5\n"; + f << "max_iterations_without_improvement = 12\n"; + f << "initial_infeasibility_weight = 500\n"; + f << "n_of_minimums_for_exit = 10000\n"; + f << "enabled_recombiners = 5\n"; + f << "cycle_detection_length = 50\n"; + f << "relaxed_lp_time_limit = 2\n"; + f << "related_vars_time_limit = 60\n"; + } + + mip_heuristics_hyper_params_t loaded; + fill_mip_heuristics_hyper_params(tmp_path, loaded); + + EXPECT_EQ(loaded.population_size, 64); + codex EXPECT_EQ(loaded.num_cpufj_threads, 4); + EXPECT_DOUBLE_EQ(loaded.presolve_time_ratio, 0.2); + EXPECT_DOUBLE_EQ(loaded.presolve_max_time, 120.0); + EXPECT_DOUBLE_EQ(loaded.root_lp_time_ratio, 0.05); + EXPECT_DOUBLE_EQ(loaded.root_lp_max_time, 30.0); + EXPECT_DOUBLE_EQ(loaded.rins_time_limit, 5.0); + EXPECT_DOUBLE_EQ(loaded.rins_max_time_limit, 40.0); + EXPECT_DOUBLE_EQ(loaded.rins_fix_rate, 0.7); + EXPECT_EQ(loaded.stagnation_trigger, 5); + EXPECT_EQ(loaded.max_iterations_without_improvement, 12); + EXPECT_DOUBLE_EQ(loaded.initial_infeasibility_weight, 500.0); + EXPECT_EQ(loaded.n_of_minimums_for_exit, 10000); + EXPECT_EQ(loaded.enabled_recombiners, 5); + EXPECT_EQ(loaded.cycle_detection_length, 50); + EXPECT_DOUBLE_EQ(loaded.relaxed_lp_time_limit, 2.0); + EXPECT_DOUBLE_EQ(loaded.related_vars_time_limit, 60.0); +} + +TEST_F(HeuristicsHyperParamsTest, PartialConfigKeepsDefaults) +{ + // Write a config with only two keys + { + std::ofstream f(tmp_path); + f << "population_size = 128\n"; + f << "rins_fix_rate = 0.3\n"; + } + + mip_heuristics_hyper_params_t loaded; + fill_mip_heuristics_hyper_params(tmp_path, loaded); + + EXPECT_EQ(loaded.population_size, 128); + EXPECT_DOUBLE_EQ(loaded.rins_fix_rate, 0.3); + // Everything else should be unchanged from struct defaults + mip_heuristics_hyper_params_t defaults; + EXPECT_EQ(loaded.num_cpufj_threads, defaults.num_cpufj_threads); + EXPECT_DOUBLE_EQ(loaded.presolve_time_ratio, defaults.presolve_time_ratio); + EXPECT_EQ(loaded.n_of_minimums_for_exit, defaults.n_of_minimums_for_exit); + EXPECT_EQ(loaded.enabled_recombiners, defaults.enabled_recombiners); +} + +TEST_F(HeuristicsHyperParamsTest, CommentsAndBlankLinesIgnored) +{ + { + std::ofstream f(tmp_path); + f << "# This is a comment\n"; + f << "\n"; + f << "# Another comment\n"; + f << "population_size = 42\n"; + f << "\n"; + } + + mip_heuristics_hyper_params_t loaded; + fill_mip_heuristics_hyper_params(tmp_path, loaded); + EXPECT_EQ(loaded.population_size, 42); +} + +TEST_F(HeuristicsHyperParamsTest, UnknownKeyThrows) +{ + { + std::ofstream f(tmp_path); + f << "bogus_key = 42\n"; + } + mip_heuristics_hyper_params_t loaded; + EXPECT_THROW(fill_mip_heuristics_hyper_params(tmp_path, loaded), cuopt::logic_error); +} + +TEST_F(HeuristicsHyperParamsTest, BadNumericValueThrows) +{ + { + std::ofstream f(tmp_path); + f << "population_size = not_a_number\n"; + } + mip_heuristics_hyper_params_t loaded; + EXPECT_THROW(fill_mip_heuristics_hyper_params(tmp_path, loaded), cuopt::logic_error); +} + +TEST_F(HeuristicsHyperParamsTest, TrailingJunkThrows) +{ + { + std::ofstream f(tmp_path); + f << "population_size = 64foo\n"; + } + mip_heuristics_hyper_params_t loaded; + EXPECT_THROW(fill_mip_heuristics_hyper_params(tmp_path, loaded), cuopt::logic_error); +} + +TEST_F(HeuristicsHyperParamsTest, RangeViolationCycleDetectionThrows) +{ + { + std::ofstream f(tmp_path); + f << "cycle_detection_length = 0\n"; + } + mip_heuristics_hyper_params_t loaded; + EXPECT_THROW(fill_mip_heuristics_hyper_params(tmp_path, loaded), cuopt::logic_error); +} + +TEST_F(HeuristicsHyperParamsTest, RangeViolationFixRateThrows) +{ + { + std::ofstream f(tmp_path); + f << "rins_fix_rate = 2.0\n"; + } + mip_heuristics_hyper_params_t loaded; + EXPECT_THROW(fill_mip_heuristics_hyper_params(tmp_path, loaded), cuopt::logic_error); +} + +TEST_F(HeuristicsHyperParamsTest, NonexistentFileThrows) +{ + mip_heuristics_hyper_params_t loaded; + EXPECT_THROW(fill_mip_heuristics_hyper_params("/tmp/does_not_exist_cuopt_test.config", loaded), + cuopt::logic_error); +} + +TEST_F(HeuristicsHyperParamsTest, DirectoryPathThrows) +{ + mip_heuristics_hyper_params_t loaded; + EXPECT_THROW(fill_mip_heuristics_hyper_params("/tmp", loaded), cuopt::logic_error); +} + +TEST_F(HeuristicsHyperParamsTest, IndentedCommentAndWhitespaceLinesIgnored) +{ + { + std::ofstream f(tmp_path); + f << " # indented comment\n"; + f << " \t \n"; + f << "population_size = 99\n"; + } + mip_heuristics_hyper_params_t loaded; + fill_mip_heuristics_hyper_params(tmp_path, loaded); + EXPECT_EQ(loaded.population_size, 99); +} + +} // namespace cuopt::linear_programming::test From cc1b185d693472135b217bdc1f2ae3bcee9c6fe4 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 24 Mar 2026 07:51:40 -0700 Subject: [PATCH 03/14] typo --- cpp/tests/mip/heuristics_hyper_params_test.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/tests/mip/heuristics_hyper_params_test.cu b/cpp/tests/mip/heuristics_hyper_params_test.cu index 3fa8db43c4..2914356a5c 100644 --- a/cpp/tests/mip/heuristics_hyper_params_test.cu +++ b/cpp/tests/mip/heuristics_hyper_params_test.cu @@ -77,7 +77,7 @@ TEST_F(HeuristicsHyperParamsTest, CustomValuesRoundTrip) fill_mip_heuristics_hyper_params(tmp_path, loaded); EXPECT_EQ(loaded.population_size, 64); - codex EXPECT_EQ(loaded.num_cpufj_threads, 4); + EXPECT_EQ(loaded.num_cpufj_threads, 4); EXPECT_DOUBLE_EQ(loaded.presolve_time_ratio, 0.2); EXPECT_DOUBLE_EQ(loaded.presolve_max_time, 120.0); EXPECT_DOUBLE_EQ(loaded.root_lp_time_ratio, 0.05); From 5b2da4118875fefd4147f75cd87f0c53b5e15204 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 24 Mar 2026 10:59:25 -0700 Subject: [PATCH 04/14] unify with cli params --- .../linear_programming/cuopt/run_mip.cpp | 42 ++-- cpp/cuopt_cli.cpp | 145 +++---------- .../cuopt/linear_programming/constants.h | 19 ++ .../mip/heuristics_hyper_params.hpp | 6 +- .../linear_programming/solver_settings.hpp | 3 + .../utilities/internals.hpp | 46 +++- cpp/src/math_optimization/solver_settings.cu | 166 ++++++++++++++- .../heuristics_hyper_params_loader.hpp | 187 ----------------- cpp/tests/mip/heuristics_hyper_params_test.cu | 197 ++++++++++++------ 9 files changed, 401 insertions(+), 410 deletions(-) delete mode 100644 cpp/src/mip_heuristics/heuristics_hyper_params_loader.hpp diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index dcd10024aa..e01e533a65 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -40,8 +40,6 @@ #include "initial_problem_check.hpp" -#include - void merge_result_files(const std::string& out_dir, const std::string& final_result_file, int n_gpus, @@ -152,8 +150,7 @@ int run_single_file(std::string file_path, int reliability_branching, double time_limit, double work_limit, - bool deterministic, - const std::string& heuristic_config_file = "") + bool deterministic) { const raft::handle_t handle_{}; cuopt::linear_programming::mip_solver_settings_t settings; @@ -218,10 +215,6 @@ int run_single_file(std::string file_path, settings.reliability_branching = reliability_branching; settings.clique_cuts = -1; settings.seed = 42; - if (!heuristic_config_file.empty()) { - cuopt::linear_programming::fill_mip_heuristics_hyper_params(heuristic_config_file, - settings.heuristic_params); - } cuopt::linear_programming::benchmark_info_t benchmark_info; settings.benchmark_info_ptr = &benchmark_info; auto start_run_solver = std::chrono::high_resolution_clock::now(); @@ -277,8 +270,7 @@ void run_single_file_mp(std::string file_path, int reliability_branching, double time_limit, double work_limit, - bool deterministic, - const std::string& heuristic_config_file = "") + bool deterministic) { std::cout << "running file " << file_path << " on gpu : " << device << std::endl; auto memory_resource = make_async(); @@ -296,8 +288,7 @@ void run_single_file_mp(std::string file_path, reliability_branching, time_limit, work_limit, - deterministic, - heuristic_config_file); + deterministic); // this is a bad design to communicate the result but better than adding complexity of IPC or // pipes exit(sol_found); @@ -395,10 +386,6 @@ int main(int argc, char* argv[]) .default_value(false) .implicit_value(true); - program.add_argument("--mip-heuristic-config") - .help("path to MIP heuristic hyper-parameters config file (key = value format)") - .default_value(std::string("")); - // Parse arguments try { program.parse_args(argc, argv); @@ -422,15 +409,14 @@ int main(int argc, char* argv[]) std::string result_file; int batch_num = -1; - bool heuristics_only = program.get("--heuristics-only")[0] == 't'; - int num_cpu_threads = program.get("--num-cpu-threads"); - bool write_log_file = program.get("--write-log-file")[0] == 't'; - bool log_to_console = program.get("--log-to-console")[0] == 't'; - double memory_limit = program.get("--memory-limit"); - bool track_allocations = program.get("--track-allocations")[0] == 't'; - int reliability_branching = program.get("--reliability-branching"); - bool deterministic = program.get("--determinism"); - std::string heuristic_config_file = program.get("--mip-heuristic-config"); + bool heuristics_only = program.get("--heuristics-only")[0] == 't'; + int num_cpu_threads = program.get("--num-cpu-threads"); + bool write_log_file = program.get("--write-log-file")[0] == 't'; + bool log_to_console = program.get("--log-to-console")[0] == 't'; + double memory_limit = program.get("--memory-limit"); + bool track_allocations = program.get("--track-allocations")[0] == 't'; + int reliability_branching = program.get("--reliability-branching"); + bool deterministic = program.get("--determinism"); if (num_cpu_threads < 0) { num_cpu_threads = omp_get_max_threads() / n_gpus; @@ -530,8 +516,7 @@ int main(int argc, char* argv[]) reliability_branching, time_limit, work_limit, - deterministic, - heuristic_config_file); + deterministic); } else if (sys_pid < 0) { std::cerr << "Fork failed!" << std::endl; exit(1); @@ -574,8 +559,7 @@ int main(int argc, char* argv[]) reliability_branching, time_limit, work_limit, - deterministic, - heuristic_config_file); + deterministic); } return 0; diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index cb91693206..9839ad11e3 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -27,54 +27,13 @@ #include #include -#include #include static char cuda_module_loading_env[] = "CUDA_MODULE_LOADING=EAGER"; -/** - * @file cuopt_cli.cpp - * @brief Command line interface for solving Linear Programming (LP) and Mixed Integer Programming - * (MIP) problems using cuOpt - * - * This CLI provides a simple interface to solve LP/MIP problems using cuOpt. It accepts MPS format - * input files and various solver parameters. - * - * Usage: - * ``` - * cuopt_cli [OPTIONS] - * cuopt_cli [OPTIONS] - * ``` - * - * Required arguments: - * - : Path to the MPS format input file containing the optimization problem - * - * Optional arguments: - * - --initial-solution: Path to initial solution file in SOL format - * - Various solver parameters that can be passed as command line arguments - * (e.g. --max-iterations, --tolerance, etc.) - * - * Example: - * ``` - * cuopt_cli problem.mps --max-iterations 1000 - * ``` - * - * The solver will read the MPS file, solve the optimization problem according to the specified - * parameters, and write the solution to a .sol file in the output directory. - */ - -/** - * @brief Make an async memory resource for RMM - * @return std::shared_ptr - */ inline auto make_async() { return std::make_shared(); } -/** - * @brief Handle logger when error happens before logger is initialized - * @param settings Solver settings - * @return cuopt::init_logger_t - */ inline cuopt::init_logger_t dummy_logger( const cuopt::linear_programming::solver_settings_t& settings) { @@ -82,21 +41,16 @@ inline cuopt::init_logger_t dummy_logger( settings.template get_parameter(CUOPT_LOG_TO_CONSOLE)); } -/** - * @brief Run a single file - * @param file_path Path to the MPS format input file containing the optimization problem - * @param initial_solution_file Path to initial solution file in SOL format - * @param settings_strings Map of solver parameters - */ int run_single_file(const std::string& file_path, const std::string& initial_solution_file, bool solve_relaxation, const std::map& settings_strings, - const std::string& heuristic_config_file = "") + const std::string& params_file = "") { cuopt::linear_programming::solver_settings_t settings; try { + if (!params_file.empty()) { settings.load_parameters_from_file(params_file); } for (auto& [key, val] : settings_strings) { settings.set_parameter_from_string(key, val); } @@ -126,8 +80,6 @@ int run_single_file(const std::string& file_path, return -1; } - // Determine memory backend and create problem using interface - // Create handle only for GPU memory backend (avoid CUDA init on CPU-only hosts) auto memory_backend = cuopt::linear_programming::get_memory_backend_type(); std::unique_ptr handle_ptr; std::unique_ptr> @@ -143,7 +95,6 @@ int run_single_file(const std::string& file_path, std::make_unique>(); } - // Populate the problem from MPS data model cuopt::linear_programming::populate_from_mps_data_model(problem_interface.get(), mps_data_model); const bool is_mip = (problem_interface->get_problem_category() == @@ -179,16 +130,13 @@ int run_single_file(const std::string& file_path, try { if (is_mip) { auto& mip_settings = settings.get_mip_settings(); - if (!heuristic_config_file.empty()) { - cuopt::linear_programming::fill_mip_heuristics_hyper_params(heuristic_config_file, - mip_settings.heuristic_params); - } auto solution = cuopt::linear_programming::solve_mip(problem_interface.get(), mip_settings); } else { auto& lp_settings = settings.get_pdlp_settings(); auto solution = cuopt::linear_programming::solve_lp(problem_interface.get(), lp_settings); } } catch (const std::exception& e) { + fprintf(stderr, "cuopt_cli error: %s\n", e.what()); CUOPT_LOG_ERROR("Error: %s", e.what()); return -1; } @@ -196,35 +144,17 @@ int run_single_file(const std::string& file_path, return 0; } -/** - * @brief Convert a parameter name to an argument name - * @param input Parameter name - * @return Argument name - */ std::string param_name_to_arg_name(const std::string& input) { std::string result = "--"; result += input; - - // Replace underscores with hyphens std::replace(result.begin(), result.end(), '_', '-'); - return result; } -/** - * @brief Set the CUDA module loading environment variable - * If the method is 0, set the CUDA module loading environment variable to EAGER - * This needs to be done before the first call to the CUDA API. In this file before dummy settings - * default constructor is called. - * @param argc Number of command line arguments - * @param argv Command line arguments - * @return 0 on success, 1 on failure - */ int set_cuda_module_loading(int argc, char* argv[]) { - // Parse method_int from argv - int method_int = 0; // Default value + int method_int = 0; for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; if ((arg == "--method" || arg == "-m") && i + 1 < argc) { @@ -236,7 +166,6 @@ int set_cuda_module_loading(int argc, char* argv[]) } break; } - // Also support --method=1 style if (arg.rfind("--method=", 0) == 0) { try { method_int = std::stoi(arg.substr(9)); @@ -256,38 +185,33 @@ int set_cuda_module_loading(int argc, char* argv[]) return 0; } -/** - * @brief Main function for the cuOpt CLI - * @param argc Number of command line arguments - * @param argv Command line arguments - * @return 0 on success, 1 on failure - */ int main(int argc, char* argv[]) { - // Handle --dump-mip-heuristic-config before argparse so no other args are required + // Handle --dump-hyper-params before argparse so no MPS file is required for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; - if (arg == "--dump-mip-heuristic-config" && i + 1 < argc) { - cuopt::linear_programming::mip_heuristics_hyper_params_t defaults; - bool ok = cuopt::linear_programming::dump_mip_heuristics_hyper_params(argv[i + 1], defaults); + if (arg == "--dump-hyper-params" && i + 1 < argc) { + cuopt::linear_programming::solver_settings_t settings; + bool ok = settings.dump_parameters_to_file(argv[i + 1], true); return ok ? 0 : 1; } + if (arg == "--show-hyper-params") { + cuopt::linear_programming::solver_settings_t settings; + settings.dump_parameters_to_file("/dev/stdout", true); + return 0; + } } if (set_cuda_module_loading(argc, argv) != 0) { return 1; } - // Get the version string from the version_config.hpp file const std::string version_string = std::string("cuOpt ") + std::to_string(CUOPT_VERSION_MAJOR) + "." + std::to_string(CUOPT_VERSION_MINOR) + "." + std::to_string(CUOPT_VERSION_PATCH); - // Create the argument parser argparse::ArgumentParser program("cuopt_cli", version_string); - // Define all arguments with appropriate defaults and help messages program.add_argument("filename").help("input mps file").nargs(1).required(); - // FIXME: use a standard format for initial solution file program.add_argument("--initial-solution") .help("path to the initial solution .sol file") .default_value(""); @@ -302,18 +226,21 @@ int main(int argc, char* argv[]) .default_value(true) .implicit_value(true); - program.add_argument("--mip-heuristic-config") - .help("path to MIP heuristic hyper-parameters config file (key = value format)") + program.add_argument("--params-file") + .help("path to parameter config file (key = value format, supports all parameters)") .default_value(std::string("")); - program.add_argument("--dump-mip-heuristic-config") - .help("write default MIP heuristic hyper-parameters to the given file and exit") + program.add_argument("--dump-hyper-params") + .help("write default hyper-parameters to the given file and exit") .default_value(std::string("")); + program.add_argument("--show-hyper-params") + .help("print hyper-parameters in config-file format and exit") + .default_value(false) + .implicit_value(true); + std::map arg_name_to_param_name; - // Register --pdlp-precision with string-to-int mapping so that it flows - // through the settings_strings map like other settings. program.add_argument("--pdlp-precision") .help( "PDLP precision mode. default: native type, single: FP32 internally, " @@ -323,7 +250,6 @@ int main(int argc, char* argv[]) arg_name_to_param_name["--pdlp-precision"] = CUOPT_PDLP_PRECISION; { - // Add all solver settings as arguments cuopt::linear_programming::solver_settings_t dummy_settings; auto int_params = dummy_settings.get_int_parameters(); @@ -333,18 +259,18 @@ int main(int argc, char* argv[]) for (auto& param : int_params) { std::string arg_name = param_name_to_arg_name(param.param_name); - // handle duplicate parameters appearing in MIP and LP settings if (arg_name_to_param_name.count(arg_name) == 0) { - program.add_argument(arg_name.c_str()).default_value(param.default_value); + auto& arg = program.add_argument(arg_name.c_str()).default_value(param.default_value); + if (param.is_hyperparameter) { arg.hidden(); } arg_name_to_param_name[arg_name] = param.param_name; } } for (auto& param : double_params) { std::string arg_name = param_name_to_arg_name(param.param_name); - // handle duplicate parameters appearing in MIP and LP settings if (arg_name_to_param_name.count(arg_name) == 0) { - program.add_argument(arg_name.c_str()).default_value(param.default_value); + auto& arg = program.add_argument(arg_name.c_str()).default_value(param.default_value); + if (param.is_hyperparameter) { arg.hidden(); } arg_name_to_param_name[arg_name] = param.param_name; } } @@ -352,22 +278,22 @@ int main(int argc, char* argv[]) for (auto& param : bool_params) { std::string arg_name = param_name_to_arg_name(param.param_name); if (arg_name_to_param_name.count(arg_name) == 0) { - program.add_argument(arg_name.c_str()).default_value(param.default_value); + auto& arg = program.add_argument(arg_name.c_str()).default_value(param.default_value); + if (param.is_hyperparameter) { arg.hidden(); } arg_name_to_param_name[arg_name] = param.param_name; } } for (auto& param : string_params) { std::string arg_name = param_name_to_arg_name(param.param_name); - // handle duplicate parameters appearing in MIP and LP settings if (arg_name_to_param_name.count(arg_name) == 0) { - program.add_argument(arg_name.c_str()).default_value(param.default_value); + auto& arg = program.add_argument(arg_name.c_str()).default_value(param.default_value); + if (param.is_hyperparameter) { arg.hidden(); } arg_name_to_param_name[arg_name] = param.param_name; } - } // done with solver settings + } } - // Parse arguments try { program.parse_args(argc, argv); } catch (const std::runtime_error& err) { @@ -376,11 +302,9 @@ int main(int argc, char* argv[]) return 1; } - // Map symbolic pdlp-precision names to integer values static const std::map precision_name_to_value = { {"default", "-1"}, {"single", "0"}, {"double", "1"}, {"mixed", "2"}}; - // Read everything as a string std::map settings_strings; for (auto& [arg_name, param_name] : arg_name_to_param_name) { if (program.is_used(arg_name.c_str())) { @@ -392,18 +316,15 @@ int main(int argc, char* argv[]) settings_strings[param_name] = val; } } - // Get the values std::string file_name = program.get("filename"); const auto initial_solution_file = program.get("--initial-solution"); const auto solve_relaxation = program.get("--relaxation"); - // Only initialize CUDA resources if using GPU memory backend (not remote execution) auto memory_backend = cuopt::linear_programming::get_memory_backend_type(); std::vector> memory_resources; if (memory_backend == cuopt::linear_programming::memory_backend_t::GPU) { - // All arguments are parsed as string, default values are parsed as int if unused. const auto num_gpus = program.is_used("--num-gpus") ? std::stoi(program.get("--num-gpus")) : program.get("--num-gpus"); @@ -416,8 +337,8 @@ int main(int argc, char* argv[]) RAFT_CUDA_TRY(cudaSetDevice(0)); } - const auto heuristic_config = program.get("--mip-heuristic-config"); + const auto params_file = program.get("--params-file"); return run_single_file( - file_name, initial_solution_file, solve_relaxation, settings_strings, heuristic_config); + file_name, initial_solution_file, solve_relaxation, settings_strings, params_file); } diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index 86becfe06d..dc726de20b 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -77,6 +77,25 @@ #define CUOPT_RANDOM_SEED "random_seed" #define CUOPT_PDLP_PRECISION "pdlp_precision" +/* @brief MIP heuristic hyper-parameter names */ +#define CUOPT_POPULATION_SIZE "population_size" +#define CUOPT_NUM_CPUFJ_THREADS "num_cpufj_threads" +#define CUOPT_PRESOLVE_TIME_RATIO "presolve_time_ratio" +#define CUOPT_PRESOLVE_MAX_TIME "presolve_max_time" +#define CUOPT_ROOT_LP_TIME_RATIO "root_lp_time_ratio" +#define CUOPT_ROOT_LP_MAX_TIME "root_lp_max_time" +#define CUOPT_RINS_TIME_LIMIT "rins_time_limit" +#define CUOPT_RINS_MAX_TIME_LIMIT "rins_max_time_limit" +#define CUOPT_RINS_FIX_RATE "rins_fix_rate" +#define CUOPT_STAGNATION_TRIGGER "stagnation_trigger" +#define CUOPT_MAX_ITERS_WITHOUT_IMPROVEMENT "max_iterations_without_improvement" +#define CUOPT_INITIAL_INFEASIBILITY_WEIGHT "initial_infeasibility_weight" +#define CUOPT_N_OF_MINIMUMS_FOR_EXIT "n_of_minimums_for_exit" +#define CUOPT_ENABLED_RECOMBINERS "enabled_recombiners" +#define CUOPT_CYCLE_DETECTION_LENGTH "cycle_detection_length" +#define CUOPT_RELAXED_LP_TIME_LIMIT "relaxed_lp_time_limit" +#define CUOPT_RELATED_VARS_TIME_LIMIT "related_vars_time_limit" + /* @brief MIP determinism mode constants */ #define CUOPT_MODE_OPPORTUNISTIC 0 #define CUOPT_MODE_DETERMINISTIC 1 diff --git a/cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp b/cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp index a0521d6f67..c0b644544a 100644 --- a/cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp +++ b/cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp @@ -14,11 +14,9 @@ namespace cuopt::linear_programming { * * All fields carry their actual defaults. A config file only needs to list * the knobs being changed; omitted keys keep the values shown here. - * Loadable via fill_mip_heuristics_hyper_params(); dumpable via - * dump_mip_heuristics_hyper_params(). + * These are registered in the unified parameter framework via solver_settings_t + * and can be loaded from a config file with load_parameters_from_file(). */ -// TODO: unify field declarations, loader tables, and dump logic via X macros -// so that adding a parameter is a single-line change. struct mip_heuristics_hyper_params_t { int population_size = 32; // max solutions in pool int num_cpufj_threads = 8; // parallel CPU FJ climbers diff --git a/cpp/include/cuopt/linear_programming/solver_settings.hpp b/cpp/include/cuopt/linear_programming/solver_settings.hpp index 61e84c6cd8..1720b0e9f9 100644 --- a/cpp/include/cuopt/linear_programming/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/solver_settings.hpp @@ -96,6 +96,9 @@ class solver_settings_t { 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: pdlp_solver_settings_t pdlp_settings; mip_solver_settings_t mip_settings; diff --git a/cpp/include/cuopt/linear_programming/utilities/internals.hpp b/cpp/include/cuopt/linear_programming/utilities/internals.hpp index fc90dec04f..ff70a49123 100644 --- a/cpp/include/cuopt/linear_programming/utilities/internals.hpp +++ b/cpp/include/cuopt/linear_programming/utilities/internals.hpp @@ -79,8 +79,20 @@ class base_solution_t { template struct parameter_info_t { - parameter_info_t(std::string_view param_name, T* value, T min, T max, T def) - : param_name(param_name), value_ptr(value), min_value(min), max_value(max), default_value(def) + parameter_info_t(std::string_view param_name, + T* value, + T min, + T max, + T def, + bool is_hyperparameter = false, + const char* description = "") + : param_name(param_name), + value_ptr(value), + min_value(min), + max_value(max), + default_value(def), + is_hyperparameter(is_hyperparameter), + description(description) { } std::string param_name; @@ -88,28 +100,50 @@ struct parameter_info_t { T min_value; T max_value; T default_value; + bool is_hyperparameter; + const char* description; }; template <> struct parameter_info_t { - parameter_info_t(std::string_view name, bool* value, bool def) - : param_name(name), value_ptr(value), default_value(def) + parameter_info_t(std::string_view name, + bool* value, + bool def, + bool is_hyperparameter = false, + const char* description = "") + : param_name(name), + value_ptr(value), + default_value(def), + is_hyperparameter(is_hyperparameter), + description(description) { } std::string param_name; bool* value_ptr; bool default_value; + bool is_hyperparameter; + const char* description; }; template <> struct parameter_info_t { - parameter_info_t(std::string_view name, std::string* value, std::string def) - : param_name(name), value_ptr(value), default_value(def) + parameter_info_t(std::string_view name, + std::string* value, + std::string def, + bool is_hyperparameter = false, + const char* description = "") + : param_name(name), + value_ptr(value), + default_value(def), + is_hyperparameter(is_hyperparameter), + description(description) { } std::string param_name; std::string* value_ptr; std::string default_value; + bool is_hyperparameter; + const char* description; }; /** diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index a60d508fac..5ae52d8a53 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -5,10 +5,16 @@ */ /* clang-format on */ +#include #include #include #include +#include +#include +#include +#include + namespace cuopt::linear_programming { namespace { @@ -16,9 +22,10 @@ namespace { bool string_to_int(const std::string& value, int& result) { try { - result = std::stoi(value); - return true; - } catch (const std::invalid_argument& e) { + size_t pos = 0; + result = std::stoi(value, &pos); + return pos == value.size(); + } catch (const std::exception&) { return false; } } @@ -27,14 +34,31 @@ template bool string_to_float(const std::string& value, f_t& result) { try { - if constexpr (std::is_same_v) { result = std::stof(value); } - if constexpr (std::is_same_v) { result = std::stod(value); } - return true; - } catch (const std::invalid_argument& e) { + 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); } + 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" || @@ -75,7 +99,18 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {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_MIP_CUT_MIN_ORTHOGONALITY, &mip_settings.cut_min_orthogonality, f_t(0.0), f_t(1.0), f_t(0.5)}, + // MIP heuristic hyper-parameters (hidden from --help) + {CUOPT_PRESOLVE_TIME_RATIO, &mip_settings.heuristic_params.presolve_time_ratio, f_t(0.0), f_t(1.0), f_t(0.1), true, "fraction of total time for presolve"}, + {CUOPT_PRESOLVE_MAX_TIME, &mip_settings.heuristic_params.presolve_max_time, f_t(0.0), std::numeric_limits::infinity(), f_t(60.0), true, "hard cap on presolve seconds"}, + {CUOPT_ROOT_LP_TIME_RATIO, &mip_settings.heuristic_params.root_lp_time_ratio, f_t(0.0), f_t(1.0), f_t(0.1), true, "fraction of total time for root LP"}, + {CUOPT_ROOT_LP_MAX_TIME, &mip_settings.heuristic_params.root_lp_max_time, f_t(0.0), std::numeric_limits::infinity(), f_t(15.0), true, "hard cap on root LP seconds"}, + {CUOPT_RINS_TIME_LIMIT, &mip_settings.heuristic_params.rins_time_limit, f_t(0.0), std::numeric_limits::infinity(), f_t(3.0), true, "per-call RINS sub-MIP time"}, + {CUOPT_RINS_MAX_TIME_LIMIT, &mip_settings.heuristic_params.rins_max_time_limit, f_t(0.0), std::numeric_limits::infinity(), f_t(20.0), true, "ceiling for RINS adaptive time budget"}, + {CUOPT_RINS_FIX_RATE, &mip_settings.heuristic_params.rins_fix_rate, f_t(0.0), f_t(1.0), f_t(0.5), true, "RINS variable fix rate"}, + {CUOPT_INITIAL_INFEASIBILITY_WEIGHT, &mip_settings.heuristic_params.initial_infeasibility_weight, f_t(1e-9), std::numeric_limits::infinity(), f_t(1000.0), true, "constraint violation penalty seed"}, + {CUOPT_RELAXED_LP_TIME_LIMIT, &mip_settings.heuristic_params.relaxed_lp_time_limit, f_t(1e-9), std::numeric_limits::infinity(), f_t(1.0), true, "base relaxed LP time cap in heuristics"}, + {CUOPT_RELATED_VARS_TIME_LIMIT, &mip_settings.heuristic_params.related_vars_time_limit, f_t(1e-9), std::numeric_limits::infinity(), f_t(30.0), true, "time for related-variable structure build"}, }; // Int parameters @@ -105,7 +140,15 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {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_PRECISION, reinterpret_cast(&pdlp_settings.pdlp_precision), CUOPT_PDLP_DEFAULT_PRECISION, CUOPT_PDLP_MIXED_PRECISION, CUOPT_PDLP_DEFAULT_PRECISION}, + // MIP heuristic hyper-parameters (hidden from --help) + {CUOPT_POPULATION_SIZE, &mip_settings.heuristic_params.population_size, 1, std::numeric_limits::max(), 32, true, "max solutions in pool"}, + {CUOPT_NUM_CPUFJ_THREADS, &mip_settings.heuristic_params.num_cpufj_threads, 0, std::numeric_limits::max(), 8, true, "parallel CPU FJ climbers"}, + {CUOPT_STAGNATION_TRIGGER, &mip_settings.heuristic_params.stagnation_trigger, 1, std::numeric_limits::max(), 3, true, "FP loops w/o improvement before recombination"}, + {CUOPT_MAX_ITERS_WITHOUT_IMPROVEMENT, &mip_settings.heuristic_params.max_iterations_without_improvement, 1, std::numeric_limits::max(), 8, true, "diversity step depth after stagnation"}, + {CUOPT_N_OF_MINIMUMS_FOR_EXIT, &mip_settings.heuristic_params.n_of_minimums_for_exit, 1, std::numeric_limits::max(), 7000, true, "FJ baseline local-minima exit threshold"}, + {CUOPT_ENABLED_RECOMBINERS, &mip_settings.heuristic_params.enabled_recombiners, 0, 15, 15, true, "bitmask: 1=BP 2=FP 4=LS 8=SubMIP"}, + {CUOPT_CYCLE_DETECTION_LENGTH, &mip_settings.heuristic_params.cycle_detection_length, 1, std::numeric_limits::max(), 30, true, "FP assignment cycle ring buffer length"}, }; // Bool parameters @@ -473,6 +516,111 @@ const std::vector solver_settings_t::get_parameter_names( return 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()); +} + +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 only the values you want to override.\n\n"; + for (const auto& p : int_parameters) { + if (hyperparameters_only && !p.is_hyperparameter) 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.is_hyperparameter) 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.is_hyperparameter) 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.is_hyperparameter) 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; +} + #if MIP_INSTANTIATE_FLOAT template class solver_settings_t; template void solver_settings_t::set_parameter(const std::string& name, int value); diff --git a/cpp/src/mip_heuristics/heuristics_hyper_params_loader.hpp b/cpp/src/mip_heuristics/heuristics_hyper_params_loader.hpp deleted file mode 100644 index a4ad97bcbb..0000000000 --- a/cpp/src/mip_heuristics/heuristics_hyper_params_loader.hpp +++ /dev/null @@ -1,187 +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 -#include -#include -#include - -namespace cuopt::linear_programming { - -namespace { - -using hp_t = mip_heuristics_hyper_params_t; -using double_member_ptr = double hp_t::*; -using int_member_ptr = int hp_t::*; - -struct double_param_t { - const char* name; - double_member_ptr field; - double min_val; - double max_val; - const char* description; -}; - -struct int_param_t { - const char* name; - int_member_ptr field; - int min_val; - int max_val; - const char* description; -}; - -constexpr double inf = std::numeric_limits::infinity(); - -// clang-format off -constexpr double_param_t double_params[] = { - {"presolve_time_ratio", &hp_t::presolve_time_ratio, 0.0, 1.0, "fraction of total time for presolve"}, - {"presolve_max_time", &hp_t::presolve_max_time, 0.0, inf, "hard cap on presolve seconds"}, - {"root_lp_time_ratio", &hp_t::root_lp_time_ratio, 0.0, 1.0, "fraction of total time for root LP"}, - {"root_lp_max_time", &hp_t::root_lp_max_time, 0.0, inf, "hard cap on root LP seconds"}, - {"rins_time_limit", &hp_t::rins_time_limit, 0.0, inf, "per-call RINS sub-MIP time"}, - {"rins_max_time_limit", &hp_t::rins_max_time_limit, 0.0, inf, "ceiling for RINS adaptive time budget"}, - {"rins_fix_rate", &hp_t::rins_fix_rate, 0.0, 1.0, "RINS variable fix rate"}, - {"initial_infeasibility_weight", &hp_t::initial_infeasibility_weight, 1e-9, inf, "constraint violation penalty seed"}, - {"relaxed_lp_time_limit", &hp_t::relaxed_lp_time_limit, 1e-9, inf, "base relaxed LP time cap in heuristics"}, - {"related_vars_time_limit", &hp_t::related_vars_time_limit, 1e-9, inf, "time for related-variable structure build"}, -}; - -constexpr int_param_t int_params[] = { - {"population_size", &hp_t::population_size, 1, std::numeric_limits::max(), "max solutions in pool"}, - {"num_cpufj_threads", &hp_t::num_cpufj_threads, 0, std::numeric_limits::max(), "parallel CPU FJ climbers"}, - {"stagnation_trigger", &hp_t::stagnation_trigger, 1, std::numeric_limits::max(), "FP loops w/o improvement before recombination"}, - {"max_iterations_without_improvement", &hp_t::max_iterations_without_improvement, 1, std::numeric_limits::max(), "diversity step depth after stagnation"}, - {"n_of_minimums_for_exit", &hp_t::n_of_minimums_for_exit, 1, std::numeric_limits::max(), "FJ baseline local-minima exit threshold"}, - {"enabled_recombiners", &hp_t::enabled_recombiners, 0, 15, "bitmask: 1=BP 2=FP 4=LS 8=SubMIP"}, - {"cycle_detection_length", &hp_t::cycle_detection_length, 1, std::numeric_limits::max(), "FP assignment cycle ring buffer length"}, -}; -// clang-format on - -template -bool try_parse_param(const ParamDesc* params, - size_t n_params, - const std::string& var, - std::istringstream& iss, - hp_t& hp, - const std::string& line) -{ - for (size_t i = 0; i < n_params; ++i) { - const auto& p = params[i]; - if (var != p.name) continue; - cuopt_expects(bool(iss >> hp.*p.field), - error_type_t::ValidationError, - "MIP heuristic config: bad value for %s: %s", - p.name, - line.c_str()); - std::string trailing; - cuopt_expects(!bool(iss >> trailing), - error_type_t::ValidationError, - "MIP heuristic config: trailing junk for %s: %s", - p.name, - line.c_str()); - cuopt_expects(hp.*p.field >= p.min_val && hp.*p.field <= p.max_val, - error_type_t::ValidationError, - "MIP heuristic config: %s = %s out of range [%s, %s]", - p.name, - std::to_string(hp.*p.field).c_str(), - std::to_string(p.min_val).c_str(), - std::to_string(p.max_val).c_str()); - CUOPT_LOG_INFO("MIP heuristic config: %s = %s", p.name, std::to_string(hp.*p.field).c_str()); - return true; - } - return false; -} - -} // namespace - -/** - * @brief Load MIP heuristic hyper-parameters from a key=value text file. - * - * Format: one assignment per line, e.g. population_size = 64 - * Lines starting with # (optionally indented) are comments. - * Unknown keys, malformed values, and out-of-range values all throw. - * Partial files are fine — omitted keys keep their struct defaults. - */ -inline void fill_mip_heuristics_hyper_params(const std::string& path, - mip_heuristics_hyper_params_t& params) -{ - cuopt_expects(!std::filesystem::is_directory(path) && std::filesystem::exists(path), - error_type_t::ValidationError, - "MIP heuristic config: not a valid file: %s", - path.c_str()); - std::ifstream file(path); - cuopt_expects(file.is_open(), - error_type_t::ValidationError, - "MIP heuristic config: cannot open: %s", - path.c_str()); - std::string line; - - while (std::getline(file, line)) { - // Trim leading whitespace, then skip blank lines and comments - 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 var; - - cuopt_expects(iss >> var >> std::ws && iss.get() == '=', - error_type_t::ValidationError, - "MIP heuristic config: bad line: %s", - line.c_str()); - - bool found = try_parse_param(double_params, std::size(double_params), var, iss, params, line) || - try_parse_param(int_params, std::size(int_params), var, iss, params, line); - - cuopt_expects(found, - error_type_t::ValidationError, - "MIP heuristic config: unknown parameter: %s", - var.c_str()); - } - - CUOPT_LOG_INFO("MIP heuristic config loaded from: %s", path.c_str()); -} - -/** - * @brief Dump current hyper-parameters to a key=value text file. - * - * Each entry is commented out and annotated with its type, allowed range, - * and description. The output is a valid config file that can be loaded - * back with fill_mip_heuristics_hyper_params() after uncommenting the - * desired overrides. - */ -inline bool dump_mip_heuristics_hyper_params(const std::string& path, - const mip_heuristics_hyper_params_t& params) -{ - std::ofstream file(path); - if (!file.is_open()) { - CUOPT_LOG_ERROR("Cannot open file for writing: %s", path.c_str()); - return false; - } - file << "# MIP heuristic hyper-parameters (auto-generated)\n"; - file << "# Uncomment and change only the values you want to override.\n\n"; - for (const auto& p : int_params) { - file << "# " << p.description << " (int, range: [" << p.min_val << ", " << p.max_val << "])\n"; - file << "# " << p.name << " = " << params.*p.field << "\n\n"; - } - for (const auto& p : double_params) { - file << "# " << p.description << " (double, range: [" << p.min_val << ", " << p.max_val - << "])\n"; - file << "# " << p.name << " = " << params.*p.field << "\n\n"; - } - return true; -} - -} // namespace cuopt::linear_programming diff --git a/cpp/tests/mip/heuristics_hyper_params_test.cu b/cpp/tests/mip/heuristics_hyper_params_test.cu index 2914356a5c..a7e88800c5 100644 --- a/cpp/tests/mip/heuristics_hyper_params_test.cu +++ b/cpp/tests/mip/heuristics_hyper_params_test.cu @@ -5,17 +5,21 @@ */ /* clang-format on */ +#include #include -#include +#include #include #include #include +#include #include namespace cuopt::linear_programming::test { +using settings_t = solver_settings_t; + class HeuristicsHyperParamsTest : public ::testing::Test { protected: std::string tmp_path; @@ -30,24 +34,23 @@ class HeuristicsHyperParamsTest : public ::testing::Test { TEST_F(HeuristicsHyperParamsTest, DumpedFileIsAllCommentedOut) { - mip_heuristics_hyper_params_t original; - dump_mip_heuristics_hyper_params(tmp_path, original); + settings_t settings; + settings.dump_parameters_to_file(tmp_path, true); // Loading the commented-out dump should leave struct defaults unchanged - mip_heuristics_hyper_params_t loaded; - loaded.population_size = 9999; - fill_mip_heuristics_hyper_params(tmp_path, loaded); - EXPECT_EQ(loaded.population_size, 9999); + settings_t reloaded; + reloaded.get_mip_settings().heuristic_params.population_size = 9999; + reloaded.load_parameters_from_file(tmp_path); + EXPECT_EQ(reloaded.get_mip_settings().heuristic_params.population_size, 9999); } TEST_F(HeuristicsHyperParamsTest, DumpedFileIsParseable) { - mip_heuristics_hyper_params_t original; - dump_mip_heuristics_hyper_params(tmp_path, original); + settings_t settings; + settings.dump_parameters_to_file(tmp_path, true); - // The dumped file should parse without errors (all lines are comments) - mip_heuristics_hyper_params_t loaded; - EXPECT_NO_THROW(fill_mip_heuristics_hyper_params(tmp_path, loaded)); + settings_t reloaded; + EXPECT_NO_THROW(reloaded.load_parameters_from_file(tmp_path)); } TEST_F(HeuristicsHyperParamsTest, CustomValuesRoundTrip) @@ -73,48 +76,49 @@ TEST_F(HeuristicsHyperParamsTest, CustomValuesRoundTrip) f << "related_vars_time_limit = 60\n"; } - mip_heuristics_hyper_params_t loaded; - fill_mip_heuristics_hyper_params(tmp_path, loaded); - - EXPECT_EQ(loaded.population_size, 64); - EXPECT_EQ(loaded.num_cpufj_threads, 4); - EXPECT_DOUBLE_EQ(loaded.presolve_time_ratio, 0.2); - EXPECT_DOUBLE_EQ(loaded.presolve_max_time, 120.0); - EXPECT_DOUBLE_EQ(loaded.root_lp_time_ratio, 0.05); - EXPECT_DOUBLE_EQ(loaded.root_lp_max_time, 30.0); - EXPECT_DOUBLE_EQ(loaded.rins_time_limit, 5.0); - EXPECT_DOUBLE_EQ(loaded.rins_max_time_limit, 40.0); - EXPECT_DOUBLE_EQ(loaded.rins_fix_rate, 0.7); - EXPECT_EQ(loaded.stagnation_trigger, 5); - EXPECT_EQ(loaded.max_iterations_without_improvement, 12); - EXPECT_DOUBLE_EQ(loaded.initial_infeasibility_weight, 500.0); - EXPECT_EQ(loaded.n_of_minimums_for_exit, 10000); - EXPECT_EQ(loaded.enabled_recombiners, 5); - EXPECT_EQ(loaded.cycle_detection_length, 50); - EXPECT_DOUBLE_EQ(loaded.relaxed_lp_time_limit, 2.0); - EXPECT_DOUBLE_EQ(loaded.related_vars_time_limit, 60.0); + settings_t settings; + settings.load_parameters_from_file(tmp_path); + const auto& hp = settings.get_mip_settings().heuristic_params; + + EXPECT_EQ(hp.population_size, 64); + EXPECT_EQ(hp.num_cpufj_threads, 4); + EXPECT_DOUBLE_EQ(hp.presolve_time_ratio, 0.2); + EXPECT_DOUBLE_EQ(hp.presolve_max_time, 120.0); + EXPECT_DOUBLE_EQ(hp.root_lp_time_ratio, 0.05); + EXPECT_DOUBLE_EQ(hp.root_lp_max_time, 30.0); + EXPECT_DOUBLE_EQ(hp.rins_time_limit, 5.0); + EXPECT_DOUBLE_EQ(hp.rins_max_time_limit, 40.0); + EXPECT_DOUBLE_EQ(hp.rins_fix_rate, 0.7); + EXPECT_EQ(hp.stagnation_trigger, 5); + EXPECT_EQ(hp.max_iterations_without_improvement, 12); + EXPECT_DOUBLE_EQ(hp.initial_infeasibility_weight, 500.0); + EXPECT_EQ(hp.n_of_minimums_for_exit, 10000); + EXPECT_EQ(hp.enabled_recombiners, 5); + EXPECT_EQ(hp.cycle_detection_length, 50); + EXPECT_DOUBLE_EQ(hp.relaxed_lp_time_limit, 2.0); + EXPECT_DOUBLE_EQ(hp.related_vars_time_limit, 60.0); } TEST_F(HeuristicsHyperParamsTest, PartialConfigKeepsDefaults) { - // Write a config with only two keys { std::ofstream f(tmp_path); f << "population_size = 128\n"; f << "rins_fix_rate = 0.3\n"; } - mip_heuristics_hyper_params_t loaded; - fill_mip_heuristics_hyper_params(tmp_path, loaded); + settings_t settings; + settings.load_parameters_from_file(tmp_path); + const auto& hp = settings.get_mip_settings().heuristic_params; + + EXPECT_EQ(hp.population_size, 128); + EXPECT_DOUBLE_EQ(hp.rins_fix_rate, 0.3); - EXPECT_EQ(loaded.population_size, 128); - EXPECT_DOUBLE_EQ(loaded.rins_fix_rate, 0.3); - // Everything else should be unchanged from struct defaults mip_heuristics_hyper_params_t defaults; - EXPECT_EQ(loaded.num_cpufj_threads, defaults.num_cpufj_threads); - EXPECT_DOUBLE_EQ(loaded.presolve_time_ratio, defaults.presolve_time_ratio); - EXPECT_EQ(loaded.n_of_minimums_for_exit, defaults.n_of_minimums_for_exit); - EXPECT_EQ(loaded.enabled_recombiners, defaults.enabled_recombiners); + EXPECT_EQ(hp.num_cpufj_threads, defaults.num_cpufj_threads); + EXPECT_DOUBLE_EQ(hp.presolve_time_ratio, defaults.presolve_time_ratio); + EXPECT_EQ(hp.n_of_minimums_for_exit, defaults.n_of_minimums_for_exit); + EXPECT_EQ(hp.enabled_recombiners, defaults.enabled_recombiners); } TEST_F(HeuristicsHyperParamsTest, CommentsAndBlankLinesIgnored) @@ -128,9 +132,9 @@ TEST_F(HeuristicsHyperParamsTest, CommentsAndBlankLinesIgnored) f << "\n"; } - mip_heuristics_hyper_params_t loaded; - fill_mip_heuristics_hyper_params(tmp_path, loaded); - EXPECT_EQ(loaded.population_size, 42); + settings_t settings; + settings.load_parameters_from_file(tmp_path); + EXPECT_EQ(settings.get_mip_settings().heuristic_params.population_size, 42); } TEST_F(HeuristicsHyperParamsTest, UnknownKeyThrows) @@ -139,8 +143,8 @@ TEST_F(HeuristicsHyperParamsTest, UnknownKeyThrows) std::ofstream f(tmp_path); f << "bogus_key = 42\n"; } - mip_heuristics_hyper_params_t loaded; - EXPECT_THROW(fill_mip_heuristics_hyper_params(tmp_path, loaded), cuopt::logic_error); + settings_t settings; + EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); } TEST_F(HeuristicsHyperParamsTest, BadNumericValueThrows) @@ -149,18 +153,38 @@ TEST_F(HeuristicsHyperParamsTest, BadNumericValueThrows) std::ofstream f(tmp_path); f << "population_size = not_a_number\n"; } - mip_heuristics_hyper_params_t loaded; - EXPECT_THROW(fill_mip_heuristics_hyper_params(tmp_path, loaded), cuopt::logic_error); + settings_t settings; + EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); +} + +TEST_F(HeuristicsHyperParamsTest, TrailingJunkSpaceSeparatedThrows) +{ + { + std::ofstream f(tmp_path); + f << "population_size = 64 foo\n"; + } + settings_t settings; + EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); } -TEST_F(HeuristicsHyperParamsTest, TrailingJunkThrows) +TEST_F(HeuristicsHyperParamsTest, TrailingJunkNoSpaceThrows) { { std::ofstream f(tmp_path); f << "population_size = 64foo\n"; } - mip_heuristics_hyper_params_t loaded; - EXPECT_THROW(fill_mip_heuristics_hyper_params(tmp_path, loaded), cuopt::logic_error); + settings_t settings; + EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); +} + +TEST_F(HeuristicsHyperParamsTest, TrailingJunkFloatThrows) +{ + { + std::ofstream f(tmp_path); + f << "rins_fix_rate = 0.5abc\n"; + } + settings_t settings; + EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); } TEST_F(HeuristicsHyperParamsTest, RangeViolationCycleDetectionThrows) @@ -169,8 +193,8 @@ TEST_F(HeuristicsHyperParamsTest, RangeViolationCycleDetectionThrows) std::ofstream f(tmp_path); f << "cycle_detection_length = 0\n"; } - mip_heuristics_hyper_params_t loaded; - EXPECT_THROW(fill_mip_heuristics_hyper_params(tmp_path, loaded), cuopt::logic_error); + settings_t settings; + EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); } TEST_F(HeuristicsHyperParamsTest, RangeViolationFixRateThrows) @@ -179,21 +203,21 @@ TEST_F(HeuristicsHyperParamsTest, RangeViolationFixRateThrows) std::ofstream f(tmp_path); f << "rins_fix_rate = 2.0\n"; } - mip_heuristics_hyper_params_t loaded; - EXPECT_THROW(fill_mip_heuristics_hyper_params(tmp_path, loaded), cuopt::logic_error); + settings_t settings; + EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); } TEST_F(HeuristicsHyperParamsTest, NonexistentFileThrows) { - mip_heuristics_hyper_params_t loaded; - EXPECT_THROW(fill_mip_heuristics_hyper_params("/tmp/does_not_exist_cuopt_test.config", loaded), + settings_t settings; + EXPECT_THROW(settings.load_parameters_from_file("/tmp/does_not_exist_cuopt_test.config"), cuopt::logic_error); } TEST_F(HeuristicsHyperParamsTest, DirectoryPathThrows) { - mip_heuristics_hyper_params_t loaded; - EXPECT_THROW(fill_mip_heuristics_hyper_params("/tmp", loaded), cuopt::logic_error); + settings_t settings; + EXPECT_THROW(settings.load_parameters_from_file("/tmp"), cuopt::logic_error); } TEST_F(HeuristicsHyperParamsTest, IndentedCommentAndWhitespaceLinesIgnored) @@ -204,9 +228,56 @@ TEST_F(HeuristicsHyperParamsTest, IndentedCommentAndWhitespaceLinesIgnored) f << " \t \n"; f << "population_size = 99\n"; } - mip_heuristics_hyper_params_t loaded; - fill_mip_heuristics_hyper_params(tmp_path, loaded); - EXPECT_EQ(loaded.population_size, 99); + settings_t settings; + settings.load_parameters_from_file(tmp_path); + EXPECT_EQ(settings.get_mip_settings().heuristic_params.population_size, 99); +} + +TEST_F(HeuristicsHyperParamsTest, MixedSolverAndHyperParamsFromFile) +{ + { + std::ofstream f(tmp_path); + f << "population_size = 100\n"; + f << "time_limit = 42\n"; + } + settings_t settings; + settings.load_parameters_from_file(tmp_path); + EXPECT_EQ(settings.get_mip_settings().heuristic_params.population_size, 100); + EXPECT_DOUBLE_EQ(settings.get_mip_settings().time_limit, 42.0); +} + +TEST_F(HeuristicsHyperParamsTest, QuotedStringValue) +{ + { + std::ofstream f(tmp_path); + f << "log_file = \"/path/with spaces/log.txt\"\n"; + } + settings_t settings; + settings.load_parameters_from_file(tmp_path); + EXPECT_EQ(settings.template get_parameter(CUOPT_LOG_FILE), + "/path/with spaces/log.txt"); +} + +TEST_F(HeuristicsHyperParamsTest, QuotedStringWithEscapedQuote) +{ + { + std::ofstream f(tmp_path); + f << R"(log_file = "/path/with \"quotes\"/log.txt")" << "\n"; + } + settings_t settings; + settings.load_parameters_from_file(tmp_path); + EXPECT_EQ(settings.template get_parameter(CUOPT_LOG_FILE), + "/path/with \"quotes\"/log.txt"); +} + +TEST_F(HeuristicsHyperParamsTest, UnterminatedQuoteThrows) +{ + { + std::ofstream f(tmp_path); + f << "log_file = \"/path/no/close\n"; + } + settings_t settings; + EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); } } // namespace cuopt::linear_programming::test From 04e047b378557ffb6c9c31144d209734dac7d2e0 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 24 Mar 2026 11:02:10 -0700 Subject: [PATCH 05/14] cleanup --- cpp/cuopt_cli.cpp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 9839ad11e3..2c4ace1789 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -190,11 +190,6 @@ int main(int argc, char* argv[]) // Handle --dump-hyper-params before argparse so no MPS file is required for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; - if (arg == "--dump-hyper-params" && i + 1 < argc) { - cuopt::linear_programming::solver_settings_t settings; - bool ok = settings.dump_parameters_to_file(argv[i + 1], true); - return ok ? 0 : 1; - } if (arg == "--show-hyper-params") { cuopt::linear_programming::solver_settings_t settings; settings.dump_parameters_to_file("/dev/stdout", true); @@ -230,10 +225,6 @@ int main(int argc, char* argv[]) .help("path to parameter config file (key = value format, supports all parameters)") .default_value(std::string("")); - program.add_argument("--dump-hyper-params") - .help("write default hyper-parameters to the given file and exit") - .default_value(std::string("")); - program.add_argument("--show-hyper-params") .help("print hyper-parameters in config-file format and exit") .default_value(false) From 390be0c13d6cf0e6f8b9e3dcd4c24efaa871aa2c Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 24 Mar 2026 11:17:11 -0700 Subject: [PATCH 06/14] restore comments --- cpp/cuopt_cli.cpp | 95 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 4 deletions(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 2c4ace1789..bc0400c2ff 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -32,8 +32,48 @@ static char cuda_module_loading_env[] = "CUDA_MODULE_LOADING=EAGER"; +/** + * @file cuopt_cli.cpp + * @brief Command line interface for solving Linear Programming (LP) and Mixed Integer Programming + * (MIP) problems using cuOpt + * + * This CLI provides a simple interface to solve LP/MIP problems using cuOpt. It accepts MPS format + * input files and various solver parameters. + * + * Usage: + * ``` + * cuopt_cli [OPTIONS] + * cuopt_cli [OPTIONS] + * ``` + * + * Required arguments: + * - : Path to the MPS format input file containing the optimization problem + * + * Optional arguments: + * - --initial-solution: Path to initial solution file in SOL format + * - Various solver parameters that can be passed as command line arguments + * (e.g. --max-iterations, --tolerance, etc.) + * + * Example: + * ``` + * cuopt_cli problem.mps --max-iterations 1000 + * ``` + * + * The solver will read the MPS file, solve the optimization problem according to the specified + * parameters, and write the solution to a .sol file in the output directory. + */ + +/** + * @brief Make an async memory resource for RMM + * @return std::shared_ptr + */ inline auto make_async() { return std::make_shared(); } +/** + * @brief Handle logger when error happens before logger is initialized + * @param settings Solver settings + * @return cuopt::init_logger_t + */ inline cuopt::init_logger_t dummy_logger( const cuopt::linear_programming::solver_settings_t& settings) { @@ -41,6 +81,12 @@ inline cuopt::init_logger_t dummy_logger( settings.template get_parameter(CUOPT_LOG_TO_CONSOLE)); } +/** + * @brief Run a single file + * @param file_path Path to the MPS format input file containing the optimization problem + * @param initial_solution_file Path to initial solution file in SOL format + * @param settings_strings Map of solver parameters + */ int run_single_file(const std::string& file_path, const std::string& initial_solution_file, bool solve_relaxation, @@ -80,6 +126,8 @@ int run_single_file(const std::string& file_path, return -1; } + // Determine memory backend and create problem using interface + // Create handle only for GPU memory backend (avoid CUDA init on CPU-only hosts) auto memory_backend = cuopt::linear_programming::get_memory_backend_type(); std::unique_ptr handle_ptr; std::unique_ptr> @@ -95,6 +143,7 @@ int run_single_file(const std::string& file_path, std::make_unique>(); } + // Populate the problem from MPS data model cuopt::linear_programming::populate_from_mps_data_model(problem_interface.get(), mps_data_model); const bool is_mip = (problem_interface->get_problem_category() == @@ -136,7 +185,6 @@ int run_single_file(const std::string& file_path, auto solution = cuopt::linear_programming::solve_lp(problem_interface.get(), lp_settings); } } catch (const std::exception& e) { - fprintf(stderr, "cuopt_cli error: %s\n", e.what()); CUOPT_LOG_ERROR("Error: %s", e.what()); return -1; } @@ -144,17 +192,35 @@ int run_single_file(const std::string& file_path, return 0; } +/** + * @brief Convert a parameter name to an argument name + * @param input Parameter name + * @return Argument name + */ std::string param_name_to_arg_name(const std::string& input) { std::string result = "--"; result += input; + + // Replace underscores with hyphens std::replace(result.begin(), result.end(), '_', '-'); + return result; } +/** + * @brief Set the CUDA module loading environment variable + * If the method is 0, set the CUDA module loading environment variable to EAGER + * This needs to be done before the first call to the CUDA API. In this file before dummy settings + * default constructor is called. + * @param argc Number of command line arguments + * @param argv Command line arguments + * @return 0 on success, 1 on failure + */ int set_cuda_module_loading(int argc, char* argv[]) { - int method_int = 0; + // Parse method_int from argv + int method_int = 0; // Default value for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; if ((arg == "--method" || arg == "-m") && i + 1 < argc) { @@ -166,6 +232,7 @@ int set_cuda_module_loading(int argc, char* argv[]) } break; } + // Also support --method=1 style if (arg.rfind("--method=", 0) == 0) { try { method_int = std::stoi(arg.substr(9)); @@ -185,9 +252,15 @@ int set_cuda_module_loading(int argc, char* argv[]) return 0; } +/** + * @brief Main function for the cuOpt CLI + * @param argc Number of command line arguments + * @param argv Command line arguments + * @return 0 on success, 1 on failure + */ int main(int argc, char* argv[]) { - // Handle --dump-hyper-params before argparse so no MPS file is required + // Handle --show-hyper-params before argparse so no other args are required for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; if (arg == "--show-hyper-params") { @@ -199,14 +272,18 @@ int main(int argc, char* argv[]) if (set_cuda_module_loading(argc, argv) != 0) { return 1; } + // Get the version string from the version_config.hpp file const std::string version_string = std::string("cuOpt ") + std::to_string(CUOPT_VERSION_MAJOR) + "." + std::to_string(CUOPT_VERSION_MINOR) + "." + std::to_string(CUOPT_VERSION_PATCH); + // Create the argument parser argparse::ArgumentParser program("cuopt_cli", version_string); + // Define all arguments with appropriate defaults and help messages program.add_argument("filename").help("input mps file").nargs(1).required(); + // FIXME: use a standard format for initial solution file program.add_argument("--initial-solution") .help("path to the initial solution .sol file") .default_value(""); @@ -232,6 +309,8 @@ int main(int argc, char* argv[]) std::map arg_name_to_param_name; + // Register --pdlp-precision with string-to-int mapping so that it flows + // through the settings_strings map like other settings. program.add_argument("--pdlp-precision") .help( "PDLP precision mode. default: native type, single: FP32 internally, " @@ -241,6 +320,7 @@ int main(int argc, char* argv[]) arg_name_to_param_name["--pdlp-precision"] = CUOPT_PDLP_PRECISION; { + // Add all solver settings as arguments cuopt::linear_programming::solver_settings_t dummy_settings; auto int_params = dummy_settings.get_int_parameters(); @@ -250,6 +330,7 @@ int main(int argc, char* argv[]) for (auto& param : int_params) { std::string arg_name = param_name_to_arg_name(param.param_name); + // handle duplicate parameters appearing in MIP and LP settings if (arg_name_to_param_name.count(arg_name) == 0) { auto& arg = program.add_argument(arg_name.c_str()).default_value(param.default_value); if (param.is_hyperparameter) { arg.hidden(); } @@ -283,8 +364,9 @@ int main(int argc, char* argv[]) arg_name_to_param_name[arg_name] = param.param_name; } } - } + } // done with solver settings + // Parse arguments try { program.parse_args(argc, argv); } catch (const std::runtime_error& err) { @@ -293,9 +375,11 @@ int main(int argc, char* argv[]) return 1; } + // Map symbolic pdlp-precision names to integer values static const std::map precision_name_to_value = { {"default", "-1"}, {"single", "0"}, {"double", "1"}, {"mixed", "2"}}; + // Read everything as a string std::map settings_strings; for (auto& [arg_name, param_name] : arg_name_to_param_name) { if (program.is_used(arg_name.c_str())) { @@ -307,15 +391,18 @@ int main(int argc, char* argv[]) settings_strings[param_name] = val; } } + // Get the values std::string file_name = program.get("filename"); const auto initial_solution_file = program.get("--initial-solution"); const auto solve_relaxation = program.get("--relaxation"); + // Only initialize CUDA resources if using GPU memory backend (not remote execution) auto memory_backend = cuopt::linear_programming::get_memory_backend_type(); std::vector> memory_resources; if (memory_backend == cuopt::linear_programming::memory_backend_t::GPU) { + // All arguments are parsed as string, default values are parsed as int if unused. const auto num_gpus = program.is_used("--num-gpus") ? std::stoi(program.get("--num-gpus")) : program.get("--num-gpus"); From c08115b8d43ef5cfd9a45c754886c1dda4059a85 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 24 Mar 2026 11:29:45 -0700 Subject: [PATCH 07/14] rename to hyper_ prefix --- .../cuopt/linear_programming/constants.h | 34 +++++------ cpp/src/math_optimization/solver_settings.cu | 34 +++++------ cpp/tests/mip/heuristics_hyper_params_test.cu | 56 +++++++++---------- 3 files changed, 62 insertions(+), 62 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index dc726de20b..512e05edd6 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -78,23 +78,23 @@ #define CUOPT_PDLP_PRECISION "pdlp_precision" /* @brief MIP heuristic hyper-parameter names */ -#define CUOPT_POPULATION_SIZE "population_size" -#define CUOPT_NUM_CPUFJ_THREADS "num_cpufj_threads" -#define CUOPT_PRESOLVE_TIME_RATIO "presolve_time_ratio" -#define CUOPT_PRESOLVE_MAX_TIME "presolve_max_time" -#define CUOPT_ROOT_LP_TIME_RATIO "root_lp_time_ratio" -#define CUOPT_ROOT_LP_MAX_TIME "root_lp_max_time" -#define CUOPT_RINS_TIME_LIMIT "rins_time_limit" -#define CUOPT_RINS_MAX_TIME_LIMIT "rins_max_time_limit" -#define CUOPT_RINS_FIX_RATE "rins_fix_rate" -#define CUOPT_STAGNATION_TRIGGER "stagnation_trigger" -#define CUOPT_MAX_ITERS_WITHOUT_IMPROVEMENT "max_iterations_without_improvement" -#define CUOPT_INITIAL_INFEASIBILITY_WEIGHT "initial_infeasibility_weight" -#define CUOPT_N_OF_MINIMUMS_FOR_EXIT "n_of_minimums_for_exit" -#define CUOPT_ENABLED_RECOMBINERS "enabled_recombiners" -#define CUOPT_CYCLE_DETECTION_LENGTH "cycle_detection_length" -#define CUOPT_RELAXED_LP_TIME_LIMIT "relaxed_lp_time_limit" -#define CUOPT_RELATED_VARS_TIME_LIMIT "related_vars_time_limit" +#define CUOPT_HYPER_POPULATION_SIZE "hyper_population_size" +#define CUOPT_HYPER_NUM_CPUFJ_THREADS "hyper_num_cpufj_threads" +#define CUOPT_HYPER_PRESOLVE_TIME_RATIO "hyper_presolve_time_ratio" +#define CUOPT_HYPER_PRESOLVE_MAX_TIME "hyper_presolve_max_time" +#define CUOPT_HYPER_ROOT_LP_TIME_RATIO "hyper_root_lp_time_ratio" +#define CUOPT_HYPER_ROOT_LP_MAX_TIME "hyper_root_lp_max_time" +#define CUOPT_HYPER_RINS_TIME_LIMIT "hyper_rins_time_limit" +#define CUOPT_HYPER_RINS_MAX_TIME_LIMIT "hyper_rins_max_time_limit" +#define CUOPT_HYPER_RINS_FIX_RATE "hyper_rins_fix_rate" +#define CUOPT_HYPER_STAGNATION_TRIGGER "hyper_stagnation_trigger" +#define CUOPT_HYPER_MAX_ITERS_WITHOUT_IMPROVEMENT "hyper_max_iterations_without_improvement" +#define CUOPT_HYPER_INITIAL_INFEASIBILITY_WEIGHT "hyper_initial_infeasibility_weight" +#define CUOPT_HYPER_N_OF_MINIMUMS_FOR_EXIT "hyper_n_of_minimums_for_exit" +#define CUOPT_HYPER_ENABLED_RECOMBINERS "hyper_enabled_recombiners" +#define CUOPT_HYPER_CYCLE_DETECTION_LENGTH "hyper_cycle_detection_length" +#define CUOPT_HYPER_RELAXED_LP_TIME_LIMIT "hyper_relaxed_lp_time_limit" +#define CUOPT_HYPER_RELATED_VARS_TIME_LIMIT "hyper_related_vars_time_limit" /* @brief MIP determinism mode constants */ #define CUOPT_MODE_OPPORTUNISTIC 0 diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 5ae52d8a53..62f15f35e3 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -101,16 +101,16 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {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)}, // MIP heuristic hyper-parameters (hidden from --help) - {CUOPT_PRESOLVE_TIME_RATIO, &mip_settings.heuristic_params.presolve_time_ratio, f_t(0.0), f_t(1.0), f_t(0.1), true, "fraction of total time for presolve"}, - {CUOPT_PRESOLVE_MAX_TIME, &mip_settings.heuristic_params.presolve_max_time, f_t(0.0), std::numeric_limits::infinity(), f_t(60.0), true, "hard cap on presolve seconds"}, - {CUOPT_ROOT_LP_TIME_RATIO, &mip_settings.heuristic_params.root_lp_time_ratio, f_t(0.0), f_t(1.0), f_t(0.1), true, "fraction of total time for root LP"}, - {CUOPT_ROOT_LP_MAX_TIME, &mip_settings.heuristic_params.root_lp_max_time, f_t(0.0), std::numeric_limits::infinity(), f_t(15.0), true, "hard cap on root LP seconds"}, - {CUOPT_RINS_TIME_LIMIT, &mip_settings.heuristic_params.rins_time_limit, f_t(0.0), std::numeric_limits::infinity(), f_t(3.0), true, "per-call RINS sub-MIP time"}, - {CUOPT_RINS_MAX_TIME_LIMIT, &mip_settings.heuristic_params.rins_max_time_limit, f_t(0.0), std::numeric_limits::infinity(), f_t(20.0), true, "ceiling for RINS adaptive time budget"}, - {CUOPT_RINS_FIX_RATE, &mip_settings.heuristic_params.rins_fix_rate, f_t(0.0), f_t(1.0), f_t(0.5), true, "RINS variable fix rate"}, - {CUOPT_INITIAL_INFEASIBILITY_WEIGHT, &mip_settings.heuristic_params.initial_infeasibility_weight, f_t(1e-9), std::numeric_limits::infinity(), f_t(1000.0), true, "constraint violation penalty seed"}, - {CUOPT_RELAXED_LP_TIME_LIMIT, &mip_settings.heuristic_params.relaxed_lp_time_limit, f_t(1e-9), std::numeric_limits::infinity(), f_t(1.0), true, "base relaxed LP time cap in heuristics"}, - {CUOPT_RELATED_VARS_TIME_LIMIT, &mip_settings.heuristic_params.related_vars_time_limit, f_t(1e-9), std::numeric_limits::infinity(), f_t(30.0), true, "time for related-variable structure build"}, + {CUOPT_HYPER_PRESOLVE_TIME_RATIO, &mip_settings.heuristic_params.presolve_time_ratio, f_t(0.0), f_t(1.0), f_t(0.1), true, "fraction of total time for presolve"}, + {CUOPT_HYPER_PRESOLVE_MAX_TIME, &mip_settings.heuristic_params.presolve_max_time, f_t(0.0), std::numeric_limits::infinity(), f_t(60.0), true, "hard cap on presolve seconds"}, + {CUOPT_HYPER_ROOT_LP_TIME_RATIO, &mip_settings.heuristic_params.root_lp_time_ratio, f_t(0.0), f_t(1.0), f_t(0.1), true, "fraction of total time for root LP"}, + {CUOPT_HYPER_ROOT_LP_MAX_TIME, &mip_settings.heuristic_params.root_lp_max_time, f_t(0.0), std::numeric_limits::infinity(), f_t(15.0), true, "hard cap on root LP seconds"}, + {CUOPT_HYPER_RINS_TIME_LIMIT, &mip_settings.heuristic_params.rins_time_limit, f_t(0.0), std::numeric_limits::infinity(), f_t(3.0), true, "per-call RINS sub-MIP time"}, + {CUOPT_HYPER_RINS_MAX_TIME_LIMIT, &mip_settings.heuristic_params.rins_max_time_limit, f_t(0.0), std::numeric_limits::infinity(), f_t(20.0), true, "ceiling for RINS adaptive time budget"}, + {CUOPT_HYPER_RINS_FIX_RATE, &mip_settings.heuristic_params.rins_fix_rate, f_t(0.0), f_t(1.0), f_t(0.5), true, "RINS variable fix rate"}, + {CUOPT_HYPER_INITIAL_INFEASIBILITY_WEIGHT, &mip_settings.heuristic_params.initial_infeasibility_weight, f_t(1e-9), std::numeric_limits::infinity(), f_t(1000.0), true, "constraint violation penalty seed"}, + {CUOPT_HYPER_RELAXED_LP_TIME_LIMIT, &mip_settings.heuristic_params.relaxed_lp_time_limit, f_t(1e-9), std::numeric_limits::infinity(), f_t(1.0), true, "base relaxed LP time cap in heuristics"}, + {CUOPT_HYPER_RELATED_VARS_TIME_LIMIT, &mip_settings.heuristic_params.related_vars_time_limit, f_t(1e-9), std::numeric_limits::infinity(), f_t(30.0), true, "time for related-variable structure build"}, }; // Int parameters @@ -142,13 +142,13 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {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}, // MIP heuristic hyper-parameters (hidden from --help) - {CUOPT_POPULATION_SIZE, &mip_settings.heuristic_params.population_size, 1, std::numeric_limits::max(), 32, true, "max solutions in pool"}, - {CUOPT_NUM_CPUFJ_THREADS, &mip_settings.heuristic_params.num_cpufj_threads, 0, std::numeric_limits::max(), 8, true, "parallel CPU FJ climbers"}, - {CUOPT_STAGNATION_TRIGGER, &mip_settings.heuristic_params.stagnation_trigger, 1, std::numeric_limits::max(), 3, true, "FP loops w/o improvement before recombination"}, - {CUOPT_MAX_ITERS_WITHOUT_IMPROVEMENT, &mip_settings.heuristic_params.max_iterations_without_improvement, 1, std::numeric_limits::max(), 8, true, "diversity step depth after stagnation"}, - {CUOPT_N_OF_MINIMUMS_FOR_EXIT, &mip_settings.heuristic_params.n_of_minimums_for_exit, 1, std::numeric_limits::max(), 7000, true, "FJ baseline local-minima exit threshold"}, - {CUOPT_ENABLED_RECOMBINERS, &mip_settings.heuristic_params.enabled_recombiners, 0, 15, 15, true, "bitmask: 1=BP 2=FP 4=LS 8=SubMIP"}, - {CUOPT_CYCLE_DETECTION_LENGTH, &mip_settings.heuristic_params.cycle_detection_length, 1, std::numeric_limits::max(), 30, true, "FP assignment cycle ring buffer length"}, + {CUOPT_HYPER_POPULATION_SIZE, &mip_settings.heuristic_params.population_size, 1, std::numeric_limits::max(), 32, true, "max solutions in pool"}, + {CUOPT_HYPER_NUM_CPUFJ_THREADS, &mip_settings.heuristic_params.num_cpufj_threads, 0, std::numeric_limits::max(), 8, true, "parallel CPU FJ climbers"}, + {CUOPT_HYPER_STAGNATION_TRIGGER, &mip_settings.heuristic_params.stagnation_trigger, 1, std::numeric_limits::max(), 3, true, "FP loops w/o improvement before recombination"}, + {CUOPT_HYPER_MAX_ITERS_WITHOUT_IMPROVEMENT, &mip_settings.heuristic_params.max_iterations_without_improvement, 1, std::numeric_limits::max(), 8, true, "diversity step depth after stagnation"}, + {CUOPT_HYPER_N_OF_MINIMUMS_FOR_EXIT, &mip_settings.heuristic_params.n_of_minimums_for_exit, 1, std::numeric_limits::max(), 7000, true, "FJ baseline local-minima exit threshold"}, + {CUOPT_HYPER_ENABLED_RECOMBINERS, &mip_settings.heuristic_params.enabled_recombiners, 0, 15, 15, true, "bitmask: 1=BP 2=FP 4=LS 8=SubMIP"}, + {CUOPT_HYPER_CYCLE_DETECTION_LENGTH, &mip_settings.heuristic_params.cycle_detection_length, 1, std::numeric_limits::max(), 30, true, "FP assignment cycle ring buffer length"}, }; // Bool parameters diff --git a/cpp/tests/mip/heuristics_hyper_params_test.cu b/cpp/tests/mip/heuristics_hyper_params_test.cu index a7e88800c5..ab2c447522 100644 --- a/cpp/tests/mip/heuristics_hyper_params_test.cu +++ b/cpp/tests/mip/heuristics_hyper_params_test.cu @@ -57,23 +57,23 @@ TEST_F(HeuristicsHyperParamsTest, CustomValuesRoundTrip) { { std::ofstream f(tmp_path); - f << "population_size = 64\n"; - f << "num_cpufj_threads = 4\n"; - f << "presolve_time_ratio = 0.2\n"; - f << "presolve_max_time = 120\n"; - f << "root_lp_time_ratio = 0.05\n"; - f << "root_lp_max_time = 30\n"; - f << "rins_time_limit = 5\n"; - f << "rins_max_time_limit = 40\n"; - f << "rins_fix_rate = 0.7\n"; - f << "stagnation_trigger = 5\n"; - f << "max_iterations_without_improvement = 12\n"; - f << "initial_infeasibility_weight = 500\n"; - f << "n_of_minimums_for_exit = 10000\n"; - f << "enabled_recombiners = 5\n"; - f << "cycle_detection_length = 50\n"; - f << "relaxed_lp_time_limit = 2\n"; - f << "related_vars_time_limit = 60\n"; + f << "hyper_population_size = 64\n"; + f << "hyper_num_cpufj_threads = 4\n"; + f << "hyper_presolve_time_ratio = 0.2\n"; + f << "hyper_presolve_max_time = 120\n"; + f << "hyper_root_lp_time_ratio = 0.05\n"; + f << "hyper_root_lp_max_time = 30\n"; + f << "hyper_rins_time_limit = 5\n"; + f << "hyper_rins_max_time_limit = 40\n"; + f << "hyper_rins_fix_rate = 0.7\n"; + f << "hyper_stagnation_trigger = 5\n"; + f << "hyper_max_iterations_without_improvement = 12\n"; + f << "hyper_initial_infeasibility_weight = 500\n"; + f << "hyper_n_of_minimums_for_exit = 10000\n"; + f << "hyper_enabled_recombiners = 5\n"; + f << "hyper_cycle_detection_length = 50\n"; + f << "hyper_relaxed_lp_time_limit = 2\n"; + f << "hyper_related_vars_time_limit = 60\n"; } settings_t settings; @@ -103,8 +103,8 @@ TEST_F(HeuristicsHyperParamsTest, PartialConfigKeepsDefaults) { { std::ofstream f(tmp_path); - f << "population_size = 128\n"; - f << "rins_fix_rate = 0.3\n"; + f << "hyper_population_size = 128\n"; + f << "hyper_rins_fix_rate = 0.3\n"; } settings_t settings; @@ -128,7 +128,7 @@ TEST_F(HeuristicsHyperParamsTest, CommentsAndBlankLinesIgnored) f << "# This is a comment\n"; f << "\n"; f << "# Another comment\n"; - f << "population_size = 42\n"; + f << "hyper_population_size = 42\n"; f << "\n"; } @@ -151,7 +151,7 @@ TEST_F(HeuristicsHyperParamsTest, BadNumericValueThrows) { { std::ofstream f(tmp_path); - f << "population_size = not_a_number\n"; + f << "hyper_population_size = not_a_number\n"; } settings_t settings; EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); @@ -161,7 +161,7 @@ TEST_F(HeuristicsHyperParamsTest, TrailingJunkSpaceSeparatedThrows) { { std::ofstream f(tmp_path); - f << "population_size = 64 foo\n"; + f << "hyper_population_size = 64 foo\n"; } settings_t settings; EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); @@ -171,7 +171,7 @@ TEST_F(HeuristicsHyperParamsTest, TrailingJunkNoSpaceThrows) { { std::ofstream f(tmp_path); - f << "population_size = 64foo\n"; + f << "hyper_population_size = 64foo\n"; } settings_t settings; EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); @@ -181,7 +181,7 @@ TEST_F(HeuristicsHyperParamsTest, TrailingJunkFloatThrows) { { std::ofstream f(tmp_path); - f << "rins_fix_rate = 0.5abc\n"; + f << "hyper_rins_fix_rate = 0.5abc\n"; } settings_t settings; EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); @@ -191,7 +191,7 @@ TEST_F(HeuristicsHyperParamsTest, RangeViolationCycleDetectionThrows) { { std::ofstream f(tmp_path); - f << "cycle_detection_length = 0\n"; + f << "hyper_cycle_detection_length = 0\n"; } settings_t settings; EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); @@ -201,7 +201,7 @@ TEST_F(HeuristicsHyperParamsTest, RangeViolationFixRateThrows) { { std::ofstream f(tmp_path); - f << "rins_fix_rate = 2.0\n"; + f << "hyper_rins_fix_rate = 2.0\n"; } settings_t settings; EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); @@ -226,7 +226,7 @@ TEST_F(HeuristicsHyperParamsTest, IndentedCommentAndWhitespaceLinesIgnored) std::ofstream f(tmp_path); f << " # indented comment\n"; f << " \t \n"; - f << "population_size = 99\n"; + f << "hyper_population_size = 99\n"; } settings_t settings; settings.load_parameters_from_file(tmp_path); @@ -237,7 +237,7 @@ TEST_F(HeuristicsHyperParamsTest, MixedSolverAndHyperParamsFromFile) { { std::ofstream f(tmp_path); - f << "population_size = 100\n"; + f << "hyper_population_size = 100\n"; f << "time_limit = 42\n"; } settings_t settings; From 42f977d7cacf785deb16df0e4ca09e22bdee41d2 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 31 Mar 2026 08:23:55 -0700 Subject: [PATCH 08/14] remove duplicate fields --- .../diversity/diversity_config.hpp | 32 +++++++--------- .../diversity/diversity_manager.cu | 37 ++++--------------- cpp/src/mip_heuristics/diversity/lns/rins.cu | 10 +++-- cpp/src/mip_heuristics/diversity/lns/rins.cuh | 3 -- 4 files changed, 28 insertions(+), 54 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_config.hpp b/cpp/src/mip_heuristics/diversity/diversity_config.hpp index de14260794..dacf7773de 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_config.hpp +++ b/cpp/src/mip_heuristics/diversity/diversity_config.hpp @@ -12,24 +12,20 @@ namespace cuopt::linear_programming::detail { struct diversity_config_t { - double time_ratio_on_init_lp = 0.1; - double max_time_on_lp = 15.0; - double time_ratio_of_probing_cache = 0.1; - double max_time_on_probing = 60.0; - int max_var_diff = 256; - size_t max_solutions = 32; - double initial_infeasibility_weight = 1000.; - double default_time_limit = 10.; - int initial_island_size = 3; - int maximum_island_size = 8; - bool use_avg_diversity = false; - double generation_time_limit_ratio = 0.6; - double max_island_gen_time = 600; - size_t n_sol_for_skip_init_gen = 3; - double max_fast_sol_time = 10; - double lp_run_time_if_feasible = 2.; - double lp_run_time_if_infeasible = 1.; - bool halve_population = false; + double time_ratio_of_probing_cache = 0.1; + double max_time_on_probing = 60.0; + int max_var_diff = 256; + double default_time_limit = 10.; + int initial_island_size = 3; + int maximum_island_size = 8; + bool use_avg_diversity = false; + double generation_time_limit_ratio = 0.6; + double max_island_gen_time = 600; + size_t n_sol_for_skip_init_gen = 3; + double max_fast_sol_time = 10; + double lp_run_time_if_feasible = 2.; + double lp_run_time_if_infeasible = 1.; + bool halve_population = false; }; } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index f15dc70cd1..52c9e6993a 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -37,45 +37,24 @@ size_t sub_mip_recombiner_config_t::max_n_of_vars_from_other = template std::vector recombiner_t::enabled_recombiners; -namespace { -diversity_config_t make_diversity_config(const mip_heuristics_hyper_params_t& hp) -{ - diversity_config_t c; - c.max_solutions = hp.population_size; - c.time_ratio_on_init_lp = hp.root_lp_time_ratio; - c.max_time_on_lp = hp.root_lp_max_time; - c.initial_infeasibility_weight = hp.initial_infeasibility_weight; - return c; -} - -rins_settings_t make_rins_settings(const mip_heuristics_hyper_params_t& hp) -{ - rins_settings_t s; - s.default_fixrate = hp.rins_fix_rate; - s.default_time_limit = hp.rins_time_limit; - s.max_time_limit = hp.rins_max_time_limit; - return s; -} -} // namespace - template diversity_manager_t::diversity_manager_t(mip_solver_context_t& context_) : context(context_), branch_and_bound_ptr(nullptr), problem_ptr(context.problem_ptr), - diversity_config(make_diversity_config(context_.settings.heuristic_params)), population("population", context, *this, diversity_config.max_var_diff, - diversity_config.max_solutions, - diversity_config.initial_infeasibility_weight * context.problem_ptr->n_constraints), + context_.settings.heuristic_params.population_size, + context_.settings.heuristic_params.initial_infeasibility_weight * + context.problem_ptr->n_constraints), lp_optimal_solution(context.problem_ptr->n_variables, context.problem_ptr->handle_ptr->get_stream()), lp_dual_optimal_solution(context.problem_ptr->n_constraints, context.problem_ptr->handle_ptr->get_stream()), ls(context, lp_optimal_solution), - rins(context, *this, make_rins_settings(context_.settings.heuristic_params)), + rins(context, *this), timer(diversity_config.default_time_limit), bound_prop_recombiner(context, context.problem_ptr->n_variables, @@ -404,10 +383,10 @@ solution_t diversity_manager_t::run_solver() return population.best_feasible(); } - population.timer = timer; - const f_t time_limit = timer.remaining_time(); - const f_t lp_time_limit = - std::min(diversity_config.max_time_on_lp, time_limit * diversity_config.time_ratio_on_init_lp); + population.timer = timer; + const f_t time_limit = timer.remaining_time(); + const auto& hp = context.settings.heuristic_params; + const f_t lp_time_limit = std::min(hp.root_lp_max_time, time_limit * hp.root_lp_time_ratio); // after every change to the problem, we should resize all the relevant vars // we need to encapsulate that to prevent repetitions recombine_stats.reset(); diff --git a/cpp/src/mip_heuristics/diversity/lns/rins.cu b/cpp/src/mip_heuristics/diversity/lns/rins.cu index d7d7601014..037abea740 100644 --- a/cpp/src/mip_heuristics/diversity/lns/rins.cu +++ b/cpp/src/mip_heuristics/diversity/lns/rins.cu @@ -31,8 +31,8 @@ rins_t::rins_t(mip_solver_context_t& context_, rins_settings_t settings_) : context(context_), problem_ptr(context.problem_ptr), dm(dm_), settings(settings_) { - fixrate = settings.default_fixrate; - time_limit = settings.default_time_limit; + fixrate = context.settings.heuristic_params.rins_fix_rate; + time_limit = context.settings.heuristic_params.rins_time_limit; } template @@ -295,7 +295,8 @@ void rins_t::run_rins() CUOPT_LOG_DEBUG("RINS submip time limit"); // do goldilocks update fixrate = std::min(fixrate + f_t(0.05), static_cast(settings.max_fixrate)); - time_limit = std::min(time_limit + f_t(2), static_cast(settings.max_time_limit)); + time_limit = std::min(time_limit + f_t(2), + static_cast(context.settings.heuristic_params.rins_max_time_limit)); } else if (branch_and_bound_status == dual_simplex::mip_status_t::INFEASIBLE) { CUOPT_LOG_DEBUG("RINS submip infeasible"); // do goldilocks update, decreasing fixrate @@ -304,7 +305,8 @@ void rins_t::run_rins() CUOPT_LOG_DEBUG("RINS solution not found"); // do goldilocks update fixrate = std::min(fixrate + f_t(0.05), static_cast(settings.max_fixrate)); - time_limit = std::min(time_limit + f_t(2), static_cast(settings.max_time_limit)); + time_limit = std::min(time_limit + f_t(2), + static_cast(context.settings.heuristic_params.rins_max_time_limit)); } cpu_fj_thread.stop_cpu_solver(); diff --git a/cpp/src/mip_heuristics/diversity/lns/rins.cuh b/cpp/src/mip_heuristics/diversity/lns/rins.cuh index 7a04b24897..0a9133f848 100644 --- a/cpp/src/mip_heuristics/diversity/lns/rins.cuh +++ b/cpp/src/mip_heuristics/diversity/lns/rins.cuh @@ -43,11 +43,8 @@ struct rins_settings_t { int nodes_after_later_improvement = 200; double min_fixrate = 0.3; double max_fixrate = 0.8; - double default_fixrate = 0.5; double min_fractional_ratio = 0.3; double min_time_limit = 3.; - double max_time_limit = 20.; - double default_time_limit = 3.; double target_mip_gap = 0.03; bool objective_cut = true; }; From 077fd1378965929a4382097c3138586a61973e3f Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Wed, 1 Apr 2026 02:02:03 -0700 Subject: [PATCH 09/14] restore RINS' time limit --- .../cuopt/linear_programming/mip/heuristics_hyper_params.hpp | 2 +- cpp/src/math_optimization/solver_settings.cu | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp b/cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp index c0b644544a..167c789526 100644 --- a/cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp +++ b/cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp @@ -24,7 +24,7 @@ struct mip_heuristics_hyper_params_t { double presolve_max_time = 60.0; // hard cap on presolve seconds double root_lp_time_ratio = 0.1; // fraction of total time for root LP double root_lp_max_time = 15.0; // hard cap on root LP seconds - double rins_time_limit = 3.0; // per-call RINS sub-MIP time + double rins_time_limit = 10.0; // per-call RINS sub-MIP time (former fixed cap) double rins_max_time_limit = 20.0; // ceiling for RINS adaptive time budget double rins_fix_rate = 0.5; // RINS variable fix rate int stagnation_trigger = 3; // FP loops w/o improvement before recombination diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 255620469d..f964b13107 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -105,7 +105,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_HYPER_PRESOLVE_MAX_TIME, &mip_settings.heuristic_params.presolve_max_time, f_t(0.0), std::numeric_limits::infinity(), f_t(60.0), true, "hard cap on presolve seconds"}, {CUOPT_HYPER_ROOT_LP_TIME_RATIO, &mip_settings.heuristic_params.root_lp_time_ratio, f_t(0.0), f_t(1.0), f_t(0.1), true, "fraction of total time for root LP"}, {CUOPT_HYPER_ROOT_LP_MAX_TIME, &mip_settings.heuristic_params.root_lp_max_time, f_t(0.0), std::numeric_limits::infinity(), f_t(15.0), true, "hard cap on root LP seconds"}, - {CUOPT_HYPER_RINS_TIME_LIMIT, &mip_settings.heuristic_params.rins_time_limit, f_t(0.0), std::numeric_limits::infinity(), f_t(3.0), true, "per-call RINS sub-MIP time"}, + {CUOPT_HYPER_RINS_TIME_LIMIT, &mip_settings.heuristic_params.rins_time_limit, f_t(0.0), std::numeric_limits::infinity(), f_t(10.0), true, "per-call RINS sub-MIP time"}, {CUOPT_HYPER_RINS_MAX_TIME_LIMIT, &mip_settings.heuristic_params.rins_max_time_limit, f_t(0.0), std::numeric_limits::infinity(), f_t(20.0), true, "ceiling for RINS adaptive time budget"}, {CUOPT_HYPER_RINS_FIX_RATE, &mip_settings.heuristic_params.rins_fix_rate, f_t(0.0), f_t(1.0), f_t(0.5), true, "RINS variable fix rate"}, {CUOPT_HYPER_INITIAL_INFEASIBILITY_WEIGHT, &mip_settings.heuristic_params.initial_infeasibility_weight, f_t(1e-9), std::numeric_limits::infinity(), f_t(1000.0), true, "constraint violation penalty seed"}, From c1ca3f9dc2aeb3af50a809595343d78634733ae9 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Wed, 1 Apr 2026 02:12:38 -0700 Subject: [PATCH 10/14] Revert "restore RINS' time limit" This reverts commit 077fd1378965929a4382097c3138586a61973e3f. --- .../cuopt/linear_programming/mip/heuristics_hyper_params.hpp | 2 +- cpp/src/math_optimization/solver_settings.cu | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp b/cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp index 167c789526..c0b644544a 100644 --- a/cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp +++ b/cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp @@ -24,7 +24,7 @@ struct mip_heuristics_hyper_params_t { double presolve_max_time = 60.0; // hard cap on presolve seconds double root_lp_time_ratio = 0.1; // fraction of total time for root LP double root_lp_max_time = 15.0; // hard cap on root LP seconds - double rins_time_limit = 10.0; // per-call RINS sub-MIP time (former fixed cap) + double rins_time_limit = 3.0; // per-call RINS sub-MIP time double rins_max_time_limit = 20.0; // ceiling for RINS adaptive time budget double rins_fix_rate = 0.5; // RINS variable fix rate int stagnation_trigger = 3; // FP loops w/o improvement before recombination diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index f964b13107..255620469d 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -105,7 +105,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_HYPER_PRESOLVE_MAX_TIME, &mip_settings.heuristic_params.presolve_max_time, f_t(0.0), std::numeric_limits::infinity(), f_t(60.0), true, "hard cap on presolve seconds"}, {CUOPT_HYPER_ROOT_LP_TIME_RATIO, &mip_settings.heuristic_params.root_lp_time_ratio, f_t(0.0), f_t(1.0), f_t(0.1), true, "fraction of total time for root LP"}, {CUOPT_HYPER_ROOT_LP_MAX_TIME, &mip_settings.heuristic_params.root_lp_max_time, f_t(0.0), std::numeric_limits::infinity(), f_t(15.0), true, "hard cap on root LP seconds"}, - {CUOPT_HYPER_RINS_TIME_LIMIT, &mip_settings.heuristic_params.rins_time_limit, f_t(0.0), std::numeric_limits::infinity(), f_t(10.0), true, "per-call RINS sub-MIP time"}, + {CUOPT_HYPER_RINS_TIME_LIMIT, &mip_settings.heuristic_params.rins_time_limit, f_t(0.0), std::numeric_limits::infinity(), f_t(3.0), true, "per-call RINS sub-MIP time"}, {CUOPT_HYPER_RINS_MAX_TIME_LIMIT, &mip_settings.heuristic_params.rins_max_time_limit, f_t(0.0), std::numeric_limits::infinity(), f_t(20.0), true, "ceiling for RINS adaptive time budget"}, {CUOPT_HYPER_RINS_FIX_RATE, &mip_settings.heuristic_params.rins_fix_rate, f_t(0.0), f_t(1.0), f_t(0.5), true, "RINS variable fix rate"}, {CUOPT_HYPER_INITIAL_INFEASIBILITY_WEIGHT, &mip_settings.heuristic_params.initial_infeasibility_weight, f_t(1e-9), std::numeric_limits::infinity(), f_t(1000.0), true, "constraint violation penalty seed"}, From 7788f3b197fd37cf9c6eed36b12f0cb3729b12b7 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Wed, 1 Apr 2026 02:38:16 -0700 Subject: [PATCH 11/14] review comments --- cpp/cuopt_cli.cpp | 14 ++--- .../cuopt/linear_programming/constants.h | 40 +++++++------ .../utilities/internals.hpp | 32 ++--------- cpp/src/math_optimization/solver_settings.cu | 48 ++++++++-------- cpp/tests/mip/heuristics_hyper_params_test.cu | 56 +++++++++---------- 5 files changed, 86 insertions(+), 104 deletions(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index b87b731b7e..88373d8247 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -261,10 +261,10 @@ int set_cuda_module_loading(int argc, char* argv[]) */ int main(int argc, char* argv[]) { - // Handle --show-hyper-params before argparse so no other args are required + // Handle --dump-hyper-params before argparse so no other args are required for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; - if (arg == "--show-hyper-params") { + if (arg == "--dump-hyper-params") { cuopt::linear_programming::solver_settings_t settings; settings.dump_parameters_to_file("/dev/stdout", true); return 0; @@ -303,7 +303,7 @@ int main(int argc, char* argv[]) .help("path to parameter config file (key = value format, supports all parameters)") .default_value(std::string("")); - program.add_argument("--show-hyper-params") + program.add_argument("--dump-hyper-params") .help("print hyper-parameters in config-file format and exit") .default_value(false) .implicit_value(true); @@ -334,7 +334,7 @@ int main(int argc, char* argv[]) // handle duplicate parameters appearing in MIP and LP settings if (arg_name_to_param_name.count(arg_name) == 0) { auto& arg = program.add_argument(arg_name.c_str()).default_value(param.default_value); - if (param.is_hyperparameter) { arg.hidden(); } + if (param.param_name.find("hyper_") != std::string::npos) { arg.hidden(); } arg_name_to_param_name[arg_name] = param.param_name; } } @@ -343,7 +343,7 @@ int main(int argc, char* argv[]) std::string arg_name = param_name_to_arg_name(param.param_name); if (arg_name_to_param_name.count(arg_name) == 0) { auto& arg = program.add_argument(arg_name.c_str()).default_value(param.default_value); - if (param.is_hyperparameter) { arg.hidden(); } + if (param.param_name.find("hyper_") != std::string::npos) { arg.hidden(); } arg_name_to_param_name[arg_name] = param.param_name; } } @@ -352,7 +352,7 @@ int main(int argc, char* argv[]) std::string arg_name = param_name_to_arg_name(param.param_name); if (arg_name_to_param_name.count(arg_name) == 0) { auto& arg = program.add_argument(arg_name.c_str()).default_value(param.default_value); - if (param.is_hyperparameter) { arg.hidden(); } + if (param.param_name.find("hyper_") != std::string::npos) { arg.hidden(); } arg_name_to_param_name[arg_name] = param.param_name; } } @@ -361,7 +361,7 @@ int main(int argc, char* argv[]) std::string arg_name = param_name_to_arg_name(param.param_name); if (arg_name_to_param_name.count(arg_name) == 0) { auto& arg = program.add_argument(arg_name.c_str()).default_value(param.default_value); - if (param.is_hyperparameter) { arg.hidden(); } + if (param.param_name.find("hyper_") != std::string::npos) { arg.hidden(); } arg_name_to_param_name[arg_name] = param.param_name; } } diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index 9ed27996bb..855aea9f2f 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -78,24 +78,28 @@ #define CUOPT_RANDOM_SEED "random_seed" #define CUOPT_PDLP_PRECISION "pdlp_precision" -/* @brief MIP heuristic hyper-parameter names */ -#define CUOPT_HYPER_POPULATION_SIZE "hyper_population_size" -#define CUOPT_HYPER_NUM_CPUFJ_THREADS "hyper_num_cpufj_threads" -#define CUOPT_HYPER_PRESOLVE_TIME_RATIO "hyper_presolve_time_ratio" -#define CUOPT_HYPER_PRESOLVE_MAX_TIME "hyper_presolve_max_time" -#define CUOPT_HYPER_ROOT_LP_TIME_RATIO "hyper_root_lp_time_ratio" -#define CUOPT_HYPER_ROOT_LP_MAX_TIME "hyper_root_lp_max_time" -#define CUOPT_HYPER_RINS_TIME_LIMIT "hyper_rins_time_limit" -#define CUOPT_HYPER_RINS_MAX_TIME_LIMIT "hyper_rins_max_time_limit" -#define CUOPT_HYPER_RINS_FIX_RATE "hyper_rins_fix_rate" -#define CUOPT_HYPER_STAGNATION_TRIGGER "hyper_stagnation_trigger" -#define CUOPT_HYPER_MAX_ITERS_WITHOUT_IMPROVEMENT "hyper_max_iterations_without_improvement" -#define CUOPT_HYPER_INITIAL_INFEASIBILITY_WEIGHT "hyper_initial_infeasibility_weight" -#define CUOPT_HYPER_N_OF_MINIMUMS_FOR_EXIT "hyper_n_of_minimums_for_exit" -#define CUOPT_HYPER_ENABLED_RECOMBINERS "hyper_enabled_recombiners" -#define CUOPT_HYPER_CYCLE_DETECTION_LENGTH "hyper_cycle_detection_length" -#define CUOPT_HYPER_RELAXED_LP_TIME_LIMIT "hyper_relaxed_lp_time_limit" -#define CUOPT_HYPER_RELATED_VARS_TIME_LIMIT "hyper_related_vars_time_limit" +#define CUOPT_MIP_HYPER_HEURISTIC_POPULATION_SIZE "mip_hyper_heuristic_population_size" +#define CUOPT_MIP_HYPER_HEURISTIC_NUM_CPUFJ_THREADS "mip_hyper_heuristic_num_cpufj_threads" +#define CUOPT_MIP_HYPER_HEURISTIC_PRESOLVE_TIME_RATIO "mip_hyper_heuristic_presolve_time_ratio" +#define CUOPT_MIP_HYPER_HEURISTIC_PRESOLVE_MAX_TIME "mip_hyper_heuristic_presolve_max_time" +#define CUOPT_MIP_HYPER_HEURISTIC_ROOT_LP_TIME_RATIO "mip_hyper_heuristic_root_lp_time_ratio" +#define CUOPT_MIP_HYPER_HEURISTIC_ROOT_LP_MAX_TIME "mip_hyper_heuristic_root_lp_max_time" +#define CUOPT_MIP_HYPER_HEURISTIC_RINS_TIME_LIMIT "mip_hyper_heuristic_rins_time_limit" +#define CUOPT_MIP_HYPER_HEURISTIC_RINS_MAX_TIME_LIMIT "mip_hyper_heuristic_rins_max_time_limit" +#define CUOPT_MIP_HYPER_HEURISTIC_RINS_FIX_RATE "mip_hyper_heuristic_rins_fix_rate" +#define CUOPT_MIP_HYPER_HEURISTIC_STAGNATION_TRIGGER "mip_hyper_heuristic_stagnation_trigger" +#define CUOPT_MIP_HYPER_HEURISTIC_MAX_ITERS_WITHOUT_IMPROVEMENT \ + "mip_hyper_heuristic_max_iterations_without_improvement" +#define CUOPT_MIP_HYPER_HEURISTIC_INITIAL_INFEASIBILITY_WEIGHT \ + "mip_hyper_heuristic_initial_infeasibility_weight" +#define CUOPT_MIP_HYPER_HEURISTIC_N_OF_MINIMUMS_FOR_EXIT \ + "mip_hyper_heuristic_n_of_minimums_for_exit" +#define CUOPT_MIP_HYPER_HEURISTIC_ENABLED_RECOMBINERS "mip_hyper_heuristic_enabled_recombiners" +#define CUOPT_MIP_HYPER_HEURISTIC_CYCLE_DETECTION_LENGTH \ + "mip_hyper_heuristic_cycle_detection_length" +#define CUOPT_MIP_HYPER_HEURISTIC_RELAXED_LP_TIME_LIMIT "mip_hyper_heuristic_relaxed_lp_time_limit" +#define CUOPT_MIP_HYPER_HEURISTIC_RELATED_VARS_TIME_LIMIT \ + "mip_hyper_heuristic_related_vars_time_limit" /* @brief MIP determinism mode constants */ #define CUOPT_MODE_OPPORTUNISTIC 0 diff --git a/cpp/include/cuopt/linear_programming/utilities/internals.hpp b/cpp/include/cuopt/linear_programming/utilities/internals.hpp index ff70a49123..bdfbb969d2 100644 --- a/cpp/include/cuopt/linear_programming/utilities/internals.hpp +++ b/cpp/include/cuopt/linear_programming/utilities/internals.hpp @@ -79,19 +79,13 @@ class base_solution_t { template struct parameter_info_t { - parameter_info_t(std::string_view param_name, - T* value, - T min, - T max, - T def, - bool is_hyperparameter = false, - const char* description = "") + parameter_info_t( + std::string_view param_name, T* value, T min, T max, T def, const char* description = "") : param_name(param_name), value_ptr(value), min_value(min), max_value(max), default_value(def), - is_hyperparameter(is_hyperparameter), description(description) { } @@ -100,28 +94,18 @@ struct parameter_info_t { T min_value; T max_value; T default_value; - bool is_hyperparameter; const char* description; }; template <> struct parameter_info_t { - parameter_info_t(std::string_view name, - bool* value, - bool def, - bool is_hyperparameter = false, - const char* description = "") - : param_name(name), - value_ptr(value), - default_value(def), - is_hyperparameter(is_hyperparameter), - description(description) + parameter_info_t(std::string_view name, bool* value, bool def, const char* description = "") + : param_name(name), value_ptr(value), default_value(def), description(description) { } std::string param_name; bool* value_ptr; bool default_value; - bool is_hyperparameter; const char* description; }; @@ -130,19 +114,13 @@ struct parameter_info_t { parameter_info_t(std::string_view name, std::string* value, std::string def, - bool is_hyperparameter = false, const char* description = "") - : param_name(name), - value_ptr(value), - default_value(def), - is_hyperparameter(is_hyperparameter), - description(description) + : param_name(name), value_ptr(value), default_value(def), description(description) { } std::string param_name; std::string* value_ptr; std::string default_value; - bool is_hyperparameter; const char* description; }; diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 255620469d..0a31bf3ecf 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -100,17 +100,17 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {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)}, - // MIP heuristic hyper-parameters (hidden from --help) - {CUOPT_HYPER_PRESOLVE_TIME_RATIO, &mip_settings.heuristic_params.presolve_time_ratio, f_t(0.0), f_t(1.0), f_t(0.1), true, "fraction of total time for presolve"}, - {CUOPT_HYPER_PRESOLVE_MAX_TIME, &mip_settings.heuristic_params.presolve_max_time, f_t(0.0), std::numeric_limits::infinity(), f_t(60.0), true, "hard cap on presolve seconds"}, - {CUOPT_HYPER_ROOT_LP_TIME_RATIO, &mip_settings.heuristic_params.root_lp_time_ratio, f_t(0.0), f_t(1.0), f_t(0.1), true, "fraction of total time for root LP"}, - {CUOPT_HYPER_ROOT_LP_MAX_TIME, &mip_settings.heuristic_params.root_lp_max_time, f_t(0.0), std::numeric_limits::infinity(), f_t(15.0), true, "hard cap on root LP seconds"}, - {CUOPT_HYPER_RINS_TIME_LIMIT, &mip_settings.heuristic_params.rins_time_limit, f_t(0.0), std::numeric_limits::infinity(), f_t(3.0), true, "per-call RINS sub-MIP time"}, - {CUOPT_HYPER_RINS_MAX_TIME_LIMIT, &mip_settings.heuristic_params.rins_max_time_limit, f_t(0.0), std::numeric_limits::infinity(), f_t(20.0), true, "ceiling for RINS adaptive time budget"}, - {CUOPT_HYPER_RINS_FIX_RATE, &mip_settings.heuristic_params.rins_fix_rate, f_t(0.0), f_t(1.0), f_t(0.5), true, "RINS variable fix rate"}, - {CUOPT_HYPER_INITIAL_INFEASIBILITY_WEIGHT, &mip_settings.heuristic_params.initial_infeasibility_weight, f_t(1e-9), std::numeric_limits::infinity(), f_t(1000.0), true, "constraint violation penalty seed"}, - {CUOPT_HYPER_RELAXED_LP_TIME_LIMIT, &mip_settings.heuristic_params.relaxed_lp_time_limit, f_t(1e-9), std::numeric_limits::infinity(), f_t(1.0), true, "base relaxed LP time cap in heuristics"}, - {CUOPT_HYPER_RELATED_VARS_TIME_LIMIT, &mip_settings.heuristic_params.related_vars_time_limit, f_t(1e-9), std::numeric_limits::infinity(), f_t(30.0), true, "time for related-variable structure build"}, + // MIP heuristic hyper-parameters (hidden from default --help: name contains "hyper_") + {CUOPT_MIP_HYPER_HEURISTIC_PRESOLVE_TIME_RATIO, &mip_settings.heuristic_params.presolve_time_ratio, f_t(0.0), f_t(1.0), f_t(0.1), "fraction of total time for presolve"}, + {CUOPT_MIP_HYPER_HEURISTIC_PRESOLVE_MAX_TIME, &mip_settings.heuristic_params.presolve_max_time, f_t(0.0), std::numeric_limits::infinity(), f_t(60.0), "hard cap on presolve seconds"}, + {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(30.0), "time for related-variable structure build"}, }; // Int parameters @@ -141,14 +141,14 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {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}, - // MIP heuristic hyper-parameters (hidden from --help) - {CUOPT_HYPER_POPULATION_SIZE, &mip_settings.heuristic_params.population_size, 1, std::numeric_limits::max(), 32, true, "max solutions in pool"}, - {CUOPT_HYPER_NUM_CPUFJ_THREADS, &mip_settings.heuristic_params.num_cpufj_threads, 0, std::numeric_limits::max(), 8, true, "parallel CPU FJ climbers"}, - {CUOPT_HYPER_STAGNATION_TRIGGER, &mip_settings.heuristic_params.stagnation_trigger, 1, std::numeric_limits::max(), 3, true, "FP loops w/o improvement before recombination"}, - {CUOPT_HYPER_MAX_ITERS_WITHOUT_IMPROVEMENT, &mip_settings.heuristic_params.max_iterations_without_improvement, 1, std::numeric_limits::max(), 8, true, "diversity step depth after stagnation"}, - {CUOPT_HYPER_N_OF_MINIMUMS_FOR_EXIT, &mip_settings.heuristic_params.n_of_minimums_for_exit, 1, std::numeric_limits::max(), 7000, true, "FJ baseline local-minima exit threshold"}, - {CUOPT_HYPER_ENABLED_RECOMBINERS, &mip_settings.heuristic_params.enabled_recombiners, 0, 15, 15, true, "bitmask: 1=BP 2=FP 4=LS 8=SubMIP"}, - {CUOPT_HYPER_CYCLE_DETECTION_LENGTH, &mip_settings.heuristic_params.cycle_detection_length, 1, std::numeric_limits::max(), 30, true, "FP assignment cycle ring buffer length"}, + // 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_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"}, }; // Bool parameters @@ -595,28 +595,28 @@ bool solver_settings_t::dump_parameters_to_file(const std::string& pat return false; } file << "# cuOpt parameter configuration (auto-generated)\n"; - file << "# Uncomment and change only the values you want to override.\n\n"; + file << "# Uncomment and change the values you wish to override.\n\n"; for (const auto& p : int_parameters) { - if (hyperparameters_only && !p.is_hyperparameter) continue; + 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.is_hyperparameter) continue; + 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.is_hyperparameter) continue; + 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.is_hyperparameter) continue; + 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"; } diff --git a/cpp/tests/mip/heuristics_hyper_params_test.cu b/cpp/tests/mip/heuristics_hyper_params_test.cu index ab2c447522..50e463b1fe 100644 --- a/cpp/tests/mip/heuristics_hyper_params_test.cu +++ b/cpp/tests/mip/heuristics_hyper_params_test.cu @@ -57,23 +57,23 @@ TEST_F(HeuristicsHyperParamsTest, CustomValuesRoundTrip) { { std::ofstream f(tmp_path); - f << "hyper_population_size = 64\n"; - f << "hyper_num_cpufj_threads = 4\n"; - f << "hyper_presolve_time_ratio = 0.2\n"; - f << "hyper_presolve_max_time = 120\n"; - f << "hyper_root_lp_time_ratio = 0.05\n"; - f << "hyper_root_lp_max_time = 30\n"; - f << "hyper_rins_time_limit = 5\n"; - f << "hyper_rins_max_time_limit = 40\n"; - f << "hyper_rins_fix_rate = 0.7\n"; - f << "hyper_stagnation_trigger = 5\n"; - f << "hyper_max_iterations_without_improvement = 12\n"; - f << "hyper_initial_infeasibility_weight = 500\n"; - f << "hyper_n_of_minimums_for_exit = 10000\n"; - f << "hyper_enabled_recombiners = 5\n"; - f << "hyper_cycle_detection_length = 50\n"; - f << "hyper_relaxed_lp_time_limit = 2\n"; - f << "hyper_related_vars_time_limit = 60\n"; + f << "mip_hyper_heuristic_population_size = 64\n"; + f << "mip_hyper_heuristic_num_cpufj_threads = 4\n"; + f << "mip_hyper_heuristic_presolve_time_ratio = 0.2\n"; + f << "mip_hyper_heuristic_presolve_max_time = 120\n"; + f << "mip_hyper_heuristic_root_lp_time_ratio = 0.05\n"; + f << "mip_hyper_heuristic_root_lp_max_time = 30\n"; + f << "mip_hyper_heuristic_rins_time_limit = 5\n"; + f << "mip_hyper_heuristic_rins_max_time_limit = 40\n"; + f << "mip_hyper_heuristic_rins_fix_rate = 0.7\n"; + f << "mip_hyper_heuristic_stagnation_trigger = 5\n"; + f << "mip_hyper_heuristic_max_iterations_without_improvement = 12\n"; + f << "mip_hyper_heuristic_initial_infeasibility_weight = 500\n"; + f << "mip_hyper_heuristic_n_of_minimums_for_exit = 10000\n"; + f << "mip_hyper_heuristic_enabled_recombiners = 5\n"; + f << "mip_hyper_heuristic_cycle_detection_length = 50\n"; + f << "mip_hyper_heuristic_relaxed_lp_time_limit = 2\n"; + f << "mip_hyper_heuristic_related_vars_time_limit = 60\n"; } settings_t settings; @@ -103,8 +103,8 @@ TEST_F(HeuristicsHyperParamsTest, PartialConfigKeepsDefaults) { { std::ofstream f(tmp_path); - f << "hyper_population_size = 128\n"; - f << "hyper_rins_fix_rate = 0.3\n"; + f << "mip_hyper_heuristic_population_size = 128\n"; + f << "mip_hyper_heuristic_rins_fix_rate = 0.3\n"; } settings_t settings; @@ -128,7 +128,7 @@ TEST_F(HeuristicsHyperParamsTest, CommentsAndBlankLinesIgnored) f << "# This is a comment\n"; f << "\n"; f << "# Another comment\n"; - f << "hyper_population_size = 42\n"; + f << "mip_hyper_heuristic_population_size = 42\n"; f << "\n"; } @@ -151,7 +151,7 @@ TEST_F(HeuristicsHyperParamsTest, BadNumericValueThrows) { { std::ofstream f(tmp_path); - f << "hyper_population_size = not_a_number\n"; + f << "mip_hyper_heuristic_population_size = not_a_number\n"; } settings_t settings; EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); @@ -161,7 +161,7 @@ TEST_F(HeuristicsHyperParamsTest, TrailingJunkSpaceSeparatedThrows) { { std::ofstream f(tmp_path); - f << "hyper_population_size = 64 foo\n"; + f << "mip_hyper_heuristic_population_size = 64 foo\n"; } settings_t settings; EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); @@ -171,7 +171,7 @@ TEST_F(HeuristicsHyperParamsTest, TrailingJunkNoSpaceThrows) { { std::ofstream f(tmp_path); - f << "hyper_population_size = 64foo\n"; + f << "mip_hyper_heuristic_population_size = 64foo\n"; } settings_t settings; EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); @@ -181,7 +181,7 @@ TEST_F(HeuristicsHyperParamsTest, TrailingJunkFloatThrows) { { std::ofstream f(tmp_path); - f << "hyper_rins_fix_rate = 0.5abc\n"; + f << "mip_hyper_heuristic_rins_fix_rate = 0.5abc\n"; } settings_t settings; EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); @@ -191,7 +191,7 @@ TEST_F(HeuristicsHyperParamsTest, RangeViolationCycleDetectionThrows) { { std::ofstream f(tmp_path); - f << "hyper_cycle_detection_length = 0\n"; + f << "mip_hyper_heuristic_cycle_detection_length = 0\n"; } settings_t settings; EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); @@ -201,7 +201,7 @@ TEST_F(HeuristicsHyperParamsTest, RangeViolationFixRateThrows) { { std::ofstream f(tmp_path); - f << "hyper_rins_fix_rate = 2.0\n"; + f << "mip_hyper_heuristic_rins_fix_rate = 2.0\n"; } settings_t settings; EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); @@ -226,7 +226,7 @@ TEST_F(HeuristicsHyperParamsTest, IndentedCommentAndWhitespaceLinesIgnored) std::ofstream f(tmp_path); f << " # indented comment\n"; f << " \t \n"; - f << "hyper_population_size = 99\n"; + f << "mip_hyper_heuristic_population_size = 99\n"; } settings_t settings; settings.load_parameters_from_file(tmp_path); @@ -237,7 +237,7 @@ TEST_F(HeuristicsHyperParamsTest, MixedSolverAndHyperParamsFromFile) { { std::ofstream f(tmp_path); - f << "hyper_population_size = 100\n"; + f << "mip_hyper_heuristic_population_size = 100\n"; f << "time_limit = 42\n"; } settings_t settings; From 2b29cf6774fdfd0f50ad2b2862c9de067a69889b Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Wed, 1 Apr 2026 03:25:05 -0700 Subject: [PATCH 12/14] AI review comments --- cpp/src/math_optimization/solver_settings.cu | 2 ++ cpp/src/mip_heuristics/problem/problem.cu | 3 +++ 2 files changed, 5 insertions(+) diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 0a31bf3ecf..0116f3362f 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -37,6 +38,7 @@ bool string_to_float(const std::string& value, f_t& result) 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; diff --git a/cpp/src/mip_heuristics/problem/problem.cu b/cpp/src/mip_heuristics/problem/problem.cu index 9b1d6a53ca..5d5fbc445a 100644 --- a/cpp/src/mip_heuristics/problem/problem.cu +++ b/cpp/src/mip_heuristics/problem/problem.cu @@ -203,6 +203,7 @@ problem_t::problem_t(const problem_t& problem_) clique_table(problem_.clique_table), vars_with_objective_coeffs(problem_.vars_with_objective_coeffs), expensive_to_fix_vars(problem_.expensive_to_fix_vars), + related_vars_time_limit(problem_.related_vars_time_limit), Q_offsets(problem_.Q_offsets), Q_indices(problem_.Q_indices), Q_values(problem_.Q_values) @@ -260,6 +261,7 @@ problem_t::problem_t(const problem_t& problem_, clique_table(problem_.clique_table), vars_with_objective_coeffs(problem_.vars_with_objective_coeffs), expensive_to_fix_vars(problem_.expensive_to_fix_vars), + related_vars_time_limit(problem_.related_vars_time_limit), Q_offsets(problem_.Q_offsets), Q_indices(problem_.Q_indices), Q_values(problem_.Q_values) @@ -360,6 +362,7 @@ problem_t::problem_t(const problem_t& problem_, bool no_deep fixing_helpers(problem_.fixing_helpers, handle_ptr), vars_with_objective_coeffs(problem_.vars_with_objective_coeffs), expensive_to_fix_vars(problem_.expensive_to_fix_vars), + related_vars_time_limit(problem_.related_vars_time_limit), Q_offsets(problem_.Q_offsets), Q_indices(problem_.Q_indices), Q_values(problem_.Q_values) From 41a32f37ca4bc1db8c8e3aeb528e7c30e3398c0e Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Wed, 1 Apr 2026 03:28:36 -0700 Subject: [PATCH 13/14] read params file before cudaSetDevice setup --- cpp/cuopt_cli.cpp | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 88373d8247..fd1a4bd476 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -85,27 +85,13 @@ inline cuopt::init_logger_t dummy_logger( * @brief Run a single file * @param file_path Path to the MPS format input file containing the optimization problem * @param initial_solution_file Path to initial solution file in SOL format - * @param settings_strings Map of solver parameters + * @param settings Merged solver settings (config file loaded in main, then CLI overrides applied) */ int run_single_file(const std::string& file_path, const std::string& initial_solution_file, bool solve_relaxation, - const std::map& settings_strings, - const std::string& params_file = "") + cuopt::linear_programming::solver_settings_t& settings) { - cuopt::linear_programming::solver_settings_t settings; - - try { - if (!params_file.empty()) { settings.load_parameters_from_file(params_file); } - for (auto& [key, val] : settings_strings) { - settings.set_parameter_from_string(key, val); - } - } catch (const std::exception& e) { - auto log = dummy_logger(settings); - CUOPT_LOG_ERROR("Error: %s", e.what()); - return -1; - } - std::string base_filename = file_path.substr(file_path.find_last_of("/\\") + 1); constexpr bool input_mps_strict = false; @@ -397,16 +383,26 @@ int main(int argc, char* argv[]) const auto initial_solution_file = program.get("--initial-solution"); const auto solve_relaxation = program.get("--relaxation"); + const auto params_file = program.get("--params-file"); + + cuopt::linear_programming::solver_settings_t settings; + try { + if (!params_file.empty()) { settings.load_parameters_from_file(params_file); } + for (auto& [key, val] : settings_strings) { + settings.set_parameter_from_string(key, val); + } + } catch (const std::exception& e) { + auto log = dummy_logger(settings); + CUOPT_LOG_ERROR("Error: %s", e.what()); + return -1; + } // Only initialize CUDA resources if using GPU memory backend (not remote execution) auto memory_backend = cuopt::linear_programming::get_memory_backend_type(); std::vector> memory_resources; if (memory_backend == cuopt::linear_programming::memory_backend_t::GPU) { - // All arguments are parsed as string, default values are parsed as int if unused. - const auto num_gpus = program.is_used("--num-gpus") - ? std::stoi(program.get("--num-gpus")) - : program.get("--num-gpus"); + const int num_gpus = settings.get_parameter(CUOPT_NUM_GPUS); for (int i = 0; i < std::min(raft::device_setter::get_device_count(), num_gpus); ++i) { RAFT_CUDA_TRY(cudaSetDevice(i)); @@ -416,8 +412,5 @@ int main(int argc, char* argv[]) RAFT_CUDA_TRY(cudaSetDevice(0)); } - const auto params_file = program.get("--params-file"); - - return run_single_file( - file_name, initial_solution_file, solve_relaxation, settings_strings, params_file); + return run_single_file(file_name, initial_solution_file, solve_relaxation, settings); } From 102f3af8ff39e54dfedf7da4e2ac9446a09b27f5 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Fri, 3 Apr 2026 01:43:16 -0700 Subject: [PATCH 14/14] cmdarg to dump all settings, not just hyper params --- cpp/cuopt_cli.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index fd1a4bd476..0cac35262f 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -247,7 +247,7 @@ int set_cuda_module_loading(int argc, char* argv[]) */ int main(int argc, char* argv[]) { - // Handle --dump-hyper-params before argparse so no other args are required + // Handle dump flags before argparse so no other args are required for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; if (arg == "--dump-hyper-params") { @@ -255,6 +255,11 @@ int main(int argc, char* argv[]) settings.dump_parameters_to_file("/dev/stdout", true); return 0; } + if (arg == "--dump-params") { + cuopt::linear_programming::solver_settings_t settings; + settings.dump_parameters_to_file("/dev/stdout", false); + return 0; + } } if (set_cuda_module_loading(argc, argv) != 0) { return 1; } @@ -290,7 +295,12 @@ int main(int argc, char* argv[]) .default_value(std::string("")); program.add_argument("--dump-hyper-params") - .help("print hyper-parameters in config-file format and exit") + .help("print hyper-parameters only in config file format and exit") + .default_value(false) + .implicit_value(true); + + program.add_argument("--dump-params") + .help("print all parameters in config file format and exit") .default_value(false) .implicit_value(true);