diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index da5f606771..0cac35262f 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -85,25 +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) + cuopt::linear_programming::solver_settings_t& settings) { - cuopt::linear_programming::solver_settings_t settings; - - try { - 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; @@ -259,6 +247,21 @@ int set_cuda_module_loading(int argc, char* argv[]) */ int main(int argc, char* argv[]) { + // 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") { + cuopt::linear_programming::solver_settings_t settings; + 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; } // Get the version string from the version_config.hpp file @@ -287,6 +290,20 @@ int main(int argc, char* argv[]) .default_value(true) .implicit_value(true); + 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-hyper-params") + .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); + std::map arg_name_to_param_name; // Register --pdlp-precision with string-to-int mapping so that it flows @@ -312,16 +329,17 @@ int main(int argc, char* argv[]) 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.param_name.find("hyper_") != std::string::npos) { 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.param_name.find("hyper_") != std::string::npos) { arg.hidden(); } arg_name_to_param_name[arg_name] = param.param_name; } } @@ -329,20 +347,21 @@ 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.param_name.find("hyper_") != std::string::npos) { 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.param_name.find("hyper_") != std::string::npos) { arg.hidden(); } arg_name_to_param_name[arg_name] = param.param_name; } - } // done with solver settings - } + } + } // done with solver settings // Parse arguments try { @@ -374,16 +393,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)); @@ -393,5 +422,5 @@ int main(int argc, char* argv[]) RAFT_CUDA_TRY(cudaSetDevice(0)); } - return run_single_file(file_name, initial_solution_file, solve_relaxation, settings_strings); + return run_single_file(file_name, initial_solution_file, solve_relaxation, settings); } diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index f431d7b733..c220423ab0 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -80,6 +80,29 @@ #define CUOPT_RANDOM_SEED "random_seed" #define CUOPT_PDLP_PRECISION "pdlp_precision" +#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 #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 new file mode 100644 index 0000000000..c0b644544a --- /dev/null +++ b/cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp @@ -0,0 +1,40 @@ +/* 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. + * 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(). + */ +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 + 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 617a8942f0..7e80c151d7 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 @@ -137,6 +138,8 @@ class mip_solver_settings_t { // TODO check with Akif and Alice pdlp_hyper_params::pdlp_hyper_params_t hyper_params; + mip_heuristics_hyper_params_t heuristic_params; + private: std::vector mip_callbacks_; 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..bdfbb969d2 100644 --- a/cpp/include/cuopt/linear_programming/utilities/internals.hpp +++ b/cpp/include/cuopt/linear_programming/utilities/internals.hpp @@ -79,8 +79,14 @@ 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, const char* description = "") + : param_name(param_name), + value_ptr(value), + min_value(min), + max_value(max), + default_value(def), + description(description) { } std::string param_name; @@ -88,28 +94,34 @@ struct parameter_info_t { T min_value; T max_value; T default_value; + 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, const char* description = "") + : param_name(name), value_ptr(value), default_value(def), description(description) { } std::string param_name; bool* value_ptr; bool default_value; + 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, + const char* description = "") + : param_name(name), value_ptr(value), default_value(def), description(description) { } std::string param_name; std::string* value_ptr; std::string default_value; + const char* description; }; /** diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 84d3ca300a..0d5400b32f 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -5,10 +5,17 @@ */ /* clang-format on */ +#include #include #include #include +#include +#include +#include +#include +#include + namespace cuopt::linear_programming { namespace { @@ -16,9 +23,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 +35,32 @@ 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); } + if (std::isnan(result)) { return false; } + return pos == value.size(); + } catch (const std::exception&) { return false; } } +std::string quote_if_needed(const std::string& s) +{ + bool needs_quoting = s.empty() || s.find(' ') != std::string::npos || + s.find('"') != std::string::npos || s.find('\t') != std::string::npos; + if (!needs_quoting) return s; + std::string out = "\""; + for (char c : s) { + if (c == '"') + out += "\\\""; + else + out += c; + } + out += '"'; + return out; +} + bool string_to_bool(const std::string& value, bool& result) { if (value == "true" || value == "True" || value == "TRUE" || value == "1" || value == "t" || @@ -75,7 +101,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 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 @@ -107,7 +144,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 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 @@ -477,6 +522,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 the values you wish to override.\n\n"; + for (const auto& p : int_parameters) { + if (hyperparameters_only && p.param_name.find("hyper_") == std::string::npos) continue; + if (p.description && p.description[0] != '\0') + file << "# " << p.description << " (int, range: [" << p.min_value << ", " << p.max_value + << "])\n"; + file << "# " << p.param_name << " = " << *p.value_ptr << "\n\n"; + } + for (const auto& p : float_parameters) { + if (hyperparameters_only && p.param_name.find("hyper_") == std::string::npos) continue; + if (p.description && p.description[0] != '\0') + file << "# " << p.description << " (double, range: [" << p.min_value << ", " << p.max_value + << "])\n"; + file << "# " << p.param_name << " = " << *p.value_ptr << "\n\n"; + } + for (const auto& p : bool_parameters) { + if (hyperparameters_only && p.param_name.find("hyper_") == std::string::npos) continue; + if (p.description && p.description[0] != '\0') file << "# " << p.description << " (bool)\n"; + file << "# " << p.param_name << " = " << (*p.value_ptr ? "true" : "false") << "\n\n"; + } + for (const auto& p : string_parameters) { + if (hyperparameters_only && p.param_name.find("hyper_") == std::string::npos) continue; + if (p.description && p.description[0] != '\0') file << "# " << p.description << " (string)\n"; + file << "# " << p.param_name << " = " << quote_if_needed(*p.value_ptr) << "\n\n"; + } + return true; +} + #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/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 2373423e95..174d910c1f 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -42,13 +42,13 @@ diversity_manager_t::diversity_manager_t(mip_solver_context_tn_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, @@ -247,7 +247,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 && @@ -423,10 +424,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(); @@ -435,7 +436,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/lns/rins.cu b/cpp/src/mip_heuristics/diversity/lns/rins.cu index 63d561cacb..0112a9c669 100644 --- a/cpp/src/mip_heuristics/diversity/lns/rins.cu +++ b/cpp/src/mip_heuristics/diversity/lns/rins.cu @@ -32,8 +32,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 @@ -298,7 +298,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 @@ -307,7 +308,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; }; 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/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 900fbd4593..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) @@ -802,8 +805,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 d18e49d906..a801cc4067 100644 --- a/cpp/src/mip_heuristics/problem/problem.cuh +++ b/cpp/src/mip_heuristics/problem/problem.cuh @@ -334,6 +334,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 8e9d2358e4..cd7c822ef1 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -180,6 +180,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); @@ -387,7 +388,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 3e55921a9d..706c069761 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -211,11 +211,14 @@ 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; - 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; // Stop early CPUFJ after cuopt presolve (probing cache) but before main solve if (context.early_cpufj_ptr) { diff --git a/cpp/tests/mip/CMakeLists.txt b/cpp/tests/mip/CMakeLists.txt index 2f2139890f..f2cf53ff6c 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(HEURISTICS_HYPER_PARAMS_TEST + ${CMAKE_CURRENT_SOURCE_DIR}/heuristics_hyper_params_test.cu +) 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..50e463b1fe --- /dev/null +++ b/cpp/tests/mip/heuristics_hyper_params_test.cu @@ -0,0 +1,283 @@ +/* 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 +#include +#include + +namespace cuopt::linear_programming::test { + +using settings_t = solver_settings_t; + +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) +{ + settings_t settings; + settings.dump_parameters_to_file(tmp_path, true); + + // Loading the commented-out dump should leave struct defaults unchanged + 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) +{ + settings_t settings; + settings.dump_parameters_to_file(tmp_path, true); + + settings_t reloaded; + EXPECT_NO_THROW(reloaded.load_parameters_from_file(tmp_path)); +} + +TEST_F(HeuristicsHyperParamsTest, CustomValuesRoundTrip) +{ + { + std::ofstream f(tmp_path); + 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; + 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) +{ + { + std::ofstream f(tmp_path); + f << "mip_hyper_heuristic_population_size = 128\n"; + f << "mip_hyper_heuristic_rins_fix_rate = 0.3\n"; + } + + 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); + + mip_heuristics_hyper_params_t defaults; + 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) +{ + { + std::ofstream f(tmp_path); + f << "# This is a comment\n"; + f << "\n"; + f << "# Another comment\n"; + f << "mip_hyper_heuristic_population_size = 42\n"; + f << "\n"; + } + + 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) +{ + { + std::ofstream f(tmp_path); + f << "bogus_key = 42\n"; + } + settings_t settings; + EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); +} + +TEST_F(HeuristicsHyperParamsTest, BadNumericValueThrows) +{ + { + std::ofstream f(tmp_path); + 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); +} + +TEST_F(HeuristicsHyperParamsTest, TrailingJunkSpaceSeparatedThrows) +{ + { + std::ofstream f(tmp_path); + f << "mip_hyper_heuristic_population_size = 64 foo\n"; + } + settings_t settings; + EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); +} + +TEST_F(HeuristicsHyperParamsTest, TrailingJunkNoSpaceThrows) +{ + { + std::ofstream f(tmp_path); + f << "mip_hyper_heuristic_population_size = 64foo\n"; + } + 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 << "mip_hyper_heuristic_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) +{ + { + std::ofstream f(tmp_path); + f << "mip_hyper_heuristic_cycle_detection_length = 0\n"; + } + settings_t settings; + EXPECT_THROW(settings.load_parameters_from_file(tmp_path), cuopt::logic_error); +} + +TEST_F(HeuristicsHyperParamsTest, RangeViolationFixRateThrows) +{ + { + std::ofstream f(tmp_path); + 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); +} + +TEST_F(HeuristicsHyperParamsTest, NonexistentFileThrows) +{ + settings_t settings; + EXPECT_THROW(settings.load_parameters_from_file("/tmp/does_not_exist_cuopt_test.config"), + cuopt::logic_error); +} + +TEST_F(HeuristicsHyperParamsTest, DirectoryPathThrows) +{ + settings_t settings; + EXPECT_THROW(settings.load_parameters_from_file("/tmp"), cuopt::logic_error); +} + +TEST_F(HeuristicsHyperParamsTest, IndentedCommentAndWhitespaceLinesIgnored) +{ + { + std::ofstream f(tmp_path); + f << " # indented comment\n"; + f << " \t \n"; + f << "mip_hyper_heuristic_population_size = 99\n"; + } + 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 << "mip_hyper_heuristic_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