diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index 0ac8c01875..787f57bec5 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -78,6 +78,7 @@ #define CUOPT_MIP_STRONG_CHVATAL_GOMORY_CUTS "mip_strong_chvatal_gomory_cuts" #define CUOPT_MIP_REDUCED_COST_STRENGTHENING "mip_reduced_cost_strengthening" #define CUOPT_MIP_RINS "mip_rins" +#define CUOPT_MIP_RENS "mip_rens" #define CUOPT_MIP_OBJECTIVE_STEP "mip_objective_step" #define CUOPT_MIP_CUT_CHANGE_THRESHOLD "mip_cut_change_threshold" #define CUOPT_MIP_CUT_MIN_ORTHOGONALITY "mip_cut_min_orthogonality" @@ -137,14 +138,16 @@ #define CUOPT_MIP_HYPER_DIVING_SHOW_TYPE "mip_hyper_diving_show_type" /* @brief Recursive sub-MIP (RINS) hyper-parameters */ -#define CUOPT_MIP_HYPER_SUBMIP_BASE_TARGET_FIXRATE "mip_hyper_submip_base_target_fixrate" -#define CUOPT_MIP_HYPER_SUBMIP_MIN_FIXRATE "mip_hyper_submip_min_fixrate" -#define CUOPT_MIP_HYPER_SUBMIP_MIN_FIXRATE_CAP "mip_hyper_submip_min_fixrate_cap" -#define CUOPT_MIP_HYPER_SUBMIP_TARGET_MIP_GAP "mip_hyper_submip_target_mip_gap" -#define CUOPT_MIP_HYPER_SUBMIP_NODE_LIMIT_BASE "mip_hyper_submip_node_limit_base" -#define CUOPT_MIP_HYPER_SUBMIP_MAX_LEVEL "mip_hyper_submip_max_level" -#define CUOPT_MIP_HYPER_SUBMIP_ITERATION_LIMIT_RATIO "mip_hyper_submip_iteration_limit_ratio" -#define CUOPT_MIP_HYPER_SUBMIP_ENABLE_CPUFJ "mip_hyper_submip_enable_cpufj" +#define CUOPT_MIP_HYPER_SUBMIP_BASE_TARGET_FIXRATE "mip_hyper_submip_base_target_fixrate" +#define CUOPT_MIP_HYPER_SUBMIP_MIN_FIXRATE "mip_hyper_submip_min_fixrate" +#define CUOPT_MIP_HYPER_SUBMIP_MIN_FIXRATE_CAP "mip_hyper_submip_min_fixrate_cap" +#define CUOPT_MIP_HYPER_SUBMIP_TARGET_MIP_GAP "mip_hyper_submip_target_mip_gap" +#define CUOPT_MIP_HYPER_SUBMIP_NODE_LIMIT_OFFSET "mip_hyper_submip_node_limit_offset" +#define CUOPT_MIP_HYPER_SUBMIP_ITERATION_LIMIT_OFFSET "mip_hyper_submip_iteration_limit_offset" +#define CUOPT_MIP_HYPER_SUBMIP_MAX_LEVEL "mip_hyper_submip_max_level" +#define CUOPT_MIP_HYPER_SUBMIP_ITERATION_LIMIT_RATIO "mip_hyper_submip_iteration_limit_ratio" +#define CUOPT_MIP_HYPER_SUBMIP_ROUND_CLOSE_RATIO "mip_hyper_submip_round_close_ratio" +#define CUOPT_MIP_HYPER_SUBMIP_ENABLE_CPUFJ "mip_hyper_submip_enable_cpufj" /* @brief Block bounded-variable-elimination step of cuOpt's internal MIP presolve */ #define CUOPT_MIP_HYPER_BLOCK_BVE "mip_hyper_block_bve" diff --git a/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp b/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp index 4464a156a7..3ea98f7ab3 100644 --- a/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp +++ b/cpp/include/cuopt/mathematical_optimization/mip/submip_hyper_params.hpp @@ -12,8 +12,9 @@ */ template struct mip_submip_hyper_params_t { - // Enable or disable (recursive) RINS: -1 automatic, 0 disabled, 1 enabled + // Enable or disable (recursive) RINS/RENS: -1 automatic, 0 disabled, 1 enabled i_t rins = -1; + i_t rens = -1; // Base for calculating the target fix rate for the neighbourhood. Actual target value is // determined automatically according to the success and infeasible rate. @@ -29,7 +30,10 @@ struct mip_submip_hyper_params_t { f_t target_mip_gap = 0.01; // The base node limit for the sub-MIP - i_t node_limit_base = 200; + i_t node_limit_offset = 200; + + // The base iteration limit for the sub-MIP + i_t iteration_limit_offset = 10000; // The current level in the recursion. This is an internal parameter and will set automatically. i_t level = 0; @@ -46,6 +50,10 @@ struct mip_submip_hyper_params_t { // the maximum number of nodes allow for backtracking. i_t dfs_max_backtrack = 5; + // How many variables a single round can fix. Set in terms of ratio of + // (1 - current fixrate). + f_t round_close_ratio = 0.8; + // Run CPU FJ over the sub-MIP bool enable_cpufj = true; }; diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 1454884a0e..29174148b7 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -213,7 +213,7 @@ inline char feasible_solution_symbol(heuristics_origin_t origin) inline char feasible_solution_symbol(search_strategy_t strategy, bool show_diving) { if (strategy == search_strategy_t::BEST_FIRST) return 'B'; - if (strategy == search_strategy_t::RINS) return 'S'; + if (strategy == search_strategy_t::RINS || strategy == search_strategy_t::RENS) return 'S'; if (!show_diving) return 'D'; switch (strategy) { @@ -225,6 +225,7 @@ inline char feasible_solution_symbol(search_strategy_t strategy, bool show_divin case search_strategy_t::FARKAS_DIVING: return 'F'; case search_strategy_t::VECTOR_LENGTH_DIVING: return 'V'; case search_strategy_t::RINS: return 'S'; + case search_strategy_t::RENS: return 'S'; } return 'U'; @@ -668,7 +669,9 @@ void branch_and_bound_t::set_solution_from_submip( const lp_problem_t& lp, const std::vector& solution, const third_party_presolve_t& presolver, - f_t fixrate) + submip_stats_t& submip_stats, + f_t fixrate, + [[maybe_unused]] std::string_view log_prefix) { bool check_postsolve = false; std::vector leaf_sol; @@ -679,10 +682,14 @@ void branch_and_bound_t::set_solution_from_submip( mutex_original_lp_.lock(); uncrush_primal_solution(original_problem_, lp, leaf_sol, user_sol); mutex_original_lp_.unlock(); - settings_.log.debug_format("SubMIP found a feasible solution with obj={:.4g}", obj); + + DEBUG_SUBMIP("{}Sub-MIP found a feasible solution with obj={:.4g}", + log_prefix, + compute_user_objective(lp, obj)); + bool success = set_solution_from_heuristics(user_sol, heuristics_origin_t::SUBMIP); if (success) { - rins_stats_.save_success(fixrate); + submip_stats.save_success(fixrate); if (settings_.solution_callback != nullptr) { settings_.solution_callback(user_sol, obj); } } } @@ -1033,6 +1040,7 @@ branch_variable_t branch_and_bound_t::variable_selection( return vector_length_diving(worker->leaf_problem, fractional, solution, log); case search_strategy_t::RINS: // This is used for solving the DFS of the sub-MIP. + case search_strategy_t::RENS: branch_var = pc_.variable_selection(fractional, solution); round_dir = martin_criteria(solution[branch_var], worker->root_solution[branch_var]); return {branch_var, round_dir}; @@ -1687,7 +1695,7 @@ void branch_and_bound_t::plunge_with(bfs_worker_t* worker, f_t rel_gap = user_relative_gap(user_obj, user_lower); f_t abs_gap = compute_user_abs_gap(original_lp_, upper_bound, lower_bound); - bool can_launch_rins = true; + bool can_launch_new_submip = true; while (stack.size() > 0 && (solver_status_ == mip_status_t::UNSET && is_running_) && rel_gap > settings_.relative_mip_gap_tol && abs_gap > settings_.absolute_mip_gap_tol) { @@ -1797,7 +1805,9 @@ void branch_and_bound_t::plunge_with(bfs_worker_t* worker, worker->recompute_bounds = node_status != node_status_t::HAS_CHILDREN; if (node_status == node_status_t::HAS_CHILDREN) { - if (can_launch_rins) { can_launch_rins = !launch_rins_worker(worker->leaf_solution.x); } + if (can_launch_new_submip) { + can_launch_new_submip = !launch_submip_worker(worker->leaf_solution.x); + } // The stack should only contain the children of the current parent. // If the stack size is greater than 0, @@ -2127,7 +2137,8 @@ void branch_and_bound_t::dive_with(diving_worker_t* worker, // This is called from the RINS method which already handle the return to the // pool part. Besides, they do not share the same pool. - if (worker->search_strategy != search_strategy_t::RINS) { + if (worker->search_strategy != search_strategy_t::RINS && + worker->search_strategy != search_strategy_t::RENS) { diving_worker_pool_.return_worker_to_pool(worker); } } @@ -2185,18 +2196,19 @@ bool branch_and_bound_t::launch_diving_worker(bfs_worker_t* } template -bool branch_and_bound_t::launch_rins_worker(const std::vector& sol) +bool branch_and_bound_t::launch_submip_worker(const std::vector& sol) { - if (settings_.submip_settings.rins == 0) return false; - if (!incumbent_.has_incumbent) return false; - if (rins_worker_pool_.num_idle() == 0) return false; + if (settings_.submip_settings.rins == 0 && settings_.submip_settings.rens == 0) return false; + if (settings_.submip_settings.rens == 0 && !incumbent_.has_incumbent) return false; + if (submip_worker_pool_.num_idle() == 0) return false; - diving_worker_t* worker = rins_worker_pool_.pop_idle_worker(); + diving_worker_t* worker = submip_worker_pool_.pop_idle_worker(); if (!worker) return false; std::vector current_incumbent; mutex_upper_.lock(); - current_incumbent = incumbent_.x; + bool use_rins = incumbent_.has_incumbent && settings_.submip_settings.rins != 0; + if (use_rins) current_incumbent = incumbent_.x; mutex_upper_.unlock(); // Note that this node does not have the vstatus (it was cleared at the start of B&B exploration) @@ -2205,17 +2217,17 @@ bool branch_and_bound_t::launch_rins_worker(const std::vector& so worker->leaf_problem.lower = original_lp_.lower; worker->leaf_problem.upper = original_lp_.upper; worker->leaf_solution.x = sol; - worker->search_strategy = search_strategy_t::RINS; + worker->search_strategy = use_rins ? search_strategy_t::RINS : search_strategy_t::RENS; worker->set_active(); if (settings_.inside_submip) { // LLVM libomp's GOMP compatibility path skips GCC's firstprivate copy // function for included tasks. - rins(worker, current_incumbent, var_types_); + recursive_submip(worker, current_incumbent, var_types_); } else { #pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) \ firstprivate(worker, current_incumbent) - rins(worker, current_incumbent, var_types_); + recursive_submip(worker, current_incumbent, var_types_); } return true; @@ -2225,28 +2237,16 @@ template void branch_and_bound_t::solve_submip(diving_worker_t* worker, const std::vector& current_incumbent, const std::vector& var_types, - i_t num_var_fixed, - i_t num_integers, - i_t submip_level, - std::string_view log_prefix, + submip_stats_t& submip_stats, + f_t fixrate, + i_t simplex_iter_used, bool is_root_heuristic) { double start_time = tic(); - std::vector& lower = worker->leaf_problem.lower; - std::vector& upper = worker->leaf_problem.upper; - std::vector& bounds_changed = worker->bounds_changed; - f_t fixrate = (f_t)num_var_fixed / num_integers; - - bool feasible = - worker->node_presolver.bounds_strengthening(settings_, bounds_changed, lower, upper); - - if (!feasible) { - // This should never happen since we are fixing bounds that are already in the incumbent. - rins_stats_.save_infeasible(fixrate); - DEBUG_SUBMIP("{} The problem is infeasible after running bound strengthening!", log_prefix); - return; - } + i_t submip_level = settings_.submip_settings.level + 1; + std::string log_prefix = + std::format("[{} {}] ", search_strategy_to_string(worker->search_strategy), submip_level); f_t user_lower = compute_user_objective(worker->leaf_problem, get_lower_bound()); f_t user_obj = compute_user_objective(worker->leaf_problem, upper_bound_.load()); @@ -2274,24 +2274,30 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke submip_settings.log.log_prefix = log_prefix; #endif - submip_settings.node_limit = settings_.submip_settings.node_limit_base + explored / 20; + submip_settings.node_limit = settings_.submip_settings.node_limit_offset + explored / 20; + + // Add offset only on the top call, we want number of simplex iteration to decay + // as we go down the recursion to avoid spending too much time in the deeper levels. + int64_t iter_offset = + settings_.inside_submip ? 0 : settings_.submip_settings.iteration_limit_offset; + int64_t simplex_iter = exploration_stats_.total_simplex_iters; + f_t iter_ratio = settings_.submip_settings.iteration_limit_ratio; + submip_settings.branch_and_bound_simplex_iteration_limit = - exploration_stats_.total_simplex_iters * settings_.submip_settings.iteration_limit_ratio; + iter_offset + simplex_iter * iter_ratio - simplex_iter_used; + if (submip_settings.branch_and_bound_simplex_iteration_limit <= 0) { return; } + submip_settings.time_limit = settings_.time_limit - toc(exploration_stats_.start_time); - if (submip_settings.time_limit < 0) { return; } + if (submip_settings.time_limit <= 0) { return; } submip_settings.relative_mip_gap_tol = std::min(settings_.submip_settings.target_mip_gap, rel_gap); bool max_recursion = submip_level > settings_.submip_settings.max_level; submip_settings.submip_settings.rins = settings_.submip_settings.rins != 0 && !max_recursion; + submip_settings.submip_settings.rens = settings_.submip_settings.rens != 0 && !max_recursion; - DEBUG_SUBMIP("{}Sub-MIP: num variables fixed={}/{} ({:.2f}%)", - log_prefix, - num_var_fixed, - num_integers, - fixrate * 100); - + DEBUG_SUBMIP("{}Sub-MIP: fixrate={:.2f}", log_prefix, fixrate) DEBUG_SUBMIP( "{}Sub-MIP solve settings: time_limit={:.2f}, node_limit={}, iter_limit={} (current_iter={}), " "tol={:g}", @@ -2318,32 +2324,18 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke if (presolver_status == third_party_presolve_status_t::INFEASIBLE || presolver_status == third_party_presolve_status_t::UNBNDORINFEAS || presolver_status == third_party_presolve_status_t::UNBOUNDED) { - rins_stats_.save_infeasible(fixrate); + DEBUG_SUBMIP("{}Presolve detected infeasibility", log_prefix); + submip_stats.save_infeasible(fixrate); return; } // Also handle optimal if (submip_problem.num_rows == 0 || submip_problem.num_cols == 0) { - DEBUG_SUBMIP("{}Sub-MIP presolved to a trivial {} x {} problem; solving by bound pushing", + DEBUG_SUBMIP("{}Reduced to a trivial {} x {} problem; solving by bound pushing", log_prefix, submip_problem.num_rows, submip_problem.num_cols); - - std::vector reduced_sol(submip_problem.num_cols); - - for (i_t j = 0; j < submip_problem.num_cols; ++j) { - const f_t c = submip_problem.objective[j]; - const f_t l = submip_problem.lower[j]; - const f_t u = submip_problem.upper[j]; - // Minimize c_j x_j over [l, u]; fall back to any finite bound (0 if both are infinite). - if (c < -settings_.zero_tol) { - reduced_sol[j] = std::isfinite(u) ? u : (std::isfinite(l) ? l : 0); - } else { - reduced_sol[j] = std::isfinite(l) ? l : (std::isfinite(u) ? u : 0); - } - } - - set_solution_from_submip(worker->leaf_problem, reduced_sol, presolver, fixrate); + submip_stats.save_empty(); return; } @@ -2355,10 +2347,12 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke submip_settings.heuristic_preemption_callback = nullptr; submip_settings.dual_simplex_objective_callback = nullptr; submip_settings.set_simplex_solution_callback = nullptr; - submip_settings.solution_callback = [this, &presolver, fixrate, worker]( - const std::vector& solution, f_t obj) { - this->set_solution_from_submip(worker->leaf_problem, solution, presolver, fixrate); - }; + submip_settings.solution_callback = + [this, &presolver, fixrate, &submip_stats, log_prefix, worker](const std::vector& solution, + f_t obj) { + this->set_solution_from_submip( + worker->leaf_problem, solution, presolver, submip_stats, fixrate, log_prefix); + }; DEBUG_SUBMIP("{}Sub-MIP: {} constraints, {} variables, {} nonzeros\n", log_prefix, @@ -2370,16 +2364,24 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke branch_and_bound_t submip_bnb(submip_problem, submip_settings, tic(), empty_probing); mip_solution_t submip_solution(submip_problem.num_cols); - // Crush the incumbent to presolve space. It may not be valid for the sub-MIP since we - // may fix integer variables that does not match the current incumbent to reach the target - // fix rate. std::vector presolved_incumbent; - presolver.crush_primal_solution(submip_problem, current_incumbent, presolved_incumbent); - submip_bnb.set_initial_guess(presolved_incumbent); - const f_t user_upper = compute_user_objective(worker->leaf_problem, upper_bound_.load()); - const f_t submip_cutoff = compute_presolved_objective(submip_bnb.original_lp_, user_upper); - submip_bnb.set_initial_upper_bound(submip_cutoff); + // We do not have an incumbent yet, so skip the initial guess. + if (!current_incumbent.empty()) { + // Crush the incumbent to presolve space. It may not be valid for the sub-MIP since we + // may fix integer variables that does not match the current incumbent to reach the target + // fix rate. + presolver.crush_primal_solution(submip_problem, current_incumbent, presolved_incumbent); + submip_bnb.set_initial_guess(presolved_incumbent); + } + + // Even if we do not have a valid incumbent now, the upper bound can still be set by the early + // heuristics. + if (std::isfinite(upper_bound_.load())) { + const f_t user_upper = compute_user_objective(worker->leaf_problem, upper_bound_.load()); + const f_t submip_cutoff = compute_presolved_objective(submip_bnb.original_lp_, user_upper); + submip_bnb.set_initial_upper_bound(submip_cutoff); + } if (!is_root_heuristic) submip_bnb.set_initial_pseudocost(pc_, presolver.get_reduced_to_original_map()); @@ -2401,6 +2403,12 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke fj_cpu_worker_t submip_fj_cpu_worker; if (settings_.submip_settings.enable_cpufj) { + // Since we do not have an incumbent, use the LP solution of the last round of variable fixing + // in RENS. + if (worker->search_strategy == search_strategy_t::RENS) { + presolver.crush_primal_solution(submip_problem, worker->leaf_solution.x, presolved_incumbent); + } + // Launch a CPU FJ worker on the presolved sub-MIP with a fixed budget (in terms of work units) // to run in parallel with the cut-and-branch algorithm with the goal of finding a quick // feasible solution for the sub-MIP problem. The CPU FJ uses the current incumbent (crushed @@ -2443,12 +2451,13 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke if (submip_status == mip_status_t::NUMERICAL) { return; } if (submip_status == mip_status_t::INFEASIBLE || submip_status == mip_status_t::UNBOUNDED) { - rins_stats_.save_infeasible(fixrate); + submip_stats.save_infeasible(fixrate); return; } if (submip_solution.has_incumbent) { - set_solution_from_submip(worker->leaf_problem, submip_solution.x, presolver, fixrate); + set_solution_from_submip( + worker->leaf_problem, submip_solution.x, presolver, submip_stats, fixrate, log_prefix); } // Accumulate simplex iterations to determine when to stop exploring the sub-MIP @@ -2494,6 +2503,8 @@ void get_unfixed_integer_variables(const std::vector& lower, if (std::abs(lower[j] - upper[j]) <= fixed_tol) { continue; } integer_list.push_back(j); } + + assert(!integer_list.empty() && "The integer list cannot be empty!"); } template @@ -2510,38 +2521,68 @@ void fix_variable(i_t j, } template -void apply_rins_fixings(const simplex_solver_settings_t& settings, - const std::vector& current_sol, - const std::vector& fractional, - const std::vector& current_incumbent, - i_t max_var_fixed, - std::vector& lower, - std::vector& upper, - std::vector& bounds_changed, - i_t& num_var_fixed) +i_t apply_rens_fixings(const simplex_solver_settings_t& settings, + const std::vector& node_solution, + const std::vector& integer_list, + i_t target_num_fixed, + std::vector& lower, + std::vector& upper, + std::vector& bounds_changed) { - for (i_t j : fractional) { - if (std::abs(lower[j] - upper[j]) <= settings.fixed_tol) { continue; } + i_t num_fixed = 0; + i_t num_bound_changed = 0; + + for (i_t j : integer_list) { + if (num_fixed >= target_num_fixed) break; + if (std::abs(lower[j] - upper[j]) <= settings.fixed_tol) continue; + f_t old_lower = lower[j]; + f_t old_upper = upper[j]; + lower[j] = std::clamp(std::floor(node_solution[j]), old_lower, old_upper); + upper[j] = std::clamp(std::ceil(node_solution[j]), old_lower, old_upper); + bounds_changed[j] = lower[j] != old_lower || upper[j] != old_upper; + num_bound_changed += bounds_changed[j]; + if (std::abs(lower[j] - upper[j]) <= settings.fixed_tol) ++num_fixed; + } + + return num_bound_changed; +} + +template +i_t apply_rins_fixings(const simplex_solver_settings_t& settings, + const std::vector& current_sol, + const std::vector& integer_list, + const std::vector& current_incumbent, + f_t target_fixrate, + std::vector& lower, + std::vector& upper, + std::vector& bounds_changed) +{ + i_t num_fixed = 0; + i_t target_num_fixed = target_fixrate * integer_list.size(); + + for (i_t j : integer_list) { + if (num_fixed >= target_num_fixed) break; + if (std::abs(lower[j] - upper[j]) <= settings.fixed_tol) continue; if (std::abs(current_sol[j] - current_incumbent[j]) <= settings.integer_tol) { f_t fixed_val = std::round(current_sol[j]); fix_variable(j, lower, upper, bounds_changed, fixed_val); - ++num_var_fixed; - if (num_var_fixed >= max_var_fixed) break; + ++num_fixed; } } + + return num_fixed; } template -void extend_variable_fixings(const simplex_solver_settings_t& settings, - const std::vector& obj_coeffs, - const std::vector& fractional, - const std::vector& current_sol, - const std::vector& root_solution, - i_t max_var_fixed, - std::vector& lower, - std::vector& upper, - std::vector& bounds_changed, - i_t& num_var_fixed) +i_t extend_variable_fixings(const simplex_solver_settings_t& settings, + const std::vector& obj_coeffs, + const std::vector& fractional, + const std::vector& current_sol, + const std::vector& root_solution, + i_t target_num_fixed, + std::vector& lower, + std::vector& upper, + std::vector& bounds_changed) { std::vector> candidates; for (i_t j : fractional) { @@ -2570,43 +2611,72 @@ void extend_variable_fixings(const simplex_solver_settings_t& settings return std::get<0>(a) < std::get<0>(b); }); - f_t change = 0; + i_t num_fixed = 0; + f_t change = 0; + for (auto [dist, j, fixed_val] : candidates) { + if (num_fixed >= target_num_fixed) break; + fix_variable(j, lower, upper, bounds_changed, fixed_val); - ++num_var_fixed; - if (num_var_fixed >= max_var_fixed) break; + ++num_fixed; // Limit the amount of fixing to the current LP. change += dist; - if (change >= 0.5) { break; } + if (change >= 0.5) break; + } + + return num_fixed; +} + +template +f_t calculate_fixrate(const std::vector& integer_list, + const std::vector& lower, + const std::vector& upper, + f_t fixed_tol) +{ + i_t num_fixed = 0; + for (i_t j : integer_list) { + if (std::abs(lower[j] - upper[j]) <= fixed_tol) ++num_fixed; } + + return (f_t)num_fixed / integer_list.size(); } template -void branch_and_bound_t::rins(diving_worker_t* worker, - const std::vector& current_incumbent, - const std::vector& var_types, - bool is_root_heuristic) +void branch_and_bound_t::recursive_submip(diving_worker_t* worker, + const std::vector& current_incumbent, + const std::vector& var_types, + bool is_root_heuristic) { - raft::common::nvtx::range scope("BB::rins_thread"); + raft::common::nvtx::range scope("BB::submip_thread"); if (worker->orbital_fixing) { worker->orbital_fixing->disable(); } - i_t submip_level = settings_.submip_settings.level + 1; - std::string log_prefix = std::format("[RINS {}] ", submip_level); + i_t submip_level = settings_.submip_settings.level + 1; + std::string log_prefix = + std::format("[{} {}] ", search_strategy_to_string(worker->search_strategy), submip_level); + + assert((worker->search_strategy == search_strategy_t::RINS || + worker->search_strategy == search_strategy_t::RENS) && + "Sub-MIP worker must be set to RINS or RENS type"); + + submip_stats_t& submip_stats = + worker->search_strategy == search_strategy_t::RINS ? rins_stats_ : rens_stats_; - ++rins_stats_.total_calls; + ++submip_stats.total_calls; bool has_submip = false; worker->recompute_bounds = false; worker->recompute_basis = true; - branch_and_bound_stats_t rins_stats; + branch_and_bound_stats_t stats; mip_node_t& node = worker->start_node; std::vector& lower = worker->leaf_problem.lower; std::vector& upper = worker->leaf_problem.upper; std::vector& bounds_changed = worker->bounds_changed; std::vector& current_sol = worker->leaf_solution.x; + std::fill(bounds_changed.begin(), bounds_changed.end(), false); + std::vector fractional; i_t num_frac = fractional_variables(settings_, current_sol, var_types, fractional); @@ -2614,102 +2684,86 @@ void branch_and_bound_t::rins(diving_worker_t* worker, get_unfixed_integer_variables(lower, upper, var_types, settings_.fixed_tol, integer_list); i_t num_integers = integer_list.size(); + f_t max_fixrate = submip_get_max_fixrate(submip_stats, settings_.submip_settings, worker->rng); + f_t min_fixrate = settings_.submip_settings.min_fixrate; + f_t fixrate = 0; + f_t close_ratio = settings_.submip_settings.round_close_ratio; - f_t max_fixrate = submip_get_max_fixrate(rins_stats_, settings_.submip_settings, worker->rng); - f_t min_fixrate = std::min(settings_.submip_settings.min_fixrate, max_fixrate); - i_t max_var_fixed = max_fixrate * num_integers; - i_t min_var_fixed = min_fixrate * num_integers; - i_t num_var_fixed = 0; + i_t round = 0; while (solver_status_ == mip_status_t::UNSET && is_running_ && !worker->halt) { - // RINS neighbourhood 1: Fix all the integer variables where the starting solution matches the - // current incumbent, considering only the fractional values in the current node - i_t prev_num_fixed = num_var_fixed; - apply_rins_fixings(settings_, - current_sol, - fractional, - current_incumbent, - max_var_fixed, - lower, - upper, - bounds_changed, - num_var_fixed); - - // Enough variables has been fixed - if (num_var_fixed >= min_var_fixed) { - DEBUG_SUBMIP("{}Fixed {} variables (max={}, min={})\n", - log_prefix, - num_var_fixed, - max_var_fixed, - min_var_fixed); - has_submip = true; - break; - } - - if (toc(exploration_stats_.start_time) > settings_.time_limit) { - solver_status_ = mip_status_t::TIME_LIMIT; - break; - } - - if (prev_num_fixed == num_var_fixed) { - // RINS neighbourhood 2: Search the entire list of integer variables where the current - // LP solution matches the current incumbent. - apply_rins_fixings(settings_, - current_sol, - integer_list, - current_incumbent, - max_var_fixed, - lower, - upper, - bounds_changed, - num_var_fixed); - - // Enough variables were fixed - if (num_var_fixed >= min_var_fixed) { - DEBUG_SUBMIP("{}Fixed {} variables (max={}, min={})\n", - log_prefix, - num_var_fixed, - max_var_fixed, - min_var_fixed); + f_t prev_fixrate = fixrate; + f_t distance = 1.0 - (1.0 - prev_fixrate) * close_ratio; + f_t round_target_fixrate = std::min(distance, max_fixrate) - prev_fixrate; + i_t round_target = round_target_fixrate * num_integers; + i_t num_bound_changed = 0; + // Shuffle the fractional and integer list, so every variable has the same chance to the picked + // (we iterate the list in order). + worker->rng.shuffle(integer_list); + worker->rng.shuffle(fractional); + if (worker->search_strategy == search_strategy_t::RINS) { + // RINS neighbourhood: Fix all the integer variables where the current solution matches the + // incumbent. We are using the `max_fixrate` here to allow RINS to fix all integer variables + // that it can within our budget. + num_bound_changed = apply_rins_fixings(settings_, + current_sol, + integer_list, + current_incumbent, + max_fixrate - prev_fixrate, + lower, + upper, + bounds_changed); + + // The RINS neighbourhood ran dry. If it is already tight enough, take it rather than + // diluting it with fixings that do not agree with the incumbent. + if (num_bound_changed == 0 && fixrate >= min_fixrate) { has_submip = true; break; } - // Even considering the entire integer list, we were unable to fix a single variable in this - // iteration. Iterate over the fractional variables again and fixing those that closest to - // an integer solution first in order to reach the fixing threshold. - if (prev_num_fixed == num_var_fixed) { - extend_variable_fixings(settings_, - worker->leaf_problem.objective, - fractional, - current_sol, - worker->root_solution, - max_var_fixed, - lower, - upper, - bounds_changed, - num_var_fixed); - - if (num_var_fixed >= min_var_fixed) { - DEBUG_SUBMIP("{}Fixed {} variables (max={}, min={})\n", - log_prefix, - num_var_fixed, - max_var_fixed, - min_var_fixed); - has_submip = true; + } else if (worker->search_strategy == search_strategy_t::RENS) { + if (round_target == 0) { + round_target_fixrate = max_fixrate - prev_fixrate; + round_target = round_target_fixrate * num_integers; + if (round_target == 0) { + has_submip = fixrate > 0; break; } + } - if (prev_num_fixed == num_var_fixed) { - DEBUG_SUBMIP("{}Could not fix more variables ({}, max={}, min={})\n", - log_prefix, - num_var_fixed, - max_var_fixed, - min_var_fixed); - has_submip = true; + num_bound_changed = apply_rens_fixings( + settings_, current_sol, integer_list, round_target, lower, upper, bounds_changed); + } + + // Even considering the entire integer list, we were unable to fix a single variable in this + // iteration. Iterate over the fractional variables again and fixing those that closest to + // an integer solution first in order to reach the fixing threshold. + if (num_bound_changed == 0) { + if (round_target == 0) { + round_target_fixrate = max_fixrate - prev_fixrate; + round_target = round_target_fixrate * num_integers; + if (round_target == 0) { + has_submip = fixrate > 0; break; } } + + num_bound_changed = extend_variable_fixings(settings_, + worker->leaf_problem.objective, + fractional, + current_sol, + worker->root_solution, + round_target, + lower, + upper, + bounds_changed); + + // Even sweep over all integer variables, we exhausted all variables that can be fixed. + // If this is the case, then tries to solve the sub-mip anyway. + if (num_bound_changed == 0) { + has_submip = true; + break; + } } if (toc(exploration_stats_.start_time) > settings_.time_limit) { @@ -2717,14 +2771,64 @@ void branch_and_bound_t::rins(diving_worker_t* worker, break; } + bool is_feasible = + worker->node_presolver.bounds_strengthening(settings_, bounds_changed, lower, upper); + fixrate = calculate_fixrate(integer_list, lower, upper, settings_.fixed_tol); + + DEBUG_SUBMIP( + "{}Round {}: fixed {:.0f} ({:.2f}) -> {:.0f} ({:.2f}) variables. target round fixrate = {} " + "({:.2f}). " + "max fixrate = {:.4g}", + log_prefix, + round, + prev_fixrate * num_integers, + prev_fixrate, + fixrate * num_integers, + fixrate, + round_target, + round_target_fixrate, + max_fixrate); + + if (!is_feasible) { + DEBUG_SUBMIP("{}Round {}: bound strengthening detected infeasibility.", log_prefix, round) + break; + } + + if (fixrate >= max_fixrate) { + has_submip = true; + break; + } + // After fixing the variables, re-solve the LP relaxation. We use the optimal solution // in the next iteration to find additional variable fixings. // We continue to do this until enough variables were fixed or no variable is left to fix. logger_t log; - log.log = false; - dual_status_t lp_status = solve_node_lp(&node, worker, rins_stats, log); + log.log = false; - if (lp_status != dual_status_t::OPTIMAL) { break; } + int64_t iter_offset = + settings_.inside_submip ? 0 : settings_.submip_settings.iteration_limit_offset; + int64_t simplex_iter = exploration_stats_.total_simplex_iters; + f_t iter_ratio = settings_.submip_settings.iteration_limit_ratio; + int64_t simplex_iter_limit = iter_offset + simplex_iter * iter_ratio; + i_t max_iter = std::min(simplex_iter_limit - stats.total_simplex_iters, + std::numeric_limits::max()); + if (max_iter <= 0) { + DEBUG_SUBMIP("{}Round {}: max iteration reached! {}/{}", + log_prefix, + round, + stats.total_simplex_iters.load(), + simplex_iter_limit) + break; + } + + dual_status_t lp_status = solve_node_lp(&node, worker, stats, log, max_iter); + if (lp_status != dual_status_t::OPTIMAL) { + DEBUG_SUBMIP("{}Round {}: simplex returned {}", + log_prefix, + round, + simplex::dual_status_to_string(lp_status)) + break; + } fractional.clear(); num_frac = fractional_variables(settings_, current_sol, var_types, fractional); @@ -2733,25 +2837,41 @@ void branch_and_bound_t::rins(diving_worker_t* worker, node.lower_bound = leaf_obj; snap_to_lattice(&node, leaf_obj); - if (leaf_obj > upper_bound_.load()) { break; } + if (leaf_obj > upper_bound_.load()) { + DEBUG_SUBMIP("{}Round {}: reached cutoff point. obj={:.4g}. upper_bound={:.4g}", + log_prefix, + round, + leaf_obj, + upper_bound_.load()) + break; + } if (num_frac == 0) { - // We found a feasible solution when fixing the variables in RINS. - add_feasible_solution(leaf_obj, current_sol, -1, search_strategy_t::RINS); + // We found a feasible solution when fixing the variables in RINS/RENS. + add_feasible_solution(leaf_obj, current_sol, -1, worker->search_strategy); + DEBUG_SUBMIP("{}Round {}: found a solution with obj={:.4g}. upper_bound={:.4g}", + log_prefix, + round, + leaf_obj, + upper_bound_.load()) break; } worker->recompute_basis = false; + ++round; } - f_t fixrate = (f_t)num_var_fixed / num_integers; + // Accumulate the iterations for sub-MIP so it stops when it reaches the allocated budget. + if (settings_.inside_submip) { + exploration_stats_.total_simplex_iters += stats.total_simplex_iters; + } if (has_submip) { // If not enough variables was fixed (the neighbourhood is too loose) or the sub-MIP already // found a solution that improved the incumbent, then do a DFS with a backtrack_limit of 5 // levels up to try to find a feasible solution quickly from the neighbourhood. if (fixrate < settings_.submip_settings.min_fixrate_cap || - (settings_.inside_submip && rins_stats_.total_success != 0)) { + (settings_.inside_submip && submip_stats.total_success != 0)) { worker->start_node.packed_vstatus = simplex::compress_vstatus(worker->leaf_vstatus); worker->start_lower = lower; worker->start_upper = upper; @@ -2780,7 +2900,11 @@ void branch_and_bound_t::rins(diving_worker_t* worker, // We need the pseudocost to do the DFS, which we do not have during the cut passes. if (!is_root_heuristic) { - DEBUG_SUBMIP("{} Running a quick DFS for the submip!", log_prefix); + DEBUG_SUBMIP("{}Running a quick DFS. fixrate={:.4g} ({}/{})", + log_prefix, + fixrate, + fixrate * num_integers, + num_integers); dive_with(worker, settings_.submip_settings.dfs_max_backtrack); } } @@ -2789,36 +2913,29 @@ void branch_and_bound_t::rins(diving_worker_t* worker, solve_submip(worker, current_incumbent, var_types, - num_var_fixed, - num_integers, - submip_level, - log_prefix, + submip_stats, + fixrate, + stats.total_simplex_iters, is_root_heuristic); } } - // Accumulate the iterations for sub-MIP so it stops when it reaches the allocated budget. - if (settings_.inside_submip) { - exploration_stats_.total_simplex_iters += rins_stats.total_simplex_iters; - } - DEBUG_SUBMIP( - "{}success={}, infeasible={}, calls={}, fixrate={:.4g} ({}), max_fixrate={:.4g} ({}), " - "min_fixrate={:.4g} ({})\n", + "{}success={}, infeasible={}, calls={}, fixrate={:.4g} ({:.0f}/{}), max_fixrate={:.4g}, " + "min_fixrate={:.4g}\n", log_prefix, - rins_stats_.total_success.load(), - rins_stats_.total_infeasible.load(), - rins_stats_.total_calls.load(), + submip_stats.total_success.load(), + submip_stats.total_infeasible.load(), + submip_stats.total_calls.load(), fixrate, - num_var_fixed, + fixrate * num_integers, + num_integers, max_fixrate, - max_var_fixed, - min_fixrate, - min_var_fixed); + min_fixrate); // If the pool is uninitialized (i.e., in the root node), then this just inactivate the worker. if (!is_root_heuristic) { - rins_worker_pool_.return_worker_to_pool(worker); + submip_worker_pool_.return_worker_to_pool(worker); } else { worker->set_inactive(); } @@ -2863,26 +2980,28 @@ void branch_and_bound_t::launch_root_heuristics( } } - if (settings_.submip_settings.rins != 0 && incumbent_.has_incumbent) { + bool use_rins = settings_.submip_settings.rins != 0 && incumbent_.has_incumbent; + if (use_rins || settings_.submip_settings.rens != 0) { + search_strategy_t strategy = use_rins ? search_strategy_t::RINS : search_strategy_t::RENS; diving_worker_t* worker = current_heuristic->create_submip_worker( - cut_pass, lp, settings_, root_objective_, root_vstatus_, sol); + cut_pass, lp, settings_, root_objective_, root_vstatus_, sol, strategy); std::vector current_incumbent; mutex_upper_.lock(); - current_incumbent = incumbent_.x; + if (use_rins) current_incumbent = incumbent_.x; mutex_upper_.unlock(); if (settings_.inside_submip) { // LLVM libomp's GOMP compatibility path skips GCC's firstprivate copy // function for included tasks. - rins(worker, current_incumbent, current_heuristic->var_types_, is_root_heuristic); - + recursive_submip(worker, current_incumbent, current_heuristic->var_types_, is_root_heuristic); } else { ++(*worker_count); #pragma omp task priority(CUOPT_DEFAULT_TASK_PRIORITY) affinity(worker) \ firstprivate(current_incumbent, current_heuristic, worker_count) depend(out : *worker) { - rins(worker, current_incumbent, current_heuristic->var_types_, is_root_heuristic); + recursive_submip( + worker, current_incumbent, current_heuristic->var_types_, is_root_heuristic); --(*worker_count); } } @@ -3794,15 +3913,15 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut settings_, root_relax_soln_.x, edge_norms_); - rins_worker_pool_.init(num_submip_workers, - original_lp_, - Arow_, - var_types_, - symmetry_, - settings_, - root_relax_soln_.x, - edge_norms_, - num_bfs_workers); + submip_worker_pool_.init(num_submip_workers, + original_lp_, + Arow_, + var_types_, + symmetry_, + settings_, + root_relax_soln_.x, + edge_norms_, + num_bfs_workers); if (num_diving_workers > 0) { diving_worker_pool_.init(num_diving_workers, @@ -3857,6 +3976,18 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (!std::isfinite(lower_bound)) { lower_bound = search_tree_.root.lower_bound; } } + DEBUG_SUBMIP("RINS: success={}, infeasible={}, empty={}, calls={}", + rins_stats_.total_success.load(), + rins_stats_.total_infeasible.load(), + rins_stats_.total_empty.load(), + rins_stats_.total_calls.load()); + + DEBUG_SUBMIP("RENS: success={}, infeasible={}, empty={}, calls={}", + rens_stats_.total_success.load(), + rens_stats_.total_infeasible.load(), + rens_stats_.total_empty.load(), + rens_stats_.total_calls.load()); + set_final_solution(solution, lower_bound); return solver_status_; } diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index fb3f9fce5d..7cf5ed3680 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -275,8 +275,9 @@ class branch_and_bound_t { diving_worker_pool_t diving_worker_pool_; // Worker pool dedicated to recursive RINS - diving_worker_pool_t rins_worker_pool_; + diving_worker_pool_t submip_worker_pool_; submip_stats_t rins_stats_; + submip_stats_t rens_stats_; // Global status of the solver. omp_atomic_t solver_status_; @@ -369,27 +370,28 @@ class branch_and_bound_t { void dive_with(diving_worker_t* worker, i_t backtrack_limit); // Launch a new RINS worker - bool launch_rins_worker(const std::vector& sol); + bool launch_submip_worker(const std::vector& sol); void set_solution_from_submip(const simplex::lp_problem_t& lp, const std::vector& solution, const third_party_presolve_t& presolver, - f_t fixrate); + submip_stats_t& submip_stats, + f_t fixrate, + std::string_view log_prefix); // Solve the RINS sub-MIP void solve_submip(diving_worker_t* worker, const std::vector& current_incumbent, const std::vector& var_types, - i_t num_var_fixed, - i_t num_integers, - i_t submip_level, - std::string_view log_prefix, + submip_stats_t& submip_stats, + f_t fixrate, + i_t simplex_iter_used, bool is_root_heuristic = false); // Creates and solves the RINS sub-MIP - void rins(diving_worker_t* worker, - const std::vector& current_incumbent, - const std::vector& var_types, - bool is_root_heuristic = false); + void recursive_submip(diving_worker_t* worker, + const std::vector& current_incumbent, + const std::vector& var_types, + bool is_root_heuristic = false); void launch_root_heuristics(const simplex::lp_problem_t& lp, const std::vector& sol, diff --git a/cpp/src/branch_and_bound/constants.hpp b/cpp/src/branch_and_bound/constants.hpp index 2bc94c8f1b..59f43549d8 100644 --- a/cpp/src/branch_and_bound/constants.hpp +++ b/cpp/src/branch_and_bound/constants.hpp @@ -25,6 +25,8 @@ enum class heuristics_origin_t { // [3] E. Danna, E. Rothberg, and C. L. Pape, “Exploring relaxation induced neighborhoods to // improve MIP solutions,” Math. Program., vol. 102, no. 1, pp. 71–90, Jan. 2005, // doi: 10.1007/s10107-004-0518-7. +// [4] T. Berthold, “RENS: The optimal rounding,” Math. Prog. Comp., vol. 6, no. 1, +// pp. 33–54, Mar. 2014, doi: 10.1007/s12532-013-0060-9. enum class search_strategy_t : int { BEST_FIRST = 0, // Best-First + Plunging. PSEUDOCOST_DIVING = 1, // Pseudocost diving [1, Section 9.2.5] @@ -34,6 +36,7 @@ enum class search_strategy_t : int { FARKAS_DIVING = 5, // Farkas Diving (see [2]) VECTOR_LENGTH_DIVING = 6, // Vector Length Diving [1, Section 9.2.6] RINS = 7, // RINS (see [3]) + RENS = 8 // RENS (see [1, Section 9.1.1], [4]) }; enum class branch_direction_t { NONE = -1, DOWN = 0, UP = 1 }; @@ -49,6 +52,7 @@ inline const char* search_strategy_to_string(search_strategy_t search_strategy) case search_strategy_t::FARKAS_DIVING: return "FARKAS_DIVING"; case search_strategy_t::VECTOR_LENGTH_DIVING: return "VECTOR_LENGTH_DIVING"; case search_strategy_t::RINS: return "RINS"; + case search_strategy_t::RENS: return "RENS"; } return "UNKNOWN"; diff --git a/cpp/src/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index 50e9a4a206..bbc2836d43 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -263,6 +263,7 @@ struct submip_stats_t { omp_atomic_t total_infeasible = 0; omp_atomic_t infeasible_fixrate_sum = 0; omp_atomic_t total_calls = 0; + omp_atomic_t total_empty = 0; void save_success(double fixrate) { @@ -276,6 +277,7 @@ struct submip_stats_t { infeasible_fixrate_sum += fixrate; } + void save_empty() { ++total_empty; } double average_infeasible_fixrate() const { return infeasible_fixrate_sum / total_infeasible; } double average_success_fixrate() const { return success_fixrate_sum / total_success; } }; diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 44f8ac8246..820f41ee3e 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -122,6 +122,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_HYPER_SUBMIP_MIN_FIXRATE_CAP, &mip_settings.submip_params.min_fixrate_cap, f_t(0.0), f_t(1.0), f_t(0.1), "hard cap on the minimum fix rate for solving a sub-MIP"}, {CUOPT_MIP_HYPER_SUBMIP_TARGET_MIP_GAP, &mip_settings.submip_params.target_mip_gap, f_t(0.0), f_t(1.0), f_t(0.01), "MIP gap target for the sub-MIP"}, {CUOPT_MIP_HYPER_SUBMIP_ITERATION_LIMIT_RATIO, &mip_settings.submip_params.iteration_limit_ratio, f_t(0.0), f_t(1.0), f_t(0.8), "sub-MIP simplex-iteration limit as a factor of parent B&B iterations"}, + {CUOPT_MIP_HYPER_SUBMIP_ROUND_CLOSE_RATIO, &mip_settings.submip_params.round_close_ratio, f_t(0.0), f_t(1.0), f_t(0.8), "share of the still-unfixed integers left for later neighbourhood rounds (0 reaches the target fix rate in a single round)"}, }; // Int parameters @@ -149,6 +150,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_STRONG_CHVATAL_GOMORY_CUTS, &mip_settings.strong_chvatal_gomory_cuts, -1, 1, -1}, {CUOPT_MIP_REDUCED_COST_STRENGTHENING, &mip_settings.reduced_cost_strengthening, -1, std::numeric_limits::max(), -1}, {CUOPT_MIP_RINS, &mip_settings.submip_params.rins, -1, 1, -1}, + {CUOPT_MIP_RENS, &mip_settings.submip_params.rens, -1, 1, -1}, {CUOPT_MIP_OBJECTIVE_STEP, &mip_settings.objective_step, 0, 1, 1}, {CUOPT_NUM_GPUS, &pdlp_settings.num_gpus, -1, 72, 1}, {CUOPT_NUM_GPUS, &mip_settings.num_gpus, -1, 72, 1}, @@ -185,7 +187,8 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_HYPER_DIVING_NODE_LIMIT, &mip_settings.diving_params.node_limit, 0, std::numeric_limits::max(), 500, "maximum nodes explored per dive"}, {CUOPT_MIP_HYPER_DIVING_BACKTRACK_LIMIT, &mip_settings.diving_params.backtrack_limit, 0, std::numeric_limits::max(), 5, "maximum backtracking allowed per dive"}, // Recursive sub-MIP (RINS) hyper-parameters (hidden from default --help: name contains "hyper_") - {CUOPT_MIP_HYPER_SUBMIP_NODE_LIMIT_BASE, &mip_settings.submip_params.node_limit_base, 0, std::numeric_limits::max(), 200, "base node limit for the sub-MIP"}, + {CUOPT_MIP_HYPER_SUBMIP_NODE_LIMIT_OFFSET, &mip_settings.submip_params.node_limit_offset, 0, std::numeric_limits::max(), 200, "base node limit for the sub-MIP"}, + {CUOPT_MIP_HYPER_SUBMIP_ITERATION_LIMIT_OFFSET, &mip_settings.submip_params.iteration_limit_offset, 0, std::numeric_limits::max(), 10000, "base sub-MIP simplex-iteration limit for root heuristics"}, {CUOPT_MIP_HYPER_SUBMIP_MAX_LEVEL, &mip_settings.submip_params.max_level, 0, std::numeric_limits::max(), 10, "maximum sub-MIP recursion level"}, {CUOPT_BARRIER_PRESOLVE_BOUND_FREE_VARIABLES, &pdlp_settings.barrier_presolve_bound_free_variables, -1, 1, -1, "Bound free variables during barrier presolve: -1 automatic (current default behavior), 0 disabled, 1 enabled"}, // QCQP (barrier) scaling hyper-parameter diff --git a/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh b/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh index 474becef25..95e5c45a8c 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh @@ -114,6 +114,7 @@ class sub_mip_recombiner_t : public recombiner_t { branch_and_bound_settings.zero_half_cuts = 0; branch_and_bound_settings.inside_submip = 1; branch_and_bound_settings.submip_settings.rins = 0; + branch_and_bound_settings.submip_settings.rens = 0; branch_and_bound_settings.strong_branching_simplex_iteration_limit = 200; branch_and_bound_settings.solution_callback = [this](std::vector& solution, f_t objective) { diff --git a/cpp/src/mip_heuristics/root_heuristics.hpp b/cpp/src/mip_heuristics/root_heuristics.hpp index f54aff7c1b..1f29b25eef 100644 --- a/cpp/src/mip_heuristics/root_heuristics.hpp +++ b/cpp/src/mip_heuristics/root_heuristics.hpp @@ -58,7 +58,8 @@ struct cut_pass_heuristics_t { const simplex::simplex_solver_settings_t& settings, f_t root_obj, const std::vector& root_vstatus, - const std::vector& sol) + const std::vector& sol, + search_strategy_t type) { submip_worker_ = std::make_unique>( id, lp, Arow_, var_types_, settings, root_solution_, root_edge_norm_); @@ -67,7 +68,7 @@ struct cut_pass_heuristics_t { submip_worker_->leaf_solution.x = sol; submip_worker_->recompute_bounds = false; submip_worker_->recompute_basis = true; - submip_worker_->search_strategy = search_strategy_t::RINS; + submip_worker_->search_strategy = type; submip_worker_->set_active(); return submip_worker_.get();